diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 29e3db01d..a0f6b69a9 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -84,6 +84,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-conan-ninja-release conan_profile: macos-intel @@ -93,6 +94,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-arm-conan-ninja-release conan_profile: macos-arm @@ -102,20 +104,23 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: ipa - preset: ios-release-conan + preset: ios-release-conan-ccache conan_profile: ios-arm64 conan_options: --options with_apple_system_libs=True - platform: msvc os: windows-latest test: 0 pack: 1 + pack_type: RelWithDebInfo extension: exe - preset: windows-msvc-release + preset: windows-msvc-release-ccache - platform: mingw-ubuntu os: ubuntu-22.04 test: 0 pack: 1 + pack_type: Release extension: exe cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` cmake_args: -G Ninja @@ -145,15 +150,57 @@ jobs: with: submodules: recursive + - name: Ensure LF line endings + if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} + 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 | \ + { ! xargs -0 grep -l -z -P '\r\n'; } + + - name: Validate JSON + # the Python yaml module doesn't seem to work on mac-arm + # also, running it on multiple presets is redundant and slightly increases already long CI built times + if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} + run: | + pip3 install json5 jstyleson + python3 CI/linux-qt6/validate_json.py + - name: Dependencies run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' env: VCMI_BUILD_PLATFORM: x64 + # 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 + uses: hendrikmuhs/ccache-action@v1.2 + if: ${{ github.event.number != '' }} + with: + key: ${{ matrix.preset }}-PR-${{ github.event.number }} + restore-keys: | + ${{ matrix.preset }}-PR-${{ github.event.number }} + ${{ matrix.preset }}-no-PR + # actual cache takes up less space, at most ~1 GB + max-size: "5G" + verbose: 2 + + - name: Ccache for everything but PRs + uses: hendrikmuhs/ccache-action@v1.2 + if: ${{ github.event.number == '' }} + with: + key: ${{ matrix.preset }}-no-PR + restore-keys: | + ${{ matrix.preset }}-no-PR + # actual cache takes up less space, at most ~1 GB + max-size: "5G" + verbose: 2 + - uses: actions/setup-python@v4 if: "${{ matrix.conan_profile != '' }}" with: python-version: '3.10' + - name: Conan setup if: "${{ matrix.conan_profile != '' }}" run: | @@ -185,9 +232,9 @@ jobs: env: PULL_REQUEST: ${{ github.event.pull_request.number }} - - name: CMake Preset + - name: CMake Preset with ccache run: | - cmake --preset ${{ matrix.preset }} + cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }} - name: Build Preset run: | @@ -204,7 +251,7 @@ jobs: run: | cd '${{github.workspace}}/out/build/${{matrix.preset}}' CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey` - "$CPACK_PATH" -C ${{env.BUILD_TYPE}} ${{ matrix.cpack_args }} + "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} 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 }}'.*)" rm -rf _CPack_Packages @@ -237,6 +284,14 @@ jobs: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: | ${{ env.ANDROID_APK_PATH }} + + - name: Symbols + if: ${{ matrix.platform == 'msvc' }} + uses: actions/upload-artifact@v3 + with: + name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols + path: | + ${{github.workspace}}/**/*.pdb - name: Android JNI ${{matrix.platform}} if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 63cb7f209..73dfd12af 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -18,45 +18,147 @@ uint64_t averageDmg(const DamageRange & range) return (range.min + range.max) / 2; } +void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + auto damage = averageDmg(hb->battleEstimateDamage(attacker, defender, 0).damage); + + damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); +} + + +void DamageCache::buildDamageCache(std::shared_ptr hb, int side) +{ + auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool + { + return u->isValidTarget(); + }); + + std::vector ourUnits, enemyUnits; + + for(auto stack : stacks) + { + if(stack->unitSide() == side) + ourUnits.push_back(stack); + else + enemyUnits.push_back(stack); + } + + for(auto ourUnit : ourUnits) + { + if(!ourUnit->alive()) + continue; + + for(auto enemyUnit : enemyUnits) + { + if(enemyUnit->alive()) + { + cacheDamage(ourUnit, enemyUnit, hb); + cacheDamage(enemyUnit, ourUnit, hb); + } + } + } +} + +int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); + + if(damage == 0) + { + cacheDamage(attacker, defender, hb); + + damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); + } + + return static_cast(damage); +} + +int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + if(parent) + { + auto attackerDamageMap = parent->damageCache.find(attacker->unitId()); + + if(attackerDamageMap != parent->damageCache.end()) + { + auto targetDamage = attackerDamageMap->second.find(defender->unitId()); + + if(targetDamage != attackerDamageMap->second.end()) + { + return static_cast(targetDamage->second * attacker->getCount()); + } + } + } + + return getDamage(attacker, defender, hb); +} + AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) : from(from), dest(dest), attack(attack) { } -int64_t AttackPossibility::damageDiff() const +float AttackPossibility::damageDiff() const { return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg; } -int64_t AttackPossibility::attackValue() const +float AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const +{ + return positiveEffectMultiplier * (defenderDamageReduce + shootersBlockedDmg) + - negativeEffectMultiplier * (attackerDamageReduce + collateralDamageReduce); +} + +float AttackPossibility::attackValue() const { return damageDiff(); } +float hpFunction(uint64_t unitHealthStart, uint64_t unitHealthEnd, uint64_t maxHealth) +{ + float ratioStart = static_cast(unitHealthStart) / maxHealth; + float ratioEnd = static_cast(unitHealthEnd) / maxHealth; + float base = 0.666666f; + + // reduce from max to 0 must be 1. + // 10 hp from end costs bit more than 10 hp from start because our goal is to kill unit, not just hurt it + // ********** 2 * base - ratioStart ********* + // * * + // * height = ratioStart - ratioEnd * + // * * + // ******************** 2 * base - ratioEnd ****** + // S = (a + b) * h / 2 + return (base * (4 - ratioStart - ratioEnd)) * (ratioStart - ratioEnd) / 2 ; +} + /// /// How enemy damage will be reduced by this attack /// Half bounty for kill, half for making damage equal to enemy health /// Bounty - the killed creature average damage calculated against attacker /// -int64_t AttackPossibility::calculateDamageReduce( +float AttackPossibility::calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, - const CBattleInfoCallback & cb) + DamageCache & damageCache, + std::shared_ptr state) { const float HEALTH_BOUNTY = 0.5; - const float KILL_BOUNTY = 1.0 - HEALTH_BOUNTY; - - vstd::amin(damageDealt, defender->getAvailableHealth()); + const float KILL_BOUNTY = 0.5; // FIXME: provide distance info for Jousting bonus auto attackerUnitForMeasurement = attacker; - if(attackerUnitForMeasurement->isTurret()) + if(!attackerUnitForMeasurement || attackerUnitForMeasurement->isTurret()) { - auto ourUnits = cb.battleGetUnitsIf([&](const battle::Unit * u) -> bool + auto ourUnits = state->battleGetUnitsIf([&](const battle::Unit * u) -> bool { - return u->unitSide() == attacker->unitSide() && !u->isTurret(); + return u->unitSide() != defender->unitSide() + && !u->isTurret() + && u->creatureId() != CreatureID::CATAPULT + && u->creatureId() != CreatureID::BALLISTA + && u->creatureId() != CreatureID::FIRST_AID_TENT + && u->getCount(); }); if(ourUnits.empty()) @@ -65,15 +167,35 @@ int64_t AttackPossibility::calculateDamageReduce( attackerUnitForMeasurement = ourUnits.front(); } - auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0); - auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0); - auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage); - auto damagePerEnemy = enemyDamage / (double)defender->getCount(); + auto maxHealth = defender->getMaxHealth(); + auto availableHealth = defender->getFirstHPleft() + ((defender->getCount() - 1) * maxHealth); - return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth())); + vstd::amin(damageDealt, availableHealth); + + auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); + auto enemiesKilled = damageDealt / maxHealth + (damageDealt % maxHealth >= defender->getFirstHPleft() ? 1 : 0); + auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); + auto exceedingDamage = (damageDealt % maxHealth); + float hpValue = (damageDealt / maxHealth); + + if(defender->getFirstHPleft() >= exceedingDamage) + { + hpValue += hpFunction(defender->getFirstHPleft(), defender->getFirstHPleft() - exceedingDamage, maxHealth); + } + else + { + hpValue += hpFunction(defender->getFirstHPleft(), 0, maxHealth); + hpValue += hpFunction(maxHealth, maxHealth + defender->getFirstHPleft() - exceedingDamage, maxHealth); + } + + return damagePerEnemy * (enemiesKilled * KILL_BOUNTY + hpValue * HEALTH_BOUNTY); } -int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state) +int64_t AttackPossibility::evaluateBlockedShootersDmg( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state) { int64_t res = 0; @@ -84,10 +206,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a auto hexes = attacker->getSurroundingHexes(hex); for(BattleHex tile : hexes) { - auto st = state.battleGetUnitByPos(tile, true); - if(!st || !state.battleMatchOwner(st, attacker)) + auto st = state->battleGetUnitByPos(tile, true); + if(!st || !state->battleMatchOwner(st, attacker)) continue; - if(!state.battleCanShoot(st)) + if(!state->battleCanShoot(st)) continue; // FIXME: provide distance info for Jousting bonus @@ -97,8 +219,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a BattleAttackInfo meleeAttackInfo(st, attacker, 0, false); meleeAttackInfo.defenderPos = hex; - auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo); - auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo); + auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); + auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; res += gain; @@ -107,13 +229,17 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a return res; } -AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state) +AttackPossibility AttackPossibility::evaluate( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state) { auto attacker = attackInfo.attacker; auto defender = attackInfo.defender; const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); - const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker)); + const auto attackerSide = state->playerToSide(state->battleGetOwner(attacker)); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo); @@ -141,9 +267,9 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf std::vector units; if (attackInfo.shooting) - units = state.getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); + units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); else - units = state.getAttackedBattleUnits(attacker, defHex, false, hex); + units = state->getAttackedBattleUnits(attacker, defHex, false, hex); // ensure the defender is also affected bool addDefender = true; @@ -169,10 +295,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf for(int i = 0; i < totalAttacks; i++) { - int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; + int64_t damageDealt, damageReceived; + float defenderDamageReduce, attackerDamageReduce; DamageEstimation retaliation; - auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation); + auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth()); vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth()); @@ -181,7 +308,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth()); damageDealt = averageDmg(attackDmg.damage); - defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state); + defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state); ap.attackerState->afterAttack(attackInfo.shooting, false); //FIXME: use ranged retaliation @@ -191,11 +318,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) { damageReceived = averageDmg(retaliation.damage); - attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state); + attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state); defenderState->afterAttack(attackInfo.shooting, true); } - bool isEnemy = state.battleMatchOwner(attacker, u); + bool isEnemy = state->battleMatchOwner(attacker, u); // this includes enemy units as well as attacker units under enemy's mind control if(isEnemy) @@ -225,7 +352,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf } // check how much damage we gain from blocking enemy shooters on this hex - bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, state); + bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, damageCache, state); #if BATTLE_TRACE_LEVEL>=1 logAi->trace("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld", diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 7a9cf766f..2181d883a 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -15,6 +15,22 @@ #define BATTLE_TRACE_LEVEL 0 +class DamageCache +{ +private: + std::unordered_map> damageCache; + DamageCache * parent; + +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 getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + void buildDamageCache(std::shared_ptr hb, int side); +}; + /// /// Evaluate attack value of one particular attack taking into account various effects like /// retaliation, 2-hex breath, collateral damage, shooters blocked damage @@ -30,24 +46,34 @@ public: std::vector> affectedUnits; - int64_t defenderDamageReduce = 0; - int64_t attackerDamageReduce = 0; //usually by counter-attack - int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) + float defenderDamageReduce = 0; + float attackerDamageReduce = 0; //usually by counter-attack + float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) int64_t shootersBlockedDmg = 0; AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); - int64_t damageDiff() const; - int64_t attackValue() const; + float damageDiff() const; + float attackValue() const; + float damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const; - static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state); + static AttackPossibility evaluate( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state); - static int64_t calculateDamageReduce( + static float calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, - const CBattleInfoCallback & cb); + DamageCache & damageCache, + std::shared_ptr cb); private: - static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state); + static int64_t evaluateBlockedShootersDmg( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state); }; diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 00f6295e7..54870c763 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -1,907 +1,292 @@ -/* - * BattleAI.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 "BattleAI.h" -#include "BattleExchangeVariant.h" - -#include "StackWithBonuses.h" -#include "EnemyInfo.h" -#include "../../lib/CStopWatch.h" -#include "../../lib/CThreadHelper.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/battle/BattleStateInfoForRetreat.h" -#include "../../lib/battle/CObstacleInstance.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 - -#define LOGL(text) print(text) -#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) - -enum class SpellTypes -{ - ADVENTURE, BATTLE, OTHER -}; - -SpellTypes spellType(const CSpell * spell) -{ - if(!spell->isCombat() || spell->isCreatureAbility()) - return SpellTypes::OTHER; - - if(spell->isOffensive() || spell->hasEffects() || spell->hasBattleEffects()) - return SpellTypes::BATTLE; - - return SpellTypes::OTHER; -} - -std::vector CBattleAI::getBrokenWallMoatHexes() const -{ - std::vector result; - - for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL }) - { - auto state = cb->battleGetWallState(wallPart); - - if(state != EWallState::DESTROYED) - continue; - - auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart); - auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); - - result.push_back(moatHex); - } - - return result; -} - -CBattleAI::CBattleAI() - : side(-1), - wasWaitingForRealize(false), - wasUnlockingGs(false) -{ -} - -CBattleAI::~CBattleAI() -{ - if(cb) - { - //Restore previous state of CB - it may be shared with the main AI (like VCAI) - cb->waitTillRealize = wasWaitingForRealize; - cb->unlockGsWhenWaiting = wasUnlockingGs; - } -} - -void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - setCbc(CB); - env = ENV; - cb = CB; - playerID = *CB->getPlayerID(); //TODO should be sth in callback - wasWaitingForRealize = CB->waitTillRealize; - wasUnlockingGs = CB->unlockGsWhenWaiting; - CB->waitTillRealize = false; - CB->unlockGsWhenWaiting = false; - movesSkippedByDefense = 0; -} - -void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) -{ - initBattleInterface(ENV, CB); - autobattlePreferences = autocombatPreferences; -} - -BattleAction CBattleAI::useHealingTent(const CStack *stack) -{ - auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); - std::map woundHpToStack; - for(const auto * stack : healingTargets) - { - if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) - woundHpToStack[woundHp] = stack; - } - - if(woundHpToStack.empty()) - return BattleAction::makeDefend(stack); - else - return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack -} - -std::optional CBattleAI::findBestCreatureSpell(const CStack *stack) -{ - //TODO: faerie dragon type spell should be selected by server - SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); - if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) - { - const CSpell * spell = creatureSpellToCast.toSpell(); - - if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) - { - std::vector possibleCasts; - spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); - for(auto & target : temp.findPotentialTargets()) - { - PossibleSpellcast ps; - ps.dest = target; - ps.spell = spell; - evaluateCreatureSpellcast(stack, ps); - possibleCasts.push_back(ps); - } - - std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); - if(!possibleCasts.empty() && possibleCasts.front().value > 0) - { - return possibleCasts.front(); - } - } - } - return std::nullopt; -} - -BattleAction CBattleAI::selectStackAction(const CStack * stack) -{ - //evaluate casting spell for spellcasting stack - std::optional bestSpellcast = findBestCreatureSpell(stack); - - HypotheticBattle hb(env.get(), cb); - - PotentialTargets targets(stack, hb); - BattleExchangeEvaluator scoreEvaluator(cb, env); - auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb); - - int64_t score = EvaluationResult::INEFFECTIVE_SCORE; - - - if(targets.possibleAttacks.empty() && bestSpellcast.has_value()) - { - movesSkippedByDefense = 0; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - if(!targets.possibleAttacks.empty()) - { -#if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Evaluating attack for %s", stack->getDescription()); -#endif - - auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb); - auto & bestAttack = evaluationResult.bestAttack; - - //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()) - { - // return because spellcast value is damage dealt and score is dps reduce - movesSkippedByDefense = 0; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - if(evaluationResult.score > score) - { - score = evaluationResult.score; - - logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", - bestAttack.attackerState->unitType()->getJsonKey(), - bestAttack.affectedUnits[0]->unitType()->getJsonKey(), - (int)bestAttack.affectedUnits[0]->getCount(), - (int)bestAttack.from, - (int)bestAttack.attack.attacker->getPosition().hex, - bestAttack.attack.chargeDistance, - bestAttack.attack.attacker->speed(0, true), - bestAttack.defenderDamageReduce, - bestAttack.attackerDamageReduce, bestAttack.attackValue() - ); - - if (moveTarget.score <= score) - { - if(evaluationResult.wait) - { - return BattleAction::makeWait(stack); - } - else if(bestAttack.attack.shooting) - { - movesSkippedByDefense = 0; - return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); - } - else - { - movesSkippedByDefense = 0; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); - } - } - } - } - - //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. - if(moveTarget.score > score) - { - score = moveTarget.score; - - if(stack->waited()) - { - return goTowardsNearest(stack, moveTarget.positions); - } - else - { - return BattleAction::makeWait(stack); - } - } - - if(score <= EvaluationResult::INEFFECTIVE_SCORE - && !stack->hasBonusOfType(BonusType::FLYING) - && stack->unitSide() == BattleSide::ATTACKER - && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) - { - auto brokenWallMoat = getBrokenWallMoatHexes(); - - if(brokenWallMoat.size()) - { - movesSkippedByDefense = 0; - - if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) - return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); - else - return goTowardsNearest(stack, brokenWallMoat); - } - } - - return BattleAction::makeDefend(stack); -} - -void CBattleAI::yourTacticPhase(int distance) -{ - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); -} - -uint64_t timeElapsed(std::chrono::time_point start) -{ - auto end = std::chrono::high_resolution_clock::now(); - - return std::chrono::duration_cast(end - start).count(); -} - -void CBattleAI::activeStack( const CStack * stack ) -{ - LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); - - BattleAction result = BattleAction::makeDefend(stack); - setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) - - auto start = std::chrono::high_resolution_clock::now(); - - try - { - if(stack->creatureId() == CreatureID::CATAPULT) - { - cb->battleMakeUnitAction(useCatapult(stack)); - return; - } - if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) - { - cb->battleMakeUnitAction(useHealingTent(stack)); - return; - } - - if(autobattlePreferences.enableSpellsUsage) - attemptCastingSpell(); - - logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); - - if(cb->battleIsFinished() || !stack->alive()) - { - //spellcast may finish battle or kill active stack - //send special preudo-action - BattleAction cancel; - cancel.actionType = EActionType::CANCEL; - cb->battleMakeUnitAction(cancel); - return; - } - - if(auto action = considerFleeingOrSurrendering()) - { - cb->battleMakeUnitAction(*action); - return; - } - - result = selectStackAction(stack); - } - catch(boost::thread_interrupted &) - { - throw; - } - catch(std::exception &e) - { - logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what()); - } - - if(result.actionType == EActionType::DEFEND) - { - movesSkippedByDefense++; - } - else if(result.actionType != EActionType::WAIT) - { - movesSkippedByDefense = 0; - } - - logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); - - cb->battleMakeUnitAction(result); -} - -BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector hexes) const -{ - auto reachability = cb->getReachability(stack); - auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); - - if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked - { - return BattleAction::makeDefend(stack); - } - - std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool - { - return reachability.distances[h1] < reachability.distances[h2]; - }); - - for(auto hex : hexes) - { - 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); - } - } - - BattleHex bestNeighbor = hexes.front(); - - if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) - { - return BattleAction::makeDefend(stack); - } - - BattleExchangeEvaluator scoreEvaluator(cb, env); - HypotheticBattle hb(env.get(), cb); - - scoreEvaluator.updateReachabilityMap(hb); - - if(stack->hasBonusOfType(BonusType::FLYING)) - { - std::set obstacleHexes; - - auto insertAffected = [](const CObstacleInstance & spellObst, std::set obstacleHexes) { - auto affectedHexes = spellObst.getAffectedTiles(); - obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); - }; - - const auto & obstacles = hb.battleGetAllObstacles(); - - for (const auto & obst: obstacles) { - - if(obst->triggersEffects()) - { - auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); - auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); - - if(triggerIsNegative) - insertAffected(*obst, obstacleHexes); - } - } - // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. - // We just check all available hexes and pick the one closest to the target. - auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int - { - const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc) - const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat - - auto distance = BattleHex::getDistance(bestNeighbor, hex); - - if(vstd::contains(obstacleHexes, hex)) - distance += NEGATIVE_OBSTACLE_PENALTY; - - return scoreEvaluator.checkPositionBlocksOurStacks(hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; - }); - - return BattleAction::makeMove(stack, *nearestAvailableHex); - } - else - { - BattleHex currentDest = bestNeighbor; - while(1) - { - if(!currentDest.isValid()) - { - return BattleAction::makeDefend(stack); - } - - if(vstd::contains(avHexes, currentDest) - && !scoreEvaluator.checkPositionBlocksOurStacks(hb, stack, currentDest)) - return BattleAction::makeMove(stack, currentDest); - - currentDest = reachability.predecessors[currentDest]; - } - } -} - -BattleAction CBattleAI::useCatapult(const CStack * stack) -{ - BattleAction attack; - BattleHex targetHex = BattleHex::INVALID; - - if(cb->battleGetGateState() == EGateState::CLOSED) - { - targetHex = cb->wallPartToBattleHex(EWallPart::GATE); - } - else - { - EWallPart wallParts[] = { - EWallPart::KEEP, - EWallPart::BOTTOM_TOWER, - EWallPart::UPPER_TOWER, - EWallPart::BELOW_GATE, - EWallPart::OVER_GATE, - EWallPart::BOTTOM_WALL, - EWallPart::UPPER_WALL - }; - - for(auto wallPart : wallParts) - { - auto wallState = cb->battleGetWallState(wallPart); - - if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) - { - targetHex = cb->wallPartToBattleHex(wallPart); - break; - } - } - } - - if(!targetHex.isValid()) - { - return BattleAction::makeDefend(stack); - } - - attack.aimToHex(targetHex); - attack.actionType = EActionType::CATAPULT; - attack.side = side; - attack.stackNumber = stack->unitId(); - - movesSkippedByDefense = 0; - - return attack; -} - -void CBattleAI::attemptCastingSpell() -{ - auto hero = cb->battleGetMyHero(); - if(!hero) - return; - - if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK) - return; - - LOGL("Casting spells sounds like fun. Let's see..."); - //Get all spells we can cast - std::vector possibleSpells; - vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool - { - return s->canBeCast(cb.get(), spells::Mode::HERO, hero); - }); - LOGFL("I can cast %d spells.", possibleSpells.size()); - - vstd::erase_if(possibleSpells, [](const CSpell *s) - { - return spellType(s) != SpellTypes::BATTLE; - }); - - LOGFL("I know how %d of them works.", possibleSpells.size()); - - //Get possible spell-target pairs - std::vector possibleCasts; - for(auto spell : possibleSpells) - { - spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); - - if(!spell->isDamage() && spell->getTargetType() == spells::AimType::LOCATION) - continue; - - const bool FAST = true; - - for(auto & target : temp.findPotentialTargets(FAST)) - { - PossibleSpellcast ps; - ps.dest = target; - ps.spell = spell; - possibleCasts.push_back(ps); - } - } - LOGFL("Found %d spell-target combinations.", possibleCasts.size()); - if(possibleCasts.empty()) - return; - - using ValueMap = PossibleSpellcast::ValueMap; - - auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, HypotheticBattle & state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool - { - bool firstRound = true; - bool enemyHadTurn = false; - size_t ourTurnSpan = 0; - - bool stop = false; - - for(auto & round : queue) - { - if(!firstRound) - state.nextRound(0);//todo: set actual value? - for(auto unit : round) - { - if(!vstd::contains(values, unit->unitId())) - values[unit->unitId()] = 0; - - if(!unit->alive()) - continue; - - if(state.battleGetOwner(unit) != playerID) - { - enemyHadTurn = true; - - if(!firstRound || state.battleCastSpells(unit->unitSide()) == 0) - { - //enemy could counter our spell at this point - //anyway, we do not know what enemy will do - //just stop evaluation - stop = true; - break; - } - } - else if(!enemyHadTurn) - { - ourTurnSpan++; - } - - state.nextTurn(unit->unitId()); - - PotentialTargets pt(unit, state); - - if(!pt.possibleAttacks.empty()) - { - AttackPossibility ap = pt.bestAction(); - - auto swb = state.getForUpdate(unit->unitId()); - *swb = *ap.attackerState; - - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilAttack); - if(ap.attackerDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - - for(auto affected : ap.affectedUnits) - { - swb = state.getForUpdate(affected->unitId()); - *swb = *affected; - - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId()) - swb->removeUnitBonus(Bonus::UntilAttack); - } - } - - auto bav = pt.bestActionValue(); - - //best action is from effective owner`s point if view, we need to convert to our point if view - if(state.battleGetOwner(unit) != playerID) - bav = -bav; - values[unit->unitId()] += bav; - } - - firstRound = false; - - if(stop) - break; - } - - if(enemyHadTurnOut) - *enemyHadTurnOut = enemyHadTurn; - - return ourTurnSpan >= minTurnSpan; - }; - - ValueMap valueOfStack; - ValueMap healthOfStack; - - TStacks all = cb->battleGetAllStacks(false); - - size_t ourRemainingTurns = 0; - - for(auto unit : all) - { - healthOfStack[unit->unitId()] = unit->getAvailableHealth(); - valueOfStack[unit->unitId()] = 0; - - if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) - ourRemainingTurns++; - } - - LOGFL("I have %d turns left in this round", ourRemainingTurns); - - const bool castNow = ourRemainingTurns <= 1; - - if(castNow) - print("I should try to cast a spell now"); - else - print("I could wait better moment to cast a spell"); - - auto amount = all.size(); - - std::vector turnOrder; - - cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once - - { - bool enemyHadTurn = false; - - HypotheticBattle state(env.get(), cb); - - evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); - - if(!enemyHadTurn) - { - auto battleIsFinishedOpt = state.battleIsFinished(); - - if(battleIsFinishedOpt) - { - print("No need to cast a spell. Battle will finish soon."); - return; - } - } - } - - struct ScriptsCache - { - //todo: re-implement scripts context cache - }; - - auto evaluateSpellcast = [&] (PossibleSpellcast * ps, std::shared_ptr) - { - HypotheticBattle state(env.get(), cb); - - spells::BattleCast cast(&state, hero, spells::Mode::HERO, ps->spell); - cast.castEval(state.getServerCallback(), ps->dest); - ValueMap newHealthOfStack; - ValueMap newValueOfStack; - - size_t ourUnits = 0; - - std::set unitIds; - - state.battleGetUnitsIf([&](const battle::Unit * u)->bool - { - if(!u->isGhost() && !u->isTurret()) - unitIds.insert(u->unitId()); - - return false; - }); - - for(auto unitId : unitIds) - { - auto localUnit = state.battleGetUnitByID(unitId); - - newHealthOfStack[unitId] = localUnit->getAvailableHealth(); - newValueOfStack[unitId] = 0; - - if(state.battleGetOwner(localUnit) == playerID && localUnit->alive() && localUnit->willMove()) - ourUnits++; - } - - size_t minTurnSpan = ourUnits/3; //todo: tweak this - - std::vector newTurnOrder; - - state.battleGetTurnOrder(newTurnOrder, amount, 2); - - const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, state, minTurnSpan, nullptr); - - if(turnSpanOK || castNow) - { - int64_t totalGain = 0; - - for(auto unitId : unitIds) - { - auto localUnit = state.battleGetUnitByID(unitId); - - auto newValue = getValOr(newValueOfStack, unitId, 0); - auto oldValue = getValOr(valueOfStack, unitId, 0); - - auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; - - if(localUnit->unitOwner() != playerID) - healthDiff = -healthDiff; - - if(healthDiff < 0) - { - ps->value = -1; - return; //do not damage own units at all - } - - totalGain += (newValue - oldValue + healthDiff); - } - - ps->value = totalGain; - } - else - { - ps->value = -1; - } - }; - - using EvalRunner = ThreadPool; - - EvalRunner::Tasks tasks; - - for(PossibleSpellcast & psc : possibleCasts) - tasks.push_back(std::bind(evaluateSpellcast, &psc, _1)); - - uint32_t threadCount = boost::thread::hardware_concurrency(); - - if(threadCount == 0) - { - logGlobal->warn("No information of CPU cores available"); - threadCount = 1; - } - - CStopWatch timer; - - std::vector> scriptsPool; - - for(uint32_t idx = 0; idx < threadCount; idx++) - { - scriptsPool.emplace_back(); - } - - EvalRunner runner(&tasks, scriptsPool); - runner.run(); - - LOGFL("Evaluation took %d ms", timer.getDiff()); - - auto pscValue = [](const PossibleSpellcast &ps) -> int64_t - { - return ps.value; - }; - auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); - - if(castToPerform.value > 0) - { - LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); - BattleAction spellcast; - spellcast.actionType = EActionType::HERO_SPELL; - spellcast.actionSubtype = castToPerform.spell->id; - spellcast.setTarget(castToPerform.dest); - spellcast.side = side; - spellcast.stackNumber = (!side) ? -1 : -2; - cb->battleMakeSpellAction(spellcast); - movesSkippedByDefense = 0; - } - else - { - LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); - } -} - -//Below method works only for offensive spells -void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) -{ - using ValueMap = PossibleSpellcast::ValueMap; - - RNGStub rngStub; - HypotheticBattle state(env.get(), cb); - TStacks all = cb->battleGetAllStacks(false); - - ValueMap healthOfStack; - ValueMap newHealthOfStack; - - for(auto unit : all) - { - healthOfStack[unit->unitId()] = unit->getAvailableHealth(); - } - - spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell); - cast.castEval(state.getServerCallback(), ps.dest); - - for(auto unit : all) - { - auto unitId = unit->unitId(); - auto localUnit = state.battleGetUnitByID(unitId); - newHealthOfStack[unitId] = localUnit->getAvailableHealth(); - } - - int64_t totalGain = 0; - - for(auto unit : all) - { - auto unitId = unit->unitId(); - auto localUnit = state.battleGetUnitByID(unitId); - - auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; - - if(localUnit->unitOwner() != getCbc()->getPlayerID()) - healthDiff = -healthDiff; - - if(healthDiff < 0) - { - ps.value = -1; - return; //do not damage own units at all - } - - totalGain += healthDiff; - } - - ps.value = totalGain; -} - -void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) -{ - LOG_TRACE(logAi); - side = Side; -} - -void CBattleAI::print(const std::string &text) const -{ - logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); -} - -std::optional CBattleAI::considerFleeingOrSurrendering() -{ - BattleStateInfoForRetreat bs; - - bs.canFlee = cb->battleCanFlee(); - bs.canSurrender = cb->battleCanSurrender(playerID); - bs.ourSide = cb->battleGetMySide(); - bs.ourHero = cb->battleGetMyHero(); - bs.enemyHero = nullptr; - - for(auto stack : cb->battleGetAllStacks(false)) - { - if(stack->alive()) - { - if(stack->unitSide() == bs.ourSide) - bs.ourStacks.push_back(stack); - else - { - bs.enemyStacks.push_back(stack); - bs.enemyHero = cb->battleGetOwnerHero(stack); - } - } - } - - bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); - - if(!bs.canFlee && !bs.canSurrender) - { - return std::nullopt; - } - - auto result = cb->makeSurrenderRetreatDecision(bs); - - if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) - { - return BattleAction::makeRetreat(bs.ourSide); - } - - return result; -} - - - +/* + * BattleAI.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 "BattleAI.h" +#include "BattleEvaluator.h" +#include "BattleExchangeVariant.h" + +#include "StackWithBonuses.h" +#include "EnemyInfo.h" +#include "tbb/parallel_for.h" +#include "../../lib/CStopWatch.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleStateInfoForRetreat.h" +#include "../../lib/battle/CObstacleInstance.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 + +#define LOGL(text) print(text) +#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) + +CBattleAI::CBattleAI() + : side(-1), + wasWaitingForRealize(false), + wasUnlockingGs(false) +{ +} + +CBattleAI::~CBattleAI() +{ + if(cb) + { + //Restore previous state of CB - it may be shared with the main AI (like VCAI) + cb->waitTillRealize = wasWaitingForRealize; + cb->unlockGsWhenWaiting = wasUnlockingGs; + } +} + +void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + setCbc(CB); + env = ENV; + cb = CB; + playerID = *CB->getPlayerID(); + wasWaitingForRealize = CB->waitTillRealize; + wasUnlockingGs = CB->unlockGsWhenWaiting; + CB->waitTillRealize = false; + CB->unlockGsWhenWaiting = false; + movesSkippedByDefense = 0; +} + +void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) +{ + initBattleInterface(ENV, CB); + autobattlePreferences = autocombatPreferences; +} + +BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack) +{ + auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); + std::map woundHpToStack; + for(const auto * stack : healingTargets) + { + if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) + woundHpToStack[woundHp] = stack; + } + + if(woundHpToStack.empty()) + return BattleAction::makeDefend(stack); + else + return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack +} + +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) +{ + auto stacks = cb->battleGetAllStacks(); + auto our = 0, enemy = 0; + + for(auto stack : stacks) + { + auto creature = stack->creatureId().toCreature(); + + if(!creature) + continue; + + if(stack->unitSide() == side) + our += stack->getCount() * creature->getAIValue(); + else + enemy += stack->getCount() * creature->getAIValue(); + } + + return enemy == 0 ? 1.0f : static_cast(our) / enemy; +} + +void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) +{ + LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); + + auto timeElapsed = [](std::chrono::time_point start) -> uint64_t + { + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); + }; + + BattleAction result = BattleAction::makeDefend(stack); + setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) + + auto start = std::chrono::high_resolution_clock::now(); + + try + { + if(stack->creatureId() == CreatureID::CATAPULT) + { + cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack)); + return; + } + if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) + { + cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack)); + return; + } + +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Build evaluator and targets"); +#endif + + BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); + + result = evaluator.selectStackAction(stack); + + if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) + { + auto spelCasted = evaluator.attemptCastingSpell(stack); + + if(spelCasted) + return; + + skipCastUntilNextBattle = true; + } + + logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); + + if(auto action = considerFleeingOrSurrendering(battleID)) + { + cb->battleMakeUnitAction(battleID, *action); + return; + } + } + catch(boost::thread_interrupted &) + { + throw; + } + catch(std::exception &e) + { + logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what()); + } + + if(result.actionType == EActionType::DEFEND) + { + movesSkippedByDefense++; + } + else if(result.actionType != EActionType::WAIT) + { + movesSkippedByDefense = 0; + } + + logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); + + cb->battleMakeUnitAction(battleID, result); +} + +BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack) +{ + BattleAction attack; + BattleHex targetHex = BattleHex::INVALID; + + if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED) + { + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE); + } + else + { + std::array wallParts { + EWallPart::KEEP, + EWallPart::BOTTOM_TOWER, + EWallPart::UPPER_TOWER, + EWallPart::BELOW_GATE, + EWallPart::OVER_GATE, + EWallPart::BOTTOM_WALL, + EWallPart::UPPER_WALL + }; + + for(auto wallPart : wallParts) + { + auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); + + if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) + { + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); + break; + } + } + } + + if(!targetHex.isValid()) + { + return BattleAction::makeDefend(stack); + } + + attack.aimToHex(targetHex); + attack.actionType = EActionType::CATAPULT; + attack.side = side; + attack.stackNumber = stack->unitId(); + + movesSkippedByDefense = 0; + + 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) +{ + LOG_TRACE(logAi); + side = Side; + + skipCastUntilNextBattle = false; +} + +void CBattleAI::print(const std::string &text) const +{ + logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); +} + +std::optional CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID) +{ + BattleStateInfoForRetreat bs; + + bs.canFlee = cb->getBattle(battleID)->battleCanFlee(); + bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID); + bs.ourSide = cb->getBattle(battleID)->battleGetMySide(); + bs.ourHero = cb->getBattle(battleID)->battleGetMyHero(); + bs.enemyHero = nullptr; + + for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false)) + { + if(stack->alive()) + { + if(stack->unitSide() == bs.ourSide) + bs.ourStacks.push_back(stack); + else + { + bs.enemyStacks.push_back(stack); + bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack); + } + } + } + + bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); + + if(!bs.canFlee && !bs.canSurrender) + { + return std::nullopt; + } + + auto result = cb->makeSurrenderRetreatDecision(battleID, bs); + + if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) + { + return BattleAction::makeRetreat(bs.ourSide); + } + + return result; +} + + + diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index a83fcfd25..497a77f3f 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -62,6 +62,7 @@ class CBattleAI : public CBattleGameInterface bool wasWaitingForRealize; bool wasUnlockingGs; int movesSkippedByDefense; + bool skipCastUntilNextBattle; public: CBattleAI(); @@ -69,22 +70,17 @@ public: void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; - void attemptCastingSpell(); - void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void yourTacticPhase(const BattleID & battleID, int distance) override; - void activeStack(const CStack * stack) override; //called when it's turn of that stack - void yourTacticPhase(int distance) override; - - std::optional considerFleeingOrSurrendering(); + std::optional considerFleeingOrSurrendering(const BattleID & battleID); void print(const std::string &text) const; - BattleAction useCatapult(const CStack *stack); - BattleAction useHealingTent(const CStack *stack); - BattleAction selectStackAction(const CStack * stack); - std::optional findBestCreatureSpell(const CStack *stack); + BattleAction useCatapult(const BattleID & battleID, const CStack *stack); + BattleAction useHealingTent(const BattleID & battleID, const CStack *stack); - void battleStart(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, bool 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 @@ -99,9 +95,5 @@ public: //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 battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack - -private: - BattleAction goTowardsNearest(const CStack * stack, std::vector hexes) const; - std::vector getBrokenWallMoatHexes() const; AutocombatPreferences autobattlePreferences = AutocombatPreferences(); }; diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp new file mode 100644 index 000000000..3e53f8ad7 --- /dev/null +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -0,0 +1,718 @@ +/* + * BattleAI.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 "BattleEvaluator.h" +#include "BattleExchangeVariant.h" + +#include "StackWithBonuses.h" +#include "EnemyInfo.h" +#include "tbb/parallel_for.h" +#include "../../lib/CStopWatch.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleStateInfoForRetreat.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/battle/BattleAction.h" + +// TODO: remove +// Eventually only IBattleInfoCallback and battle::Unit should be used, +// CUnitState should be private and CStack should be removed completely +#include "../../lib/CStack.h" + +#define LOGL(text) print(text) +#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) + +enum class SpellTypes +{ + ADVENTURE, BATTLE, OTHER +}; + +SpellTypes spellType(const CSpell * spell) +{ + if(!spell->isCombat() || spell->isCreatureAbility()) + return SpellTypes::OTHER; + + if(spell->isOffensive() || spell->hasEffects() || spell->hasBattleEffects()) + return SpellTypes::BATTLE; + + return SpellTypes::OTHER; +} + +std::vector BattleEvaluator::getBrokenWallMoatHexes() const +{ + std::vector result; + + for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL }) + { + auto state = cb->getBattle(battleID)->battleGetWallState(wallPart); + + if(state != EWallState::DESTROYED) + continue; + + auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); + auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); + + result.push_back(moatHex); + } + + return result; +} + +std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) +{ + //TODO: faerie dragon type spell should be selected by server + SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); + if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) + { + const CSpell * spell = creatureSpellToCast.toSpell(); + + if(spell->canBeCast(cb->getBattle(battleID).get(), spells::Mode::CREATURE_ACTIVE, stack)) + { + std::vector possibleCasts; + spells::BattleCast temp(cb->getBattle(battleID).get(), stack, spells::Mode::CREATURE_ACTIVE, spell); + for(auto & target : temp.findPotentialTargets()) + { + PossibleSpellcast ps; + ps.dest = target; + ps.spell = spell; + evaluateCreatureSpellcast(stack, ps); + possibleCasts.push_back(ps); + } + + std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); + if(!possibleCasts.empty() && possibleCasts.front().value > 0) + { + return possibleCasts.front(); + } + } + } + return std::nullopt; +} + +BattleAction BattleEvaluator::selectStackAction(const CStack * stack) +{ +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Select stack action"); +#endif + //evaluate casting spell for spellcasting stack + std::optional bestSpellcast = findBestCreatureSpell(stack); + + auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); + float score = EvaluationResult::INEFFECTIVE_SCORE; + + if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) + { + activeActionMade = true; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(!targets->possibleAttacks.empty()) + { +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Evaluating attack for %s", stack->getDescription()); +#endif + + auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); + auto & bestAttack = evaluationResult.bestAttack; + + cachedAttack = bestAttack; + cachedScore = evaluationResult.score; + + //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()) + { + // return because spellcast value is damage dealt and score is dps reduce + activeActionMade = true; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(evaluationResult.score > score) + { + score = evaluationResult.score; + + logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%2f -%2f = %2f", + bestAttack.attackerState->unitType()->getJsonKey(), + bestAttack.affectedUnits[0]->unitType()->getJsonKey(), + bestAttack.affectedUnits[0]->getCount(), + (int)bestAttack.from, + (int)bestAttack.attack.attacker->getPosition().hex, + bestAttack.attack.chargeDistance, + bestAttack.attack.attacker->speed(0, true), + bestAttack.defenderDamageReduce, + bestAttack.attackerDamageReduce, + score + ); + + if (moveTarget.scorePerTurn <= score) + { + if(evaluationResult.wait) + { + return BattleAction::makeWait(stack); + } + else if(bestAttack.attack.shooting) + { + activeActionMade = true; + return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); + } + else + { + if(bestAttack.collateralDamageReduce + && bestAttack.collateralDamageReduce >= bestAttack.defenderDamageReduce / 2 + && score < 0) + { + return BattleAction::makeDefend(stack); + } + else + { + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); + } + } + } + } + } + + //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. + if(moveTarget.scorePerTurn > score) + { + score = moveTarget.score; + cachedAttack = moveTarget.cachedAttack; + cachedScore = score; + + if(stack->waited()) + { + logAi->debug( + "Moving %s towards hex %s[%d], score: %2f/%2f", + stack->getDescription(), + moveTarget.cachedAttack->attack.defender->getDescription(), + moveTarget.cachedAttack->attack.defender->getPosition().hex, + moveTarget.score, + moveTarget.scorePerTurn); + + return goTowardsNearest(stack, moveTarget.positions); + } + else + { + return BattleAction::makeWait(stack); + } + } + + if(score <= EvaluationResult::INEFFECTIVE_SCORE + && !stack->hasBonusOfType(BonusType::FLYING) + && stack->unitSide() == BattleSide::ATTACKER + && cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + { + auto brokenWallMoat = getBrokenWallMoatHexes(); + + if(brokenWallMoat.size()) + { + activeActionMade = true; + + if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) + return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); + else + return goTowardsNearest(stack, brokenWallMoat); + } + } + + return BattleAction::makeDefend(stack); +} + +uint64_t timeElapsed(std::chrono::time_point start) +{ + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); +} + +BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) +{ + auto reachability = cb->getBattle(battleID)->getReachability(stack); + auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); + + if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked + { + return BattleAction::makeDefend(stack); + } + + std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool + { + return reachability.distances[h1] < reachability.distances[h2]; + }); + + for(auto hex : hexes) + { + 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); + } + } + + BattleHex bestNeighbor = hexes.front(); + + if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) + { + return BattleAction::makeDefend(stack); + } + + scoreEvaluator.updateReachabilityMap(hb); + + if(stack->hasBonusOfType(BonusType::FLYING)) + { + std::set obstacleHexes; + + auto insertAffected = [](const CObstacleInstance & spellObst, std::set obstacleHexes) { + auto affectedHexes = spellObst.getAffectedTiles(); + obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); + }; + + const auto & obstacles = hb->battleGetAllObstacles(); + + for (const auto & obst: obstacles) { + + if(obst->triggersEffects()) + { + auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); + auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); + + if(triggerIsNegative) + insertAffected(*obst, obstacleHexes); + } + } + // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. + // We just check all available hexes and pick the one closest to the target. + auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int + { + const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc) + const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat + + auto distance = BattleHex::getDistance(bestNeighbor, hex); + + if(vstd::contains(obstacleHexes, hex)) + distance += NEGATIVE_OBSTACLE_PENALTY; + + return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; + }); + + return BattleAction::makeMove(stack, *nearestAvailableHex); + } + else + { + BattleHex currentDest = bestNeighbor; + while(true) + { + if(!currentDest.isValid()) + { + return BattleAction::makeDefend(stack); + } + + if(vstd::contains(avHexes, currentDest) + && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) + return BattleAction::makeMove(stack, currentDest); + + currentDest = reachability.predecessors[currentDest]; + } + } +} + +bool BattleEvaluator::canCastSpell() +{ + auto hero = cb->getBattle(battleID)->battleGetMyHero(); + if(!hero) + return false; + + return cb->getBattle(battleID)->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK; +} + +bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) +{ + auto hero = cb->getBattle(battleID)->battleGetMyHero(); + if(!hero) + return false; + + LOGL("Casting spells sounds like fun. Let's see..."); + //Get all spells we can cast + std::vector possibleSpells; + vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool + { + return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero); + }); + LOGFL("I can cast %d spells.", possibleSpells.size()); + + vstd::erase_if(possibleSpells, [](const CSpell *s) + { + return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; + }); + + LOGFL("I know how %d of them works.", possibleSpells.size()); + + //Get possible spell-target pairs + std::vector possibleCasts; + for(auto spell : possibleSpells) + { + 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)) + { + PossibleSpellcast ps; + ps.dest = target; + ps.spell = spell; + possibleCasts.push_back(ps); + } + } + LOGFL("Found %d spell-target combinations.", possibleCasts.size()); + if(possibleCasts.empty()) + return false; + + using ValueMap = PossibleSpellcast::ValueMap; + + auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, std::shared_ptr state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool + { + bool firstRound = true; + bool enemyHadTurn = false; + size_t ourTurnSpan = 0; + + bool stop = false; + + for(auto & round : queue) + { + if(!firstRound) + state->nextRound(); + for(auto unit : round) + { + if(!vstd::contains(values, unit->unitId())) + values[unit->unitId()] = 0; + + if(!unit->alive()) + continue; + + if(state->battleGetOwner(unit) != playerID) + { + enemyHadTurn = true; + + if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0) + { + //enemy could counter our spell at this point + //anyway, we do not know what enemy will do + //just stop evaluation + stop = true; + break; + } + } + else if(!enemyHadTurn) + { + ourTurnSpan++; + } + + state->nextTurn(unit->unitId()); + + PotentialTargets pt(unit, damageCache, state); + + if(!pt.possibleAttacks.empty()) + { + AttackPossibility ap = pt.bestAction(); + + auto swb = state->getForUpdate(unit->unitId()); + *swb = *ap.attackerState; + + if(ap.defenderDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilAttack); + if(ap.attackerDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilBeingAttacked); + + for(auto affected : ap.affectedUnits) + { + swb = state->getForUpdate(affected->unitId()); + *swb = *affected; + + if(ap.defenderDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilBeingAttacked); + if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId()) + swb->removeUnitBonus(Bonus::UntilAttack); + } + } + + auto bav = pt.bestActionValue(); + + //best action is from effective owner`s point if view, we need to convert to our point if view + if(state->battleGetOwner(unit) != playerID) + bav = -bav; + values[unit->unitId()] += bav; + } + + firstRound = false; + + if(stop) + break; + } + + if(enemyHadTurnOut) + *enemyHadTurnOut = enemyHadTurn; + + return ourTurnSpan >= minTurnSpan; + }; + + ValueMap valueOfStack; + ValueMap healthOfStack; + + TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false); + + size_t ourRemainingTurns = 0; + + for(auto unit : all) + { + healthOfStack[unit->unitId()] = unit->getAvailableHealth(); + valueOfStack[unit->unitId()] = 0; + + if(cb->getBattle(battleID)->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) + ourRemainingTurns++; + } + + LOGFL("I have %d turns left in this round", ourRemainingTurns); + + const bool castNow = ourRemainingTurns <= 1; + + if(castNow) + print("I should try to cast a spell now"); + else + print("I could wait better moment to cast a spell"); + + auto amount = all.size(); + + std::vector turnOrder; + + cb->getBattle(battleID)->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once + + { + bool enemyHadTurn = false; + + auto state = std::make_shared(env.get(), cb->getBattle(battleID)); + + evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); + + if(!enemyHadTurn) + { + auto battleIsFinishedOpt = state->battleIsFinished(); + + if(battleIsFinishedOpt) + { + print("No need to cast a spell. Battle will finish soon."); + return false; + } + } + } + + CStopWatch timer; + +#if BATTLE_TRACE_LEVEL >= 1 + tbb::blocked_range r(0, possibleCasts.size()); +#else + tbb::parallel_for(tbb::blocked_range(0, possibleCasts.size()), [&](const tbb::blocked_range & r) + { +#endif + for(auto i = r.begin(); i != r.end(); i++) + { + auto & ps = possibleCasts[i]; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); +#endif + + auto state = std::make_shared(env.get(), cb->getBattle(battleID)); + + 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 needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool + { + auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId()); + return !original || u->speed() != original->speed(); + }); + + DamageCache safeCopy = damageCache; + DamageCache innerCache(&safeCopy); + innerCache.buildDamageCache(state, side); + + if(needFullEval || !cachedAttack) + { +#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); + + if(!innerTargets.possibleAttacks.empty()) + { + innerEvaluator.updateReachabilityMap(state); + + auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); + + ps.value = newStackAction.score; + } + else + { + ps.value = 0; + } + } + else + { + ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); + } + + for(auto unit : allUnits) + { + auto newHealth = unit->getAvailableHealth(); + auto oldHealth = healthOfStack[unit->unitId()]; + + if(oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId()); + + auto dpsReduce = AttackPossibility::calculateDamageReduce( + nullptr, + originalDefender && originalDefender->alive() ? originalDefender : unit, + damage, + innerCache, + state); + + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + + if(ourUnit * goodEffect == 1) + { + if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot())) + continue; + + ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); + } + else + ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "Spell affects %s (%d), dps: %2f", + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce); +#endif + } + } +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Total score: %2f", ps.value); +#endif + } +#if BATTLE_TRACE_LEVEL == 0 + }); +#endif + + LOGFL("Evaluation took %d ms", timer.getDiff()); + + auto pscValue = [](const PossibleSpellcast &ps) -> float + { + return ps.value; + }; + auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); + + if(castToPerform.value > cachedScore) + { + LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); + BattleAction spellcast; + spellcast.actionType = EActionType::HERO_SPELL; + spellcast.spell = castToPerform.spell->id; + spellcast.setTarget(castToPerform.dest); + spellcast.side = side; + spellcast.stackNumber = (!side) ? -1 : -2; + cb->battleMakeSpellAction(battleID, spellcast); + activeActionMade = true; + + return true; + } + + LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); + + return false; +} + +//Below method works only for offensive spells +void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) +{ + using ValueMap = PossibleSpellcast::ValueMap; + + RNGStub rngStub; + HypotheticBattle state(env.get(), cb->getBattle(battleID)); + TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false); + + ValueMap healthOfStack; + ValueMap newHealthOfStack; + + for(auto unit : all) + { + healthOfStack[unit->unitId()] = unit->getAvailableHealth(); + } + + spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell); + cast.castEval(state.getServerCallback(), ps.dest); + + for(auto unit : all) + { + auto unitId = unit->unitId(); + auto localUnit = state.battleGetUnitByID(unitId); + newHealthOfStack[unitId] = localUnit->getAvailableHealth(); + } + + int64_t totalGain = 0; + + for(auto unit : all) + { + auto unitId = unit->unitId(); + auto localUnit = state.battleGetUnitByID(unitId); + + auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; + + if(localUnit->unitOwner() != cb->getBattle(battleID)->getPlayerID()) + healthDiff = -healthDiff; + + if(healthDiff < 0) + { + ps.value = -1; + return; //do not damage own units at all + } + + totalGain += healthDiff; + } + + ps.value = totalGain; +} + +void BattleEvaluator::print(const std::string & text) const +{ + logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); +} + + + diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h new file mode 100644 index 000000000..6198d56a4 --- /dev/null +++ b/AI/BattleAI/BattleEvaluator.h @@ -0,0 +1,83 @@ +/* + * BattleEvaluator.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/AI_Base.h" +#include "../../lib/battle/ReachabilityInfo.h" +#include "PossibleSpellcast.h" +#include "PotentialTargets.h" +#include "BattleExchangeVariant.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class EnemyInfo; + +class BattleEvaluator +{ + std::unique_ptr targets; + std::shared_ptr hb; + BattleExchangeEvaluator scoreEvaluator; + std::shared_ptr cb; + std::shared_ptr env; + bool activeActionMade = false; + std::optional cachedAttack; + PlayerColor playerID; + BattleID battleID; + int side; + float cachedScore; + DamageCache damageCache; + float strengthRatio; + +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); + std::vector getBrokenWallMoatHexes() const; + void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only + void print(const std::string & text) const; + + BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + 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; + } + + BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + std::shared_ptr hb, + DamageCache & damageCache, + 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; + } +}; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index d796134cd..dc37412da 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -12,81 +12,143 @@ #include "../../lib/CStack.h" AttackerValue::AttackerValue() + : value(0), + isRetalitated(false) { - value = 0; - isRetalitated = false; } MoveTarget::MoveTarget() - : positions() + : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) { - score = EvaluationResult::INEFFECTIVE_SCORE; + turnsToRich = 1; } -int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state) +float BattleExchangeVariant::trackAttack( + const AttackPossibility & ap, + std::shared_ptr hb, + DamageCache & damageCache) { + 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; auto affectedUnits = ap.affectedUnits; affectedUnits.push_back(ap.attackerState); for(auto affectedUnit : affectedUnits) { - auto unitToUpdate = state.getForUpdate(affectedUnit->unitId()); + auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); - unitToUpdate->health = affectedUnit->health; - unitToUpdate->shots = affectedUnit->shots; - unitToUpdate->counterAttacks = affectedUnit->counterAttacks; - unitToUpdate->movedThisRound = affectedUnit->movedThisRound; - } + if(unitToUpdate->unitSide() == attacker->unitSide()) + { + if(unitToUpdate->unitId() == attacker->unitId()) + { + auto defender = hb->getForUpdate(ap.attack.defender->unitId()); - auto attackValue = ap.attackValue(); + if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate()) + continue; - dpsScore += attackValue; + 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); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, ap attack, %s, dps: %lld, score: %lld", - ap.attack.attacker->getDescription(), - ap.attack.defender->getDescription(), - ap.attack.shooting ? "shot" : "mellee", - ap.damageDealt, - attackValue); + logAi->trace( + "%s -> %s, ap retalitation, %s, dps: %2f, score: %2f", + defender->getDescription(), + unitToUpdate->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + retaliationDamage, + attackerDamageReduce); #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(), + unitToUpdate->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + collateralDamage, + collateralDamageReduce); +#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 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); +#endif + } + } + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg); +#endif + + attackValue += ap.shootersBlockedDmg; + dpsScore.enemyDamageReduce += ap.shootersBlockedDmg; + attacker->afterAttack(ap.attack.shooting, false); return attackValue; } -int64_t BattleExchangeVariant::trackAttack( +float BattleExchangeVariant::trackAttack( std::shared_ptr attacker, std::shared_ptr defender, bool shooting, bool isOurAttack, - const CBattleInfoCallback & cb, + DamageCache & damageCache, + std::shared_ptr hb, bool evaluateOnly) { const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - DamageEstimation retaliation; - // FIXME: provide distance info for Jousting bonus - BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); - - if(shooting) - { - bai.attackerPos.setXY(8, 5); - } - - auto attack = cb.battleEstimateDamage(bai, &retaliation); - int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2; - int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb); - int64_t attackerDamageReduce = 0; + int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb); + float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb); + float attackerDamageReduce = 0; if(!evaluateOnly) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace( - "%s -> %s, normal attack, %s, dps: %lld, %lld", + "%s -> %s, normal attack, %s, dps: %lld, %2f", attacker->getDescription(), defender->getDescription(), shooting ? "shot" : "mellee", @@ -96,49 +158,43 @@ int64_t BattleExchangeVariant::trackAttack( if(isOurAttack) { - dpsScore += defenderDamageReduce; + dpsScore.enemyDamageReduce += defenderDamageReduce; attackerValue[attacker->unitId()].value += defenderDamageReduce; } else - dpsScore -= defenderDamageReduce; + dpsScore.ourDamageReduce += defenderDamageReduce; defender->damage(attackDamage); attacker->afterAttack(shooting, false); } - if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) + if(!evaluateOnly && defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) { - if(retaliation.damage.max != 0) - { - auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2; - attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb); + auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb); + attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb); - if(!evaluateOnly) - { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, retaliation, dps: %lld, %lld", - defender->getDescription(), - attacker->getDescription(), - retaliationDamage, - attackerDamageReduce); + logAi->trace( + "%s -> %s, retaliation, dps: %lld, %2f", + defender->getDescription(), + attacker->getDescription(), + retaliationDamage, + attackerDamageReduce); #endif - if(isOurAttack) - { - dpsScore -= attackerDamageReduce; - attackerValue[attacker->unitId()].isRetalitated = true; - } - else - { - dpsScore += attackerDamageReduce; - attackerValue[defender->unitId()].value += attackerDamageReduce; - } - - attacker->damage(retaliationDamage); - defender->afterAttack(false, true); - } + if(isOurAttack) + { + dpsScore.ourDamageReduce += attackerDamageReduce; + attackerValue[attacker->unitId()].isRetalitated = true; } + else + { + dpsScore.enemyDamageReduce += attackerDamageReduce; + attackerValue[defender->unitId()].value += attackerDamageReduce; + } + + attacker->damage(retaliationDamage); + defender->afterAttack(false, true); } auto score = defenderDamageReduce - attackerDamageReduce; @@ -146,58 +202,92 @@ int64_t BattleExchangeVariant::trackAttack( #if BATTLE_TRACE_LEVEL>=1 if(!score) { - logAi->trace("Attack has zero score d:%lld a:%lld", defenderDamageReduce, attackerDamageReduce); + logAi->trace("Attack has zero score def:%2f att:%2f", defenderDamageReduce, attackerDamageReduce); } #endif return score; } -EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb) +float BattleExchangeEvaluator::scoreValue(const BattleScore & score) const +{ + return score.enemyDamageReduce * getPositiveEffectMultiplier() - score.ourDamageReduce * getNegativeEffectMultiplier(); +} + +EvaluationResult BattleExchangeEvaluator::findBestTarget( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) { EvaluationResult result(targets.bestAction()); - updateReachabilityMap(hb); - - for(auto & ap : targets.possibleAttacks) - { - int64_t score = calculateExchange(ap, targets, hb); - - if(score > result.score) - { - result.score = score; - result.bestAttack = ap; - } - } - - if(!activeStack->waited()) + if(!activeStack->waited() && !activeStack->acquireState()->hadMorale) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Evaluating waited attack for %s", activeStack->getDescription()); #endif - hb.getForUpdate(activeStack->unitId())->waiting = true; - hb.getForUpdate(activeStack->unitId())->waitedThisTurn = true; + auto hbWaited = std::make_shared(env.get(), hb); - updateReachabilityMap(hb); + hbWaited->getForUpdate(activeStack->unitId())->waiting = true; + hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true; + + updateReachabilityMap(hbWaited); for(auto & ap : targets.possibleAttacks) { - int64_t score = calculateExchange(ap, targets, hb); + float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited); if(score > result.score) { result.score = score; result.bestAttack = ap; result.wait = true; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score %2f", result.score); +#endif } } } +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Evaluating normal attack for %s", activeStack->getDescription()); +#endif + + updateReachabilityMap(hb); + + if(result.bestAttack.attack.shooting && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) + { + if(!canBeHitThisTurn(result.bestAttack)) + return result; // lets wait + } + + for(auto & ap : targets.possibleAttacks) + { + float score = evaluateExchange(ap, 0, targets, damageCache, hb); + + if(score > result.score || (score == result.score && result.wait)) + { + result.score = score; + result.bestAttack = ap; + result.wait = false; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score %2f", result.score); +#endif + } + } + return result; } -MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb) +MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) { MoveTarget result; BattleExchangeVariant ev; @@ -232,21 +322,31 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni auto turnsToRich = (distance - 1) / speed + 1; auto hexes = closestStack->getSurroundingHexes(); + auto enemySpeed = closestStack->speed(); + auto speedRatio = speed / static_cast(enemySpeed); + auto multiplier = speedRatio > 1 ? 1 : speedRatio; + + if(enemy->canShoot()) + multiplier *= 1.5f; for(auto hex : hexes) { // FIXME: provide distance info for Jousting bonus auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); - auto attack = AttackPossibility::evaluate(bai, hex, hb); + 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, targets, hb) / turnsToRich; + auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); + auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce); - if(result.score < score) + if(result.scorePerTurn < scoreValue(scorePerTurn)) { - result.score = score; + result.scorePerTurn = scoreValue(scorePerTurn); + result.score = scoreValue(score); result.positions = closestStack->getAttackableHexes(activeStack); + result.cachedAttack = attack; + result.turnsToRich = turnsToRich; } } } @@ -254,7 +354,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni return result; } -std::vector BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) +std::vector BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) const { std::queue queue; std::vector checkedStacks; @@ -284,21 +384,23 @@ std::vector BattleExchangeEvaluator::getAdjacentUnits(cons return checkedStacks; } -std::vector BattleExchangeEvaluator::getExchangeUnits( +ReachabilityData BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, - HypotheticBattle & hb) + std::shared_ptr hb) { - auto hexes = ap.attack.defender->getHexes(); + ReachabilityData result; + + auto hexes = ap.attack.defender->getSurroundingHexes(); if(!ap.attack.shooting) hexes.push_back(ap.from); - std::vector exchangeUnits; std::vector allReachableUnits; for(auto hex : hexes) { - vstd::concatenate(allReachableUnits, reachabilityMap[hex]); + vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap[hex] : getOneTurnReachableUnits(turn, hex)); } vstd::removeDuplicates(allReachableUnits); @@ -308,7 +410,7 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( { for(auto adjacentUnit : getAdjacentUnits(unit)) { - auto unitWithBonuses = hb.battleGetUnitByID(adjacentUnit->unitId()); + auto unitWithBonuses = hb->battleGetUnitByID(adjacentUnit->unitId()); if(vstd::contains(targets.unreachableEnemies, adjacentUnit) && !vstd::contains(allReachableUnits, unitWithBonuses)) @@ -331,7 +433,28 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( logAi->trace("Reachability map contains only %d stacks", allReachableUnits.size()); #endif - return exchangeUnits; + return result; + } + + for(auto unit : allReachableUnits) + { + auto accessible = !unit->canShoot(); + + if(!accessible) + { + for(auto hex : unit->getSurroundingHexes()) + { + if(ap.attack.defender->coversPos(hex)) + { + accessible = true; + } + } + } + + if(accessible) + result.melleeAccessible.push_back(unit); + else + result.shooters.push_back(unit); } for(int turn = 0; turn < turnOrder.size(); turn++) @@ -339,75 +462,114 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( for(auto unit : turnOrder[turn]) { if(vstd::contains(allReachableUnits, unit)) - exchangeUnits.push_back(unit); + result.units.push_back(unit); } } - return exchangeUnits; + vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool + { + return !hb->battleGetUnitByID(u->unitId())->alive(); + }); + + return result; } -int64_t BattleExchangeEvaluator::calculateExchange( +float BattleExchangeEvaluator::evaluateExchange( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, - HypotheticBattle & hb) + DamageCache & damageCache, + std::shared_ptr hb) +{ + if(ap.from.hex == 127) + { + logAi->trace("x"); + } + + BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "calculateExchange score +%2f -%2fx%2f = %2f", + score.enemyDamageReduce, + score.ourDamageReduce, + getNegativeEffectMultiplier(), + scoreValue(score)); +#endif + + return scoreValue(score); +} + +BattleScore BattleExchangeEvaluator::calculateExchange( + const AttackPossibility & ap, + uint8_t turn, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from); + logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); #endif if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE && cb->battleGetGateState() == EGateState::BLOCKED - && ap.attack.defender->coversPos(ESiegeHex::GATE_BRIDGE)) + && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) { - return EvaluationResult::INEFFECTIVE_SCORE; + return BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); } std::vector ourStacks; std::vector enemyStacks; - enemyStacks.push_back(ap.attack.defender); + if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) + enemyStacks.push_back(ap.attack.defender); - std::vector exchangeUnits = getExchangeUnits(ap, targets, hb); + ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); - if(exchangeUnits.empty()) + if(exchangeUnits.units.empty()) { - return 0; + return BattleScore(); } - HypotheticBattle exchangeBattle(env.get(), cb); + auto exchangeBattle = std::make_shared(env.get(), hb); BattleExchangeVariant v; + + for(auto unit : exchangeUnits.units) + { + 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()); +#endif + } + } + auto melleeAttackers = ourStacks; vstd::removeDuplicates(melleeAttackers); vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool { - return !cb->battleCanShoot(u); + return cb->battleCanShoot(u); }); - for(auto unit : exchangeUnits) - { - if(unit->isTurret()) - continue; - - bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true); - auto & attackerQueue = isOur ? ourStacks : enemyStacks; - - - if(!vstd::contains(attackerQueue, unit)) - { - attackerQueue.push_back(unit); - } - } - bool canUseAp = true; - for(auto activeUnit : exchangeUnits) + for(auto activeUnit : exchangeUnits.units) { - bool isOur = cb->battleMatchOwner(ap.attack.attacker, activeUnit, true); + 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 attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); if(!attacker->alive()) { @@ -420,35 +582,46 @@ int64_t BattleExchangeEvaluator::calculateExchange( auto targetUnit = ap.attack.defender; - if(!isOur || !exchangeBattle.getForUpdate(targetUnit->unitId())->alive()) + if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) { - auto estimateAttack = [&](const battle::Unit * u) -> int64_t + auto estimateAttack = [&](const battle::Unit * u) -> float { - auto stackWithBonuses = exchangeBattle.getForUpdate(u->unitId()); + auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto score = v.trackAttack( attacker, stackWithBonuses, - exchangeBattle.battleCanShoot(stackWithBonuses.get()), + exchangeBattle->battleCanShoot(stackWithBonuses.get()), isOur, - *cb, + damageCache, + hb, true); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Best target selector %s->%s score = %lld", attacker->getDescription(), u->getDescription(), score); + logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); #endif return score; }; - if(!oppositeQueue.empty()) + auto unitsInOppositeQueueExceptInaccessible = oppositeQueue; + + vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool + { + return vstd::contains(exchangeUnits.shooters, u); + }); + + if(!unitsInOppositeQueueExceptInaccessible.empty()) { - targetUnit = *vstd::maxElementByFun(oppositeQueue, estimateAttack); + targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); } else { - auto reachable = exchangeBattle.battleGetUnitsIf([&](const battle::Unit * u) -> bool + auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool { - if(!u->alive() || u->unitSide() == attacker->unitSide()) + if(u->unitSide() == attacker->unitSide()) + return false; + + if(!exchangeBattle->getForUpdate(u->unitId())->alive()) return false; return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool @@ -472,19 +645,20 @@ int64_t BattleExchangeEvaluator::calculateExchange( } } - auto defender = exchangeBattle.getForUpdate(targetUnit->unitId()); - auto shooting = cb->battleCanShoot(attacker.get()); + auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); + auto shooting = exchangeBattle->battleCanShoot(attacker.get()); const int totalAttacks = attacker->getTotalAttacks(shooting); - if(canUseAp && activeUnit == ap.attack.attacker && targetUnit == ap.attack.defender) + if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() + && targetUnit->unitId() == ap.attack.defender->unitId()) { - v.trackAttack(ap, exchangeBattle); + v.trackAttack(ap, exchangeBattle, damageCache); } else { for(int i = 0; i < totalAttacks; i++) { - v.trackAttack(attacker, defender, shooting, isOur, exchangeBattle); + v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle); if(!attacker->alive() || !defender->alive()) break; @@ -495,21 +669,31 @@ int64_t BattleExchangeEvaluator::calculateExchange( vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle.getForUpdate(u->unitId())->alive(); + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); }); vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle.getForUpdate(u->unitId())->alive(); + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); }); } // avoid blocking path for stronger stack by weaker stack // the method checks if all stacks can be placed around enemy - v.adjustPositions(melleeAttackers, ap, reachabilityMap); + std::map reachabilityMap; + + auto hexes = ap.attack.defender->getSurroundingHexes(); + + for(auto hex : hexes) + reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); + + if(!ap.attack.shooting) + { + v.adjustPositions(melleeAttackers, ap, reachabilityMap); + } #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Exchange score: %lld", v.getScore()); + logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); #endif return v.getScore(); @@ -533,13 +717,10 @@ void BattleExchangeVariant::adjustPositions( return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value; }); - if(!ap.attack.shooting) - { - vstd::erase_if_present(hexes, ap.from); - vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); - } + vstd::erase_if_present(hexes, ap.from); + vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); - int64_t notRealizedDamage = 0; + float notRealizedDamage = 0; for(auto unit : attackers) { @@ -555,7 +736,7 @@ void BattleExchangeVariant::adjustPositions( continue; } - auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> int64_t + auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float { auto score = vstd::contains(reachabilityMap[h], unit) ? reachabilityMap[h].size() @@ -577,22 +758,58 @@ void BattleExchangeVariant::adjustPositions( if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value) { - dpsScore = EvaluationResult::INEFFECTIVE_SCORE; + dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); } } -void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) +bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) +{ + for(auto pos : ap.attack.attacker->getSurroundingHexes()) + { + for(auto u : reachabilityMap[pos]) + { + if(u->unitSide() != ap.attack.attacker->unitSide()) + { + return true; + } + } + } + + return false; +} + +void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr hb) { const int TURN_DEPTH = 2; turnOrder.clear(); - - hb.battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); - reachabilityMap.clear(); - for(int turn = 0; turn < turnOrder.size(); turn++) + hb->battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); + + for(auto turn : turnOrder) { - auto & turnQueue = turnOrder[turn]; + for(auto u : turn) + { + if(!vstd::contains(reachabilityCache, u->unitId())) + { + reachabilityCache[u->unitId()] = hb->getReachability(u); + } + } + } + + for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) + { + reachabilityMap[hex] = getOneTurnReachableUnits(0, hex); + } +} + +std::vector BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) +{ + std::vector result; + + for(int i = 0; i < turnOrder.size(); i++, turn++) + { + auto & turnQueue = turnOrder[i]; HypotheticBattle turnBattle(env.get(), cb); for(const battle::Unit * unit : turnQueue) @@ -600,46 +817,49 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) if(unit->isTurret()) continue; - auto unitSpeed = unit->speed(turn); - if(turnBattle.battleCanShoot(unit)) { - for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) - { - reachabilityMap[hex].push_back(unit); - } + result.push_back(unit); continue; } - auto unitReachability = turnBattle.getReachability(unit); + auto unitSpeed = unit->speed(turn); + auto radius = unitSpeed * (turn + 1); - for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) - { - bool reachable = unitReachability.distances[hex] <= unitSpeed; - - if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) + ReachabilityInfo unitReachability = vstd::getOrCompute( + reachabilityCache, + unit->unitId(), + [&](ReachabilityInfo & data) { - const battle::Unit * hexStack = cb->battleGetUnitByPos(hex); + data = turnBattle.getReachability(unit); + }); - if(hexStack && cb->battleMatchOwner(unit, hexStack, false)) + bool reachable = unitReachability.distances[hex] <= radius; + + if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) + { + const battle::Unit * hexStack = cb->battleGetUnitByPos(hex); + + if(hexStack && cb->battleMatchOwner(unit, hexStack, false)) + { + for(BattleHex neighbor : hex.neighbouringTiles()) { - for(BattleHex neighbor : hex.neighbouringTiles()) - { - reachable = unitReachability.distances[neighbor] <= unitSpeed; + reachable = unitReachability.distances[neighbor] <= radius; - if(reachable) break; - } + if(reachable) break; } } + } - if(reachable) - { - reachabilityMap[hex].push_back(unit); - } + if(reachable) + { + result.push_back(unit); } } } + + return result; } // avoid blocking path for stronger stack by weaker stack @@ -670,11 +890,12 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage); auto unitReachability = turnBattle.getReachability(unit); + auto unitSpeed = unit->speed(turn); // Cached value, to avoid performance hit for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) { bool enemyUnit = false; - bool reachable = unitReachability.distances[hex] <= unit->speed(turn); + bool reachable = unitReachability.distances[hex] <= unitSpeed; if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) { @@ -686,7 +907,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb for(BattleHex neighbor : hex.neighbouringTiles()) { - reachable = unitReachability.distances[neighbor] <= unit->speed(turn); + reachable = unitReachability.distances[neighbor] <= unitSpeed; if(reachable) break; } diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 2cfba35d0..cca98ae12 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -14,9 +14,37 @@ #include "PotentialTargets.h" #include "StackWithBonuses.h" +struct BattleScore +{ + float ourDamageReduce; + float enemyDamageReduce; + + BattleScore(float enemyDamageReduce, float ourDamageReduce) + :enemyDamageReduce(enemyDamageReduce), ourDamageReduce(ourDamageReduce) + { + } + + BattleScore() : BattleScore(0, 0) {} + + float value() + { + return enemyDamageReduce - ourDamageReduce; + } + + BattleScore operator+(BattleScore & other) + { + BattleScore result = *this; + + result.ourDamageReduce += other.ourDamageReduce; + result.enemyDamageReduce += other.enemyDamageReduce; + + return result; + } +}; + struct AttackerValue { - int64_t value; + float value; bool isRetalitated; BattleHex position; @@ -25,20 +53,23 @@ struct AttackerValue struct MoveTarget { - int64_t score; + float score; + float scorePerTurn; std::vector positions; + std::optional cachedAttack; + uint8_t turnsToRich; MoveTarget(); }; struct EvaluationResult { - static const int64_t INEFFECTIVE_SCORE = -1000000; + static const int64_t INEFFECTIVE_SCORE = -10000; AttackPossibility bestAttack; MoveTarget bestMove; bool wait; - int64_t score; + float score; bool defend; EvaluationResult(const AttackPossibility & ap) @@ -56,19 +87,24 @@ struct EvaluationResult class BattleExchangeVariant { public: - BattleExchangeVariant(): dpsScore(0) {} + BattleExchangeVariant() + : dpsScore() {} - int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state); + float trackAttack( + const AttackPossibility & ap, + std::shared_ptr hb, + DamageCache & damageCache); - int64_t trackAttack( + float trackAttack( std::shared_ptr attacker, std::shared_ptr defender, bool shooting, bool isOurAttack, - const CBattleInfoCallback & cb, + DamageCache & damageCache, + std::shared_ptr hb, bool evaluateOnly = false); - int64_t getScore() const { return dpsScore; } + const BattleScore & getScore() const { return dpsScore; } void adjustPositions( std::vector attackers, @@ -76,26 +112,82 @@ public: std::map & reachabilityMap); private: - int64_t dpsScore; + BattleScore dpsScore; std::map attackerValue; }; +struct ReachabilityData +{ + std::vector units; + + // shooters which are within mellee attack and mellee units + std::vector melleeAccessible; + + // far shooters + std::vector shooters; +}; + class BattleExchangeEvaluator { private: std::shared_ptr cb; std::shared_ptr env; + std::map reachabilityCache; std::map> reachabilityMap; std::vector turnOrder; + float negativeEffectMultiplier; + + float scoreValue(const BattleScore & score) const; + + BattleScore calculateExchange( + const AttackPossibility & ap, + uint8_t turn, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + bool canBeHitThisTurn(const AttackPossibility & ap); public: - BattleExchangeEvaluator(std::shared_ptr cb, std::shared_ptr env): cb(cb), env(env) {} + BattleExchangeEvaluator( + std::shared_ptr cb, + std::shared_ptr env, + float strengthRatio): cb(cb), env(env) { + negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio; + } + + EvaluationResult findBestTarget( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + float evaluateExchange( + const AttackPossibility & ap, + uint8_t turn, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + std::vector getOneTurnReachableUnits(uint8_t turn, BattleHex hex); + void updateReachabilityMap(std::shared_ptr hb); + + ReachabilityData getExchangeUnits( + const AttackPossibility & ap, + uint8_t turn, + PotentialTargets & targets, + std::shared_ptr hb); - EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb); - int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); - void updateReachabilityMap(HypotheticBattle & hb); - std::vector getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); - MoveTarget findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb); - std::vector getAdjacentUnits(const battle::Unit * unit); -}; \ No newline at end of file + + MoveTarget findMoveTowardsUnreachable( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + std::vector getAdjacentUnits(const battle::Unit * unit) const; + + float getPositiveEffectMultiplier() const { return 1; } + float getNegativeEffectMultiplier() const { return negativeEffectMultiplier; } +}; diff --git a/AI/BattleAI/CMakeLists.txt b/AI/BattleAI/CMakeLists.txt index 7feed93e2..335c92f5c 100644 --- a/AI/BattleAI/CMakeLists.txt +++ b/AI/BattleAI/CMakeLists.txt @@ -1,6 +1,7 @@ set(battleAI_SRCS AttackPossibility.cpp BattleAI.cpp + BattleEvaluator.cpp common.cpp EnemyInfo.cpp PossibleSpellcast.cpp @@ -15,6 +16,7 @@ set(battleAI_HEADERS AttackPossibility.h BattleAI.h + BattleEvaluator.h common.h EnemyInfo.h PotentialTargets.h @@ -37,7 +39,11 @@ else() endif() target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET}) +target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb) 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/PossibleSpellcast.h b/AI/BattleAI/PossibleSpellcast.h index a4e0021c7..4581d381d 100644 --- a/AI/BattleAI/PossibleSpellcast.h +++ b/AI/BattleAI/PossibleSpellcast.h @@ -27,7 +27,7 @@ public: const CSpell * spell; spells::Target dest; - int64_t value; + float value; PossibleSpellcast(); virtual ~PossibleSpellcast(); diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index 268350b5d..a341921e6 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -11,11 +11,14 @@ #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove -PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state) +PotentialTargets::PotentialTargets( + const battle::Unit * attacker, + DamageCache & damageCache, + std::shared_ptr state) { - auto attackerInfo = state.battleGetUnitByID(attacker->unitId()); - auto reachability = state.getReachability(attackerInfo); - auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, false); + auto attackerInfo = state->battleGetUnitByID(attacker->unitId()); + auto reachability = state->getReachability(attackerInfo); + auto avHexes = state->battleGetAvailableHexes(reachability, attackerInfo, false); //FIXME: this should part of battleGetAvailableHexes bool forceTarget = false; @@ -25,7 +28,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet if(attackerInfo->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) { forceTarget = true; - auto nearest = state.getNearestStack(attackerInfo); + auto nearest = state->getNearestStack(attackerInfo); if(nearest.first != nullptr) { @@ -34,14 +37,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet } } - auto aliveUnits = state.battleGetUnitsIf([=](const battle::Unit * unit) + auto aliveUnits = state->battleGetUnitsIf([=](const battle::Unit * unit) { return unit->isValidTarget() && unit->unitId() != attackerInfo->unitId(); }); for(auto defender : aliveUnits) { - if(!forceTarget && !state.battleMatchOwner(attackerInfo, defender)) + if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender)) continue; auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility @@ -49,7 +52,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet int distance = hex.isValid() ? reachability.distances[hex] : 0; auto bai = BattleAttackInfo(attackerInfo, defender, distance, shooting); - return AttackPossibility::evaluate(bai, hex, state); + return AttackPossibility::evaluate(bai, hex, damageCache, state); }; if(forceTarget) @@ -59,7 +62,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet else unreachableEnemies.push_back(defender); } - else if(state.battleCanShoot(attackerInfo, defender->getPosition())) + else if(state->battleCanShoot(attackerInfo, defender->getPosition())) { possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID)); } diff --git a/AI/BattleAI/PotentialTargets.h b/AI/BattleAI/PotentialTargets.h index fbb855339..e7b622026 100644 --- a/AI/BattleAI/PotentialTargets.h +++ b/AI/BattleAI/PotentialTargets.h @@ -17,7 +17,10 @@ public: std::vector unreachableEnemies; PotentialTargets(){}; - PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state); + PotentialTargets( + const battle::Unit * attacker, + DamageCache & damageCache, + std::shared_ptr hb); const AttackPossibility & bestAction() const; int64_t bestActionValue() const; diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 5370c3aac..21246b515 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -12,9 +12,10 @@ #include -#include "../../lib/NetPacks.h" #include "../../lib/CStack.h" #include "../../lib/ScriptHandler.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" #if SCRIPTING_ENABLED using scripting::Pool; @@ -45,13 +46,32 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: id(Stack->unitId()), side(Stack->unitSide()), player(Stack->unitOwner()), - slot(Stack->unitSlot()) + slot(Stack->unitSlot()), + treeVersionLocal(0) { localInit(Owner); battle::CUnitState::operator=(*Stack); } +StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack) + : battle::CUnitState(), + origBearer(Stack->getBonusBearer()), + owner(Owner), + type(Stack->unitType()), + baseAmount(Stack->unitBaseAmount()), + id(Stack->unitId()), + side(Stack->unitSide()), + player(Stack->unitOwner()), + slot(Stack->unitSlot()), + treeVersionLocal(0) +{ + localInit(Owner); + + auto state = Stack->acquireState(); + battle::CUnitState::operator=(*state); +} + StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info) : battle::CUnitState(), origBearer(nullptr), @@ -59,7 +79,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: baseAmount(info.count), id(info.id), side(info.side), - slot(SlotID::SUMMONED_SLOT_PLACEHOLDER) + slot(SlotID::SUMMONED_SLOT_PLACEHOLDER), + treeVersionLocal(0) { type = info.type.toCreature(); origBearer = type; @@ -124,7 +145,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c for(const Bonus & bonus : bonusesToUpdate) { - if(selector(&bonus) && (!limit || !limit(&bonus))) + if(selector(&bonus) && (!limit || limit(&bonus))) { if(ret->getFirst(Selector::source(BonusSource::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype)))) { @@ -150,12 +171,18 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c int64_t StackWithBonuses::getTreeVersion() const { - return owner->getTreeVersion(); + auto result = owner->getTreeVersion(); + + if(bonusesToAdd.empty() && bonusesToUpdate.empty() && bonusesToRemove.empty()) + return result; + else + return result + treeVersionLocal; } void StackWithBonuses::addUnitBonus(const std::vector & bonus) { vstd::concatenate(bonusesToAdd, bonus); + treeVersionLocal++; } void StackWithBonuses::updateUnitBonus(const std::vector & bonus) @@ -163,6 +190,7 @@ void StackWithBonuses::updateUnitBonus(const std::vector & bonus) //TODO: optimize, actualize to last value vstd::concatenate(bonusesToUpdate, bonus); + treeVersionLocal++; } void StackWithBonuses::removeUnitBonus(const std::vector & bonus) @@ -197,12 +225,14 @@ void StackWithBonuses::removeUnitBonus(const CSelector & selector) vstd::erase_if(bonusesToAdd, [&](const Bonus & b){return selector(&b);}); vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);}); + + treeVersionLocal++; } std::string StackWithBonuses::getDescription() const { std::ostringstream oss; - oss << unitOwner().getStr(); + oss << unitOwner().toString(); oss << " battle stack [" << unitId() << "]: " << getCount() << " of "; if(type) oss << type->getJsonKey(); @@ -256,7 +286,7 @@ std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) if(iter == stackStates.end()) { - const CStack * s = subject->battleGetStackByID(id, false); + const battle::Unit * s = subject->battleGetUnitByID(id); auto ret = std::make_shared(this, s); stackStates[id] = ret; @@ -268,7 +298,7 @@ std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) } } -battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const +battle::Units HypotheticBattle::getUnitsIf(const battle::UnitFilter & predicate) const { battle::Units proxyed = BattleProxy::getUnitsIf(predicate); @@ -291,12 +321,17 @@ battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const return ret; } +BattleID HypotheticBattle::getBattleID() const +{ + return subject->getBattle()->getBattleID(); +} + int32_t HypotheticBattle::getActiveStackID() const { return activeUnitId; } -void HypotheticBattle::nextRound(int32_t roundNr) +void HypotheticBattle::nextRound() { //TODO:HypotheticBattle::nextRound for(auto unit : battleAliveUnits()) @@ -433,6 +468,24 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at return (damage.min + damage.max) / 2; } +std::vector HypotheticBattle::getUsedSpells(ui8 side) const +{ + // TODO + return {}; +} + +int3 HypotheticBattle::getLocation() const +{ + // TODO + return int3(-1, -1, -1); +} + +bool HypotheticBattle::isCreatureBank() const +{ + // TODO + return false; +} + int64_t HypotheticBattle::getTreeVersion() const { return getBonusBearer()->getTreeVersion() + bonusTreeVersion; @@ -523,8 +576,9 @@ const Services * HypotheticBattle::HypotheticEnvironment::services() const return env->services(); } -const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle() const +const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle(const BattleID & battleID) const { + assert(battleID == owner->getBattleID()); return owner; } diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 87743ba6b..6a50f6f84 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -47,9 +47,12 @@ public: std::vector bonusesToAdd; std::vector bonusesToUpdate; std::set> bonusesToRemove; + int treeVersionLocal; StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack); + StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack); + StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info); virtual ~StackWithBonuses(); @@ -107,11 +110,13 @@ public: std::shared_ptr getForUpdate(uint32_t id); + BattleID getBattleID() const override; + int32_t getActiveStackID() const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; - void nextRound(int32_t roundNr) override; + void nextRound() override; void nextTurn(uint32_t unitId) override; void addUnit(uint32_t id, const JsonNode & data) override; @@ -133,6 +138,9 @@ 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; + int3 getLocation() const override; + bool isCreatureBank() const override; int64_t getTreeVersion() const; @@ -174,7 +182,7 @@ private: HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment); const Services * services() const override; - const BattleCb * battle() const override; + const BattleCb * battle(const BattleID & battleID) const override; const GameCb * game() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; diff --git a/AI/BattleAI/StdInc.cpp b/AI/BattleAI/StdInc.cpp index a284091d2..277dd9af0 100644 --- a/AI/BattleAI/StdInc.cpp +++ b/AI/BattleAI/StdInc.cpp @@ -1,11 +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" +/* + * 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/AI/BattleAI/StdInc.h b/AI/BattleAI/StdInc.h index ba1e8b9ee..310e3d172 100644 --- a/AI/BattleAI/StdInc.h +++ b/AI/BattleAI/StdInc.h @@ -1,17 +1,17 @@ -/* - * 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" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +/* + * 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" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/BattleAI/main.cpp b/AI/BattleAI/main.cpp index 349a205a6..101491d93 100644 --- a/AI/BattleAI/main.cpp +++ b/AI/BattleAI/main.cpp @@ -1,33 +1,33 @@ -/* - * main.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 "../../lib/AI_Base.h" -#include "BattleAI.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -static const char *g_cszAiName = "Battle AI"; - -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "../../lib/AI_Base.h" +#include "BattleAI.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +static const char *g_cszAiName = "Battle AI"; + +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 60c13382c..2812b7140 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -1,75 +1,82 @@ -/* - * CEmptyAI.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 "CEmptyAI.h" - -#include "../../lib/CRandomGenerator.h" -#include "../../lib/CStack.h" - -void CEmptyAI::saveGame(BinarySerializer & h, const int version) -{ -} - -void CEmptyAI::loadGame(BinaryDeserializer & h, const int version) -{ -} - -void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - cb = CB; - env = ENV; - human=false; - playerID = *cb->getMyColor(); -} - -void CEmptyAI::yourTurn() -{ - cb->endTurn(); -} - -void CEmptyAI::activeStack(const CStack * stack) -{ - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); -} - -void CEmptyAI::yourTacticPhase(int distance) -{ - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); -} - -void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) -{ - cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); -} - -void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); -} - -void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) -{ - cb->selectionMade(0, askID); -} - -void CEmptyAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ - cb->selectionMade(0, askID); -} - -void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) -{ - cb->selectionMade(0, queryID); -} - -void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - cb->selectionMade(0, askID); -} +/* + * CEmptyAI.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 "CEmptyAI.h" + +#include "../../lib/CRandomGenerator.h" +#include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" + +void CEmptyAI::saveGame(BinarySerializer & h, const int version) +{ +} + +void CEmptyAI::loadGame(BinaryDeserializer & h, const int version) +{ +} + +void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + cb = CB; + env = ENV; + human=false; + playerID = *cb->getPlayerID(); +} + +void CEmptyAI::yourTurn(QueryID queryID) +{ + cb->selectionMade(0, queryID); + cb->endTurn(); +} + +void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); +} + +void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); +} + +void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) +{ + cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); +} + +void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); +} + +void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) +{ + cb->selectionMade(0, askID); +} + +void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + cb->selectionMade(0, askID); +} + +void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) +{ + cb->selectionMade(0, queryID); +} + +void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + cb->selectionMade(0, askID); +} + +std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 2598f5dbe..a2f125fbe 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -1,37 +1,38 @@ -/* - * CEmptyAI.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/AI_Base.h" -#include "../../CCallback.h" - -struct HeroMoveDetails; - -class CEmptyAI : public CGlobalAI -{ - std::shared_ptr cb; - -public: - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; - - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn() override; - void yourTacticPhase(int distance) override; - void activeStack(const CStack * stack) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; - void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; - void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; - void showTeleportDialog(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; -}; - -#define NAME "EmptyAI 0.1" +/* + * CEmptyAI.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/AI_Base.h" +#include "../../CCallback.h" + +struct HeroMoveDetails; + +class CEmptyAI : public CGlobalAI +{ + std::shared_ptr cb; + +public: + virtual void saveGame(BinarySerializer & h, const int version) override; + virtual void loadGame(BinaryDeserializer & h, const int version) 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; + void activeStack(const BattleID & battleID, const CStack * stack) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; + void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; + 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; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; +}; + +#define NAME "EmptyAI 0.1" diff --git a/AI/EmptyAI/StdInc.cpp b/AI/EmptyAI/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/AI/EmptyAI/StdInc.cpp +++ b/AI/EmptyAI/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/AI/EmptyAI/StdInc.h b/AI/EmptyAI/StdInc.h index 02b2c08f3..020481377 100644 --- a/AI/EmptyAI/StdInc.h +++ b/AI/EmptyAI/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/EmptyAI/main.cpp b/AI/EmptyAI/main.cpp index 931033075..e6ae9483c 100644 --- a/AI/EmptyAI/main.cpp +++ b/AI/EmptyAI/main.cpp @@ -1,28 +1,28 @@ -/* - * main.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 "CEmptyAI.h" - -std::set ais; -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy(name,NAME); -} - -extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "CEmptyAI.h" + +std::set ais; +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy(name,NAME); +} + +extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/GeniusAI.brain b/AI/GeniusAI.brain index 5dc5d3d7b..8377ade3e 100644 --- a/AI/GeniusAI.brain +++ b/AI/GeniusAI.brain @@ -1,736 +1,736 @@ -o 34 16 17 -R -o 34 16 17 -R -o 47 16 -R -o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24 -R -o 98 16 17 -R -o 98 16 17 -R -o 100 16 -R -o 38 16 -R -o 61 16 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 28 16 -R -o 81 16 -R -o 83 25 -R -o 31 16 -R -o 57 24 -R -o 23 16 -R -o 102 16 -R -o 37 24 -R -o 51 16 -R -t 0 0 25 -R -t 0 1 25 -R -t 0 2 25 -R -t 0 3 25 -R -t 0 4 25 -R -t 0 5 25 -R -t 0 6 25 -R -t 0 7 25 -R -t 0 8 25 -R -t 0 9 25 -R -t 0 10 25 -R -t 0 11 25 -R -t 0 12 25 -R -t 0 13 25 -R -t 0 14 25 -R -t 0 15 25 -R -t 0 16 25 -R -t 0 17 25 -R -t 0 18 25 -R -t 0 19 25 -R -t 0 20 25 -R -t 0 21 25 -R -t 0 22 25 -R -t 0 23 25 -R -t 0 30 25 -R -t 0 31 25 -R -t 0 32 25 -R -t 0 33 25 -R -t 0 34 25 -R -t 0 35 25 -R -t 0 36 25 -R -t 0 37 25 -R -t 0 38 25 -R -t 0 39 25 -R -t 0 40 25 -R -t 0 41 25 -R -t 0 42 25 -R -t 0 43 25 -R -t 1 0 25 -R -t 1 1 25 -R -t 1 2 25 -R -t 1 3 25 -R -t 1 4 25 -R -t 1 5 25 -R -t 1 6 25 -R -t 1 7 25 -R -t 1 8 25 -R -t 1 9 25 -R -t 1 10 25 -R -t 1 11 25 -R -t 1 12 25 -R -t 1 13 25 -R -t 1 14 25 -R -t 1 15 25 -R -t 1 16 25 -R -t 1 17 25 -R -t 1 18 25 -R -t 1 19 25 -R -t 1 20 25 -R -t 1 21 25 -R -t 1 22 25 -R -t 1 23 25 -R -t 1 30 25 -R -t 1 31 25 -R -t 1 32 25 -R -t 1 33 25 -R -t 1 34 25 -R -t 1 35 25 -R -t 1 36 25 -R -t 1 37 25 -R -t 1 38 25 -R -t 1 39 25 -R -t 1 40 25 -R -t 1 41 25 -R -t 1 42 25 -R -t 1 43 25 -R -t 2 0 25 -R -t 2 1 25 -R -t 2 2 25 -R -t 2 3 25 -R -t 2 4 25 -R -t 2 5 25 -R -t 2 6 25 -R -t 2 7 25 -R -t 2 8 25 -R -t 2 9 25 -R -t 2 10 25 -R -t 2 11 25 -R -t 2 12 25 -R -t 2 13 25 -R -t 2 14 25 -R -t 2 15 25 -R -t 2 16 25 -R -t 2 17 25 -R -t 2 18 25 -R -t 2 19 25 -R -t 2 20 25 -R -t 2 21 25 -R -t 2 22 25 -R -t 2 23 25 -R -t 2 30 25 -R -t 2 31 25 -R -t 2 32 25 -R -t 2 33 25 -R -t 2 34 25 -R -t 2 35 25 -R -t 2 36 25 -R -t 2 37 25 -R -t 2 38 25 -R -t 2 39 25 -R -t 2 40 25 -R -t 2 41 25 -R -t 2 42 25 -R -t 2 43 25 -R -t 3 0 25 -R -t 3 1 25 -R -t 3 2 25 -R -t 3 3 25 -R -t 3 4 25 -R -t 3 5 25 -R -t 3 6 25 -R -t 3 7 25 -R -t 3 8 25 -R -t 3 9 25 -R -t 3 10 25 -R -t 3 11 25 -R -t 3 12 25 -R -t 3 13 25 -R -t 3 14 25 -R -t 3 15 25 -R -t 3 16 25 -R -t 3 17 25 -R -t 3 18 25 -R -t 3 19 25 -R -t 3 20 25 -R -t 3 21 25 -R -t 3 22 25 -R -t 3 23 25 -R -t 3 30 25 -R -t 3 31 25 -R -t 3 32 25 -R -t 3 33 25 -R -t 3 34 25 -R -t 3 35 25 -R -t 3 36 25 -R -t 3 37 25 -R -t 3 38 25 -R -t 3 39 25 -R -t 3 40 25 -R -t 3 41 25 -R -t 3 42 25 -R -t 3 43 25 -R -t 4 0 25 -R -t 4 1 25 -R -t 4 2 25 -R -t 4 3 25 -R -t 4 4 25 -R -t 4 5 25 -R -t 4 6 25 -R -t 4 7 25 -R -t 4 8 25 -R -t 4 9 25 -R -t 4 10 25 -R -t 4 11 25 -R -t 4 12 25 -R -t 4 13 25 -R -t 4 14 25 -R -t 4 15 25 -R -t 4 16 25 -R -t 4 17 25 -R -t 4 18 25 -R -t 4 19 25 -R -t 4 20 25 -R -t 4 21 25 -R -t 4 22 25 -R -t 4 23 25 -R -t 4 30 25 -R -t 4 31 25 -R -t 4 32 25 -R -t 4 33 25 -R -t 4 34 25 -R -t 4 35 25 -R -t 4 36 25 -R -t 4 37 25 -R -t 4 38 25 -R -t 4 39 25 -R -t 4 40 25 -R -t 4 41 25 -R -t 4 42 25 -R -t 4 43 25 -R -t 5 0 25 -R -t 5 1 25 -R -t 5 2 25 -R -t 5 3 25 -R -t 5 4 25 -R -t 5 5 25 -R -t 5 6 25 -R -t 5 7 25 -R -t 5 8 25 -R -t 5 9 25 -R -t 5 10 25 -R -t 5 11 25 -R -t 5 12 25 -R -t 5 13 25 -R -t 5 14 25 -R -t 5 15 25 -R -t 5 16 25 -R -t 5 17 25 -R -t 5 18 25 -R -t 5 19 25 -R -t 5 20 25 -R -t 5 21 25 -R -t 5 22 25 -R -t 5 23 25 -R -t 5 30 25 -R -t 5 31 25 -R -t 5 32 25 -R -t 5 33 25 -R -t 5 34 25 -R -t 5 35 25 -R -t 5 36 25 -R -t 5 37 25 -R -t 5 38 25 -R -t 5 39 25 -R -t 5 40 25 -R -t 5 41 25 -R -t 5 42 25 -R -t 5 43 25 -R -t 6 0 25 -R -t 6 1 25 -R -t 6 2 25 -R -t 6 3 25 -R -t 6 4 25 -R -t 6 5 25 -R -t 6 6 25 -R -t 6 7 25 -R -t 6 8 25 -R -t 6 9 25 -R -t 6 10 25 -R -t 6 11 25 -R -t 6 12 25 -R -t 6 13 25 -R -t 6 14 25 -R -t 6 15 25 -R -t 6 16 25 -R -t 6 17 25 -R -t 6 18 25 -R -t 6 19 25 -R -t 6 20 25 -R -t 6 21 25 -R -t 6 22 25 -R -t 6 23 25 -R -t 6 30 25 -R -t 6 31 25 -R -t 6 32 25 -R -t 6 33 25 -R -t 6 34 25 -R -t 6 35 25 -R -t 6 36 25 -R -t 6 37 25 -R -t 6 38 25 -R -t 6 39 25 -R -t 6 40 25 -R -t 6 41 25 -R -t 6 42 25 -R -t 6 43 25 -R -t 7 0 25 -R -t 7 1 25 -R -t 7 2 25 -R -t 7 3 25 -R -t 7 4 25 -R -t 7 5 25 -R -t 7 6 25 -R -t 7 7 25 -R -t 7 8 25 -R -t 7 9 25 -R -t 7 10 25 -R -t 7 11 25 -R -t 7 12 25 -R -t 7 13 25 -R -t 7 14 25 -R -t 7 15 25 -R -t 7 16 25 -R -t 7 17 25 -R -t 7 18 25 -R -t 7 19 25 -R -t 7 20 25 -R -t 7 21 25 -R -t 7 22 25 -R -t 7 23 25 -R -t 7 30 25 -R -t 7 31 25 -R -t 7 32 25 -R -t 7 33 25 -R -t 7 34 25 -R -t 7 35 25 -R -t 7 36 25 -R -t 7 37 25 -R -t 7 38 25 -R -t 7 39 25 -R -t 7 40 25 -R -t 7 41 25 -R -t 7 42 25 -R -t 7 43 25 -R -t 8 0 25 -R -t 8 1 25 -R -t 8 2 25 -R -t 8 3 25 -R -t 8 4 25 -R -t 8 5 25 -R -t 8 6 25 -R -t 8 7 25 -R -t 8 8 25 -R -t 8 9 25 -R -t 8 10 25 -R -t 8 11 25 -R -t 8 12 25 -R -t 8 13 25 -R -t 8 14 25 -R -t 8 15 25 -R -t 8 16 25 -R -t 8 17 25 -R -t 8 18 25 -R -t 8 19 25 -R -t 8 20 25 -R -t 8 21 25 -R -t 8 22 25 -R -t 8 23 25 -R -t 8 30 25 -R -t 8 31 25 -R -t 8 32 25 -R -t 8 33 25 -R -t 8 34 25 -R -t 8 35 25 -R -t 8 36 25 -R -t 8 37 25 -R -t 8 38 25 -R -t 8 39 25 -R -t 8 40 25 -R -t 8 41 25 -R -t 8 42 25 -R -t 8 43 25 -R +o 34 16 17 +R +o 34 16 17 +R +o 47 16 +R +o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24 +R +o 98 16 17 +R +o 98 16 17 +R +o 100 16 +R +o 38 16 +R +o 61 16 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 28 16 +R +o 81 16 +R +o 83 25 +R +o 31 16 +R +o 57 24 +R +o 23 16 +R +o 102 16 +R +o 37 24 +R +o 51 16 +R +t 0 0 25 +R +t 0 1 25 +R +t 0 2 25 +R +t 0 3 25 +R +t 0 4 25 +R +t 0 5 25 +R +t 0 6 25 +R +t 0 7 25 +R +t 0 8 25 +R +t 0 9 25 +R +t 0 10 25 +R +t 0 11 25 +R +t 0 12 25 +R +t 0 13 25 +R +t 0 14 25 +R +t 0 15 25 +R +t 0 16 25 +R +t 0 17 25 +R +t 0 18 25 +R +t 0 19 25 +R +t 0 20 25 +R +t 0 21 25 +R +t 0 22 25 +R +t 0 23 25 +R +t 0 30 25 +R +t 0 31 25 +R +t 0 32 25 +R +t 0 33 25 +R +t 0 34 25 +R +t 0 35 25 +R +t 0 36 25 +R +t 0 37 25 +R +t 0 38 25 +R +t 0 39 25 +R +t 0 40 25 +R +t 0 41 25 +R +t 0 42 25 +R +t 0 43 25 +R +t 1 0 25 +R +t 1 1 25 +R +t 1 2 25 +R +t 1 3 25 +R +t 1 4 25 +R +t 1 5 25 +R +t 1 6 25 +R +t 1 7 25 +R +t 1 8 25 +R +t 1 9 25 +R +t 1 10 25 +R +t 1 11 25 +R +t 1 12 25 +R +t 1 13 25 +R +t 1 14 25 +R +t 1 15 25 +R +t 1 16 25 +R +t 1 17 25 +R +t 1 18 25 +R +t 1 19 25 +R +t 1 20 25 +R +t 1 21 25 +R +t 1 22 25 +R +t 1 23 25 +R +t 1 30 25 +R +t 1 31 25 +R +t 1 32 25 +R +t 1 33 25 +R +t 1 34 25 +R +t 1 35 25 +R +t 1 36 25 +R +t 1 37 25 +R +t 1 38 25 +R +t 1 39 25 +R +t 1 40 25 +R +t 1 41 25 +R +t 1 42 25 +R +t 1 43 25 +R +t 2 0 25 +R +t 2 1 25 +R +t 2 2 25 +R +t 2 3 25 +R +t 2 4 25 +R +t 2 5 25 +R +t 2 6 25 +R +t 2 7 25 +R +t 2 8 25 +R +t 2 9 25 +R +t 2 10 25 +R +t 2 11 25 +R +t 2 12 25 +R +t 2 13 25 +R +t 2 14 25 +R +t 2 15 25 +R +t 2 16 25 +R +t 2 17 25 +R +t 2 18 25 +R +t 2 19 25 +R +t 2 20 25 +R +t 2 21 25 +R +t 2 22 25 +R +t 2 23 25 +R +t 2 30 25 +R +t 2 31 25 +R +t 2 32 25 +R +t 2 33 25 +R +t 2 34 25 +R +t 2 35 25 +R +t 2 36 25 +R +t 2 37 25 +R +t 2 38 25 +R +t 2 39 25 +R +t 2 40 25 +R +t 2 41 25 +R +t 2 42 25 +R +t 2 43 25 +R +t 3 0 25 +R +t 3 1 25 +R +t 3 2 25 +R +t 3 3 25 +R +t 3 4 25 +R +t 3 5 25 +R +t 3 6 25 +R +t 3 7 25 +R +t 3 8 25 +R +t 3 9 25 +R +t 3 10 25 +R +t 3 11 25 +R +t 3 12 25 +R +t 3 13 25 +R +t 3 14 25 +R +t 3 15 25 +R +t 3 16 25 +R +t 3 17 25 +R +t 3 18 25 +R +t 3 19 25 +R +t 3 20 25 +R +t 3 21 25 +R +t 3 22 25 +R +t 3 23 25 +R +t 3 30 25 +R +t 3 31 25 +R +t 3 32 25 +R +t 3 33 25 +R +t 3 34 25 +R +t 3 35 25 +R +t 3 36 25 +R +t 3 37 25 +R +t 3 38 25 +R +t 3 39 25 +R +t 3 40 25 +R +t 3 41 25 +R +t 3 42 25 +R +t 3 43 25 +R +t 4 0 25 +R +t 4 1 25 +R +t 4 2 25 +R +t 4 3 25 +R +t 4 4 25 +R +t 4 5 25 +R +t 4 6 25 +R +t 4 7 25 +R +t 4 8 25 +R +t 4 9 25 +R +t 4 10 25 +R +t 4 11 25 +R +t 4 12 25 +R +t 4 13 25 +R +t 4 14 25 +R +t 4 15 25 +R +t 4 16 25 +R +t 4 17 25 +R +t 4 18 25 +R +t 4 19 25 +R +t 4 20 25 +R +t 4 21 25 +R +t 4 22 25 +R +t 4 23 25 +R +t 4 30 25 +R +t 4 31 25 +R +t 4 32 25 +R +t 4 33 25 +R +t 4 34 25 +R +t 4 35 25 +R +t 4 36 25 +R +t 4 37 25 +R +t 4 38 25 +R +t 4 39 25 +R +t 4 40 25 +R +t 4 41 25 +R +t 4 42 25 +R +t 4 43 25 +R +t 5 0 25 +R +t 5 1 25 +R +t 5 2 25 +R +t 5 3 25 +R +t 5 4 25 +R +t 5 5 25 +R +t 5 6 25 +R +t 5 7 25 +R +t 5 8 25 +R +t 5 9 25 +R +t 5 10 25 +R +t 5 11 25 +R +t 5 12 25 +R +t 5 13 25 +R +t 5 14 25 +R +t 5 15 25 +R +t 5 16 25 +R +t 5 17 25 +R +t 5 18 25 +R +t 5 19 25 +R +t 5 20 25 +R +t 5 21 25 +R +t 5 22 25 +R +t 5 23 25 +R +t 5 30 25 +R +t 5 31 25 +R +t 5 32 25 +R +t 5 33 25 +R +t 5 34 25 +R +t 5 35 25 +R +t 5 36 25 +R +t 5 37 25 +R +t 5 38 25 +R +t 5 39 25 +R +t 5 40 25 +R +t 5 41 25 +R +t 5 42 25 +R +t 5 43 25 +R +t 6 0 25 +R +t 6 1 25 +R +t 6 2 25 +R +t 6 3 25 +R +t 6 4 25 +R +t 6 5 25 +R +t 6 6 25 +R +t 6 7 25 +R +t 6 8 25 +R +t 6 9 25 +R +t 6 10 25 +R +t 6 11 25 +R +t 6 12 25 +R +t 6 13 25 +R +t 6 14 25 +R +t 6 15 25 +R +t 6 16 25 +R +t 6 17 25 +R +t 6 18 25 +R +t 6 19 25 +R +t 6 20 25 +R +t 6 21 25 +R +t 6 22 25 +R +t 6 23 25 +R +t 6 30 25 +R +t 6 31 25 +R +t 6 32 25 +R +t 6 33 25 +R +t 6 34 25 +R +t 6 35 25 +R +t 6 36 25 +R +t 6 37 25 +R +t 6 38 25 +R +t 6 39 25 +R +t 6 40 25 +R +t 6 41 25 +R +t 6 42 25 +R +t 6 43 25 +R +t 7 0 25 +R +t 7 1 25 +R +t 7 2 25 +R +t 7 3 25 +R +t 7 4 25 +R +t 7 5 25 +R +t 7 6 25 +R +t 7 7 25 +R +t 7 8 25 +R +t 7 9 25 +R +t 7 10 25 +R +t 7 11 25 +R +t 7 12 25 +R +t 7 13 25 +R +t 7 14 25 +R +t 7 15 25 +R +t 7 16 25 +R +t 7 17 25 +R +t 7 18 25 +R +t 7 19 25 +R +t 7 20 25 +R +t 7 21 25 +R +t 7 22 25 +R +t 7 23 25 +R +t 7 30 25 +R +t 7 31 25 +R +t 7 32 25 +R +t 7 33 25 +R +t 7 34 25 +R +t 7 35 25 +R +t 7 36 25 +R +t 7 37 25 +R +t 7 38 25 +R +t 7 39 25 +R +t 7 40 25 +R +t 7 41 25 +R +t 7 42 25 +R +t 7 43 25 +R +t 8 0 25 +R +t 8 1 25 +R +t 8 2 25 +R +t 8 3 25 +R +t 8 4 25 +R +t 8 5 25 +R +t 8 6 25 +R +t 8 7 25 +R +t 8 8 25 +R +t 8 9 25 +R +t 8 10 25 +R +t 8 11 25 +R +t 8 12 25 +R +t 8 13 25 +R +t 8 14 25 +R +t 8 15 25 +R +t 8 16 25 +R +t 8 17 25 +R +t 8 18 25 +R +t 8 19 25 +R +t 8 20 25 +R +t 8 21 25 +R +t 8 22 25 +R +t 8 23 25 +R +t 8 30 25 +R +t 8 31 25 +R +t 8 32 25 +R +t 8 33 25 +R +t 8 34 25 +R +t 8 35 25 +R +t 8 36 25 +R +t 8 37 25 +R +t 8 38 25 +R +t 8 39 25 +R +t 8 40 25 +R +t 8 41 25 +R +t 8 42 25 +R +t 8 43 25 +R diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 8be0aa310..c57a0fb34 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -9,18 +9,24 @@ */ #include "StdInc.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.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/gameState/CGameState.h" -#include "../../lib/NetPacks.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 "AIGateway.h" #include "Goals/Goals.h" @@ -34,26 +40,26 @@ 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 -boost::thread_specific_ptr cb; -boost::thread_specific_ptr ai; +thread_local CCallback * cb = nullptr; +thread_local AIGateway * ai = nullptr; //helper RAII to manage global ai/cb ptrs struct SetGlobalState { SetGlobalState(AIGateway * AI) { - assert(!ai.get()); - assert(!cb.get()); + assert(!ai); + assert(!cb); - ai.reset(AI); - cb.reset(AI->myCb.get()); + ai = AI; + cb = AI->myCb.get(); } ~SetGlobalState() { //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully //TODO: to ensure that, make rm unique_ptr - ai.release(); - cb.release(); + ai = nullptr; + cb = nullptr; } }; @@ -153,10 +159,13 @@ void AIGateway::artifactAssembled(const ArtifactLocation & al) NET_EVENT_HANDLER; } -void AIGateway::showTavernWindow(const CGObjectInstance * townOrTavern) +void AIGateway::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "TavernWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::showThievesGuildWindow(const CGObjectInstance * obj) @@ -192,7 +201,7 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic { LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); NET_EVENT_HANDLER; - logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost")); + logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); // some whitespace to flush stream logAi->debug(std::string(200, ' ')); @@ -201,12 +210,12 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic { if(victoryLossCheckResult.victory()) { - logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.getStr()); + logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.toString()); logAi->debug("Turn nr %d", myCb->getDate()); } else { - logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr()); + logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); } // some whitespace to flush stream @@ -314,16 +323,23 @@ void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID her }); } -void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) +void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) { - LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val); + LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); NET_EVENT_HANDLER; } -void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) +void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "level '%i'", level); NET_EVENT_HANDLER; + + status.addQuery(queryID, "RecruitmentDialog"); + + requestActionASAP([=](){ + recruitCreatures(dwelling, dst); + answerQuery(queryID, 0); + }); } void AIGateway::heroMovePointsChanged(const CGHeroInstance * hero) @@ -348,7 +364,7 @@ void AIGateway::newObject(const CGObjectInstance * obj) //to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight //see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes -void AIGateway::objectRemoved(const CGObjectInstance * obj) +void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) { LOG_TRACE(logAi); NET_EVENT_HANDLER; @@ -387,7 +403,7 @@ void AIGateway::heroCreated(const CGHeroInstance * h) NET_EVENT_HANDLER; } -void AIGateway::advmapSpellCast(const CGHeroInstance * caster, int spellID) +void AIGateway::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) { LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); NET_EVENT_HANDLER; @@ -405,14 +421,14 @@ void AIGateway::requestRealized(PackageApplied * pa) NET_EVENT_HANDLER; if(status.haveTurn()) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { if(pa->result) status.madeTurn(); } } - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } @@ -424,10 +440,13 @@ void AIGateway::receivedResource() NET_EVENT_HANDLER; } -void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) +void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "UniversityWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::heroManaPointsChanged(const CGHeroInstance * hero) @@ -462,7 +481,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop) NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { - auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val); + auto relations = myCb->getPlayerRelations(playerID, sop->identifier.as()); auto obj = myCb->getObj(sop->id, false); if(!nullkiller) // crash protection @@ -497,10 +516,13 @@ void AIGateway::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonu NET_EVENT_HANDLER; } -void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) +void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "MarketWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) @@ -510,7 +532,7 @@ void AIGateway::showWorldViewEx(const std::vector & objectPositio NET_EVENT_HANDLER; } -std::optional AIGateway::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional AIGateway::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { LOG_TRACE(logAi); NET_EVENT_HANDLER; @@ -535,7 +557,7 @@ void AIGateway::initGameInterface(std::shared_ptr env, std::shared_ cbc = CB; NET_EVENT_HANDLER; - playerID = *myCb->getMyColor(); + playerID = *myCb->getPlayerID(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; @@ -544,15 +566,17 @@ void AIGateway::initGameInterface(std::shared_ptr env, std::shared_ retrieveVisitableObjs(); } -void AIGateway::yourTurn() +void AIGateway::yourTurn(QueryID queryID) { - LOG_TRACE(logAi); + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; + status.addQuery(queryID, "YourTurn"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); status.startedTurn(); makingTurn = std::make_unique(&AIGateway::makeTurn, this); } -void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) +void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; @@ -640,7 +664,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectorheroManager->getHeroRole(hero) != HeroRole::MAIN || nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE)) { @@ -651,7 +675,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectornodeName() : "NONE"; - std::string s2 = down ? down->nodeName() : "NONE"; + std::string s1 = up->nodeName(); + std::string s2 = down->nodeName(); status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); @@ -708,7 +732,9 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan requestActionASAP([=]() { if(removableUnits && up->tempOwner == down->tempOwner) + { pickBestCreatures(down, up); + } answerQuery(queryID, 0); }); @@ -757,7 +783,7 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj) { UpgradeInfo ui; myCb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID >= 0 && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count)) + if(ui.oldID != CreatureID::NONE && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count)) { myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]); upgraded = true; @@ -773,8 +799,8 @@ void AIGateway::makeTurn() { MAKING_TURN; - auto day = cb->getDate(Date::EDateType::DAY); - logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); + 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); setThreadName("AIGateway::makeTurn"); @@ -828,9 +854,6 @@ 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()); switch(obj->ID) { - case Obj::CREATURE_GENERATOR1: - recruitCreatures(dynamic_cast(obj), h.get()); - break; case Obj::TOWN: if(h->visitedTown) //we are inside, not just attacking { @@ -973,21 +996,21 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto p : h->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); } } for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); } for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero @@ -999,13 +1022,13 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto location : allArtifacts) { - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult continue; - auto s = location.getSlot(); + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; @@ -1016,9 +1039,9 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance bool emptySlotFound = false; for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { + ArtifactLocation destLocation(target->id, slot); cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; @@ -1032,11 +1055,11 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { - ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } @@ -1078,26 +1101,26 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re } } -void AIGateway::battleStart(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, bool side, bool replayAllowed) { NET_EVENT_HANDLER; - assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); + assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); + CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); } -void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) +void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); - bool won = br->winner == myCb->battleGetMySide(); - logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); + bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); + logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); - if (queryID != -1) + if (queryID != QueryID::NONE) { status.addQuery(queryID, "Combat result dialog"); const int confirmAction = 0; @@ -1106,7 +1129,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) answerQuery(queryID, confirmAction); }); } - CAdventureAI::battleEnd(br, queryID); + CAdventureAI::battleEnd(battleID, br, queryID); } void AIGateway::waitTillFree() @@ -1396,7 +1419,7 @@ 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, g.resID, toGive); + 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()); } @@ -1419,7 +1442,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade void AIGateway::endTurn() { - logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr()); + logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); if(!status.haveTurn()) { logAi->error("Not having turn at the end of turn???"); @@ -1439,7 +1462,7 @@ void AIGateway::endTurn() } while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over - logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr()); + logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); } void AIGateway::buildArmyIn(const CGTownInstance * t) @@ -1472,6 +1495,8 @@ void AIGateway::requestActionASAP(std::function whatToDo) boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); + + newThread.detach(); } void AIGateway::lostHero(HeroPtr h) @@ -1605,7 +1630,7 @@ void AIStatus::waitTillFree() { boost::unique_lock lock(mx); while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) - cv.timed_wait(lock, boost::posix_time::milliseconds(10)); + cv.wait_for(lock, boost::chrono::milliseconds(10)); } bool AIStatus::haveTurn() diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 3a9c23b31..63d7b1a44 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -111,13 +111,13 @@ public: std::string getBattleAIName() const override; void initGameInterface(std::shared_ptr env, std::shared_ptr CB) override; - void yourTurn() override; + void yourTurn(QueryID queryID) override; - void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. 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(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + 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, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading @@ -130,7 +130,7 @@ public: void tileHidden(const std::unordered_set & pos) override; void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; void artifactAssembled(const ArtifactLocation & al) override; - void showTavernWindow(const CGObjectInstance * townOrTavern) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; void showThievesGuildWindow(const CGObjectInstance * obj) override; void playerBlocked(int reason, bool start) override; void showPuzzleMap() override; @@ -144,20 +144,20 @@ public: void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; - void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void newObject(const CGObjectInstance * obj) override; void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; void playerBonusChanged(const Bonus & bonus, bool gain) override; void heroCreated(const CGHeroInstance *) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; - void objectRemoved(const CGObjectInstance * obj) override; - void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void battleResultsApplied() override; @@ -165,12 +165,12 @@ public: void objectPropertyChanged(const SetObjectProperty * sop) override; void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; - void battleEnd(const BattleResult * br, QueryID queryID) 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 battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index e6962673e..501d882c8 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -25,10 +25,6 @@ namespace NKAI { -extern boost::thread_specific_ptr ai; - -//extern static const int3 dirs[8]; - const CGObjectInstance * ObjectIdRef::operator->() const { return cb->getObj(id, false); @@ -245,7 +241,7 @@ bool isObjectPassable(const CGObjectInstance * obj) } // Pathfinder internal helper -bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations) +bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations) { if((obj->ID == Obj::GARRISON || obj->ID == Obj::GARRISON2) && objectRelations != PlayerRelations::ENEMIES) @@ -278,7 +274,7 @@ creInfo infoFromDC(const dwellingContent & dc) creInfo ci; ci.count = dc.first; ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed - if (ci.creID != -1) + if (ci.creID != CreatureID::NONE) { ci.cre = VLC->creatures()->getById(ci.creID); ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 60fef0f24..646616f1a 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -57,6 +57,7 @@ using dwellingContent = std::pair>; namespace NKAI { struct creInfo; +class AIGateway; class Nullkiller; const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; @@ -67,7 +68,8 @@ const int ALLOWED_ROAMING_HEROES = 8; extern const float SAFE_ATTACK_CONSTANT; extern const int GOLD_RESERVE; -extern boost::thread_specific_ptr cb; +extern thread_local CCallback * cb; +extern thread_local AIGateway * ai; enum HeroRole { @@ -149,7 +151,7 @@ struct ObjectIdRef } }; -template +template bool objWithID(const CGObjectInstance * obj) { return obj->ID == id; @@ -201,7 +203,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev template void foreach_neighbour(const int3 & pos, const Func & foo) { - CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer + CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; @@ -224,7 +226,7 @@ void foreach_neighbour(CCallback * cbp, const int3 & pos, const Func & foo) // a bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater); bool isObjectPassable(const CGObjectInstance * obj); bool isObjectPassable(const Nullkiller * ai, const CGObjectInstance * obj); -bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations); +bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations); bool isBlockVisitObj(const int3 & pos); bool isWeeklyRevisitable(const CGObjectInstance * obj); diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 19f48ed65..e0eb6333b 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -153,7 +153,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, for(auto bonus : *bonusModifiers) { // army bonuses will change and object bonuses are temporary - if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT) + if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE) { newArmyInstance.addNewBonus(std::make_shared(*bonus)); } @@ -225,7 +225,8 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, if(weakest->count == 1) { - assert(resultingArmy.size() > 1); + if (resultingArmy.size() == 1) + logAi->warn("Unexpected resulting army size!"); resultingArmy.erase(weakest); } @@ -255,7 +256,7 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( { auto ci = infoFromDC(dwelling->creatures[i]); - if(!ci.count || ci.creID == -1) + if(!ci.count || ci.creID == CreatureID::NONE) continue; vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford @@ -315,7 +316,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( { auto ci = infoFromDC(dwelling->creatures[i]); - if(ci.creID == -1) continue; + if(ci.creID == CreatureID::NONE) continue; if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) { @@ -392,7 +393,7 @@ void ArmyManager::update() } } - for(auto army : totalArmy) + for(auto & army : totalArmy) { army.second.creature = army.first.toCreature(); army.second.power = evaluateStackPower(army.second.creature, army.second.count); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 359ccc1ca..8a6edc8c7 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -24,7 +24,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) for(auto &pair : townInfo->buildings) { - if(pair.second->upgrade != -1) + if(pair.second->upgrade != BuildingID::NONE) { parentMap[pair.second->upgrade] = pair.first; } @@ -160,7 +160,7 @@ void BuildAnalyzer::update() updateDailyIncome(); - if(ai->cb->getDate(Date::EDateType::DAY) == 1) + if(ai->cb->getDate(Date::DAY) == 1) { goldPreasure = 1; } @@ -256,7 +256,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( { logAi->trace("cant build. Need other dwelling"); } - else + else if(missingBuildings[0] != toBuild) { logAi->trace("cant build. Need %d", missingBuildings[0].num); @@ -274,6 +274,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( return prerequisite; } + else + { + logAi->trace("Cant build. The building requires itself as prerequisite"); + + return info; + } } } else @@ -312,7 +318,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const { for(auto tdi : developmentInfos) { - if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid)) + if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid)) return true; } diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 3651e567f..1ee4e57f5 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -15,7 +15,7 @@ namespace NKAI { -HitMapInfo HitMapInfo::NoTreat; +HitMapInfo HitMapInfo::NoThreat; double HitMapInfo::value() const { @@ -39,7 +39,7 @@ void DangerHitMapAnalyzer::updateHitMap() hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); enemyHeroAccessibleObjects.clear(); - townTreats.clear(); + townThreats.clear(); std::map> heroes; @@ -57,7 +57,7 @@ void DangerHitMapAnalyzer::updateHitMap() for(auto town : ourTowns) { - townTreats[town->id]; // insert empty list + townThreats[town->id]; // insert empty list } foreach_tile_pos([&](const int3 & pos){ @@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap() if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES) continue; - ai->pathfinder->updatePaths(pair.second, PathfinderSettings()); + PathfinderSettings ps; + + ps.mainTurnDistanceLimit = 10; + ps.scoutTurnDistanceLimit = 10; + ps.useHeroChain = false; + + ai->pathfinder->updatePaths(pair.second, ps); boost::this_thread::interruption_point(); @@ -85,21 +91,21 @@ void DangerHitMapAnalyzer::updateHitMap() auto & node = hitMap[pos.x][pos.y][pos.z]; - HitMapInfo newTreat; + HitMapInfo newThreat; - newTreat.hero = path.targetHero; - newTreat.turn = path.turn(); - newTreat.danger = path.getHeroStrength(); + newThreat.hero = path.targetHero; + newThreat.turn = path.turn(); + newThreat.danger = path.getHeroStrength(); - if(newTreat.value() > node.maximumDanger.value()) + if(newThreat.value() > node.maximumDanger.value()) { - node.maximumDanger = newTreat; + node.maximumDanger = newThreat; } - if(newTreat.turn < node.fastestDanger.turn - || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger)) + if(newThreat.turn < node.fastestDanger.turn + || (newThreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newThreat.danger)) { - node.fastestDanger = newTreat; + node.fastestDanger = newThreat; } auto objects = cb->getVisitableObjs(pos, false); @@ -108,24 +114,24 @@ void DangerHitMapAnalyzer::updateHitMap() { if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) { - auto & treats = townTreats[obj->id]; - auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + auto & threats = townThreats[obj->id]; + auto threat = std::find_if(threats.begin(), threats.end(), [&](const HitMapInfo & i) -> bool { return i.hero.hid == path.targetHero->id; }); - if(treat == treats.end()) + if(threat == threats.end()) { - treats.emplace_back(); - treat = std::prev(treats.end(), 1); + threats.emplace_back(); + threat = std::prev(threats.end(), 1); } - if(newTreat.value() > treat->value()) + if(newThreat.value() > threat->value()) { - *treat = newTreat; + *threat = newThreat; } - if(newTreat.turn == 0) + if(newThreat.turn == 0) { if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) enemyHeroAccessibleObjects.emplace_back(path.targetHero, obj); @@ -234,13 +240,13 @@ void DangerHitMapAnalyzer::calculateTileOwners() }); } -const std::vector & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const +const std::vector & DangerHitMapAnalyzer::getTownThreats(const CGTownInstance * town) const { static const std::vector empty = {}; - auto result = townTreats.find(town->id); + auto result = townThreats.find(town->id); - return result == townTreats.end() ? empty : result->second; + return result == townThreats.end() ? empty : result->second; } PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const @@ -265,14 +271,14 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger)); } -const HitMapNode & DangerHitMapAnalyzer::getObjectTreat(const CGObjectInstance * obj) const +const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const { auto tile = obj->visitablePos(); - return getTileTreat(tile); + return getTileThreat(tile); } -const HitMapNode & DangerHitMapAnalyzer::getTileTreat(const int3 & tile) const +const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const { const HitMapNode & info = hitMap[tile.x][tile.y][tile.z]; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 614312649..45538c99b 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -18,7 +18,7 @@ struct AIPath; struct HitMapInfo { - static HitMapInfo NoTreat; + static HitMapInfo NoThreat; uint64_t danger; uint8_t turn; @@ -74,7 +74,7 @@ private: bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; - std::map> townTreats; + std::map> townThreats; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} @@ -82,14 +82,14 @@ public: void updateHitMap(); void calculateTileOwners(); uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; - const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; - const HitMapNode & getTileTreat(const int3 & tile) const; + const HitMapNode & getObjectThreat(const CGObjectInstance * obj) const; + const HitMapNode & getTileThreat(const int3 & tile) const; std::set getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } PlayerColor getTileOwner(const int3 & tile) const; const CGTownInstance * getClosestTown(const int3 & tile) const; - const std::vector & getTownTreats(const CGTownInstance * town) const; + const std::vector & getTownThreats(const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index b896ab728..4131182cb 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -71,7 +71,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const { - auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, hero->type->getIndex()); + auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId())); auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL); auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus)); auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL)); @@ -83,7 +83,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const if(hasBonus) { - SecondarySkill bonusSkill = SecondarySkill(bonus->sid); + SecondarySkill bonusSkill = bonus->sid.as(); float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill); if(bonusScore > 0) @@ -187,7 +187,43 @@ bool HeroManager::heroCapReached() const int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); return heroCount >= ALLOWED_ROAMING_HEROES - || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); +} + +float HeroManager::getMagicStrength(const CGHeroInstance * hero) const +{ + auto hasFly = hero->spellbookContainsSpell(SpellID::FLY); + auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL); + auto manaLimit = hero->manaLimit(); + auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER); + auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0; + + auto score = 0.0f; + + for(auto spellId : hero->getSpellsInSpellbook()) + { + auto spell = spellId.toSpell(); + auto schoolLevel = hero->getSpellSchoolLevel(spell); + + score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f; + } + + vstd::amin(score, 1); + + score *= std::min(1.0f, spellPower / 10.0f); + + if(hasFly) + score += 0.3f; + + if(hasTownPortal && hasEarth) + score += 0.6f; + + vstd::amin(score, 1); + + score *= std::min(1.0f, manaLimit / 100.0f); + + return std::min(score, 1.0f); } bool HeroManager::canRecruitHero(const CGTownInstance * town) const @@ -278,7 +314,7 @@ void ExistingSkillRule::evaluateScore(const CGHeroInstance * hero, SecondarySkil if(heroSkill.first == skill) return; - upgradesLeft += SecSkillLevel::EXPERT - heroSkill.second; + upgradesLeft += MasteryLevel::EXPERT - heroSkill.second; } if(score >= 2 || (score >= 1 && upgradesLeft <= 1)) @@ -292,7 +328,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill auto wisdomLevel = hero->getSecSkillLevel(SecondarySkill::WISDOM); - if(hero->level > 10 && wisdomLevel == SecSkillLevel::NONE) + if(hero->level > 10 && wisdomLevel == MasteryLevel::NONE) score += 1.5; } @@ -310,7 +346,7 @@ void AtLeastOneMagicRule::evaluateScore(const CGHeroInstance * hero, SecondarySk bool heroHasAnyMagic = vstd::contains_if(magicSchools, [&](SecondarySkill skill) -> bool { - return hero->getSecSkillLevel(skill) > SecSkillLevel::NONE; + return hero->getSecSkillLevel(skill) > MasteryLevel::NONE; }); if(!heroHasAnyMagic) diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 1009fd31e..a7744ad1f 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -34,6 +34,7 @@ public: virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; + virtual float getMagicStrength(const CGHeroInstance * hero) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -76,6 +77,7 @@ public: bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; + float getMagicStrength(const CGHeroInstance * hero) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5e2b41977..a228f1b4d 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -94,16 +94,22 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons { for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++) { - auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); - auto blockers = ai->cb->getVisitableObjs(node->coord); - - if(guardPos.valid()) - { - auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + std::vector blockers = {}; - if(guard) + if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL) + { + auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); + + blockers = ai->cb->getVisitableObjs(node->coord); + + if(guardPos.valid()) { - blockers.insert(blockers.begin(), guard); + auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + + if(guard) + { + blockers.insert(blockers.begin(), guard); + } } } diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 3220ff891..d21b92965 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -20,9 +20,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string BuildingBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 7b2a57396..b5260ac3a 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string BuyArmyBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 155f45af6..9ce74a72f 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -19,9 +19,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; template diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp index ff0679564..cc376acb8 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp @@ -19,9 +19,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string ClusterBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index d9bdf3904..7d2ab874e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -25,10 +25,7 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - -const float TREAT_IGNORE_RATIO = 2; +const float THREAT_IGNORE_RATIO = 2; using namespace Goals; @@ -49,20 +46,20 @@ Goals::TGoalVec DefenceBehavior::decompose() const return tasks; } -bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector & paths) +bool isThreatUnderControl(const CGTownInstance * town, const HitMapInfo & threat, const std::vector & paths) { int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); for(const AIPath & path : paths) { - bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; - bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; + bool threatIsWeak = path.getHeroStrength() / (float)threat.danger > THREAT_IGNORE_RATIO; + bool needToSaveGrowth = threat.turn == 0 && dayOfWeek == 7; - if(treatIsWeak && !needToSaveGrowth) + if(threatIsWeak && !needToSaveGrowth) { - if((path.exchangeCount == 1 && path.turn() < treat.turn) - || path.turn() < treat.turn - 1 - || (path.turn() < treat.turn && treat.turn >= 2)) + if((path.exchangeCount == 1 && path.turn() < threat.turn) + || path.turn() < threat.turn - 1 + || (path.turn() < threat.turn && threat.turn >= 2)) { #if NKAI_TRACE_LEVEL >= 1 logAi->trace( @@ -82,16 +79,16 @@ bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, void handleCounterAttack( const CGTownInstance * town, - const HitMapInfo & treat, + const HitMapInfo & threat, const HitMapInfo & maximumDanger, Goals::TGoalVec & tasks) { - if(treat.hero.validAndSet() - && treat.turn <= 1 - && (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn)) + if(threat.hero.validAndSet() + && threat.turn <= 1 + && (threat.danger == maximumDanger.danger || threat.turn < maximumDanger.turn)) { - auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); - auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(threat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, threat.hero.get()); for(int i = 0; i < heroCapturingPaths.size(); i++) { @@ -102,7 +99,7 @@ void handleCounterAttack( Composition composition; - composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + composition.addNext(DefendTown(town, threat, path, true)).addNext(goal); tasks.push_back(Goals::sptr(composition)); } @@ -114,7 +111,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", + "Hero %s in garrison of town %s is supposed to defend the town", town->garrisonHero->getNameTranslated(), town->getNameTranslated()); @@ -155,19 +152,19 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { logAi->trace("Evaluating defence for %s", town->getNameTranslated()); - auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + auto threatNode = ai->nullkiller->dangerHitMap->getObjectThreat(town); + std::vector threats = ai->nullkiller->dangerHitMap->getTownThreats(town); - treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there + threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks)) { return; } - if(!treatNode.fastestDanger.hero) + if(!threatNode.fastestDanger.hero) { - logAi->trace("No treat found for town %s", town->getNameTranslated()); + logAi->trace("No threat found for town %s", town->getNameTranslated()); return; } @@ -182,23 +179,23 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos()); - for(auto & treat : treats) + for(auto & threat : threats) { logAi->trace( - "Town %s has treat %lld in %s turns, hero: %s", + "Town %s has threat %lld in %s turns, hero: %s", town->getNameTranslated(), - treat.danger, - std::to_string(treat.turn), - treat.hero ? treat.hero->getNameTranslated() : std::string("")); + threat.danger, + std::to_string(threat.turn), + threat.hero ? threat.hero->getNameTranslated() : std::string("")); - handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); + handleCounterAttack(town, threat, threatNode.maximumDanger, tasks); - if(isTreatUnderControl(town, treat, paths)) + if(isThreatUnderControl(town, threat, paths)) { continue; } - evaluateRecruitingHero(tasks, treat, town); + evaluateRecruitingHero(tasks, threat, town); if(paths.empty()) { @@ -239,7 +236,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(path.turn() <= treat.turn - 2) + 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", @@ -267,7 +264,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { tasks.push_back( Goals::sptr(Composition() - .addNext(DefendTown(town, treat, path.targetHero)) + .addNext(DefendTown(town, threat, path.targetHero)) .addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE)))); } @@ -284,7 +281,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta tasks.push_back( Goals::sptr(Composition() - .addNext(DefendTown(town, treat, path)) + .addNext(DefendTown(town, threat, path)) .addNextSequence({ sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())), sptr(ExecuteHeroChain(path, town)), @@ -294,7 +291,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)) + if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger)) { if(ai->nullkiller->arePathHeroesLocked(path)) { @@ -327,7 +324,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta } Composition composition; - composition.addNext(DefendTown(town, treat, path)); + composition.addNext(DefendTown(town, threat, path)); TGoalVec sequence; if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1) @@ -405,7 +402,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->debug("Found %d tasks", tasks.size()); } -void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const +void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const { if(town->hasBuilt(BuildingID::TAVERN) && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) @@ -414,7 +411,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM for(auto hero : heroesInTavern) { - if(hero->getTotalStrength() < treat.danger) + if(hero->getTotalStrength() < threat.danger) continue; auto myHeroes = cb->getHeroesInfo(); @@ -466,7 +463,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM sequence.push_back(sptr(Goals::RecruitHero(town, hero))); - tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence))); + tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, threat, hero)).addNextSequence(sequence))); } } } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index b9b7c486e..fab4745f5 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -1,5 +1,5 @@ /* -* BuyArmyBehavior.h, part of VCMI engine +* DefenceBehavior.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -39,7 +39,7 @@ namespace Goals private: void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; - void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const; + void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c73b374c0..935e782f5 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -23,9 +23,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string GatherArmyBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 8f9b80f6d..885cc7af2 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string RecruitHeroBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 820beb75f..3a7f59f72 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -21,9 +21,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string StartupBehavior::toString() const @@ -74,7 +71,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown) for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + if((obj->ID == Obj::RESOURCE && dynamic_cast(obj)->resourceID() == EGameResID::GOLD) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || obj->ID == Obj::WATER_WHEEL) diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp new file mode 100644 index 000000000..2f7c89b2f --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -0,0 +1,70 @@ +/* +* StartupBehavior.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 "StayAtTownBehavior.h" +#include "../AIGateway.h" +#include "../AIUtility.h" +#include "../Goals/StayAtTown.h" +#include "../Goals/Composition.h" +#include "../Goals/ExecuteHeroChain.h" +#include "lib/mapObjects/MapObjects.h" //for victory conditions +#include "../Engine/Nullkiller.h" + +namespace NKAI +{ + +using namespace Goals; + +std::string StayAtTownBehavior::toString() const +{ + return "StayAtTownBehavior"; +} + +Goals::TGoalVec StayAtTownBehavior::decompose() const +{ + Goals::TGoalVec tasks; + auto towns = cb->getTownsInfo(); + + if(!towns.size()) + return tasks; + + for(auto town : towns) + { + if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) + continue; + + auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos()); + + for(auto & path : paths) + { + if(town->visitingHero && town->visitingHero.get() != path.targetHero) + continue; + + if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + { + if(path.targetHero->mana == path.targetHero->manaLimit()) + continue; + + Composition stayAtTown; + + stayAtTown.addNextSequence({ + sptr(ExecuteHeroChain(path)), + sptr(StayAtTown(town, path)) + }); + + tasks.push_back(sptr(stayAtTown)); + } + } + } + + return tasks; +} + +} diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h new file mode 100644 index 000000000..260cf136a --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h @@ -0,0 +1,39 @@ +/* +* StayAtTownBehavior.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/VCMI_Lib.h" +#include "../Goals/CGoal.h" +#include "../AIUtility.h" + +namespace NKAI +{ +namespace Goals +{ + class StayAtTownBehavior : public CGoal + { + public: + StayAtTownBehavior() + :CGoal(STAY_AT_TOWN_BEHAVIOR) + { + } + + virtual TGoalVec decompose() const override; + virtual std::string toString() const override; + + virtual bool operator==(const StayAtTownBehavior & other) const override + { + return true; + } + }; +} + + +} diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index a6560989f..042cb5a0d 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -9,6 +9,7 @@ set(Nullkiller_SRCS Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BoatActions.cpp Pathfinding/Actions/TownPortalAction.cpp + Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Rules/AILayerTransitionRule.cpp Pathfinding/Rules/AIMovementAfterDestinationRule.cpp Pathfinding/Rules/AIMovementToDestinationRule.cpp @@ -34,6 +35,7 @@ set(Nullkiller_SRCS Goals/ExecuteHeroChain.cpp Goals/ExchangeSwapTownHeroes.cpp Goals/CompleteQuest.cpp + Goals/StayAtTown.cpp Markers/ArmyUpgrade.cpp Markers/HeroExchange.cpp Markers/UnlockCluster.cpp @@ -52,6 +54,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Behaviors/StayAtTownBehavior.cpp Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -69,6 +72,7 @@ set(Nullkiller_HEADERS Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BoatActions.h Pathfinding/Actions/TownPortalAction.h + Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Rules/AILayerTransitionRule.h Pathfinding/Rules/AIMovementAfterDestinationRule.h Pathfinding/Rules/AIMovementToDestinationRule.h @@ -97,6 +101,7 @@ set(Nullkiller_HEADERS Goals/ExchangeSwapTownHeroes.h Goals/CompleteQuest.h Goals/Goals.h + Goals/StayAtTown.h Markers/ArmyUpgrade.h Markers/HeroExchange.h Markers/UnlockCluster.h @@ -115,6 +120,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Behaviors/StayAtTownBehavior.h Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/DeepDecomposer.cpp b/AI/Nullkiller/Engine/DeepDecomposer.cpp index 8e649634f..88fd3ed5b 100644 --- a/AI/Nullkiller/Engine/DeepDecomposer.cpp +++ b/AI/Nullkiller/Engine/DeepDecomposer.cpp @@ -24,9 +24,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; void DeepDecomposer::reset() diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index 2afe4f5ef..c20b39143 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -20,8 +20,6 @@ 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 -extern boost::thread_specific_ptr ai; - engineBase::engineBase() { rules = new fl::RuleBlock(); diff --git a/AI/Nullkiller/Engine/FuzzyEngines.h b/AI/Nullkiller/Engine/FuzzyEngines.h index 1694d175c..1873b97a5 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.h +++ b/AI/Nullkiller/Engine/FuzzyEngines.h @@ -8,7 +8,11 @@ * */ #pragma once -#include +#if __has_include() +# include +#else +# include +#endif #include "../Goals/AbstractGoal.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 2757cb35a..5252f89c3 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -24,7 +24,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); @@ -111,7 +111,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { auto cb = ai->cb.get(); - if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat + if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat return 0; switch(obj->ID) @@ -161,10 +161,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) } case Obj::PYRAMID: { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; + return estimateBankDanger(dynamic_cast(obj)); } default: return 0; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d6d7f41dc..79c21d1ab 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -18,15 +18,13 @@ #include "../Behaviors/BuildingBehavior.h" #include "../Behaviors/GatherArmyBehavior.h" #include "../Behaviors/ClusterBehavior.h" +#include "../Behaviors/StayAtTownBehavior.h" #include "../Goals/Invalid.h" #include "../Goals/Composition.h" namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; #if NKAI_TRACE_LEVEL >= 1 @@ -141,8 +139,8 @@ void Nullkiller::updateAiState(int pass, bool fast) { memory->removeInvisibleObjects(cb.get()); - dangerHitMap->calculateTileOwners(); dangerHitMap->updateHitMap(); + dangerHitMap->calculateTileOwners(); boost::this_thread::interruption_point(); @@ -265,7 +263,8 @@ void Nullkiller::makeTurn() choseBestTask(sptr(CaptureObjectsBehavior()), 1), choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH), choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH), - choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH) + choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH), + choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH) }; if(cb->getDate(Date::DAY) == 1) @@ -275,6 +274,7 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(bestTasks); + std::string taskDescription = bestTask->toString(); HeroPtr hero = bestTask->getHero(); HeroRole heroRole = HeroRole::MAIN; @@ -293,7 +293,7 @@ void Nullkiller::makeTurn() logAi->trace( "Goal %s has low priority %f so decreasing scan depth to gain performance.", - bestTask->toString(), + taskDescription, bestTask->priority); } @@ -309,7 +309,7 @@ void Nullkiller::makeTurn() { logAi->trace( "Goal %s has too low priority %f so increasing scan depth to full.", - bestTask->toString(), + taskDescription, bestTask->priority); scanDepth = ScanDepth::ALL_FULL; @@ -317,7 +317,7 @@ void Nullkiller::makeTurn() continue; } - logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); + logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription); return; } @@ -326,7 +326,7 @@ void Nullkiller::makeTurn() if(i == MAXPASS) { - logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", bestTask->toString()); + logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); } } } @@ -341,7 +341,7 @@ void Nullkiller::executeTask(Goals::TTask task) try { - task->accept(ai.get()); + task->accept(ai); logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start)); } catch(goalFulfilledException &) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 6e3e5f754..be3d87845 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -22,6 +22,7 @@ #include "../../../lib/filesystem/Filesystem.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" +#include "../Goals/StayAtTown.h" #include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" @@ -68,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator() void PriorityEvaluator::initVisitTile() { - auto file = CResourceHandler::get()->load(ResourceID("config/ai/object-priorities.txt"))->readAll(); + auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll(); std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); @@ -121,7 +122,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer { //Fixme: unused variable hero - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); auto resources = bankInfo->getPossibleResourcesReward(); TResources result = TResources(); @@ -138,7 +139,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero) { - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); auto creatures = bankInfo->getPossibleCreaturesReward(); uint64_t result = 0; @@ -241,13 +242,13 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) return 1500; auto statsValue = - 10 * art->valOfBonuses(BonusType::MOVEMENT, 1) + 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 700 * art->valOfBonuses(BonusType::MORALE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::KNOWLEDGE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::SPELL_POWER)) + 500 * art->valOfBonuses(BonusType::LUCK); auto classValue = 0; @@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward( : 0; case Obj::PANDORAS_BOX: return 5000; + case Obj::MAGIC_WELL: + case Obj::MAGIC_SPRING: + return getManaRecoveryArmyReward(hero); default: return 0; } @@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } +uint64_t 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 { if(!target) @@ -458,14 +467,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons switch(target->ID) { case Obj::MINE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto mine = dynamic_cast(target); + return mine->producedResource == EGameResID::GOLD ? 0.5f - : 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID); + : 0.4f * getTotalResourceRequirementStrength(mine->producedResource) + 0.1f * getResourceRequirementStrength(mine->producedResource); + } case Obj::RESOURCE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto resource = dynamic_cast(target); + return resource->resourceID() == EGameResID::GOLD ? 0 - : 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID); + : 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID()); + } case Obj::CREATURE_BANK: { @@ -519,14 +534,17 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } } -float RewardEvaluator::evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const +float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { + auto rewardable = dynamic_cast(hut); + assert(rewardable); + + auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill")); + if(!hut->wasVisited(hero->tempOwner)) return role == HeroRole::SCOUT ? 2 : 0; - auto skill = SecondarySkill(hut->ability); - - if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE + if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) return 0; @@ -566,7 +584,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: - return evaluateWitchHutSkillScore(dynamic_cast(target), hero, role); + return evaluateWitchHutSkillScore(target, hero, role); case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; @@ -581,15 +599,15 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const { - auto & treatNode = ai->dangerHitMap->getTileTreat(tile); + auto & treatNode = ai->dangerHitMap->getTileThreat(tile); if(treatNode.maximumDanger.danger == 0) - return HitMapInfo::NoTreat; + return HitMapInfo::NoThreat; if(treatNode.maximumDanger.turn <= turn) return treatNode.maximumDanger; - return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoTreat; + return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoThreat; } int32_t getArmyCost(const CArmedInstance * army) @@ -614,12 +632,14 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; - auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power switch(target->ID) { case Obj::RESOURCE: - return isGold ? 600 : 100; + { + auto * res = dynamic_cast(target); + return res->resourceID() == GameResID::GOLD ? 600 : 100; + } case Obj::TREASURE_CHEST: return 1500; case Obj::WATER_WHEEL: @@ -628,7 +648,10 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG return dailyIncomeMultiplier * estimateTownIncome(ai->cb.get(), target, hero); case Obj::MINE: case Obj::ABANDONED_MINE: - return dailyIncomeMultiplier * (isGold ? 1000 : 75); + { + auto * mine = dynamic_cast(target); + return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); + } case Obj::MYSTICAL_GARDEN: case Obj::WINDMILL: return 100; @@ -693,6 +716,22 @@ public: } }; +class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder +{ +public: + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::STAY_AT_TOWN) + return; + + Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + + evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get()); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + } +}; + void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength) { HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn); @@ -977,7 +1016,7 @@ public: uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const { - if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id)) + if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id)) return 0; auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); @@ -998,6 +1037,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared(ai)); + evaluationContextBuilders.push_back(std::make_shared()); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index fb8085494..beccbef62 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -8,14 +8,16 @@ * */ #pragma once -#include "fl/Headers.h" +#if __has_include() +# include +#else +# include +#endif #include "../Goals/CGoal.h" #include "../Pathfinding/AIPathfinder.h" VCMI_LIB_NAMESPACE_BEGIN -class CGWitchHut; - VCMI_LIB_NAMESPACE_END namespace NKAI @@ -39,12 +41,13 @@ public: float getResourceRequirementStrength(int resType) const; float getStrategicalValue(const CGObjectInstance * target) const; float getTotalResourceRequirementStrength(int resType) const; - float evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const; + float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; uint64_t townArmyGrowth(const CGTownInstance * town) const; + uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Goals/AbstractGoal.cpp b/AI/Nullkiller/Goals/AbstractGoal.cpp index fd27579c5..98f6a4c49 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.cpp +++ b/AI/Nullkiller/Goals/AbstractGoal.cpp @@ -10,14 +10,11 @@ #include "StdInc.h" #include "AbstractGoal.h" #include "../AIGateway.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index a5b170c9f..d089083bf 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -71,7 +71,9 @@ namespace Goals ARMY_UPGRADE, DEFEND_TOWN, CAPTURE_OBJECT, - SAVE_RESOURCES + SAVE_RESOURCES, + STAY_AT_TOWN_BEHAVIOR, + STAY_AT_TOWN }; class DLL_EXPORT TSubgoal : public std::shared_ptr diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 8a8a3cf96..7e62e7d42 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -14,9 +14,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const diff --git a/AI/Nullkiller/Goals/BuildBoat.cpp b/AI/Nullkiller/Goals/BuildBoat.cpp index e69b90d3e..86b274f5f 100644 --- a/AI/Nullkiller/Goals/BuildBoat.cpp +++ b/AI/Nullkiller/Goals/BuildBoat.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool BuildBoat::operator==(const BuildBoat & other) const diff --git a/AI/Nullkiller/Goals/BuildThis.cpp b/AI/Nullkiller/Goals/BuildThis.cpp index d61caae44..c73cb57e3 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -11,18 +11,14 @@ #include "BuildThis.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; - BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid) : ElementarGoal(Goals::BUILD_STRUCTURE) { diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index f2e4aca05..55a8ec16e 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool BuyArmy::operator==(const BuyArmy & other) const @@ -54,7 +51,7 @@ void BuyArmy::accept(AIGateway * ai) auto res = cb->getResourceAmount(); auto & ci = armyToBuy[i]; - if(objid != -1 && ci.creID != objid) + if(objid != CreatureID::NONE && ci.creID.getNum() != objid) continue; vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); diff --git a/AI/Nullkiller/Goals/CaptureObject.cpp b/AI/Nullkiller/Goals/CaptureObject.cpp index 0dd71dc93..35a5d4417 100644 --- a/AI/Nullkiller/Goals/CaptureObject.cpp +++ b/AI/Nullkiller/Goals/CaptureObject.cpp @@ -18,8 +18,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; - using namespace Goals; bool CaptureObject::operator==(const CaptureObject & other) const diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 6e39c3d47..d0e981a78 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool isKeyMaster(const QuestInfo & q) @@ -40,42 +37,29 @@ TGoalVec CompleteQuest::decompose() const } logAi->debug("Trying to realize quest: %s", questToString()); - - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: + + if(!q.quest->mission.artifacts.empty()) return missionArt(); - case CQuest::MISSION_HERO: + if(!q.quest->mission.heroes.empty()) return missionHero(); - case CQuest::MISSION_ARMY: + if(!q.quest->mission.creatures.empty()) return missionArmy(); - case CQuest::MISSION_RESOURCES: + if(q.quest->mission.resources.nonZero()) return missionResources(); - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + if(q.quest->killTarget != ObjectInstanceID::NONE) return missionDestroyObj(); - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); + for(auto & s : q.quest->mission.primary) + if(s) + return missionIncreasePrimaryStat(); - case CQuest::MISSION_LEVEL: + if(q.quest->mission.heroLevel > 0) return missionLevel(); - case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - - break; - - case CQuest::MISSION_KEYMASTER: - return missionKeymaster(); - - } //end of switch - return TGoalVec(); } @@ -110,7 +94,7 @@ std::string CompleteQuest::questToString() const return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent"; } - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; @@ -140,7 +124,7 @@ TGoalVec CompleteQuest::missionArt() const CaptureObjectsBehavior findArts; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); } @@ -226,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const TGoalVec CompleteQuest::missionDestroyObj() const { - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return CaptureObjectsBehavior(q.obj).decompose(); diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index 30d3791f9..aff59aa7b 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -11,15 +11,12 @@ #include "Composition.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool Composition::operator==(const Composition & other) const diff --git a/AI/Nullkiller/Goals/DigAtTile.cpp b/AI/Nullkiller/Goals/DigAtTile.cpp index a573bfbd0..6dce16b59 100644 --- a/AI/Nullkiller/Goals/DigAtTile.cpp +++ b/AI/Nullkiller/Goals/DigAtTile.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const diff --git a/AI/Nullkiller/Goals/DismissHero.cpp b/AI/Nullkiller/Goals/DismissHero.cpp index ce26e4f10..543b16e29 100644 --- a/AI/Nullkiller/Goals/DismissHero.cpp +++ b/AI/Nullkiller/Goals/DismissHero.cpp @@ -14,9 +14,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool DismissHero::operator==(const DismissHero & other) const diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index 80e8af201..12ff31847 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ExchangeSwapTownHeroes::ExchangeSwapTownHeroes( diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index d367f96b4..438c88b07 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * obj) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index e787c7529..c6a6c4d4e 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -11,15 +11,12 @@ #include "Goals.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string RecruitHero::toString() const diff --git a/AI/Nullkiller/Goals/SaveResources.cpp b/AI/Nullkiller/Goals/SaveResources.cpp index 2cf03fc4c..6499ea457 100644 --- a/AI/Nullkiller/Goals/SaveResources.cpp +++ b/AI/Nullkiller/Goals/SaveResources.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool SaveResources::operator==(const SaveResources & other) const diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp new file mode 100644 index 000000000..82342cb51 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -0,0 +1,52 @@ +/* +* ArmyUpgrade.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 "StayAtTown.h" +#include "../AIGateway.h" +#include "../Engine/Nullkiller.h" +#include "../AIUtility.h" + +namespace NKAI +{ + +using namespace Goals; + +StayAtTown::StayAtTown(const CGTownInstance * town, AIPath & path) + : ElementarGoal(Goals::STAY_AT_TOWN) +{ + sethero(path.targetHero); + settown(town); + movementWasted = static_cast(hero->movementPointsRemaining()) / hero->movementPointsLimit(!hero->boat) - path.movementCost(); + vstd::amax(movementWasted, 0); +} + +bool StayAtTown::operator==(const StayAtTown & other) const +{ + return hero == other.hero && town == other.town; +} + +std::string StayAtTown::toString() const +{ + return "Stay at town " + town->getNameTranslated() + + " hero " + hero->getNameTranslated() + + ", mana: " + std::to_string(hero->mana); +} + +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.get(), HeroLockedReason::DEFENCE); +} + +} diff --git a/AI/Nullkiller/Goals/StayAtTown.h b/AI/Nullkiller/Goals/StayAtTown.h new file mode 100644 index 000000000..880881386 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.h @@ -0,0 +1,36 @@ +/* +* ArmyUpgrade.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 "../Goals/CGoal.h" +#include "../Pathfinding/AINodeStorage.h" +#include "../Analyzers/ArmyManager.h" +#include "../Analyzers/DangerHitMapAnalyzer.h" + +namespace NKAI +{ +namespace Goals +{ + class DLL_EXPORT StayAtTown : public ElementarGoal + { + private: + float movementWasted; + + public: + StayAtTown(const CGTownInstance * town, AIPath & path); + + virtual bool operator==(const StayAtTown & other) const override; + virtual std::string toString() const override; + void accept(AIGateway * ai) override; + float getMovementWasted() const { return movementWasted; } + }; +} + +} diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index 0f6d41090..ff61a6454 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 499122327..cef89c1db 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool HeroExchange::operator==(const HeroExchange & other) const diff --git a/AI/Nullkiller/Markers/UnlockCluster.cpp b/AI/Nullkiller/Markers/UnlockCluster.cpp index bbf7c99c7..c52ee8345 100644 --- a/AI/Nullkiller/Markers/UnlockCluster.cpp +++ b/AI/Nullkiller/Markers/UnlockCluster.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool UnlockCluster::operator==(const UnlockCluster & other) const diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 4ff2e4ef9..7e029fe64 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -90,7 +90,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta //TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline const PlayerColor fowPlayer = ai->playerID; - const auto fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; const int3 sizes = gs->getMapSize(); //Each thread gets different x, but an array of y located next to each other in memory @@ -279,9 +279,10 @@ void AINodeStorage::commit( #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", + "Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", source->coord.toString(), destination->coord.toString(), + destination->layer, destination->getCost(), std::to_string(destination->turns), destination->moveRemains, @@ -983,7 +984,7 @@ std::vector AINodeStorage::calculateTeleportations( struct TowmPortalFinder { const std::vector & initialNodes; - SecSkillLevel::SecSkillLevel townPortalSkillLevel; + MasteryLevel::Type townPortalSkillLevel; uint64_t movementNeeded; const ChainActor * actor; const CGHeroInstance * hero; @@ -1005,8 +1006,8 @@ struct TowmPortalFinder townPortal = spellID.toSpell(); // TODO: Copy/Paste from TownPortalMechanics - townPortalSkillLevel = SecSkillLevel::SecSkillLevel(hero->getSpellSchoolLevel(townPortal)); - movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= SecSkillLevel::EXPERT ? 2 : 3); + townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal)); + movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3); } bool actorCanCastTownPortal() @@ -1027,7 +1028,7 @@ struct TowmPortalFinder continue; } - if(townPortalSkillLevel < SecSkillLevel::ADVANCED) + if(townPortalSkillLevel < MasteryLevel::ADVANCED) { const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int { @@ -1208,7 +1209,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); @@ -1232,7 +1233,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); @@ -1258,7 +1259,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); @@ -1343,6 +1344,7 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa pathNode.coord = node->coord; pathNode.parentIndex = parentIndex; pathNode.actionIsBlocked = false; + pathNode.layer = node->layer; if(pathNode.specialAction) { diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index c127f294b..068304955 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -45,7 +45,7 @@ struct AIPathNode : public CGPathNode { uint64_t danger; uint64_t armyLoss; - uint32_t manaCost; + int32_t manaCost; const AIPathNode * chainOther; std::shared_ptr specialAction; const ChainActor * actor; @@ -65,6 +65,7 @@ struct AIPathNodeInfo float cost; uint8_t turns; int3 coord; + EPathfindingLayer layer; uint64_t danger; const CGHeroInstance * targetHero; int parentIndex; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index b7314b3d1..2259ef029 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -44,6 +44,7 @@ namespace AIPathfinding std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage) { + options.canUseCast = true; } AIPathfinderConfig::~AIPathfinderConfig() = default; diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp new file mode 100644 index 000000000..ff4934b36 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp @@ -0,0 +1,82 @@ +/* +* AdventureSpellCastMovementActions.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 "../../AIGateway.h" +#include "../../Goals/AdventureSpellCast.h" +#include "../../Goals/CaptureObject.h" +#include "../../Goals/Invalid.h" +#include "../../Goals/BuildBoat.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "AdventureSpellCastMovementActions.h" + +namespace NKAI +{ + +namespace AIPathfinding +{ + AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero) + :spellToCast(spellToCast), hero(hero) + { + manaCost = hero->getSpellCost(spellToCast.toSpell()); + } + + WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero) + :AdventureCastAction(SpellID::WATER_WALK, hero) + { } + + AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero) + : AdventureCastAction(SpellID::FLY, hero) + { + } + + void AdventureCastAction::applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const + { + dstMode->manaCost = srcNode->manaCost + manaCost; + dstMode->theNodeBefore = source.node; + } + + void AdventureCastAction::execute(const CGHeroInstance * hero) const + { + assert(hero == this->hero); + + Goals::AdventureSpellCast(hero, spellToCast).accept(ai); + } + + bool AdventureCastAction::canAct(const AIPathNode * source) const + { + assert(hero == this->hero); + + auto hero = source->actor->hero; + +#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 + manaCost; + } + + std::string AdventureCastAction::toString() const + { + return "Cast " + spellToCast.toSpell()->getNameTranslated() + " by " + hero->getNameTranslated(); + } +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h new file mode 100644 index 000000000..0667e400a --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -0,0 +1,58 @@ +/* +* AdventureSpellCastMovementActions.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" + +namespace NKAI +{ + +namespace AIPathfinding +{ + class AdventureCastAction : public SpecialAction + { + private: + SpellID spellToCast; + const CGHeroInstance * hero; + int manaCost; + + public: + AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero); + + virtual void execute(const CGHeroInstance * hero) const override; + + virtual void applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const override; + + virtual bool canAct(const AIPathNode * source) const override; + + virtual std::string toString() const override; + }; + + class WaterWalkingAction : public AdventureCastAction + { + public: + WaterWalkingAction(const CGHeroInstance * hero); + }; + + class AirWalkingAction : public AdventureCastAction + { + public: + AirWalkingAction(const CGHeroInstance * hero); + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp b/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp index b85bff624..33248df50 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BattleAction::execute(const CGHeroInstance * hero) const diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index 9fcf052ce..7a6ab3f10 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp @@ -20,14 +20,11 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BuildBoatAction::execute(const CGHeroInstance * hero) const { - return Goals::BuildBoat(shipyard).accept(ai.get()); + return Goals::BuildBoat(shipyard).accept(ai); } Goals::TSubgoal BuildBoatAction::decompose(const CGHeroInstance * hero) const @@ -80,7 +77,7 @@ namespace AIPathfinding void SummonBoatAction::execute(const CGHeroInstance * hero) const { - Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai.get()); + Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai); } const ChainActor * SummonBoatAction::getActor(const ChainActor * sourceActor) const @@ -117,7 +114,7 @@ namespace AIPathfinding source->manaCost); #endif - return hero->mana >= (si32)(source->manaCost + getManaCost(hero)); + return hero->mana >= source->manaCost + getManaCost(hero); } std::string SummonBoatAction::toString() const @@ -125,7 +122,7 @@ namespace AIPathfinding return "Summon Boat"; } - uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const + int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const { SpellID summonBoat = SpellID::SUMMON_BOAT; diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 92249eb78..5e6ca50d4 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -20,8 +20,6 @@ namespace AIPathfinding { class VirtualBoatAction : public SpecialAction { - public: - virtual const ChainActor * getActor(const ChainActor * sourceActor) const = 0; }; class SummonBoatAction : public VirtualBoatAction @@ -43,7 +41,7 @@ namespace AIPathfinding virtual std::string toString() const override; private: - uint32_t getManaCost(const CGHeroInstance * hero) const; + int32_t getManaCost(const CGHeroInstance * hero) const; }; class BuildBoatAction : public VirtualBoatAction diff --git a/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp b/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp index 683f42246..a676ad5c1 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BuyArmyAction::execute(const CGHeroInstance * hero) const diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index a552ff2af..1efebafca 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { bool QuestAction::canAct(const AIPathNode * node) const @@ -28,7 +25,7 @@ namespace AIPathfinding return dynamic_cast(questInfo.obj)->checkQuest(node->actor->hero); } - return questInfo.quest->progress == CQuest::NOT_ACTIVE + return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner()) || questInfo.quest->checkQuest(node->actor->hero); } diff --git a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h index c14589e75..77270ce1a 100644 --- a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h @@ -22,6 +22,7 @@ namespace NKAI { struct AIPathNode; +class ChainActor; class SpecialAction { @@ -54,6 +55,11 @@ public: { return {}; } + + virtual const ChainActor * getActor(const ChainActor * sourceActor) const + { + return sourceActor; + } }; class CompositeAction : public SpecialAction diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp index 2304e39dd..e32a13231 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp @@ -18,9 +18,6 @@ namespace NKAI using namespace AIPathfinding; -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - void TownPortalAction::execute(const CGHeroInstance * hero) const { auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL); @@ -28,7 +25,7 @@ void TownPortalAction::execute(const CGHeroInstance * hero) const goal.town = target; goal.tile = target->visitablePos(); - goal.accept(ai.get()); + goal.accept(ai); } std::string TownPortalAction::toString() const diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index f555b18c3..a70826252 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "AILayerTransitionRule.h" #include "../../Engine/Nullkiller.h" +#include "../../../../lib/pathfinder/CPathfinder.h" +#include "../../../../lib/pathfinder/TurnInfo.h" namespace NKAI { @@ -31,23 +33,79 @@ namespace AIPathfinding if(!destination.blocked) { - return; + if(source.node->layer == EPathfindingLayer::LAND + && (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER)) + { + if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer)) + return; + else + destination.blocked = true; + } + else + { + return; + } } if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { std::shared_ptr virtualBoat = findVirtualBoat(destination, source); - if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) + if(virtualBoat && tryUseSpecialAction(destination, source, virtualBoat, EPathNodeAction::EMBARK)) { #if NKAI_PATHFINDER_TRACE_LEVEL >= 1 logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER) + { + auto action = waterWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting water walk while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR) + { + auto action = airWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting fly while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); #endif } } } void AILayerTransitionRule::setup() + { + SpellID waterWalk = SpellID::WATER_WALK; + SpellID airWalk = SpellID::FLY; + + for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) + { + if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell())) + { + waterWalkingActions[hero] = std::make_shared(hero); + } + + if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell())) + { + airWalkingActions[hero] = std::make_shared(hero); + } + } + + collectVirtualBoats(); + } + + void AILayerTransitionRule::collectVirtualBoats() { std::vector shipyards; @@ -81,7 +139,7 @@ namespace AIPathfinding auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) + && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoats[hero] = std::make_shared(); @@ -113,50 +171,51 @@ namespace AIPathfinding return virtualBoat; } - bool AILayerTransitionRule::tryEmbarkVirtualBoat( + bool AILayerTransitionRule::tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const + std::shared_ptr specialAction, + EPathNodeAction targetAction) const { bool result = false; nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) - { - auto boatNodeOptional = nodeStorage->getOrCreateNode( - node->coord, - node->layer, - virtualBoat->getActor(node->actor)); - - if(boatNodeOptional) { - AIPathNode * boatNode = boatNodeOptional.value(); + auto castNodeOptional = nodeStorage->getOrCreateNode( + node->coord, + node->layer, + specialAction->getActor(node->actor)); - if(boatNode->action == EPathNodeAction::UNKNOWN) + if(castNodeOptional) { - boatNode->addSpecialAction(virtualBoat); - destination.blocked = false; - destination.action = EPathNodeAction::EMBARK; - destination.node = boatNode; - result = true; + AIPathNode * castNode = castNodeOptional.value(); + + if(castNode->action == EPathNodeAction::UNKNOWN) + { + castNode->addSpecialAction(specialAction); + destination.blocked = false; + destination.action = targetAction; + destination.node = castNode; + result = true; + } + else + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 + logAi->trace( + "Special transition node already allocated. Blocked moving %s -> %s", + source.coord.toString(), + destination.coord.toString()); +#endif + } } else { -#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 - logAi->trace( - "Special transition node already allocated. Blocked moving %s -> %s", + logAi->debug( + "Can not allocate special transition node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); -#endif } - } - else - { - logAi->debug( - "Can not allocate special transition node while moving %s -> %s", - source.coord.toString(), - destination.coord.toString()); - } - }); + }); return result; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h index f7d5e27b8..243cb96a9 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h @@ -13,6 +13,7 @@ #include "../AINodeStorage.h" #include "../../AIGateway.h" #include "../Actions/BoatActions.h" +#include "../Actions/AdventureSpellCastMovementActions.h" #include "../../../../CCallback.h" #include "../../../../lib/mapObjects/MapObjects.h" #include "../../../../lib/pathfinder/PathfindingRules.h" @@ -29,6 +30,8 @@ namespace AIPathfinding std::map> virtualBoats; std::shared_ptr nodeStorage; std::map> summonableVirtualBoats; + std::map> waterWalkingActions; + std::map> airWalkingActions; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr nodeStorage); @@ -41,15 +44,17 @@ namespace AIPathfinding private: void setup(); + void collectVirtualBoats(); std::shared_ptr findVirtualBoat( CDestinationNodeInfo & destination, const PathNodeInfo & source) const; - bool tryEmbarkVirtualBoat( + bool tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const; + std::shared_ptr specialAction, + EPathNodeAction targetAction) const; }; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index dafcb4758..7f6664a22 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,9 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE) + if(destination.nodeObject->ID == Obj::QUEST_GUARD + && questObj->quest->mission == Rewardable::Limiter{} + && questObj->quest->killTarget == ObjectInstanceID::NONE) { return false; } diff --git a/AI/StupidAI/StdInc.cpp b/AI/StupidAI/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/AI/StupidAI/StdInc.cpp +++ b/AI/StupidAI/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/AI/StupidAI/StdInc.h b/AI/StupidAI/StdInc.h index 02b2c08f3..020481377 100644 --- a/AI/StupidAI/StdInc.h +++ b/AI/StupidAI/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index aad18351e..3888e5a6b 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -1,329 +1,333 @@ -/* - * StupidAI.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 "../../lib/AI_Base.h" -#include "StupidAI.h" -#include "../../lib/CStack.h" -#include "../../CCallback.h" -#include "../../lib/CCreatureHandler.h" - -static std::shared_ptr cbc; - -CStupidAI::CStupidAI() - : side(-1) - , wasWaitingForRealize(false) - , wasUnlockingGs(false) -{ - print("created"); -} - -CStupidAI::~CStupidAI() -{ - print("destroyed"); - if(cb) - { - //Restore previous state of CB - it may be shared with the main AI (like VCAI) - cb->waitTillRealize = wasWaitingForRealize; - cb->unlockGsWhenWaiting = wasUnlockingGs; - } -} - -void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - print("init called, saving ptr to IBattleCallback"); - env = ENV; - cbc = cb = CB; - - wasWaitingForRealize = CB->waitTillRealize; - wasUnlockingGs = CB->unlockGsWhenWaiting; - CB->waitTillRealize = false; - CB->unlockGsWhenWaiting = false; -} - -void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) -{ - initBattleInterface(ENV, CB); -} - -void CStupidAI::actionFinished(const BattleAction &action) -{ - print("actionFinished called"); -} - -void CStupidAI::actionStarted(const BattleAction &action) -{ - print("actionStarted called"); -} - -class EnemyInfo -{ -public: - const CStack * s; - int adi, adr; - std::vector attackFrom; //for melee fight - EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) - {} - void calcDmg(const CStack * ourStack) - { - // FIXME: provide distance info for Jousting bonus - DamageEstimation retal; - DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); - adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); - adr = static_cast((retal.damage.min + retal.damage.max) / 2); - } - - bool operator==(const EnemyInfo& ei) const - { - return s == ei.s; - } -}; - -bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) -{ - return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); -} - -static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2) -{ - int shooters[2] = {0}; //count of shooters on hexes - - for(int i = 0; i < 2; i++) - { - for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) - if(const auto * s = cbc->battleGetUnitByPos(neighbour)) - if(s->isShooter()) - shooters[i]++; - } - - return shooters[0] < shooters[1]; -} - -void CStupidAI::yourTacticPhase(int distance) -{ - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); -} - -void CStupidAI::activeStack( const CStack * stack ) -{ - //boost::this_thread::sleep(boost::posix_time::seconds(2)); - print("activeStack called for " + stack->nodeName()); - ReachabilityInfo dists = cb->getReachability(stack); - std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; - - if(stack->creatureId() == CreatureID::CATAPULT) - { - BattleAction attack; - static const std::vector wallHexes = {50, 183, 182, 130, 78, 29, 12, 95}; - auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); - attack.aimToHex(seletectedHex); - attack.actionType = EActionType::CATAPULT; - attack.side = side; - attack.stackNumber = stack->unitId(); - - cb->battleMakeUnitAction(attack); - return; - } - else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) - { - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); - return; - } - - for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY)) - { - if(cb->battleCanShoot(stack, s->getPosition())) - { - enemiesShootable.push_back(s); - } - else - { - std::vector avHexes = cb->battleGetAvailableHexes(stack, false); - - for (BattleHex hex : avHexes) - { - if(CStack::isMeleeAttackPossible(stack, s, hex)) - { - std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); - if(i == enemiesReachable.end()) - { - enemiesReachable.push_back(s); - i = enemiesReachable.begin() + (enemiesReachable.size() - 1); - } - - i->attackFrom.push_back(hex); - } - } - - if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid()) - enemiesUnreachable.push_back(s); - } - } - - for ( auto & enemy : enemiesReachable ) - enemy.calcDmg( stack ); - - for ( auto & enemy : enemiesShootable ) - enemy.calcDmg( stack ); - - if(enemiesShootable.size()) - { - const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); - cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s)); - return; - } - else if(enemiesReachable.size()) - { - const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); - cb->battleMakeUnitAction(BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters))); - return; - } - else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies - { - auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int - { - return dists.distToNearestNeighbour(stack, ei.s); - }); - - if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) - { - cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack))); - return; - } - } - - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); - return; -} - -void CStupidAI::battleAttack(const BattleAttack *ba) -{ - print("battleAttack called"); -} - -void CStupidAI::battleStacksAttacked(const std::vector & bsa, bool ranged) -{ - print("battleStacksAttacked called"); -} - -void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) -{ - print("battleEnd called"); -} - -// void CStupidAI::battleResultsApplied() -// { -// print("battleResultsApplied called"); -// } - -void CStupidAI::battleNewRoundFirst(int round) -{ - print("battleNewRoundFirst called"); -} - -void CStupidAI::battleNewRound(int round) -{ - print("battleNewRound called"); -} - -void CStupidAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) -{ - print("battleStackMoved called"); -} - -void CStupidAI::battleSpellCast(const BattleSpellCast *sc) -{ - print("battleSpellCast called"); -} - -void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse) -{ - print("battleStacksEffectsSet called"); -} - -void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) -{ - print("battleStart called"); - side = Side; -} - -void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca) -{ - print("battleCatapultAttacked called"); -} - -void CStupidAI::print(const std::string &text) const -{ - logAi->trace("CStupidAI [%p]: %s", this, text); -} - -BattleAction CStupidAI::goTowards(const CStack * stack, std::vector hexes) const -{ - auto reachability = cb->getReachability(stack); - auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); - - if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked - { - return BattleAction::makeDefend(stack); - } - - std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool - { - return reachability.distances[h1] < reachability.distances[h2]; - }); - - for(auto hex : hexes) - { - 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); - } - } - - BattleHex bestNeighbor = hexes.front(); - - if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) - { - return BattleAction::makeDefend(stack); - } - - if(stack->hasBonusOfType(BonusType::FLYING)) - { - // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. - // We just check all available hexes and pick the one closest to the target. - auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int - { - return BattleHex::getDistance(bestNeighbor, hex); - }); - - return BattleAction::makeMove(stack, *nearestAvailableHex); - } - else - { - BattleHex currentDest = bestNeighbor; - while(1) - { - if(!currentDest.isValid()) - { - logAi->error("CBattleAI::goTowards: internal error"); - return BattleAction::makeDefend(stack); - } - - if(vstd::contains(avHexes, currentDest)) - return BattleAction::makeMove(stack, currentDest); - - currentDest = reachability.predecessors[currentDest]; - } - } -} +/* + * StupidAI.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 "../../lib/AI_Base.h" +#include "StupidAI.h" +#include "../../lib/CStack.h" +#include "../../CCallback.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleInfo.h" + +static std::shared_ptr cbc; + +CStupidAI::CStupidAI() + : side(-1) + , wasWaitingForRealize(false) + , wasUnlockingGs(false) +{ + print("created"); +} + +CStupidAI::~CStupidAI() +{ + print("destroyed"); + if(cb) + { + //Restore previous state of CB - it may be shared with the main AI (like VCAI) + cb->waitTillRealize = wasWaitingForRealize; + cb->unlockGsWhenWaiting = wasUnlockingGs; + } +} + +void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + print("init called, saving ptr to IBattleCallback"); + env = ENV; + cbc = cb = CB; + + wasWaitingForRealize = CB->waitTillRealize; + wasUnlockingGs = CB->unlockGsWhenWaiting; + CB->waitTillRealize = false; + CB->unlockGsWhenWaiting = false; +} + +void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) +{ + initBattleInterface(ENV, CB); +} + +void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action) +{ + print("actionFinished called"); +} + +void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action) +{ + print("actionStarted called"); +} + +class EnemyInfo +{ +public: + const CStack * s; + int adi, adr; + std::vector attackFrom; //for melee fight + EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) + {} + void calcDmg(const BattleID & battleID, const CStack * ourStack) + { + // FIXME: provide distance info for Jousting bonus + DamageEstimation retal; + DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); + adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); + adr = static_cast((retal.damage.min + retal.damage.max) / 2); + } + + bool operator==(const EnemyInfo& ei) const + { + return s == ei.s; + } +}; + +bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) +{ + return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); +} + +static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) +{ + int shooters[2] = {0}; //count of shooters on hexes + + for(int i = 0; i < 2; i++) + { + for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) + if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour)) + if(s->isShooter()) + shooters[i]++; + } + + return shooters[0] < shooters[1]; +} + +void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); +} + +void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + //boost::this_thread::sleep_for(boost::chrono::seconds(2)); + print("activeStack called for " + stack->nodeName()); + ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack); + std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; + + if(stack->creatureId() == CreatureID::CATAPULT) + { + BattleAction attack; + static const std::vector wallHexes = {50, 183, 182, 130, 78, 29, 12, 95}; + auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); + attack.aimToHex(seletectedHex); + attack.actionType = EActionType::CATAPULT; + attack.side = side; + attack.stackNumber = stack->unitId(); + + cb->battleMakeUnitAction(battleID, attack); + return; + } + else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) + { + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return; + } + + for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY)) + { + if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition())) + { + enemiesShootable.push_back(s); + } + else + { + std::vector avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false); + + for (BattleHex hex : avHexes) + { + if(CStack::isMeleeAttackPossible(stack, s, hex)) + { + std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); + if(i == enemiesReachable.end()) + { + enemiesReachable.push_back(s); + i = enemiesReachable.begin() + (enemiesReachable.size() - 1); + } + + i->attackFrom.push_back(hex); + } + } + + if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid()) + enemiesUnreachable.push_back(s); + } + } + + for ( auto & enemy : enemiesReachable ) + enemy.calcDmg(battleID, stack); + + for ( auto & enemy : enemiesShootable ) + enemy.calcDmg(battleID, stack); + + if(enemiesShootable.size()) + { + const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); + cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s)); + return; + } + else if(enemiesReachable.size()) + { + const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); + BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);}); + + cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex)); + return; + } + else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies + { + auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int + { + return dists.distToNearestNeighbour(stack, ei.s); + }); + + if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) + { + cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack))); + return; + } + } + + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return; +} + +void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba) +{ + print("battleAttack called"); +} + +void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + print("battleStacksAttacked called"); +} + +void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) +{ + print("battleEnd called"); +} + +// void CStupidAI::battleResultsApplied() +// { +// print("battleResultsApplied called"); +// } + +void CStupidAI::battleNewRoundFirst(const BattleID & battleID) +{ + print("battleNewRoundFirst called"); +} + +void CStupidAI::battleNewRound(const BattleID & battleID) +{ + print("battleNewRound called"); +} + +void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + print("battleStackMoved called"); +} + +void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) +{ + print("battleSpellCast called"); +} + +void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + 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) +{ + print("battleStart called"); + side = Side; +} + +void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + print("battleCatapultAttacked called"); +} + +void CStupidAI::print(const std::string &text) const +{ + logAi->trace("CStupidAI [%p]: %s", this, text); +} + +BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const +{ + auto reachability = cb->getBattle(battleID)->getReachability(stack); + auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); + + if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked + { + return BattleAction::makeDefend(stack); + } + + std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool + { + return reachability.distances[h1] < reachability.distances[h2]; + }); + + for(auto hex : hexes) + { + 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); + } + } + + BattleHex bestNeighbor = hexes.front(); + + if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) + { + return BattleAction::makeDefend(stack); + } + + if(stack->hasBonusOfType(BonusType::FLYING)) + { + // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. + // We just check all available hexes and pick the one closest to the target. + auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int + { + return BattleHex::getDistance(bestNeighbor, hex); + }); + + return BattleAction::makeMove(stack, *nearestAvailableHex); + } + else + { + BattleHex currentDest = bestNeighbor; + while(1) + { + if(!currentDest.isValid()) + { + logAi->error("CBattleAI::goTowards: internal error"); + return BattleAction::makeDefend(stack); + } + + if(vstd::contains(avHexes, currentDest)) + return BattleAction::makeMove(stack, currentDest); + + currentDest = reachability.predecessors[currentDest]; + } + } +} diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 6b0d230ad..7c81a864c 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -1,54 +1,56 @@ -/* - * StupidAI.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/battle/BattleHex.h" -#include "../../lib/battle/ReachabilityInfo.h" - -class EnemyInfo; - -class CStupidAI : public CBattleGameInterface -{ - int side; - std::shared_ptr cb; - std::shared_ptr env; - - bool wasWaitingForRealize; - bool wasUnlockingGs; - - void print(const std::string &text) const; -public: - CStupidAI(); - ~CStupidAI(); - - void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) 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 activeStack(const CStack * stack) override; //called when it's turn of that stack - void yourTacticPhase(int distance) override; - - void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack - void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) - void battleEnd(const BattleResult *br, QueryID queryID) override; - //void battleResultsApplied() override; //called when all effects of last battle are applied - void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; - void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - 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, bool replayAllowed) 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 - -private: - BattleAction goTowards(const CStack * stack, std::vector hexes) const; -}; - +/* + * StupidAI.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/battle/BattleHex.h" +#include "../../lib/battle/ReachabilityInfo.h" +#include "../../lib/CGameInterface.h" + +class EnemyInfo; + +class CStupidAI : public CBattleGameInterface +{ + int side; + std::shared_ptr cb; + std::shared_ptr env; + + bool wasWaitingForRealize; + bool wasUnlockingGs; + + void print(const std::string &text) const; +public: + CStupidAI(); + ~CStupidAI(); + + void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; + + void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void yourTacticPhase(const BattleID & battleID, int distance) override; + + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + //void battleResultsApplied() override; //called when all effects of last battle are applied + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + 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 battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack + +private: + BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const; +}; + diff --git a/AI/StupidAI/main.cpp b/AI/StupidAI/main.cpp index 55e2d1f01..93f2ff517 100644 --- a/AI/StupidAI/main.cpp +++ b/AI/StupidAI/main.cpp @@ -1,34 +1,34 @@ -/* - * main.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 "../../lib/AI_Base.h" -#include "StupidAI.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -static const char *g_cszAiName = "Stupid AI 0.1"; - -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "../../lib/AI_Base.h" +#include "StupidAI.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +static const char *g_cszAiName = "Stupid AI 0.1"; + +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index e73ddf191..ac68fa71b 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -1,263 +1,259 @@ -/* - * AIUtility.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 "AIUtility.h" -#include "VCAI.h" -#include "FuzzyHelper.h" -#include "Goals/Goals.h" - -#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" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - -//extern static const int3 dirs[8]; - -const CGObjectInstance * ObjectIdRef::operator->() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::operator const CGObjectInstance *() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::operator bool() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::ObjectIdRef(ObjectInstanceID _id) - : id(_id) -{ - -} - -ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj) - : id(obj->id) -{ - -} - -bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const -{ - return id < rhs.id; -} - -HeroPtr::HeroPtr(const CGHeroInstance * H) -{ - if(!H) - { - //init from nullptr should equal to default init - *this = HeroPtr(); - return; - } - - h = H; - name = h->getNameTranslated(); - hid = H->id; -// infosCount[ai->playerID][hid]++; -} - -HeroPtr::HeroPtr() -{ - h = nullptr; - hid = ObjectInstanceID(); -} - -HeroPtr::~HeroPtr() -{ -// if(hid >= 0) -// infosCount[ai->playerID][hid]--; -} - -bool HeroPtr::operator<(const HeroPtr & rhs) const -{ - return hid < rhs.hid; -} - -const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const -{ - //TODO? check if these all assertions every time we get info about hero affect efficiency - // - //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) - assert(doWeExpectNull || h); - - if(h) - { - auto obj = cb->getObj(hid); - const bool owned = obj && obj->tempOwner == ai->playerID; - - if(doWeExpectNull && !owned) - { - return nullptr; - } - else - { - assert(obj); - assert(owned); - } - } - - return h; -} - -const CGHeroInstance * HeroPtr::operator->() const -{ - return get(); -} - -bool HeroPtr::validAndSet() const -{ - return get(true); -} - -const CGHeroInstance * HeroPtr::operator*() const -{ - return get(); -} - -bool HeroPtr::operator==(const HeroPtr & rhs) const -{ - return h == rhs.get(true); -} - -bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const -{ - const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()); - const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); - - return ln->getCost() < rn->getCost(); -} - -bool isSafeToVisit(HeroPtr h, crint3 tile) -{ - return isSafeToVisit(h, fh->evaluateDanger(tile, h.get())); -} - -bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength) -{ - const ui64 heroStrength = h->getTotalStrength(); - - if(dangerStrength) - { - return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; - } - - return true; //there's no danger -} - -bool isObjectRemovable(const CGObjectInstance * obj) -{ - //FIXME: move logic to object property! - switch (obj->ID) - { - case Obj::MONSTER: - case Obj::RESOURCE: - case Obj::CAMPFIRE: - case Obj::TREASURE_CHEST: - case Obj::ARTIFACT: - case Obj::BORDERGUARD: - case Obj::FLOTSAM: - case Obj::PANDORAS_BOX: - case Obj::OCEAN_BOTTLE: - case Obj::SEA_CHEST: - case Obj::SHIPWRECK_SURVIVOR: - case Obj::SPELL_SCROLL: - return true; - break; - default: - return false; - break; - } - -} - -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) - { - return true; - } - else if(!fromWater) // do not try to board when in water sector - { - if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) - return true; - } - return false; -} - -bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder -{ - if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE) - return false; - auto gate = dynamic_cast(cb->getTile(tileToHit)->topVisitableObj()); - return !gate->passableFor(ai->playerID); -} - -bool isBlockVisitObj(const int3 & pos) -{ - if(auto obj = cb->getTopObj(pos)) - { - if(obj->isBlockedVisitable()) //we can't stand on that object - return true; - } - - return false; -} - -creInfo infoFromDC(const dwellingContent & dc) -{ - creInfo ci; - ci.count = dc.first; - ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed - if (ci.creID != -1) - { - ci.cre = VLC->creatures()->getById(ci.creID); - ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. - } - else - { - ci.cre = nullptr; - ci.level = 0; - } - return ci; -} - -bool compareHeroStrength(HeroPtr h1, HeroPtr h2) -{ - return h1->getTotalStrength() < h2->getTotalStrength(); -} - -bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) -{ - return a1->getArmyStrength() < a2->getArmyStrength(); -} - -bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) -{ - auto art1 = a1->artType; - auto art2 = a2->artType; - - if(art1->getPrice() == art2->getPrice()) - return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); - else - return art1->getPrice() > art2->getPrice(); -} +/* + * AIUtility.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 "AIUtility.h" +#include "VCAI.h" +#include "FuzzyHelper.h" +#include "Goals/Goals.h" + +#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" + +extern FuzzyHelper * fh; + +const CGObjectInstance * ObjectIdRef::operator->() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::operator const CGObjectInstance *() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::operator bool() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::ObjectIdRef(ObjectInstanceID _id) + : id(_id) +{ + +} + +ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj) + : id(obj->id) +{ + +} + +bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const +{ + return id < rhs.id; +} + +HeroPtr::HeroPtr(const CGHeroInstance * H) +{ + if(!H) + { + //init from nullptr should equal to default init + *this = HeroPtr(); + return; + } + + h = H; + name = h->getNameTranslated(); + hid = H->id; +// infosCount[ai->playerID][hid]++; +} + +HeroPtr::HeroPtr() +{ + h = nullptr; + hid = ObjectInstanceID(); +} + +HeroPtr::~HeroPtr() +{ +// if(hid >= 0) +// infosCount[ai->playerID][hid]--; +} + +bool HeroPtr::operator<(const HeroPtr & rhs) const +{ + return hid < rhs.hid; +} + +const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const +{ + //TODO? check if these all assertions every time we get info about hero affect efficiency + // + //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) + assert(doWeExpectNull || h); + + if(h) + { + auto obj = cb->getObj(hid); + const bool owned = obj && obj->tempOwner == ai->playerID; + + if(doWeExpectNull && !owned) + { + return nullptr; + } + else + { + assert(obj); + assert(owned); + } + } + + return h; +} + +const CGHeroInstance * HeroPtr::operator->() const +{ + return get(); +} + +bool HeroPtr::validAndSet() const +{ + return get(true); +} + +const CGHeroInstance * HeroPtr::operator*() const +{ + return get(); +} + +bool HeroPtr::operator==(const HeroPtr & rhs) const +{ + return h == rhs.get(true); +} + +bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const +{ + const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()); + const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); + + return ln->getCost() < rn->getCost(); +} + +bool isSafeToVisit(HeroPtr h, crint3 tile) +{ + return isSafeToVisit(h, fh->evaluateDanger(tile, h.get())); +} + +bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength) +{ + const ui64 heroStrength = h->getTotalStrength(); + + if(dangerStrength) + { + return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; + } + + return true; //there's no danger +} + +bool isObjectRemovable(const CGObjectInstance * obj) +{ + //FIXME: move logic to object property! + switch (obj->ID) + { + case Obj::MONSTER: + case Obj::RESOURCE: + case Obj::CAMPFIRE: + case Obj::TREASURE_CHEST: + case Obj::ARTIFACT: + case Obj::BORDERGUARD: + case Obj::FLOTSAM: + case Obj::PANDORAS_BOX: + case Obj::OCEAN_BOTTLE: + case Obj::SEA_CHEST: + case Obj::SHIPWRECK_SURVIVOR: + case Obj::SPELL_SCROLL: + return true; + break; + default: + return false; + break; + } + +} + +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) + { + return true; + } + else if(!fromWater) // do not try to board when in water sector + { + if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) + return true; + } + return false; +} + +bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder +{ + if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE) + return false; + auto gate = dynamic_cast(cb->getTile(tileToHit)->topVisitableObj()); + return !gate->passableFor(ai->playerID); +} + +bool isBlockVisitObj(const int3 & pos) +{ + if(auto obj = cb->getTopObj(pos)) + { + if(obj->isBlockedVisitable()) //we can't stand on that object + return true; + } + + return false; +} + +creInfo infoFromDC(const dwellingContent & dc) +{ + creInfo ci; + ci.count = dc.first; + ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed + if (ci.creID != CreatureID::NONE) + { + ci.cre = VLC->creatures()->getById(ci.creID); + ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. + } + else + { + ci.cre = nullptr; + ci.level = 0; + } + return ci; +} + +bool compareHeroStrength(HeroPtr h1, HeroPtr h2) +{ + return h1->getTotalStrength() < h2->getTotalStrength(); +} + +bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) +{ + return a1->getArmyStrength() < a2->getArmyStrength(); +} + +bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) +{ + auto art1 = a1->artType; + auto art2 = a2->artType; + + if(art1->getPrice() == art2->getPrice()) + return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); + else + return art1->getPrice() > art2->getPrice(); +} diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index ab1dc6521..2b0fe415e 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -18,6 +18,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" +class VCAI; class CCallback; struct creInfo; @@ -25,7 +26,6 @@ using crint3 = const int3 &; using crstring = const std::string &; using dwellingContent = std::pair>; -const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; const int ALLOWED_ROAMING_HEROES = 8; @@ -33,7 +33,8 @@ const int ALLOWED_ROAMING_HEROES = 8; extern const double SAFE_ATTACK_CONSTANT; extern const int GOLD_RESERVE; -extern boost::thread_specific_ptr cb; +extern thread_local CCallback * cb; +extern thread_local VCAI * ai; //provisional class for AI to store a reference to an owned hero object //checks if it's valid on access, should be used in place of const CGHeroInstance* @@ -140,7 +141,7 @@ class ObjsVector : public std::vector { }; -template +template bool objWithID(const CGObjectInstance * obj) { return obj->ID == id; @@ -192,7 +193,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev template void foreach_neighbour(const int3 & pos, const Func & foo) { - CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer + CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; diff --git a/AI/VCAI/ArmyManager.cpp b/AI/VCAI/ArmyManager.cpp index c42c580d2..d639c125e 100644 --- a/AI/VCAI/ArmyManager.cpp +++ b/AI/VCAI/ArmyManager.cpp @@ -118,12 +118,12 @@ ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDw { creInfo ci = infoFromDC(dc); - if(!ci.count || ci.creID == -1) + if(!ci.count || ci.creID == CreatureID::NONE) continue; vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford - if(ci.count && ci.creID != -1) //valid creature at this level + if(ci.count && ci.creID != CreatureID::NONE) //valid creature at this level { //can be merged with another stack? SlotID dst = h->getSlotFor(ci.creID); diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index 083abe9c4..f5529777f 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -38,7 +38,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID for (BuildingID buildID : toBuild) { - EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); + EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::HAVE_CAPITAL || canBuild == EBuildingState::FORBIDDEN || canBuild == EBuildingState::NO_WATER) return false; //we won't be able to build this } @@ -52,7 +52,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID { const CBuilding * b = t->town->buildings.at(buildID); - EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); + EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::ALLOWED) { PotentialBuilding pb; @@ -222,7 +222,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) std::vector extraBuildings; for (auto buildingInfo : t->town->buildings) { - if (buildingInfo.first > 43) + if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST) extraBuildings.push_back(buildingInfo.first); } return tryBuildAnyStructure(t, extraBuildings); diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 39003ee2f..f4702f3a7 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -18,9 +18,6 @@ #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 -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - engineBase::engineBase() { rules = new fl::RuleBlock(); @@ -409,7 +406,7 @@ float VisitObjEngine::evaluate(Goals::VisitObj & goal) else { MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0); - logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue)); + logGlobal->error("AI met object type it doesn't know - ID: %d, subID: %d - adding to database with value %d ", obj->ID, obj->subID, objValue); } setSharedFuzzyVariables(goal); diff --git a/AI/VCAI/FuzzyEngines.h b/AI/VCAI/FuzzyEngines.h index f09920da7..63e8279bc 100644 --- a/AI/VCAI/FuzzyEngines.h +++ b/AI/VCAI/FuzzyEngines.h @@ -8,7 +8,11 @@ * */ #pragma once -#include +#if __has_include() +# include +#else +# include +#endif #include "Goals/AbstractGoal.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index e6973b597..a47cbafa0 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -23,9 +23,6 @@ FuzzyHelper * fh; -extern boost::thread_specific_ptr ai; -extern boost::thread_specific_ptr cb; - Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) { if(vec.empty()) @@ -69,7 +66,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); @@ -216,7 +213,7 @@ void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pa ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor) { - return evaluateDanger(tile, visitor, ai.get()); + return evaluateDanger(tile, visitor, ai); } ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai) @@ -285,7 +282,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) { auto cb = ai->myCb; - if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat + if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat return 0; switch(obj->ID) @@ -327,15 +324,8 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); case Obj::PYRAMID: - { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; - } + return estimateBankDanger(dynamic_cast(obj)); default: return 0; } diff --git a/AI/VCAI/FuzzyHelper.h b/AI/VCAI/FuzzyHelper.h index 6973df463..7542e7f70 100644 --- a/AI/VCAI/FuzzyHelper.h +++ b/AI/VCAI/FuzzyHelper.h @@ -51,3 +51,5 @@ public: ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai); ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor); }; + +extern FuzzyHelper * fh; diff --git a/AI/VCAI/Goals/AbstractGoal.cpp b/AI/VCAI/Goals/AbstractGoal.cpp index 87a04f089..3ed71f08d 100644 --- a/AI/VCAI/Goals/AbstractGoal.cpp +++ b/AI/VCAI/Goals/AbstractGoal.cpp @@ -14,11 +14,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/AdventureSpellCast.cpp b/AI/VCAI/Goals/AdventureSpellCast.cpp index c67d81d66..7c3f7500f 100644 --- a/AI/VCAI/Goals/AdventureSpellCast.cpp +++ b/AI/VCAI/Goals/AdventureSpellCast.cpp @@ -14,10 +14,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CGTownInstance.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const diff --git a/AI/VCAI/Goals/Build.cpp b/AI/VCAI/Goals/Build.cpp index 3bd78b9ef..4a57d0a5c 100644 --- a/AI/VCAI/Goals/Build.cpp +++ b/AI/VCAI/Goals/Build.cpp @@ -17,12 +17,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/BuildBoat.cpp b/AI/VCAI/Goals/BuildBoat.cpp index 76e76791f..9f7f452ca 100644 --- a/AI/VCAI/Goals/BuildBoat.cpp +++ b/AI/VCAI/Goals/BuildBoat.cpp @@ -13,10 +13,6 @@ #include "../FuzzyHelper.h" #include "../AIhelper.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool BuildBoat::operator==(const BuildBoat & other) const diff --git a/AI/VCAI/Goals/BuildThis.cpp b/AI/VCAI/Goals/BuildThis.cpp index 9a62539a7..065db0b01 100644 --- a/AI/VCAI/Goals/BuildThis.cpp +++ b/AI/VCAI/Goals/BuildThis.cpp @@ -16,12 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/BuyArmy.cpp b/AI/VCAI/Goals/BuyArmy.cpp index f442c3dc5..7c3facb29 100644 --- a/AI/VCAI/Goals/BuyArmy.cpp +++ b/AI/VCAI/Goals/BuyArmy.cpp @@ -13,11 +13,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CGTownInstance.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool BuyArmy::operator==(const BuyArmy & other) const diff --git a/AI/VCAI/Goals/ClearWayTo.cpp b/AI/VCAI/Goals/ClearWayTo.cpp index e20c26ad5..ddea93732 100644 --- a/AI/VCAI/Goals/ClearWayTo.cpp +++ b/AI/VCAI/Goals/ClearWayTo.cpp @@ -16,11 +16,6 @@ #include "../FuzzyHelper.h" #include "../AIhelper.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool ClearWayTo::operator==(const ClearWayTo & other) const diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index 35d66413d..d4513dbeb 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -16,12 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGMarket.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; @@ -48,10 +43,10 @@ TGoalVec CollectRes::getAllPossibleSubgoals() return resID == GameResID(EGameResID::GOLD); break; case Obj::RESOURCE: - return obj->subID == resID; + return dynamic_cast(obj)->resourceID() == GameResID(resID); break; case Obj::MINE: - return (obj->subID == resID && + return (dynamic_cast(obj)->producedResource == GameResID(resID) && (cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines break; case Obj::CAMPFIRE: @@ -65,14 +60,11 @@ TGoalVec CollectRes::getAllPossibleSubgoals() return false; } break; - case Obj::WATER_WHEEL: - if (resID != GameResID(EGameResID::GOLD)) - return false; - break; case Obj::MYSTICAL_GARDEN: if ((resID != GameResID(EGameResID::GOLD)) && (resID != GameResID(EGameResID::GEMS))) return false; break; + case Obj::WATER_WHEEL: case Obj::LEAN_TO: case Obj::WAGON: if (resID != GameResID(EGameResID::GOLD)) @@ -175,10 +167,10 @@ TSubgoal CollectRes::whatToDoToTrade() int howManyCanWeBuy = 0; for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) { - if (GameResID(i) == resID) + if (i.getNum() == resID) continue; int toGive = -1, toReceive = -1; - m->getOffer(GameResID(i), resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); + m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); assert(toGive > 0 && toReceive > 0); howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive); } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 164aacb3c..cb78ee8db 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -14,10 +14,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CQuest.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool CompleteQuest::operator==(const CompleteQuest & other) const @@ -25,48 +21,43 @@ bool CompleteQuest::operator==(const CompleteQuest & other) const return q.quest->qid == other.q.quest->qid; } +bool isKeyMaster(const QuestInfo & q) +{ + return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD); +} + TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; - if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) + if(!q.quest->isCompleted) { logAi->debug("Trying to realize quest: %s", questToString()); - - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: - return missionArt(); - - case CQuest::MISSION_HERO: - return missionHero(); - - case CQuest::MISSION_ARMY: - return missionArmy(); - - case CQuest::MISSION_RESOURCES: - return missionResources(); - - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: - return missionDestroyObj(); - - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); - - case CQuest::MISSION_LEVEL: - return missionLevel(); - - case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - - break; - case CQuest::MISSION_KEYMASTER: + if(isKeyMaster(q)) return missionKeymaster(); - } //end of switch + if(!q.quest->mission.artifacts.empty()) + return missionArt(); + + if(!q.quest->mission.heroes.empty()) + return missionHero(); + + if(!q.quest->mission.creatures.empty()) + return missionArmy(); + + if(q.quest->mission.resources.nonZero()) + return missionResources(); + + if(q.quest->killTarget != ObjectInstanceID::NONE) + return missionDestroyObj(); + + for(auto & s : q.quest->mission.primary) + if(s) + return missionIncreasePrimaryStat(); + + if(q.quest->mission.heroLevel > 0) + return missionLevel(); } return TGoalVec(); @@ -74,7 +65,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() TSubgoal CompleteQuest::whatToDoToAchieve() { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->mission == Rewardable::Limiter{}) { throw cannotFulfillGoalException("Can not complete inactive quest"); } @@ -108,7 +99,7 @@ std::string CompleteQuest::completeMessage() const std::string CompleteQuest::questToString() const { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; @@ -141,7 +132,7 @@ TGoalVec CompleteQuest::missionArt() const if(!solutions.empty()) return solutions; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } @@ -169,7 +160,7 @@ TGoalVec CompleteQuest::missionArmy() const if(!solutions.empty()) return solutions; - for(auto creature : q.quest->m6creatures) + for(auto creature : q.quest->mission.creatures) { solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); } @@ -183,7 +174,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const if(solutions.empty()) { - for(int i = 0; i < q.quest->m2stats.size(); ++i) + for(int i = 0; i < q.quest->mission.primary.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); @@ -199,7 +190,7 @@ TGoalVec CompleteQuest::missionLevel() const if(solutions.empty()) { - logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); + logAi->debug("Don't know how to reach hero level %d", q.quest->mission.heroLevel); } return solutions; @@ -231,10 +222,10 @@ TGoalVec CompleteQuest::missionResources() const } else { - for(int i = 0; i < q.quest->m7resources.size(); ++i) + for(int i = 0; i < q.quest->mission.resources.size(); ++i) { - if(q.quest->m7resources[i]) - solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->m7resources[i]))); + if(q.quest->mission.resources[i]) + solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->mission.resources[i]))); } } } @@ -250,7 +241,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const { TGoalVec solutions; - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return ai->ah->howToVisitObj(q.obj); diff --git a/AI/VCAI/Goals/Conquer.cpp b/AI/VCAI/Goals/Conquer.cpp index 3f9f4b16c..bf0d368f1 100644 --- a/AI/VCAI/Goals/Conquer.cpp +++ b/AI/VCAI/Goals/Conquer.cpp @@ -15,12 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/DigAtTile.cpp b/AI/VCAI/Goals/DigAtTile.cpp index 230d62a5e..2ff0343e9 100644 --- a/AI/VCAI/Goals/DigAtTile.cpp +++ b/AI/VCAI/Goals/DigAtTile.cpp @@ -13,11 +13,6 @@ #include "../VCAI.h" #include "../AIUtility.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const diff --git a/AI/VCAI/Goals/Explore.cpp b/AI/VCAI/Goals/Explore.cpp index 777b1a8ae..4c25c74c0 100644 --- a/AI/VCAI/Goals/Explore.cpp +++ b/AI/VCAI/Goals/Explore.cpp @@ -15,13 +15,9 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" #include "../../../lib/CPlayerState.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; namespace Goals @@ -41,8 +37,8 @@ namespace Goals ExplorationHelper(HeroPtr h, bool gatherArmy) { - cbp = cb.get(); - aip = ai.get(); + cbp = cb; + aip = ai; hero = h; ts = cbp->getPlayerTeam(ai->playerID); sightRadius = hero->getSightRadius(); diff --git a/AI/VCAI/Goals/FindObj.cpp b/AI/VCAI/Goals/FindObj.cpp index 4588ca4e5..189a5c44b 100644 --- a/AI/VCAI/Goals/FindObj.cpp +++ b/AI/VCAI/Goals/FindObj.cpp @@ -14,10 +14,6 @@ #include "../VCAI.h" #include "../AIUtility.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool FindObj::operator==(const FindObj & other) const @@ -32,7 +28,7 @@ TSubgoal FindObj::whatToDoToAchieve() { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == objid && obj->subID == resID) + if(obj->ID.getNum() == objid && obj->subID == resID) { o = obj; break; //TODO: consider multiple objects and choose best @@ -43,7 +39,7 @@ TSubgoal FindObj::whatToDoToAchieve() { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == objid) + if(obj->ID.getNum() == objid) { o = obj; break; //TODO: consider multiple objects and choose best @@ -63,7 +59,7 @@ bool FindObj::fulfillsMe(TSubgoal goal) if (!hero || hero == goal->hero) for (auto obj : cb->getVisitableObjs(goal->tile)) //check if any object on that tile matches criteria if (obj->visitablePos() == goal->tile) //object could be removed - if (obj->ID == objid && obj->subID == resID) //same type and subtype + if (obj->ID.getNum() == objid && obj->subID == resID) //same type and subtype return true; } return false; diff --git a/AI/VCAI/Goals/GatherArmy.cpp b/AI/VCAI/Goals/GatherArmy.cpp index c36e282e5..e6e0557f0 100644 --- a/AI/VCAI/Goals/GatherArmy.cpp +++ b/AI/VCAI/Goals/GatherArmy.cpp @@ -16,12 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index a93960237..ed4e1a6c0 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -16,12 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; @@ -93,7 +88,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() } auto creature = VLC->creatures()->getByIndex(objid); - if(t->subID == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O + if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O { auto tryFindCreature = [&]() -> std::optional> { @@ -139,7 +134,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() { for(auto type : creature.second) { - if(type == objid && ai->ah->freeResources().canAfford(VLC->creatures()->getById(type)->getFullRecruitCost())) + if(type.getNum() == objid && ai->ah->freeResources().canAfford(VLC->creatures()->getById(type)->getFullRecruitCost())) vstd::concatenate(solutions, ai->ah->howToVisitObj(obj)); } } diff --git a/AI/VCAI/Goals/GetArtOfType.cpp b/AI/VCAI/Goals/GetArtOfType.cpp index 5195796ec..2f0ef2227 100644 --- a/AI/VCAI/Goals/GetArtOfType.cpp +++ b/AI/VCAI/Goals/GetArtOfType.cpp @@ -13,11 +13,6 @@ #include "../VCAI.h" #include "../AIUtility.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool GetArtOfType::operator==(const GetArtOfType & other) const @@ -28,4 +23,4 @@ bool GetArtOfType::operator==(const GetArtOfType & other) const TSubgoal GetArtOfType::whatToDoToAchieve() { return sptr(FindObj(Obj::ARTIFACT, aid)); -} \ No newline at end of file +} diff --git a/AI/VCAI/Goals/RecruitHero.cpp b/AI/VCAI/Goals/RecruitHero.cpp index 73f24762c..971260202 100644 --- a/AI/VCAI/Goals/RecruitHero.cpp +++ b/AI/VCAI/Goals/RecruitHero.cpp @@ -15,12 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/VisitHero.cpp b/AI/VCAI/Goals/VisitHero.cpp index efb4840d3..5e57743fd 100644 --- a/AI/VCAI/Goals/VisitHero.cpp +++ b/AI/VCAI/Goals/VisitHero.cpp @@ -18,10 +18,6 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool VisitHero::operator==(const VisitHero & other) const diff --git a/AI/VCAI/Goals/VisitObj.cpp b/AI/VCAI/Goals/VisitObj.cpp index 5aab8cf06..a31b37af0 100644 --- a/AI/VCAI/Goals/VisitObj.cpp +++ b/AI/VCAI/Goals/VisitObj.cpp @@ -15,12 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/VisitTile.cpp b/AI/VCAI/Goals/VisitTile.cpp index eaec6efbb..4e036fbd4 100644 --- a/AI/VCAI/Goals/VisitTile.cpp +++ b/AI/VCAI/Goals/VisitTile.cpp @@ -15,12 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index 4b4958ed7..d2ab613e3 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -17,12 +17,7 @@ #include "../BuildingManager.h" #include "../../../lib/mapping/CMapHeader.h" //for victory conditions #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" - - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; +#include "../../../lib/constants/StringConstants.h" using namespace Goals; @@ -56,18 +51,18 @@ TSubgoal Win::whatToDoToAchieve() switch(goal.condition) { case EventCondition::HAVE_ARTIFACT: - return sptr(GetArtOfType(goal.objectType)); + return sptr(GetArtOfType(goal.objectType.as())); case EventCondition::DESTROY: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto obj = cb->getObj(goal.object->id); + auto obj = cb->getObj(goal.objectID); if(obj) if(obj->getOwner() == ai->playerID) //we can't capture our own object return sptr(Conquer()); - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { @@ -83,7 +78,7 @@ TSubgoal Win::whatToDoToAchieve() // 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) - if(goal.objectType == BuildingID::GRAIL) + if(goal.objectType.as() == BuildingID::GRAIL) { if(auto h = ai->getHeroWithGrail()) { @@ -129,13 +124,13 @@ TSubgoal Win::whatToDoToAchieve() } case EventCondition::CONTROL: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto objRelations = cb->getPlayerRelations(ai->playerID, goal.object->tempOwner); + auto obj = cb->getObj(goal.objectID); - if(objRelations == PlayerRelations::ENEMIES) + if(obj && cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES) { - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { @@ -154,9 +149,9 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::HAVE_RESOURCES: //TODO mines? piles? marketplace? //save? - return sptr(CollectRes(static_cast(goal.objectType), goal.value)); + return sptr(CollectRes(goal.objectType.as(), goal.value)); case EventCondition::HAVE_CREATURES: - return sptr(GatherTroops(goal.objectType, goal.value)); + return sptr(GatherTroops(goal.objectType.as(), goal.value)); case EventCondition::TRANSPORT: { //TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it @@ -178,11 +173,6 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::CONST_VALUE: break; - case EventCondition::HAVE_0: - case EventCondition::HAVE_BUILDING_0: - case EventCondition::DESTROY_0: - //TODO: support new condition format - return sptr(Conquer()); default: assert(0); } diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index eee053ac9..8e7bc4a8b 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -250,7 +250,7 @@ void AINodeStorage::calculateTownPortalTeleportations( return; } - if(skillLevel < SecSkillLevel::ADVANCED) + if(skillLevel < MasteryLevel::ADVANCED) { const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int { diff --git a/AI/VCAI/Pathfinding/PathfindingManager.cpp b/AI/VCAI/Pathfinding/PathfindingManager.cpp index e418a6dd3..c983150bc 100644 --- a/AI/VCAI/Pathfinding/PathfindingManager.cpp +++ b/AI/VCAI/Pathfinding/PathfindingManager.cpp @@ -190,7 +190,7 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet if(isBlockedBorderGate(firstTileToGet)) { //FIXME: this way we'll not visit gate and activate quest :? - return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID)); + return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->getObjTypeIndex())); } auto topObj = cb->getTopObj(firstTileToGet); @@ -238,6 +238,6 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet void PathfindingManager::updatePaths(std::vector heroes) { - logAi->debug("AIPathfinder has been reseted."); + logAi->debug("AIPathfinder has been reset."); pathfinder->updatePaths(heroes); } diff --git a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp index eaf4459a6..9603189fc 100644 --- a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -77,7 +77,7 @@ namespace AIPathfinding auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) + && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoat.reset(new SummonBoatAction()); diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index c8c3f3a33..b69c4e9bc 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -59,19 +59,7 @@ TResources ResourceManager::estimateIncome() const if (obj->ID == Obj::MINE) { auto mine = dynamic_cast(obj); - switch (mine->producedResource.toEnum()) - { - case EGameResID::WOOD: - case EGameResID::ORE: - ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; - break; - case EGameResID::GOLD: - ret[EGameResID::GOLD] += GOLD_MINE_PRODUCTION; - break; - default: - ret[obj->subID] += RESOURCE_MINE_PRODUCTION; - break; - } + ret += mine->dailyIncome(); } } @@ -90,7 +78,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o { auto allResources = cb->getResourceAmount(); auto income = estimateIncome(); - GameResID resourceType = EGameResID::INVALID; + GameResID resourceType = EGameResID::NONE; TResource amountToCollect = 0; using resPair = std::pair; @@ -129,7 +117,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o break; } } - if (resourceType == EGameResID::INVALID) //no needed resources has 0 income, + if (resourceType == EGameResID::NONE) //no needed resources has 0 income, { //find the one which takes longest to collect using timePair = std::pair; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index f20ceda1f..c0b4efa3a 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1,2893 +1,2914 @@ -/* - * VCAI.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 "VCAI.h" -#include "FuzzyHelper.h" -#include "ResourceManager.h" -#include "BuildingManager.h" -#include "Goals/Goals.h" - -#include "../../lib/UnlockGuard.h" -#include "../../lib/mapObjects/MapObjects.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacksBase.h" -#include "../../lib/NetPacks.h" -#include "../../lib/bonuses/CBonusSystemNode.h" -#include "../../lib/bonuses/Limiters.h" -#include "../../lib/bonuses/Updaters.h" -#include "../../lib/bonuses/Propagators.h" -#include "../../lib/serializer/CTypeList.h" -#include "../../lib/serializer/BinarySerializer.h" -#include "../../lib/serializer/BinaryDeserializer.h" - -#include "AIhelper.h" - -extern FuzzyHelper * fh; - -const double SAFE_ATTACK_CONSTANT = 1.5; - -//one thread may be turn of AI and another will be handling a side effect for AI2 -boost::thread_specific_ptr cb; -boost::thread_specific_ptr ai; - -//std::map > HeroView::infosCount; - -//helper RAII to manage global ai/cb ptrs -struct SetGlobalState -{ - SetGlobalState(VCAI * AI) - { - assert(!ai.get()); - assert(!cb.get()); - - ai.reset(AI); - cb.reset(AI->myCb.get()); - } - ~SetGlobalState() - { - //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully - //TODO: to ensure that, make rm unique_ptr - ai.release(); - cb.release(); - } -}; - - -#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); - -#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) -#define MAKING_TURN SET_GLOBAL_STATE(this) - -VCAI::VCAI() -{ - LOG_TRACE(logAi); - makingTurn = nullptr; - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - - ah = new AIhelper(); - ah->setAI(this); -} - -VCAI::~VCAI() -{ - delete ah; - LOG_TRACE(logAi); - finish(); -} - -void VCAI::availableCreaturesChanged(const CGDwelling * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroMoved(const TryMoveHero & details, bool verbose) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - //enemy hero may have left visible area - validateObject(details.id); - auto hero = cb->getHero(details.id); - - const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; - const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); - - const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); - const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose)); - - if(details.result == TryMoveHero::TELEPORTATION) - { - auto t1 = dynamic_cast(o1); - auto t2 = dynamic_cast(o2); - if(t1 && t2) - { - if(cb->isTeleportChannelBidirectional(t1->channel)) - { - if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels - { - knownSubterraneanGates[o1] = o2; - knownSubterraneanGates[o2] = o1; - logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString()); - } - } - } - //FIXME: teleports are not correctly visited - unreserveObject(hero, t1); - unreserveObject(hero, t2); - } - else if(details.result == TryMoveHero::EMBARK && hero) - { - //make sure AI not attempt to visit used boat - validateObject(hero->boat); - } - else if(details.result == TryMoveHero::DISEMBARK && o1) - { - auto boat = dynamic_cast(o1); - if(boat) - addVisitableObj(boat); - } -} - -void VCAI::heroInGarrisonChange(const CGTownInstance * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::centerView(int3 pos, int focusTime) -{ - LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime); - NET_EVENT_HANDLER; -} - -void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactAssembled(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showTavernWindow(const CGObjectInstance * townOrTavern) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::playerBlocked(int reason, bool start) -{ - LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start); - NET_EVENT_HANDLER; - if(start && reason == PlayerBlocked::UPCOMING_BATTLE) - status.setBattle(UPCOMING_BATTLE); - - if(reason == PlayerBlocked::ONGOING_MOVEMENT) - status.setMove(start); -} - -void VCAI::showPuzzleMap() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showShipyardDialog(const IShipyard * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) -{ - LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); - NET_EVENT_HANDLER; - logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost")); - if(player == playerID) - { - if(victoryLossCheckResult.victory()) - { - logAi->debug("VCAI: I won! Incredible!"); - logAi->debug("Turn nr %d", myCb->getDate()); - } - else - { - logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr()); - } - - finish(); - } -} - -void VCAI::artifactPut(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactRemoved(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactDisassembled(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) -{ - LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a"))); - NET_EVENT_HANDLER; - - if(start && visitedObj) //we can end visit with null object, anyway - { - markObjectVisited(visitedObj); - unreserveObject(visitor, visitedObj); - completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore - //TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..) - if (visitedObj->ID == Obj::HERO) - { - visitedHeroes[visitor].insert(HeroPtr(dynamic_cast(visitedObj))); - } - } - - status.heroVisit(visitedObj, start); -} - -void VCAI::availableArtifactsChanged(const CGBlackMarket * bm) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - //buildArmyIn(town); - //moveCreaturesToHero(town); -} - -void VCAI::tileHidden(const std::unordered_set & pos) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - validateVisitableObjs(); - clearPathsInfo(); -} - -void VCAI::tileRevealed(const std::unordered_set & pos) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - for(int3 tile : pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) - addVisitableObj(obj); - } - - clearPathsInfo(); -} - -void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - auto firstHero = cb->getHero(hero1); - auto secondHero = cb->getHero(hero2); - - status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner)); - - requestActionASAP([=]() - { - float goalpriority1 = 0, goalpriority2 = 0; - - auto firstGoal = getGoal(firstHero); - if(firstGoal->goalType == Goals::GATHER_ARMY) - goalpriority1 = firstGoal->priority; - auto secondGoal = getGoal(secondHero); - if(secondGoal->goalType == Goals::GATHER_ARMY) - goalpriority2 = secondGoal->priority; - - auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void - { - this->pickBestCreatures(h1, h2); - this->pickBestArtifacts(h1, h2); - }; - - //Do not attempt army or artifacts exchange if we visited ally player - //Visits can still be useful if hero have skills like Scholar - if(firstHero->tempOwner != secondHero->tempOwner) - { - logAi->debug("Heroes owned by different players. Do not exchange army or artifacts."); - } - else if(goalpriority1 > goalpriority2) - { - transferFrom2to1(firstHero, secondHero); - } - else if(goalpriority1 < goalpriority2) - { - transferFrom2to1(secondHero, firstHero); - } - else //regular criteria - { - if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero)) - transferFrom2to1(firstHero, secondHero); - else if(ah->canGetArmy(secondHero, firstHero)) - transferFrom2to1(secondHero, firstHero); - } - - completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime? - completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum()))); - - answerQuery(query, 0); - }); -} - -void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) -{ - LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val); - NET_EVENT_HANDLER; -} - -void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) -{ - LOG_TRACE_PARAMS(logAi, "level '%i'", level); - NET_EVENT_HANDLER; -} - -void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::newObject(const CGObjectInstance * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(obj->isVisitable()) - addVisitableObj(obj); -} - -//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight -//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes -void VCAI::objectRemoved(const CGObjectInstance * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - vstd::erase_if_present(visitableObjs, obj); - vstd::erase_if_present(alreadyVisited, obj); - - for(auto h : cb->getHeroesInfo()) - unreserveObject(h, obj); - - std::function checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool - { - if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum())) - return true; - else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal - return true; - else - return false; - }; - - //clear VCAI / main loop caches - vstd::erase_if(lockedHeroes, [&](const std::pair & x) -> bool - { - return checkRemovalValidity(x.second); - }); - - vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool - { - return checkRemovalValidity(x.first); - }); - - vstd::erase_if(basicGoals, checkRemovalValidity); - vstd::erase_if(goalsToAdd, checkRemovalValidity); - vstd::erase_if(goalsToRemove, checkRemovalValidity); - - for(auto goal : ultimateGoalsFromBasic) - vstd::erase_if(goal.second, checkRemovalValidity); - - //clear resource manager goal cache - ah->removeOutdatedObjectives(checkRemovalValidity); - - //TODO: Find better way to handle hero boat removal - if(auto hero = dynamic_cast(obj)) - { - if(hero->boat) - { - vstd::erase_if_present(visitableObjs, hero->boat); - vstd::erase_if_present(alreadyVisited, hero->boat); - - for(auto h : cb->getHeroesInfo()) - unreserveObject(h, hero->boat); - } - } - - //TODO - //there are other places where CGObjectinstance ptrs are stored... - // - - if(obj->ID == Obj::HERO && obj->tempOwner == playerID) - { - lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion - } -} - -void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - requestActionASAP([=]() - { - makePossibleUpgrades(visitor); - }); -} - -void VCAI::playerBonusChanged(const Bonus & bonus, bool gain) -{ - LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); - NET_EVENT_HANDLER; -} - -void VCAI::heroCreated(const CGHeroInstance * h) -{ - LOG_TRACE(logAi); - if(h->visitedTown) - townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown); - NET_EVENT_HANDLER; -} - -void VCAI::advmapSpellCast(const CGHeroInstance * caster, int spellID) -{ - LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); - NET_EVENT_HANDLER; -} - -void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) -{ - LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID); - NET_EVENT_HANDLER; -} - -void VCAI::requestRealized(PackageApplied * pa) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(status.haveTurn()) - { - if(pa->packType == typeList.getTypeID()) - { - if(pa->result) - status.madeTurn(); - } - } - - if(pa->packType == typeList.getTypeID()) - { - status.receivedAnswerConfirmation(pa->requestID, pa->result); - } -} - -void VCAI::receivedResource() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) -{ - LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val); - NET_EVENT_HANDLER; -} - -void VCAI::battleResultsApplied() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - assert(status.getBattle() == ENDING_BATTLE); - status.setBattle(NO_BATTLE); -} - -void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop) -{ - -} - -void VCAI::objectPropertyChanged(const SetObjectProperty * sop) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(sop->what == ObjProperty::OWNER) - { - if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) - { - //we want to visit objects owned by oppponents - 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 - vstd::erase_if_present(alreadyVisited, obj); - } - } - } -} - -void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) -{ - LOG_TRACE_PARAMS(logAi, "what '%i'", what); - NET_EVENT_HANDLER; - - if(town->getOwner() == playerID && what == 1) //built - completeGoal(sptr(Goals::BuildThis(buildingID, town))); -} - -void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) -{ - LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); - NET_EVENT_HANDLER; -} - -void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) -{ - //TODO: AI support for ViewXXX spell - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - LOG_TRACE(logAi); - env = ENV; - myCb = CB; - cbc = CB; - - ah->init(CB.get()); - - NET_EVENT_HANDLER; //sets ah->rm->cb - playerID = *myCb->getMyColor(); - myCb->waitTillRealize = true; - myCb->unlockGsWhenWaiting = true; - - if(!fh) - fh = new FuzzyHelper(); - - retrieveVisitableObjs(); -} - -void VCAI::yourTurn() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - status.startedTurn(); - makingTurn = std::make_unique(&VCAI::makeTurn, this); -} - -void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); - NET_EVENT_HANDLER; - status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level)); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); - NET_EVENT_HANDLER; - status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level)); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) -{ - LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel); - NET_EVENT_HANDLER; - int sel = 0; - status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s") - % components.size() % text)); - - if(selection) //select from multiple components -> take the last one (they're indexed [1-size]) - sel = static_cast(components.size()); - - if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :) - sel = 1; - - requestActionASAP([=]() - { - answerQuery(askID, sel); - }); -} - -void VCAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ -// LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits); - NET_EVENT_HANDLER; - status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") - % exits.size())); - - int choosenExit = -1; - if(impassable) - { - knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; - } - else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid()) - { - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); - } - - for(auto exit : exits) - { - if(status.channelProbing() && exit.first == destinationTeleport) - { - choosenExit = vstd::find_pos(exits, exit); - break; - } - else - { - // TODO: Implement checking if visiting that teleport will uncovert any FoW - // So far this is the best option to handle decision about probing - auto obj = cb->getObj(exit.first, false); - if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first)) - { - if(exit.first != destinationTeleport) - teleportChannelProbingList.push_back(exit.first); - } - } - } - - requestActionASAP([=]() - { - answerQuery(askID, choosenExit); - }); -} - -void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); - NET_EVENT_HANDLER; - - std::string s1 = up ? up->nodeName() : "NONE"; - std::string s2 = down ? down->nodeName() : "NONE"; - - status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); - - //you can't request action from action-response thread - requestActionASAP([=]() - { - if(removableUnits) - pickBestCreatures(down, up); - - answerQuery(queryID, 0); - }); -} - -void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - NET_EVENT_HANDLER; - status.addQuery(askID, "Map object select query"); - requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); -} - -void VCAI::saveGame(BinarySerializer & h, const int version) -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - NET_EVENT_HANDLER; - validateVisitableObjs(); - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::saveGame(h, version); - serializeInternal(h, version); -} - -void VCAI::loadGame(BinaryDeserializer & h, const int version) -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - //NET_EVENT_HANDLER; - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::loadGame(h, version); - serializeInternal(h, version); -} - -void makePossibleUpgrades(const CArmedInstance * obj) -{ - if(!obj) - return; - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) - { - UpgradeInfo ui; - cb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) - { - cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); - } - } - } -} - -void VCAI::makeTurn() -{ - MAKING_TURN; - - auto day = cb->getDate(Date::EDateType::DAY); - logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); - - boost::shared_lock gsLock(CGameState::mutex); - setThreadName("VCAI::makeTurn"); - - switch(cb->getDate(Date::DAY_OF_WEEK)) - { - case 1: - { - townVisitsThisWeek.clear(); - std::vector objs; - retrieveVisitableObjs(objs, true); - for(const CGObjectInstance * obj : objs) - { - if(isWeeklyRevisitable(obj)) - { - addVisitableObj(obj); - vstd::erase_if_present(alreadyVisited, obj); - } - } - break; - } - } - markHeroAbleToExplore(primaryHero()); - visitedHeroes.clear(); - - try - { - //it looks messy here, but it's better to have armed heroes before attempting realizing goals - for (const CGTownInstance * t : cb->getTownsInfo()) - moveCreaturesToHero(t); - - mainLoop(); - - /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. - Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/ - performTypicalActions(); - - //for debug purpose - for (auto h : cb->getHeroesInfo()) - { - if (h->movementPointsRemaining()) - logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); - } - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); - return; - } - catch (std::exception & e) - { - logAi->debug("Making turn thread has caught an exception: %s", e.what()); - } - - endTurn(); -} - -std::vector VCAI::getMyHeroes() const -{ - std::vector ret; - - for(auto h : cb->getHeroesInfo()) - { - ret.push_back(h); - } - - return ret; -} - -void VCAI::mainLoop() -{ - std::vector elementarGoals; //no duplicates allowed (operator ==) - basicGoals.clear(); - - validateVisitableObjs(); - - //get all potential and saved goals - //TODO: not lose - basicGoals.push_back(sptr(Goals::Win())); - for (auto goalPair : lockedHeroes) - { - fh->setPriority(goalPair.second); //re-evaluate, as heroes moved in the meantime - basicGoals.push_back(goalPair.second); - } - if (ah->hasTasksLeft()) - basicGoals.push_back(ah->whatToDo()); - for (auto quest : myCb->getMyQuests()) - { - basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); - } - basicGoals.push_back(sptr(Goals::Build())); - - invalidPathHeroes.clear(); - - for (int pass = 0; pass< 30 && basicGoals.size(); pass++) - { - vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice - goalsToAdd.clear(); - goalsToRemove.clear(); - elementarGoals.clear(); - ultimateGoalsFromBasic.clear(); - - ah->updatePaths(getMyHeroes()); - - logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size()); - - for (auto basicGoal : basicGoals) - { - logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name()); - - auto goalToDecompose = basicGoal; - Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); - int maxAbstractGoals = 10; - while (!elementarGoal->isElementar && maxAbstractGoals) - { - try - { - elementarGoal = decomposeGoal(goalToDecompose); - } - catch (goalFulfilledException & e) - { - //it is impossible to continue some goals (like exploration, for example) - //complete abstract goal for now, but maybe main goal finds another path - logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); - completeGoal(e.goal); //put in goalsToRemove - break; - } - catch(cannotFulfillGoalException & e) - { - //it is impossible to continue some goals (like exploration, for example) - //complete abstract goal for now, but maybe main goal finds another path - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what()); - break; - } - catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree - { - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); - break; - } - if (elementarGoal->isAbstract) //we can decompose it further - { - goalsToAdd.push_back(elementarGoal); - //decompose further now - this is necesssary if we can't add over 10 goals in the pool - goalToDecompose = elementarGoal; - //there is a risk of infinite abstract goal loop, though it indicates failed logic - maxAbstractGoals--; - } - else if (elementarGoal->isElementar) //should be - { - logAi->debug("Found elementar goal %s", elementarGoal->name()); - elementarGoals.push_back(elementarGoal); - ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? - break; - } - else //should never be here - throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); - } - } - - logAi->trace("Main loop: selecting best elementar goal"); - - //now choose one elementar goal to realize - Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector - Goals::TSubgoal goalToRealize = sptr(Goals::Invalid()); - while (possibleGoals.size()) - { - //allow assign goals to heroes with 0 movement, but don't realize them - //maybe there are beter ones left - - auto bestGoal = fh->chooseSolution(possibleGoals); - if (bestGoal->hero) //lock this hero to fulfill goal - { - setGoal(bestGoal->hero, bestGoal); - if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero)) - { - if (!vstd::erase_if_present(possibleGoals, bestGoal)) - { - logAi->error("erase_if_preset failed? Something very wrong!"); - break; - } - continue; //chose next from the list - } - } - goalToRealize = bestGoal; //we found our goal to execute - break; - } - - //realize best goal - if (!goalToRealize->invalid()) - { - logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority); - - try - { - boost::this_thread::interruption_point(); - goalToRealize->accept(this); //visitor pattern - boost::this_thread::interruption_point(); - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Player %d: Making turn thread received an interruption!", playerID); - throw; //rethrow, we want to truly end this thread - } - catch (goalFulfilledException & e) - { - //the sub-goal was completed successfully - completeGoal(e.goal); - //local goal was also completed? - completeGoal(goalToRealize); - - // remove abstract visit tile if we completed the elementar one - vstd::erase_if_present(goalsToAdd, goalToRealize); - } - catch (std::exception & e) - { - logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name()); - logAi->debug("The error message was: %s", e.what()); - - //erase base goal if we failed to execute decomposed goal - for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize]) - goalsToRemove.push_back(basicGoal); - - // sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn. - ai->ah->notifyGoalCompleted(goalToRealize); - - //we failed to realize best goal, but maybe others are still possible? - } - - //remove goals we couldn't decompose - for (auto goal : goalsToRemove) - vstd::erase_if_present(basicGoals, goal); - - //add abstract goals - boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool - { - return lhs->priority > rhs->priority; //highest priority at the beginning - }); - - //max number of goals = 10 - int i = 0; - while (basicGoals.size() < 10 && goalsToAdd.size() > i) - { - if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates - basicGoals.push_back(goalsToAdd[i]); - i++; - } - } - else //no elementar goals possible - { - logAi->debug("Goal decomposition exhausted"); - break; - } - } -} - -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()); - switch(obj->ID) - { - case Obj::CREATURE_GENERATOR1: - recruitCreatures(dynamic_cast(obj), h.get()); - checkHeroArmy(h); - break; - case Obj::TOWN: - moveCreaturesToHero(dynamic_cast(obj)); - if(h->visitedTown) //we are inside, not just attacking - { - townVisitsThisWeek[h].insert(h->visitedTown); - if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) - { - if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) - cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); - } - } - break; - } - completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h))); -} - -void VCAI::moveCreaturesToHero(const CGTownInstance * t) -{ - if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner) - { - pickBestCreatures(t->visitingHero, t); - } -} - -void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source) -{ - const CArmedInstance * armies[] = {destinationArmy, source}; - - auto bestArmy = ah->getSortedSlots(destinationArmy, source); - - //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs - for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot - { - const CCreature * targetCreature = bestArmy[i.getNum()].creature; - - for(auto armyPtr : armies) - { - for(SlotID j = SlotID(0); j.validSlot(); j.advance(1)) - { - if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT - { - //can't take away last creature without split. generate a new stack with 1 creature which is weak but fast - if(armyPtr == source - && source->needsLastStack() - && source->stacksCount() == 1 - && (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature)) - { - auto weakest = ah->getWeakestCreature(bestArmy); - - if(weakest->creature == targetCreature) - { - if(1 == source->getStackCount(j)) - break; - - // move all except 1 of weakest creature from source to destination - cb->splitStack( - source, - destinationArmy, - j, - destinationArmy->getSlotFor(targetCreature), - destinationArmy->getStackCount(i) + source->getStackCount(j) - 1); - - break; - } - else - { - // Source last stack is not weakest. Move 1 of weakest creature from destination to source - cb->splitStack( - destinationArmy, - source, - destinationArmy->getSlotFor(weakest->creature), - source->getFreeSlot(), - 1); - } - } - - cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i); - } - } - } - } - - //TODO - having now strongest possible army, we may want to think about arranging stacks - - auto hero = dynamic_cast(destinationArmy); - if(hero) - checkHeroArmy(hero); -} - -void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other) -{ - auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void - { - bool changeMade = false; - do - { - changeMade = false; - - //we collect gear always in same order - std::vector allArtifacts; - if(giveStuffToFirstHero) - { - for(auto p : h->artifactsWorn) - { - if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); - } - } - for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); - - if(otherh) - { - for(auto p : otherh->artifactsWorn) - { - if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); - } - for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); - } - //we give stuff to one hero or another, depending on giveStuffToFirstHero - - const CGHeroInstance * target = nullptr; - if(giveStuffToFirstHero || !otherh) - target = h; - else - target = otherh; - - for(auto location : allArtifacts) - { - if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) - continue; // don't attempt to move catapult and spellbook - - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) - continue; //don't reequip artifact we already wear - - auto s = location.getSlot(); - if(!s || s->locked) //we can't move locks - continue; - auto artifact = s->artifact; - if(!artifact) - continue; - //FIXME: why are the above possible to be null? - - bool emptySlotFound = false; - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) - { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move - { - cb->swapArtifacts(location, destLocation); //just put into empty slot - emptySlotFound = true; - changeMade = true; - break; - } - } - if(!emptySlotFound) //try to put that atifact in already occupied slot - { - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) - { - auto otherSlot = target->getSlot(slot); - if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one - { - ArtifactLocation destLocation(target, slot); - //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move - { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); - changeMade = true; - break; - } - } - } - } - if(changeMade) - break; //start evaluating artifacts from scratch - } - } - while(changeMade); - }; - - equipBest(h, other, true); - - if(other) - equipBest(h, other, false); -} - -void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) -{ - //now used only for visited dwellings / towns, not BuyArmy goal - for(int i = 0; i < d->creatures.size(); i++) - { - if(!d->creatures[i].second.size()) - continue; - - int count = d->creatures[i].first; - CreatureID creID = d->creatures[i].second.back(); - - vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost()); - if(count > 0) - cb->recruitCreatures(d, recruiter, creID, count, i); - } -} - -bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit) -{ - int3 op = obj->visitablePos(); - auto paths = ah->getPathsToTile(h, op); - - for(const auto & path : paths) - { - if(movementCostLimit && movementCostLimit.value() < path.movementCost()) - return false; - - if(isGoodForVisit(obj, h, path)) - return true; - } - - return false; -} - -bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const -{ - const int3 pos = obj->visitablePos(); - const int3 targetPos = path.firstTileToGet(); - if (!targetPos.valid()) - return false; - if (!isTileNotReserved(h.get(), targetPos)) - return false; - if (obj->wasVisited(playerID)) - return false; - if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj)) - return false; // Otherwise we flag or get weekly resources / creatures - if (!isSafeToVisit(h, pos)) - return false; - if (!shouldVisit(h, obj)) - return false; - if (vstd::contains(alreadyVisited, obj)) - return false; - if (vstd::contains(reservedObjs, obj)) - return false; - - // TODO: looks extra if we already have AIPath - //if (!isAccessibleForHero(targetPos, h)) - // return false; - - const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj - //we don't try visiting object on which allied or owned hero stands - // -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited - return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met -} - -bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const -{ - if(t.valid()) - { - auto obj = cb->getTopObj(t); - if(obj && vstd::contains(ai->reservedObjs, obj) - && vstd::contains(reservedHeroesMap, h) - && !vstd::contains(reservedHeroesMap.at(h), obj)) - return false; //do not capture object reserved by another hero - else - return true; - } - else - { - return false; - } -} - -bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const -{ - //TODO: make gathering gold, building tavern or conquering town (?) possible subgoals - if(!t) - t = findTownWithTavern(); - if(!t) - 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)) - return false; - if(!cb->getAvailableHeroes(t).size()) - return false; - - return true; -} - -void VCAI::wander(HeroPtr h) -{ - auto visitTownIfAny = [this](HeroPtr h) -> bool - { - if (h->visitedTown) - { - townVisitsThisWeek[h].insert(h->visitedTown); - buildArmyIn(h->visitedTown); - return true; - } - return false; - }; - - //unclaim objects that are now dangerous for us - auto reservedObjsSetCopy = reservedHeroesMap[h]; - for(auto obj : reservedObjsSetCopy) - { - if(!isSafeToVisit(h, obj->visitablePos())) - unreserveObject(h, obj); - } - - TimeCheck tc("looking for wander destination"); - - for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) - { - validateVisitableObjs(); - ah->updatePaths(getMyHeroes()); - - std::vector dests; - - //also visit our reserved objects - but they are not prioritized to avoid running back and forth - vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool - { - return ah->isTileAccessible(h, obj->visitablePos()); - }); - - int pass = 0; - std::vector> distanceLimits = {1.0, 2.0, std::nullopt}; - - while(!dests.size() && pass < distanceLimits.size()) - { - auto & distanceLimit = distanceLimits[pass]; - - logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0)); - - vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool - { - return isGoodForVisit(obj, h, distanceLimit); - }); - - pass++; - } - - if(!dests.size()) - { - logAi->debug("Looking for town destination"); - - if(cb->getVisitableObjs(h->visitablePos()).size() > 1) - moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate - - auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool - { - const CGHeroInstance * hptr = h.get(); - auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs), - r2 = ah->howManyReinforcementsCanGet(hptr, rhs); - if (r1 != r2) - return r1 < r2; - else - return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs); - }; - - std::vector townsReachable; - std::vector townsNotReachable; - for(const CGTownInstance * t : cb->getTownsInfo()) - { - if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t)) - { - if(isAccessibleForHero(t->visitablePos(), h)) - townsReachable.push_back(t); - else - townsNotReachable.push_back(t); - } - } - if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing - { - dests.push_back(*boost::max_element(townsReachable, compareReinforcements)); - } - else if(townsNotReachable.size()) - { - //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; - 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(canRecruitAnyHero(t)) - recruitHero(t); - } - break; - } - else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST) - { - std::vector towns = cb->getTownsInfo(); - vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool - { - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t)) - return true; - } - return false; - }); - if (towns.size()) - { - recruitHero(*boost::max_element(towns, compareArmyStrength)); - } - break; - } - else - { - logAi->debug("Nowhere more to go..."); - break; - } - } - //end of objs empty - - if(dests.size()) //performance improvement - { - Goals::TGoalVec targetObjectGoals; - for(auto destination : dests) - { - vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false)); - } - - if(targetObjectGoals.size()) - { - auto bestObjectGoal = fh->chooseSolution(targetObjectGoals); - - //wander should not cause heroes to be reserved - they are always considered free - if(bestObjectGoal->goalType == Goals::VISIT_OBJ) - { - 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()); - } - else - logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); - - try - { - decomposeGoal(bestObjectGoal)->accept(this); - } - catch(const goalFulfilledException & e) - { - if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) - continue; - - throw e; - } - } - else - { - logAi->debug("Nowhere more to go..."); - break; - } - - visitTownIfAny(h); - } - } - - visitTownIfAny(h); //in case hero is just sitting in town -} - -void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) -{ - if(goal->invalid()) - { - vstd::erase_if_present(lockedHeroes, h); - } - else - { - lockedHeroes[h] = goal; - goal->setisElementar(false); //Force always evaluate goals before realizing - } -} -void VCAI::evaluateGoal(HeroPtr h) -{ - if(vstd::contains(lockedHeroes, h)) - fh->setPriority(lockedHeroes[h]); -} - -void VCAI::completeGoal(Goals::TSubgoal goal) -{ - if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won - return; - - logAi->debug("Completing goal: %s", goal->name()); - - //notify Managers - ah->notifyGoalCompleted(goal); - //notify mainLoop() - goalsToRemove.push_back(goal); //will be removed from mainLoop() goals - for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals - { - if (basicGoal->fulfillsMe(goal)) - goalsToRemove.push_back(basicGoal); - } - - //unreserve heroes - if(const CGHeroInstance * h = goal->hero.get(true)) - { - 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 - { - logAi->debug(goal->completeMessage()); - lockedHeroes.erase(it); //goal fulfilled, free hero - } - } - } - else //complete goal for all heroes maybe? - { - vstd::erase_if(lockedHeroes, [goal](std::pair p) - { - if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance - { - logAi->debug(p.second->completeMessage()); - return true; - } - return false; - }); - } - -} - -void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) -{ - NET_EVENT_HANDLER; - assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); - status.setBattle(ONGOING_BATTLE); - const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit - battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); -} - -void VCAI::battleEnd(const BattleResult * br, QueryID queryID) -{ - NET_EVENT_HANDLER; - assert(status.getBattle() == ONGOING_BATTLE); - status.setBattle(ENDING_BATTLE); - bool won = br->winner == myCb->battleGetMySide(); - logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); - battlename.clear(); - - if (queryID != -1) - { - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() - { - answerQuery(queryID, confirmAction); - }); - } - CAdventureAI::battleEnd(br, queryID); -} - -void VCAI::waitTillFree() -{ - auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex); - status.waitTillFree(); -} - -void VCAI::markObjectVisited(const CGObjectInstance * obj) -{ - if(!obj) - return; - - if(const auto * rewardable = dynamic_cast(obj)) //we may want to visit it with another hero - { - if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero - return; - - if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time - return; - } - - if(obj->ID == Obj::MONSTER) - return; - - alreadyVisited.insert(obj); -} - -void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj) -{ - reservedObjs.insert(obj); - reservedHeroesMap[h].insert(obj); - logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName()); -} - -void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj) -{ - vstd::erase_if_present(reservedObjs, obj); //unreserve objects - vstd::erase_if_present(reservedHeroesMap[h], obj); -} - -void VCAI::markHeroUnableToExplore(HeroPtr h) -{ - heroesUnableToExplore.insert(h); -} -void VCAI::markHeroAbleToExplore(HeroPtr h) -{ - vstd::erase_if_present(heroesUnableToExplore, h); -} -bool VCAI::isAbleToExplore(HeroPtr h) -{ - return !vstd::contains(heroesUnableToExplore, h); -} -void VCAI::clearPathsInfo() -{ - heroesUnableToExplore.clear(); -} - -void VCAI::validateVisitableObjs() -{ - std::string errorMsg; - auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool - { - if(obj) - return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility - else - return true; - }; - - //errorMsg is captured by ref so lambda will take the new text - errorMsg = " shouldn't be on the visitable objects list!"; - vstd::erase_if(visitableObjs, shouldBeErased); - - //FIXME: how comes our own heroes become inaccessible? - vstd::erase_if(reservedHeroesMap, [](std::pair> hp) -> bool - { - return !hp.first.get(true); - }); - for(auto & p : reservedHeroesMap) - { - errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!"; - vstd::erase_if(p.second, shouldBeErased); - } - - errorMsg = " shouldn't be on the reserved objs list!"; - vstd::erase_if(reservedObjs, shouldBeErased); - - //TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game. - errorMsg = " shouldn't be on the already visited objs list!"; - vstd::erase_if(alreadyVisited, shouldBeErased); -} - -void VCAI::retrieveVisitableObjs(std::vector & out, bool includeOwned) const -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(includeOwned || obj->tempOwner != playerID) - out.push_back(obj); - } - }); -} - -void VCAI::retrieveVisitableObjs() -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(obj->tempOwner != playerID) - addVisitableObj(obj); - } - }); -} - -std::vector VCAI::getFlaggedObjects() const -{ - std::vector ret; - for(const CGObjectInstance * obj : visitableObjs) - { - if(obj->tempOwner == playerID) - ret.push_back(obj); - } - return ret; -} - -void VCAI::addVisitableObj(const CGObjectInstance * obj) -{ - if(obj->ID == Obj::EVENT) - return; - - visitableObjs.insert(obj); - - // All teleport objects seen automatically assigned to appropriate channels - auto teleportObj = dynamic_cast(obj); - if(teleportObj) - CGTeleport::addToChannel(knownTeleportChannels, teleportObj); -} - -const CGObjectInstance * VCAI::lookForArt(int aid) const -{ - for(const CGObjectInstance * obj : ai->visitableObjs) - { - if(obj->ID == Obj::ARTIFACT && obj->subID == aid) - return obj; - } - - return nullptr; - - //TODO what if more than one artifact is available? return them all or some slection criteria -} - -bool VCAI::isAccessible(const int3 & pos) const -{ - //TODO precalculate for speed - - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(isAccessibleForHero(pos, h)) - return true; - } - - return false; -} - -HeroPtr VCAI::getHeroWithGrail() const -{ - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(h->hasArt(ArtifactID::GRAIL)) - return h; - } - return nullptr; -} - -const CGObjectInstance * VCAI::getUnvisitedObj(const std::function & predicate) -{ - //TODO smarter definition of unvisited - for(const CGObjectInstance * obj : visitableObjs) - { - if(predicate(obj) && !vstd::contains(alreadyVisited, obj)) - return obj; - } - return nullptr; -} - -bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const -{ - // Don't visit tile occupied by allied hero - if(!includeAllies) - { - for(auto obj : cb->getVisitableObjs(pos)) - { - if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES) - { - if(obj != h.get()) - return false; - } - } - } - return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); -} - -bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) -{ - //TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj() - - auto afterMovementCheck = [&]() -> void - { - waitTillFree(); //movement may cause battle or blocking dialog - if(!h) - { - lostHero(h); - teleportChannelProbingList.clear(); - if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off - status.setChannelProbing(false); - throw cannotFulfillGoalException("Hero was lost!"); - } - }; - - logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString()); - int3 startHpos = h->visitablePos(); - bool ret = false; - if(startHpos == dst) - { - //FIXME: this assertion fails also if AI moves onto defeated guarded object - assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object - cb->moveHero(*h, h->convertFromVisitablePos(dst)); - afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? - // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared - teleportChannelProbingList.clear(); - // not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information. - ret = true; - } - else - { - CGPath path; - cb->getPathsInfo(h.get())->getPath(path, dst); - if(path.nodes.empty()) - { - logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString()); - throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h))); - } - int i = (int)path.nodes.size() - 1; - - auto getObj = [&](int3 coord, bool ignoreHero) - { - auto tile = cb->getTile(coord, false); - assert(tile); - return tile->topVisitableObj(ignoreHero); - //return cb->getTile(coord,false)->topVisitableObj(ignoreHero); - }; - - auto isTeleportAction = [&](EPathNodeAction action) -> bool - { - if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT) - { - if(action != EPathNodeAction::TELEPORT_BATTLE) - { - return false; - } - } - - return true; - }; - - auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * - { - if(CGTeleport::isConnected(currentObject, nextObjectTop)) - return nextObjectTop; - if(nextObjectTop && nextObjectTop->ID == Obj::HERO) - { - if(CGTeleport::isConnected(currentObject, nextObject)) - return nextObject; - } - - return nullptr; - }; - - auto doMovement = [&](int3 dst, bool transit) - { - cb->moveHero(*h, h->convertFromVisitablePos(dst), transit); - }; - - auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) - { - destinationTeleport = exitId; - if(exitPos.valid()) - destinationTeleportPos = h->convertFromVisitablePos(exitPos); - cb->moveHero(*h, h->pos); - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - afterMovementCheck(); - }; - - auto doChannelProbing = [&]() -> void - { - auto currentPos = h->visitablePos(); - auto currentExit = getObj(currentPos, true)->id; - - status.setChannelProbing(true); - for(auto exit : teleportChannelProbingList) - doTeleportMovement(exit, int3(-1)); - teleportChannelProbingList.clear(); - status.setChannelProbing(false); - - doTeleportMovement(currentExit, currentPos); - }; - - for(; i > 0; i--) - { - int3 currentCoord = path.nodes[i].coord; - int3 nextCoord = path.nodes[i - 1].coord; - - auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos()); - auto nextObjectTop = getObj(nextCoord, false); - auto nextObject = getObj(nextCoord, true); - auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); - if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr) - { - //we use special login if hero standing on teleporter it's mean we need - doTeleportMovement(destTeleportObj->id, nextCoord); - if(teleportChannelProbingList.size()) - doChannelProbing(); - markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited - - continue; - } - - //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) - if(path.nodes[i - 1].turns) - { - //blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs - break; - } - - int3 endpos = path.nodes[i - 1].coord; - if(endpos == h->visitablePos()) - continue; - - bool isConnected = false; - bool isNextObjectTeleport = false; - // Check there is node after next one; otherwise transit is pointless - if(i - 2 >= 0) - { - isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false)); - isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop); - } - if(isConnected || isNextObjectTeleport) - { - // Hero should be able to go through object if it's allow transit - doMovement(endpos, true); - } - else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR) - { - doMovement(endpos, true); - } - else - { - doMovement(endpos, false); - } - - afterMovementCheck(); - - if(teleportChannelProbingList.size()) - doChannelProbing(); - } - - if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT) - { - ret = h && i == 0; // when we take resource we do not reach its position. We even might not move - } - } - if(h) - { - if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting - { - if(visitedObject != *h) - performObjectInteraction(visitedObject, h); - } - } - if(h) //we could have lost hero after last move - { - completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway - completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h))); - - ret = ret || (dst == h->visitablePos()); - - if(!ret) //reserve object we are heading towards - { - auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst)); - if(obj && obj != *h) - reserveObject(h, obj); - } - - if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target - { - vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move - invalidPathHeroes.insert(h); - throw cannotFulfillGoalException("Invalid path found!"); - } - evaluateGoal(h); //new hero position means new game situation - logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret); - } - return ret; -} - -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()); - cb->buildBuilding(t, building); //just do this; -} - -void VCAI::tryRealize(Goals::Explore & g) -{ - throw cannotFulfillGoalException("EXPLORE is not an elementar goal!"); -} - -void VCAI::tryRealize(Goals::RecruitHero & g) -{ - if(const CGTownInstance * t = findTownWithTavern()) - { - recruitHero(t, true); - //TODO try to free way to blocked town - //TODO: adventure map tavern or prison? - } - else - { - throw cannotFulfillGoalException("No town to recruit hero!"); - } -} - -void VCAI::tryRealize(Goals::VisitTile & g) -{ - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!"); - if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) - { - logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); - throw goalFulfilledException(sptr(g)); - } - if(ai->moveHeroToTile(g.tile, g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } -} - -void VCAI::tryRealize(Goals::VisitObj & g) -{ - auto position = g.tile; - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!"); - if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) - { - logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); - throw goalFulfilledException(sptr(g)); - } - if(ai->moveHeroToTile(position, g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } -} - -void VCAI::tryRealize(Goals::VisitHero & g) -{ - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!"); - - const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid)); - if(obj) - { - if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } - } - else - { - throw cannotFulfillGoalException("Cannot visit hero: object not found!"); - } -} - -void VCAI::tryRealize(Goals::BuildThis & g) -{ - auto b = BuildingID(g.bid); - auto t = g.town; - - if (t) - { - 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()); - cb->buildBuilding(t, b); - throw goalFulfilledException(sptr(g)); - } - } - throw cannotFulfillGoalException("Cannot build a given structure!"); -} - -void VCAI::tryRealize(Goals::DigAtTile & g) -{ - assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? - if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) - { - cb->dig(g.hero.get()); - completeGoal(sptr(g)); // finished digging - } - else - { - ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else - throw cannotFulfillGoalException("A hero can't dig!\n"); - } -} - -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; - if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) - { - if(const IMarket * m = IMarket::castFrom(obj, false)) - { - auto freeRes = ah->freeResources(); //trade only resources which are not reserved - for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) - { - auto res = it->resType; - if(res.getNum() == g.resID) //sell any other resource - continue; - - int toGive, toGet; - m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); - toGive = static_cast(toGive * (it->resVal / toGive)); //round down - //TODO trade only as much as needed - if (toGive) //don't try to sell 0 resources - { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, 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()); - } - if (ah->freeResources()[g.resID] >= g.value) - throw goalFulfilledException(sptr(g)); //we traded all we needed - } - - throw cannotFulfillGoalException("I cannot get needed resources by trade!"); - } - else - { - throw cannotFulfillGoalException("I don't know how to use this object to raise resources!"); - } - } - else - { - throw cannotFulfillGoalException("No object that could be used to raise resources!"); - } -} - -void VCAI::tryRealize(Goals::BuyArmy & g) -{ - auto t = g.town; - - ui64 valueBought = 0; - //buy the stacks with largest AI value - - makePossibleUpgrades(t); - - while (valueBought < g.value) - { - auto res = ah->allResources(); - std::vector creaturesInDwellings; - - for (int i = 0; i < t->creatures.size(); i++) - { - auto ci = infoFromDC(t->creatures[i]); - - if(!ci.count - || ci.creID == -1 - || (g.objid != -1 && ci.creID != g.objid) - || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) - continue; - - vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford - - if(!ci.count) - continue; - - ci.level = i; //this is important for Dungeon Summoning Portal - creaturesInDwellings.push_back(ci); - } - - if (creaturesInDwellings.empty()) - throw cannotFulfillGoalException("Can't buy any more creatures!"); - - creInfo ci = - *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) - { - //max value of creatures we can buy with our res - int value1 = lhs.cre->getAIValue() * lhs.count, - value2 = rhs.cre->getAIValue() * rhs.count; - - return value1 < value2; - }); - - - cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); - valueBought += ci.count * ci.cre->getAIValue(); - } - - throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted -} - -void VCAI::tryRealize(Goals::Invalid & g) -{ - throw cannotFulfillGoalException("I don't know how to fulfill this!"); -} - -void VCAI::tryRealize(Goals::AbstractGoal & g) -{ - logAi->debug("Attempting realizing goal with code %s", g.name()); - throw cannotFulfillGoalException("Unknown type of goal !"); -} - -const CGTownInstance * VCAI::findTownWithTavern() const -{ - for(const CGTownInstance * t : cb->getTownsInfo()) - if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero) - return t; - - return nullptr; -} - -Goals::TSubgoal VCAI::getGoal(HeroPtr h) const -{ - auto it = lockedHeroes.find(h); - if(it != lockedHeroes.end()) - return it->second; - else - return sptr(Goals::Invalid()); -} - - -std::vector VCAI::getUnblockedHeroes() const -{ - std::vector ret; - for(auto h : cb->getHeroesInfo()) - { - //&& !vstd::contains(lockedHeroes, h) - //at this point we assume heroes exhausted their locked goals - if(canAct(h)) - ret.push_back(h); - } - return ret; -} - -bool VCAI::canAct(HeroPtr h) const -{ - auto mission = lockedHeroes.find(h); - if(mission != lockedHeroes.end()) - { - //FIXME: I'm afraid there can be other conditions when heroes can act but not move :? - if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar) - return false; - } - - return h->movementPointsRemaining(); -} - -HeroPtr VCAI::primaryHero() const -{ - auto hs = cb->getHeroesInfo(); - if (hs.empty()) - return nullptr; - else - return *boost::max_element(hs, compareHeroStrength); -} - -void VCAI::endTurn() -{ - logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr()); - if(!status.haveTurn()) - { - logAi->error("Not having turn at the end of turn???"); - } - logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); - do - { - cb->endTurn(); - } - while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over - - logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr()); -} - -void VCAI::striveToGoal(Goals::TSubgoal basicGoal) -{ - //TODO: this function is deprecated and should be dropped altogether - - auto goalToDecompose = basicGoal; - Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); - int maxAbstractGoals = 10; - while (!elementarGoal->isElementar && maxAbstractGoals) - { - try - { - elementarGoal = decomposeGoal(goalToDecompose); - } - catch (goalFulfilledException & e) - { - //it is impossible to continue some goals (like exploration, for example) - completeGoal(e.goal); //put in goalsToRemove - logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); - return; - } - catch (std::exception & e) - { - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); - return; - } - if (elementarGoal->isAbstract) //we can decompose it further - { - goalsToAdd.push_back(elementarGoal); - //decompose further now - this is necesssary if we can't add over 10 goals in the pool - goalToDecompose = elementarGoal; - //there is a risk of infinite abstract goal loop, though it indicates failed logic - maxAbstractGoals--; - } - else if (elementarGoal->isElementar) //should be - { - logAi->debug("Found elementar goal %s", elementarGoal->name()); - ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? - break; - } - else //should never be here - throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); - } - - //realize best goal - if (!elementarGoal->invalid()) - { - logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority); - - try - { - boost::this_thread::interruption_point(); - elementarGoal->accept(this); //visitor pattern - boost::this_thread::interruption_point(); - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Player %d: Making turn thread received an interruption!", playerID); - throw; //rethrow, we want to truly end this thread - } - catch (goalFulfilledException & e) - { - //the sub-goal was completed successfully - completeGoal(e.goal); - //local goal was also completed - completeGoal(elementarGoal); - } - catch (std::exception & e) - { - logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name()); - logAi->debug("The error message was: %s", e.what()); - - //erase base goal if we failed to execute decomposed goal - for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal]) - goalsToRemove.push_back(basicGoalToRemove); - } - } -} - -Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) -{ - if(ultimateGoal->isElementar) - { - logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name()); - - return ultimateGoal; - } - - const int searchDepth = 30; - - Goals::TSubgoal goal = ultimateGoal; - logAi->debug("Decomposing goal %s", ultimateGoal->name()); - int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals - while (maxGoals) - { - boost::this_thread::interruption_point(); - - goal = goal->whatToDoToAchieve(); //may throw if decomposition fails - --maxGoals; - if (goal == ultimateGoal) //compare objects by value - if (goal->isElementar == ultimateGoal->isElementar) - throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!") - % ultimateGoal->name()).str()); - if (goal->isAbstract || goal->isElementar) - return goal; - else - logAi->debug("Considering: %s", goal->name()); - } - - throw cannotFulfillGoalException("Too many subgoals, don't know what to do"); -} - -void VCAI::performTypicalActions() -{ - for(auto h : getUnblockedHeroes()) - { - if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn - continue; - - logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining()); - makePossibleUpgrades(*h); - pickBestArtifacts(*h); - try - { - wander(h); - } - catch(std::exception & e) - { - logAi->debug("Cannot use this hero anymore, received exception: %s", e.what()); - continue; - } - } -} - -void VCAI::buildArmyIn(const CGTownInstance * t) -{ - makePossibleUpgrades(t->visitingHero); - makePossibleUpgrades(t); - recruitCreatures(t, t->getUpperArmy()); - moveCreaturesToHero(t); -} - -void VCAI::checkHeroArmy(HeroPtr h) -{ - auto it = lockedHeroes.find(h); - if(it != lockedHeroes.end()) - { - if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength()) - completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h))); - } -} - -void VCAI::recruitHero(const CGTownInstance * t, bool throwing) -{ - logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString()); - - auto heroes = cb->getAvailableHeroes(t); - if(heroes.size()) - { - auto hero = heroes[0]; - if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week - { - if(heroes[1]->getTotalStrength() > hero->getTotalStrength()) - hero = heroes[1]; - } - cb->recruitHero(t, hero); - throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); - } - else if(throwing) - { - throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); - } -} - -void VCAI::finish() -{ - //we want to lock to avoid multiple threads from calling makingTurn->join() at same time - boost::lock_guard multipleCleanupGuard(turnInterruptionMutex); - if(makingTurn) - { - makingTurn->interrupt(); - makingTurn->join(); - makingTurn.reset(); - } -} - -void VCAI::requestActionASAP(std::function whatToDo) -{ - boost::thread newThread([this, whatToDo]() - { - setThreadName("VCAI::requestActionASAP::whatToDo"); - SET_GLOBAL_STATE(this); - boost::shared_lock gsLock(CGameState::mutex); - whatToDo(); - }); -} - -void VCAI::lostHero(HeroPtr h) -{ - logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name); - - vstd::erase_if_present(lockedHeroes, h); - for(auto obj : reservedHeroesMap[h]) - { - vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero - } - vstd::erase_if_present(reservedHeroesMap, h); - vstd::erase_if_present(visitedHeroes, h); - for (auto heroVec : visitedHeroes) - { - vstd::erase_if_present(heroVec.second, h); - } - - //remove goals with removed hero assigned from main loop - vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool - { - if(x.first->hero == h) - return true; - else - return false; - }); - - auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool - { - if(x->hero == h) - return true; - else - return false; - }; - - vstd::erase_if(basicGoals, removedHeroGoalPredicate); - vstd::erase_if(goalsToAdd, removedHeroGoalPredicate); - vstd::erase_if(goalsToRemove, removedHeroGoalPredicate); - - for(auto goal : ultimateGoalsFromBasic) - vstd::erase_if(goal.second, removedHeroGoalPredicate); -} - -void VCAI::answerQuery(QueryID queryID, int selection) -{ - logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection); - if(queryID != QueryID(-1)) - { - cb->selectionMade(selection, queryID); - } - else - { - logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID); - //do nothing - } -} - -void VCAI::requestSent(const CPackForServer * pack, int requestID) -{ - //BNLOG("I have sent request of type %s", typeid(*pack).name()); - if(auto reply = dynamic_cast(pack)) - { - status.attemptedAnsweringQuery(reply->qid, requestID); - } -} - -std::string VCAI::getBattleAIName() const -{ - if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING) - return settings["server"]["enemyAI"].String(); - else - return "BattleAI"; -} - -void VCAI::validateObject(const CGObjectInstance * obj) -{ - validateObject(obj->id); -} - -void VCAI::validateObject(ObjectIdRef obj) -{ - auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool - { - return hlpObj->id == obj.id; - }; - if(!obj) - { - vstd::erase_if(visitableObjs, matchesId); - - for(auto & p : reservedHeroesMap) - vstd::erase_if(p.second, matchesId); - - vstd::erase_if(reservedObjs, matchesId); - } -} - -AIStatus::AIStatus() -{ - battle = NO_BATTLE; - havingTurn = false; - ongoingHeroMovement = false; - ongoingChannelProbing = false; -} - -AIStatus::~AIStatus() -{ - -} - -void AIStatus::setBattle(BattleState BS) -{ - boost::unique_lock lock(mx); - LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS); - battle = BS; - cv.notify_all(); -} - -BattleState AIStatus::getBattle() -{ - boost::unique_lock lock(mx); - return battle; -} - -void AIStatus::addQuery(QueryID ID, std::string description) -{ - if(ID == QueryID(-1)) - { - logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description); - return; - } - - assert(ID.getNum() >= 0); - boost::unique_lock lock(mx); - - assert(!vstd::contains(remainingQueries, ID)); - - remainingQueries[ID] = description; - - cv.notify_all(); - logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); -} - -void AIStatus::removeQuery(QueryID ID) -{ - boost::unique_lock lock(mx); - assert(vstd::contains(remainingQueries, ID)); - - std::string description = remainingQueries[ID]; - remainingQueries.erase(ID); - - cv.notify_all(); - logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); -} - -int AIStatus::getQueriesCount() -{ - boost::unique_lock lock(mx); - return static_cast(remainingQueries.size()); -} - -void AIStatus::startedTurn() -{ - boost::unique_lock lock(mx); - havingTurn = true; - cv.notify_all(); -} - -void AIStatus::madeTurn() -{ - boost::unique_lock lock(mx); - havingTurn = false; - cv.notify_all(); -} - -void AIStatus::waitTillFree() -{ - boost::unique_lock lock(mx); - while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) - cv.timed_wait(lock, boost::posix_time::milliseconds(100)); -} - -bool AIStatus::haveTurn() -{ - boost::unique_lock lock(mx); - return havingTurn; -} - -void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID) -{ - boost::unique_lock lock(mx); - assert(vstd::contains(remainingQueries, queryID)); - std::string description = remainingQueries[queryID]; - logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID); - requestToQueryID[answerRequestID] = queryID; -} - -void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result) -{ - QueryID query; - - { - boost::unique_lock lock(mx); - - assert(vstd::contains(requestToQueryID, answerRequestID)); - query = requestToQueryID[answerRequestID]; - assert(vstd::contains(remainingQueries, query)); - requestToQueryID.erase(answerRequestID); - } - - if(result) - { - removeQuery(query); - } - else - { - logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]); - //TODO safely retry - } -} - -void AIStatus::heroVisit(const CGObjectInstance * obj, bool started) -{ - boost::unique_lock lock(mx); - if(started) - { - objectsBeingVisited.push_back(obj); - } - else - { - // There can be more than one object visited at the time (eg. hero visits Subterranean Gate - // causing visit to hero on the other side. - // However, we are guaranteed that start/end visit notification maintain stack order. - assert(!objectsBeingVisited.empty()); - objectsBeingVisited.pop_back(); - } - cv.notify_all(); -} - -void AIStatus::setMove(bool ongoing) -{ - boost::unique_lock lock(mx); - ongoingHeroMovement = ongoing; - cv.notify_all(); -} - -void AIStatus::setChannelProbing(bool ongoing) -{ - boost::unique_lock lock(mx); - ongoingChannelProbing = ongoing; - cv.notify_all(); -} - -bool AIStatus::channelProbing() -{ - return ongoingChannelProbing; -} - - - -bool isWeeklyRevisitable(const CGObjectInstance * obj) -{ - //TODO: allow polling of remaining creatures in dwelling - if(const auto * rewardable = dynamic_cast(obj)) - return rewardable->configuration.getResetDuration() == 7; - - if(dynamic_cast(obj)) - return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; - - switch(obj->ID) - { - case Obj::STABLES: - case Obj::MAGIC_WELL: - case Obj::HILL_FORT: - return true; - case Obj::BORDER_GATE: - case Obj::BORDERGUARD: - return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week - } - return false; -} - -bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) -{ - switch(obj->ID) - { - case Obj::TOWN: - case Obj::HERO: //never visit our heroes at random - return obj->tempOwner != h->tempOwner; //do not visit our towns at random - case Obj::BORDER_GATE: - { - for(auto q : ai->myCb->getMyQuests()) - { - if(q.obj == obj) - { - return false; // do not visit guards or gates when wandering - } - } - return true; //we don't have this quest yet - } - case Obj::BORDERGUARD: //open borderguard if possible - return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); - case Obj::SEER_HUT: - case Obj::QUEST_GUARD: - { - for(auto q : ai->myCb->getMyQuests()) - { - if(q.obj == obj) - { - if(q.quest->checkQuest(h.h)) - return true; //we completed the quest - else - return false; //we can't complete this quest - } - } - return true; //we don't have this quest yet - } - case Obj::CREATURE_GENERATOR1: - { - if(obj->tempOwner != h->tempOwner) - return true; //flag just in case - bool canRecruitCreatures = false; - const CGDwelling * d = dynamic_cast(obj); - for(auto level : d->creatures) - { - for(auto c : level.second) - { - if(h->getSlotFor(CreatureID(c)) != SlotID()) - canRecruitCreatures = true; - } - } - return canRecruitCreatures; - } - case Obj::HILL_FORT: - { - for(auto slot : h->Slots()) - { - if(slot.second->type->hasUpgrades()) - return true; //TODO: check price? - } - return false; - } - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_ONE_WAY_EXIT: - case Obj::MONOLITH_TWO_WAY: - case Obj::WHIRLPOOL: - return false; - case Obj::SCHOOL_OF_MAGIC: - case Obj::SCHOOL_OF_WAR: - { - if (ai->ah->freeGold() < 1000) - return false; - break; - } - case Obj::LIBRARY_OF_ENLIGHTENMENT: - if(h->level < 12) - return false; - break; - case Obj::TREE_OF_KNOWLEDGE: - { - TResources myRes = ai->ah->freeResources(); - if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10) - return false; - break; - } - 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); - 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)) - return false; - else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) - return false; - break; - } - case Obj::BOAT: - return false; - //Boats are handled by pathfinder - case Obj::EYE_OF_MAGI: - return false; //this object is useless to visit, but could be visited indefinitely - } - - if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); - return false; - - return true; -} - - +/* + * VCAI.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 "VCAI.h" +#include "FuzzyHelper.h" +#include "ResourceManager.h" +#include "BuildingManager.h" +#include "Goals/Goals.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/mapObjects/MapObjects.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/GameSettings.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/bonuses/CBonusSystemNode.h" +#include "../../lib/bonuses/Limiters.h" +#include "../../lib/bonuses/Updaters.h" +#include "../../lib/bonuses/Propagators.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" + +extern FuzzyHelper * fh; + +const double SAFE_ATTACK_CONSTANT = 1.5; + +//one thread may be turn of AI and another will be handling a side effect for AI2 +thread_local CCallback * cb = nullptr; +thread_local VCAI * ai = nullptr; + +//std::map > HeroView::infosCount; + +//helper RAII to manage global ai/cb ptrs +struct SetGlobalState +{ + SetGlobalState(VCAI * AI) + { + assert(!ai); + assert(!cb); + + ai = AI; + cb = AI->myCb.get(); + } + ~SetGlobalState() + { + //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully + //TODO: to ensure that, make rm unique_ptr + ai = nullptr; + cb = nullptr; + } +}; + + +#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); + +#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) +#define MAKING_TURN SET_GLOBAL_STATE(this) + +VCAI::VCAI() +{ + LOG_TRACE(logAi); + makingTurn = nullptr; + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + + ah = new AIhelper(); + ah->setAI(this); +} + +VCAI::~VCAI() +{ + delete ah; + LOG_TRACE(logAi); + finish(); +} + +void VCAI::availableCreaturesChanged(const CGDwelling * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroMoved(const TryMoveHero & details, bool verbose) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + //enemy hero may have left visible area + validateObject(details.id); + auto hero = cb->getHero(details.id); + + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; + const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); + + const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); + const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose)); + + if(details.result == TryMoveHero::TELEPORTATION) + { + auto t1 = dynamic_cast(o1); + auto t2 = dynamic_cast(o2); + if(t1 && t2) + { + if(cb->isTeleportChannelBidirectional(t1->channel)) + { + if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels + { + knownSubterraneanGates[o1] = o2; + knownSubterraneanGates[o2] = o1; + logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString()); + } + } + } + //FIXME: teleports are not correctly visited + unreserveObject(hero, t1); + unreserveObject(hero, t2); + } + else if(details.result == TryMoveHero::EMBARK && hero) + { + //make sure AI not attempt to visit used boat + validateObject(hero->boat); + } + else if(details.result == TryMoveHero::DISEMBARK && o1) + { + auto boat = dynamic_cast(o1); + if(boat) + addVisitableObj(boat); + } +} + +void VCAI::heroInGarrisonChange(const CGTownInstance * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::centerView(int3 pos, int focusTime) +{ + LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime); + NET_EVENT_HANDLER; +} + +void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactAssembled(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "TavernWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::playerBlocked(int reason, bool start) +{ + LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start); + NET_EVENT_HANDLER; + if(start && reason == PlayerBlocked::UPCOMING_BATTLE) + status.setBattle(UPCOMING_BATTLE); + + if(reason == PlayerBlocked::ONGOING_MOVEMENT) + status.setMove(start); +} + +void VCAI::showPuzzleMap() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showShipyardDialog(const IShipyard * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) +{ + LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); + NET_EVENT_HANDLER; + logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); + if(player == playerID) + { + if(victoryLossCheckResult.victory()) + { + logAi->debug("VCAI: I won! Incredible!"); + logAi->debug("Turn nr %d", myCb->getDate()); + } + else + { + logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); + } + + finish(); + } +} + +void VCAI::artifactPut(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactRemoved(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactDisassembled(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) +{ + LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a"))); + NET_EVENT_HANDLER; + + if(start && visitedObj) //we can end visit with null object, anyway + { + markObjectVisited(visitedObj); + unreserveObject(visitor, visitedObj); + completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore + //TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..) + if (visitedObj->ID == Obj::HERO) + { + visitedHeroes[visitor].insert(HeroPtr(dynamic_cast(visitedObj))); + } + } + + status.heroVisit(visitedObj, start); +} + +void VCAI::availableArtifactsChanged(const CGBlackMarket * bm) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + //buildArmyIn(town); + //moveCreaturesToHero(town); +} + +void VCAI::tileHidden(const std::unordered_set & pos) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + validateVisitableObjs(); + clearPathsInfo(); +} + +void VCAI::tileRevealed(const std::unordered_set & pos) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + for(int3 tile : pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) + addVisitableObj(obj); + } + + clearPathsInfo(); +} + +void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + auto firstHero = cb->getHero(hero1); + auto secondHero = cb->getHero(hero2); + + status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner)); + + requestActionASAP([=]() + { + float goalpriority1 = 0, goalpriority2 = 0; + + auto firstGoal = getGoal(firstHero); + if(firstGoal->goalType == Goals::GATHER_ARMY) + goalpriority1 = firstGoal->priority; + auto secondGoal = getGoal(secondHero); + if(secondGoal->goalType == Goals::GATHER_ARMY) + goalpriority2 = secondGoal->priority; + + auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void + { + this->pickBestCreatures(h1, h2); + this->pickBestArtifacts(h1, h2); + }; + + //Do not attempt army or artifacts exchange if we visited ally player + //Visits can still be useful if hero have skills like Scholar + if(firstHero->tempOwner != secondHero->tempOwner) + { + logAi->debug("Heroes owned by different players. Do not exchange army or artifacts."); + } + else if(goalpriority1 > goalpriority2) + { + transferFrom2to1(firstHero, secondHero); + } + else if(goalpriority1 < goalpriority2) + { + transferFrom2to1(secondHero, firstHero); + } + else //regular criteria + { + if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero)) + transferFrom2to1(firstHero, secondHero); + else if(ah->canGetArmy(secondHero, firstHero)) + transferFrom2to1(secondHero, firstHero); + } + + completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime? + completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum()))); + + answerQuery(query, 0); + }); +} + +void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) +{ + LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); + NET_EVENT_HANDLER; +} + +void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "level '%i'", level); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "RecruitmentDialog"); + requestActionASAP([=](){ + recruitCreatures(dwelling, dst); + checkHeroArmy(dynamic_cast(dst)); + answerQuery(queryID, 0); + }); +} + +void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::newObject(const CGObjectInstance * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(obj->isVisitable()) + addVisitableObj(obj); +} + +//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight +//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes +void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + vstd::erase_if_present(visitableObjs, obj); + vstd::erase_if_present(alreadyVisited, obj); + + for(auto h : cb->getHeroesInfo()) + unreserveObject(h, obj); + + std::function checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool + { + if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum())) + return true; + else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal + return true; + else + return false; + }; + + //clear VCAI / main loop caches + vstd::erase_if(lockedHeroes, [&](const std::pair & x) -> bool + { + return checkRemovalValidity(x.second); + }); + + vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool + { + return checkRemovalValidity(x.first); + }); + + vstd::erase_if(basicGoals, checkRemovalValidity); + vstd::erase_if(goalsToAdd, checkRemovalValidity); + vstd::erase_if(goalsToRemove, checkRemovalValidity); + + for(auto goal : ultimateGoalsFromBasic) + vstd::erase_if(goal.second, checkRemovalValidity); + + //clear resource manager goal cache + ah->removeOutdatedObjectives(checkRemovalValidity); + + //TODO: Find better way to handle hero boat removal + if(auto hero = dynamic_cast(obj)) + { + if(hero->boat) + { + vstd::erase_if_present(visitableObjs, hero->boat); + vstd::erase_if_present(alreadyVisited, hero->boat); + + for(auto h : cb->getHeroesInfo()) + unreserveObject(h, hero->boat); + } + } + + //TODO + //there are other places where CGObjectinstance ptrs are stored... + // + + if(obj->ID == Obj::HERO && obj->tempOwner == playerID) + { + lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion + } +} + +void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + requestActionASAP([=]() + { + makePossibleUpgrades(visitor); + }); +} + +void VCAI::playerBonusChanged(const Bonus & bonus, bool gain) +{ + LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); + NET_EVENT_HANDLER; +} + +void VCAI::heroCreated(const CGHeroInstance * h) +{ + LOG_TRACE(logAi); + if(h->visitedTown) + townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown); + NET_EVENT_HANDLER; +} + +void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) +{ + LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); + NET_EVENT_HANDLER; +} + +void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) +{ + LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID); + NET_EVENT_HANDLER; +} + +void VCAI::requestRealized(PackageApplied * pa) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(status.haveTurn()) + { + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) + { + if(pa->result) + status.madeTurn(); + } + } + + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) + { + status.receivedAnswerConfirmation(pa->requestID, pa->result); + } +} + +void VCAI::receivedResource() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "UniversityWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) +{ + LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val); + NET_EVENT_HANDLER; +} + +void VCAI::battleResultsApplied() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + assert(status.getBattle() == ENDING_BATTLE); + status.setBattle(NO_BATTLE); +} + +void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop) +{ + +} + +void VCAI::objectPropertyChanged(const SetObjectProperty * sop) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(sop->what == ObjProperty::OWNER) + { + if(myCb->getPlayerRelations(playerID, sop->identifier.as()) == PlayerRelations::ENEMIES) + { + //we want to visit objects owned by oppponents + 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 + vstd::erase_if_present(alreadyVisited, obj); + } + } + } +} + +void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) +{ + LOG_TRACE_PARAMS(logAi, "what '%i'", what); + NET_EVENT_HANDLER; + + if(town->getOwner() == playerID && what == 1) //built + completeGoal(sptr(Goals::BuildThis(buildingID, town))); +} + +void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) +{ + LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); + NET_EVENT_HANDLER; +} + +void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "MarketWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) +{ + //TODO: AI support for ViewXXX spell + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + LOG_TRACE(logAi); + env = ENV; + myCb = CB; + cbc = CB; + + ah->init(CB.get()); + + NET_EVENT_HANDLER; //sets ah->rm->cb + playerID = *myCb->getPlayerID(); + myCb->waitTillRealize = true; + myCb->unlockGsWhenWaiting = true; + + if(!fh) + fh = new FuzzyHelper(); + + retrieveVisitableObjs(); +} + +void VCAI::yourTurn(QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, "YourTurn"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); + status.startedTurn(); + makingTurn = std::make_unique(&VCAI::makeTurn, this); +} + +void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level)); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level)); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) +{ + LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel); + NET_EVENT_HANDLER; + int sel = 0; + status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s") + % components.size() % text)); + + if(selection) //select from multiple components -> take the last one (they're indexed [1-size]) + sel = static_cast(components.size()); + + if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :) + sel = 1; + + requestActionASAP([=]() + { + answerQuery(askID, sel); + }); +} + +void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ +// LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits); + NET_EVENT_HANDLER; + status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") + % exits.size())); + + int choosenExit = -1; + if(impassable) + { + knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; + } + else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid()) + { + auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); + if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) + choosenExit = vstd::find_pos(exits, neededExit); + } + + for(auto exit : exits) + { + if(status.channelProbing() && exit.first == destinationTeleport) + { + choosenExit = vstd::find_pos(exits, exit); + break; + } + else + { + // TODO: Implement checking if visiting that teleport will uncovert any FoW + // So far this is the best option to handle decision about probing + auto obj = cb->getObj(exit.first, false); + if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first)) + { + if(exit.first != destinationTeleport) + teleportChannelProbingList.push_back(exit.first); + } + } + } + + requestActionASAP([=]() + { + answerQuery(askID, choosenExit); + }); +} + +void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); + NET_EVENT_HANDLER; + + std::string s1 = up ? up->nodeName() : "NONE"; + std::string s2 = down ? down->nodeName() : "NONE"; + + status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); + + //you can't request action from action-response thread + requestActionASAP([=]() + { + if(removableUnits) + pickBestCreatures(down, up); + + answerQuery(queryID, 0); + }); +} + +void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + NET_EVENT_HANDLER; + status.addQuery(askID, "Map object select query"); + requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); +} + +void VCAI::saveGame(BinarySerializer & h, const int version) +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + NET_EVENT_HANDLER; + validateVisitableObjs(); + + #if 0 + //disabled due to issue 2890 + registerGoals(h); + #endif // 0 + CAdventureAI::saveGame(h, version); + serializeInternal(h, version); +} + +void VCAI::loadGame(BinaryDeserializer & h, const int version) +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + //NET_EVENT_HANDLER; + + #if 0 + //disabled due to issue 2890 + registerGoals(h); + #endif // 0 + CAdventureAI::loadGame(h, version); + serializeInternal(h, version); +} + +void makePossibleUpgrades(const CArmedInstance * obj) +{ + if(!obj) + return; + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) + { + UpgradeInfo ui; + cb->fillUpgradeInfo(obj, SlotID(i), ui); + if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) + { + cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); + } + } + } +} + +void VCAI::makeTurn() +{ + MAKING_TURN; + + 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); + setThreadName("VCAI::makeTurn"); + + switch(cb->getDate(Date::DAY_OF_WEEK)) + { + case 1: + { + townVisitsThisWeek.clear(); + std::vector objs; + retrieveVisitableObjs(objs, true); + for(const CGObjectInstance * obj : objs) + { + if(isWeeklyRevisitable(obj)) + { + addVisitableObj(obj); + vstd::erase_if_present(alreadyVisited, obj); + } + } + break; + } + } + markHeroAbleToExplore(primaryHero()); + visitedHeroes.clear(); + + try + { + //it looks messy here, but it's better to have armed heroes before attempting realizing goals + for (const CGTownInstance * t : cb->getTownsInfo()) + moveCreaturesToHero(t); + + mainLoop(); + + /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. + Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/ + performTypicalActions(); + + //for debug purpose + for (auto h : cb->getHeroesInfo()) + { + if (h->movementPointsRemaining()) + logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); + } + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); + return; + } + catch (std::exception & e) + { + logAi->debug("Making turn thread has caught an exception: %s", e.what()); + } + + endTurn(); +} + +std::vector VCAI::getMyHeroes() const +{ + std::vector ret; + + for(auto h : cb->getHeroesInfo()) + { + ret.push_back(h); + } + + return ret; +} + +void VCAI::mainLoop() +{ + std::vector elementarGoals; //no duplicates allowed (operator ==) + basicGoals.clear(); + + validateVisitableObjs(); + + //get all potential and saved goals + //TODO: not lose + basicGoals.push_back(sptr(Goals::Win())); + for (auto goalPair : lockedHeroes) + { + fh->setPriority(goalPair.second); //re-evaluate, as heroes moved in the meantime + basicGoals.push_back(goalPair.second); + } + if (ah->hasTasksLeft()) + basicGoals.push_back(ah->whatToDo()); + for (auto quest : myCb->getMyQuests()) + { + basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); + } + basicGoals.push_back(sptr(Goals::Build())); + + invalidPathHeroes.clear(); + + for (int pass = 0; pass< 30 && basicGoals.size(); pass++) + { + vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice + goalsToAdd.clear(); + goalsToRemove.clear(); + elementarGoals.clear(); + ultimateGoalsFromBasic.clear(); + + ah->updatePaths(getMyHeroes()); + + logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size()); + + for (auto basicGoal : basicGoals) + { + logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name()); + + auto goalToDecompose = basicGoal; + Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); + int maxAbstractGoals = 10; + while (!elementarGoal->isElementar && maxAbstractGoals) + { + try + { + elementarGoal = decomposeGoal(goalToDecompose); + } + catch (goalFulfilledException & e) + { + //it is impossible to continue some goals (like exploration, for example) + //complete abstract goal for now, but maybe main goal finds another path + logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); + completeGoal(e.goal); //put in goalsToRemove + break; + } + catch(cannotFulfillGoalException & e) + { + //it is impossible to continue some goals (like exploration, for example) + //complete abstract goal for now, but maybe main goal finds another path + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what()); + break; + } + catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree + { + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); + break; + } + if (elementarGoal->isAbstract) //we can decompose it further + { + goalsToAdd.push_back(elementarGoal); + //decompose further now - this is necesssary if we can't add over 10 goals in the pool + goalToDecompose = elementarGoal; + //there is a risk of infinite abstract goal loop, though it indicates failed logic + maxAbstractGoals--; + } + else if (elementarGoal->isElementar) //should be + { + logAi->debug("Found elementar goal %s", elementarGoal->name()); + elementarGoals.push_back(elementarGoal); + ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? + break; + } + else //should never be here + throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); + } + } + + logAi->trace("Main loop: selecting best elementar goal"); + + //now choose one elementar goal to realize + Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector + Goals::TSubgoal goalToRealize = sptr(Goals::Invalid()); + while (possibleGoals.size()) + { + //allow assign goals to heroes with 0 movement, but don't realize them + //maybe there are beter ones left + + auto bestGoal = fh->chooseSolution(possibleGoals); + if (bestGoal->hero) //lock this hero to fulfill goal + { + setGoal(bestGoal->hero, bestGoal); + if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero)) + { + if (!vstd::erase_if_present(possibleGoals, bestGoal)) + { + logAi->error("erase_if_preset failed? Something very wrong!"); + break; + } + continue; //chose next from the list + } + } + goalToRealize = bestGoal; //we found our goal to execute + break; + } + + //realize best goal + if (!goalToRealize->invalid()) + { + logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority); + + try + { + boost::this_thread::interruption_point(); + goalToRealize->accept(this); //visitor pattern + boost::this_thread::interruption_point(); + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Player %d: Making turn thread received an interruption!", playerID); + throw; //rethrow, we want to truly end this thread + } + catch (goalFulfilledException & e) + { + //the sub-goal was completed successfully + completeGoal(e.goal); + //local goal was also completed? + completeGoal(goalToRealize); + + // remove abstract visit tile if we completed the elementar one + vstd::erase_if_present(goalsToAdd, goalToRealize); + } + catch (std::exception & e) + { + logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name()); + logAi->debug("The error message was: %s", e.what()); + + //erase base goal if we failed to execute decomposed goal + for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize]) + goalsToRemove.push_back(basicGoal); + + // sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn. + ai->ah->notifyGoalCompleted(goalToRealize); + + //we failed to realize best goal, but maybe others are still possible? + } + + //remove goals we couldn't decompose + for (auto goal : goalsToRemove) + vstd::erase_if_present(basicGoals, goal); + + //add abstract goals + boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool + { + return lhs->priority > rhs->priority; //highest priority at the beginning + }); + + //max number of goals = 10 + int i = 0; + while (basicGoals.size() < 10 && goalsToAdd.size() > i) + { + if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates + basicGoals.push_back(goalsToAdd[i]); + i++; + } + } + else //no elementar goals possible + { + logAi->debug("Goal decomposition exhausted"); + break; + } + } +} + +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()); + switch(obj->ID) + { + case Obj::TOWN: + moveCreaturesToHero(dynamic_cast(obj)); + if(h->visitedTown) //we are inside, not just attacking + { + townVisitsThisWeek[h].insert(h->visitedTown); + if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) + { + if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) + cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); + } + } + break; + } + completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h))); +} + +void VCAI::moveCreaturesToHero(const CGTownInstance * t) +{ + if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner) + { + pickBestCreatures(t->visitingHero, t); + } +} + +void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source) +{ + const CArmedInstance * armies[] = {destinationArmy, source}; + + auto bestArmy = ah->getSortedSlots(destinationArmy, source); + + //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs + for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot + { + const CCreature * targetCreature = bestArmy[i.getNum()].creature; + + for(auto armyPtr : armies) + { + for(SlotID j = SlotID(0); j.validSlot(); j.advance(1)) + { + if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT + { + //can't take away last creature without split. generate a new stack with 1 creature which is weak but fast + if(armyPtr == source + && source->needsLastStack() + && source->stacksCount() == 1 + && (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature)) + { + auto weakest = ah->getWeakestCreature(bestArmy); + + if(weakest->creature == targetCreature) + { + if(1 == source->getStackCount(j)) + break; + + // move all except 1 of weakest creature from source to destination + cb->splitStack( + source, + destinationArmy, + j, + destinationArmy->getSlotFor(targetCreature), + destinationArmy->getStackCount(i) + source->getStackCount(j) - 1); + + break; + } + else + { + // Source last stack is not weakest. Move 1 of weakest creature from destination to source + cb->splitStack( + destinationArmy, + source, + destinationArmy->getSlotFor(weakest->creature), + source->getFreeSlot(), + 1); + } + } + + cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i); + } + } + } + } + + //TODO - having now strongest possible army, we may want to think about arranging stacks + + auto hero = dynamic_cast(destinationArmy); + if(hero) + checkHeroArmy(hero); +} + +void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other) +{ + auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void + { + bool changeMade = false; + do + { + changeMade = false; + + //we collect gear always in same order + std::vector allArtifacts; + if(giveStuffToFirstHero) + { + for(auto p : h->artifactsWorn) + { + if(p.second.artifact) + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); + } + } + for(auto slot : h->artifactsInBackpack) + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); + + if(otherh) + { + for(auto p : otherh->artifactsWorn) + { + if(p.second.artifact) + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); + } + for(auto slot : otherh->artifactsInBackpack) + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); + } + //we give stuff to one hero or another, depending on giveStuffToFirstHero + + const CGHeroInstance * target = nullptr; + if(giveStuffToFirstHero || !otherh) + target = h; + else + target = otherh; + + for(auto location : allArtifacts) + { + if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) + continue; // don't attempt to move catapult and spellbook + + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) + continue; //don't reequip artifact we already wear + + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); + if(!s || s->locked) //we can't move locks + continue; + auto artifact = s->artifact; + if(!artifact) + continue; + //FIXME: why are the above possible to be null? + + bool emptySlotFound = false; + for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + { + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move + { + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, destLocation); //just put into empty slot + emptySlotFound = true; + changeMade = true; + break; + } + } + if(!emptySlotFound) //try to put that atifact in already occupied slot + { + for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + { + auto otherSlot = target->getSlot(slot); + if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one + { + //if that artifact is better than what we have, pick it + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move + { + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); + changeMade = true; + break; + } + } + } + } + if(changeMade) + break; //start evaluating artifacts from scratch + } + } + while(changeMade); + }; + + equipBest(h, other, true); + + if(other) + equipBest(h, other, false); +} + +void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) +{ + //now used only for visited dwellings / towns, not BuyArmy goal + for(int i = 0; i < d->creatures.size(); i++) + { + if(!d->creatures[i].second.size()) + continue; + + int count = d->creatures[i].first; + CreatureID creID = d->creatures[i].second.back(); + + vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost()); + if(count > 0) + cb->recruitCreatures(d, recruiter, creID, count, i); + } +} + +bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit) +{ + int3 op = obj->visitablePos(); + auto paths = ah->getPathsToTile(h, op); + + for(const auto & path : paths) + { + if(movementCostLimit && movementCostLimit.value() < path.movementCost()) + return false; + + if(isGoodForVisit(obj, h, path)) + return true; + } + + return false; +} + +bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const +{ + const int3 pos = obj->visitablePos(); + const int3 targetPos = path.firstTileToGet(); + if (!targetPos.valid()) + return false; + if (!isTileNotReserved(h.get(), targetPos)) + return false; + if (obj->wasVisited(playerID)) + return false; + if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj)) + return false; // Otherwise we flag or get weekly resources / creatures + if (!isSafeToVisit(h, pos)) + return false; + if (!shouldVisit(h, obj)) + return false; + if (vstd::contains(alreadyVisited, obj)) + return false; + if (vstd::contains(reservedObjs, obj)) + return false; + + // TODO: looks extra if we already have AIPath + //if (!isAccessibleForHero(targetPos, h)) + // return false; + + const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj + //we don't try visiting object on which allied or owned hero stands + // -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited + return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met +} + +bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const +{ + if(t.valid()) + { + auto obj = cb->getTopObj(t); + if(obj && vstd::contains(ai->reservedObjs, obj) + && vstd::contains(reservedHeroesMap, h) + && !vstd::contains(reservedHeroesMap.at(h), obj)) + return false; //do not capture object reserved by another hero + else + return true; + } + else + { + return false; + } +} + +bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const +{ + //TODO: make gathering gold, building tavern or conquering town (?) possible subgoals + if(!t) + t = findTownWithTavern(); + if(!t) + 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)) + return false; + if(!cb->getAvailableHeroes(t).size()) + return false; + + return true; +} + +void VCAI::wander(HeroPtr h) +{ + auto visitTownIfAny = [this](HeroPtr h) -> bool + { + if (h->visitedTown) + { + townVisitsThisWeek[h].insert(h->visitedTown); + buildArmyIn(h->visitedTown); + return true; + } + return false; + }; + + //unclaim objects that are now dangerous for us + auto reservedObjsSetCopy = reservedHeroesMap[h]; + for(auto obj : reservedObjsSetCopy) + { + if(!isSafeToVisit(h, obj->visitablePos())) + unreserveObject(h, obj); + } + + TimeCheck tc("looking for wander destination"); + + for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) + { + validateVisitableObjs(); + ah->updatePaths(getMyHeroes()); + + std::vector dests; + + //also visit our reserved objects - but they are not prioritized to avoid running back and forth + vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool + { + return ah->isTileAccessible(h, obj->visitablePos()); + }); + + int pass = 0; + std::vector> distanceLimits = {1.0, 2.0, std::nullopt}; + + while(!dests.size() && pass < distanceLimits.size()) + { + auto & distanceLimit = distanceLimits[pass]; + + logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0)); + + vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool + { + return isGoodForVisit(obj, h, distanceLimit); + }); + + pass++; + } + + if(!dests.size()) + { + logAi->debug("Looking for town destination"); + + if(cb->getVisitableObjs(h->visitablePos()).size() > 1) + moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate + + auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool + { + const CGHeroInstance * hptr = h.get(); + auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs), + r2 = ah->howManyReinforcementsCanGet(hptr, rhs); + if (r1 != r2) + return r1 < r2; + else + return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs); + }; + + std::vector townsReachable; + std::vector townsNotReachable; + for(const CGTownInstance * t : cb->getTownsInfo()) + { + if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t)) + { + if(isAccessibleForHero(t->visitablePos(), h)) + townsReachable.push_back(t); + else + townsNotReachable.push_back(t); + } + } + if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing + { + dests.push_back(*boost::max_element(townsReachable, compareReinforcements)); + } + else if(townsNotReachable.size()) + { + //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; + 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(canRecruitAnyHero(t)) + recruitHero(t); + } + break; + } + else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST) + { + std::vector towns = cb->getTownsInfo(); + vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool + { + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t)) + return true; + } + return false; + }); + if (towns.size()) + { + recruitHero(*boost::max_element(towns, compareArmyStrength)); + } + break; + } + else + { + logAi->debug("Nowhere more to go..."); + break; + } + } + //end of objs empty + + if(dests.size()) //performance improvement + { + Goals::TGoalVec targetObjectGoals; + for(auto destination : dests) + { + vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false)); + } + + if(targetObjectGoals.size()) + { + auto bestObjectGoal = fh->chooseSolution(targetObjectGoals); + + //wander should not cause heroes to be reserved - they are always considered free + if(bestObjectGoal->goalType == Goals::VISIT_OBJ) + { + 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()); + } + else + logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); + + try + { + decomposeGoal(bestObjectGoal)->accept(this); + } + catch(const goalFulfilledException & e) + { + if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) + continue; + + throw; + } + } + else + { + logAi->debug("Nowhere more to go..."); + break; + } + + visitTownIfAny(h); + } + } + + visitTownIfAny(h); //in case hero is just sitting in town +} + +void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) +{ + if(goal->invalid()) + { + vstd::erase_if_present(lockedHeroes, h); + } + else + { + lockedHeroes[h] = goal; + goal->setisElementar(false); //Force always evaluate goals before realizing + } +} +void VCAI::evaluateGoal(HeroPtr h) +{ + if(vstd::contains(lockedHeroes, h)) + fh->setPriority(lockedHeroes[h]); +} + +void VCAI::completeGoal(Goals::TSubgoal goal) +{ + if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won + return; + + logAi->debug("Completing goal: %s", goal->name()); + + //notify Managers + ah->notifyGoalCompleted(goal); + //notify mainLoop() + goalsToRemove.push_back(goal); //will be removed from mainLoop() goals + for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals + { + if (basicGoal->fulfillsMe(goal)) + goalsToRemove.push_back(basicGoal); + } + + //unreserve heroes + if(const CGHeroInstance * h = goal->hero.get(true)) + { + 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 + { + logAi->debug(goal->completeMessage()); + lockedHeroes.erase(it); //goal fulfilled, free hero + } + } + } + else //complete goal for all heroes maybe? + { + vstd::erase_if(lockedHeroes, [goal](std::pair p) + { + if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance + { + logAi->debug(p.second->completeMessage()); + return true; + } + return false; + }); + } + +} + +void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +{ + NET_EVENT_HANDLER; + assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); + status.setBattle(ONGOING_BATTLE); + const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit + battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); + CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); +} + +void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) +{ + NET_EVENT_HANDLER; + assert(status.getBattle() == ONGOING_BATTLE); + status.setBattle(ENDING_BATTLE); + bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); + logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); + battlename.clear(); + + if (queryID != QueryID::NONE) + { + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + } + CAdventureAI::battleEnd(battleID, br, queryID); +} + +void VCAI::waitTillFree() +{ + auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex); + status.waitTillFree(); +} + +void VCAI::markObjectVisited(const CGObjectInstance * obj) +{ + if(!obj) + return; + + if(const auto * rewardable = dynamic_cast(obj)) //we may want to visit it with another hero + { + if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero + return; + + if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time + return; + } + + if(obj->ID == Obj::MONSTER) + return; + + alreadyVisited.insert(obj); +} + +void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj) +{ + reservedObjs.insert(obj); + reservedHeroesMap[h].insert(obj); + logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName()); +} + +void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj) +{ + vstd::erase_if_present(reservedObjs, obj); //unreserve objects + vstd::erase_if_present(reservedHeroesMap[h], obj); +} + +void VCAI::markHeroUnableToExplore(HeroPtr h) +{ + heroesUnableToExplore.insert(h); +} +void VCAI::markHeroAbleToExplore(HeroPtr h) +{ + vstd::erase_if_present(heroesUnableToExplore, h); +} +bool VCAI::isAbleToExplore(HeroPtr h) +{ + return !vstd::contains(heroesUnableToExplore, h); +} +void VCAI::clearPathsInfo() +{ + heroesUnableToExplore.clear(); +} + +void VCAI::validateVisitableObjs() +{ + std::string errorMsg; + auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool + { + if(obj) + return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility + else + return true; + }; + + //errorMsg is captured by ref so lambda will take the new text + errorMsg = " shouldn't be on the visitable objects list!"; + vstd::erase_if(visitableObjs, shouldBeErased); + + //FIXME: how comes our own heroes become inaccessible? + vstd::erase_if(reservedHeroesMap, [](std::pair> hp) -> bool + { + return !hp.first.get(true); + }); + for(auto & p : reservedHeroesMap) + { + errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!"; + vstd::erase_if(p.second, shouldBeErased); + } + + errorMsg = " shouldn't be on the reserved objs list!"; + vstd::erase_if(reservedObjs, shouldBeErased); + + //TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game. + errorMsg = " shouldn't be on the already visited objs list!"; + vstd::erase_if(alreadyVisited, shouldBeErased); +} + +void VCAI::retrieveVisitableObjs(std::vector & out, bool includeOwned) const +{ + foreach_tile_pos([&](const int3 & pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) + { + if(includeOwned || obj->tempOwner != playerID) + out.push_back(obj); + } + }); +} + +void VCAI::retrieveVisitableObjs() +{ + foreach_tile_pos([&](const int3 & pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) + { + if(obj->tempOwner != playerID) + addVisitableObj(obj); + } + }); +} + +std::vector VCAI::getFlaggedObjects() const +{ + std::vector ret; + for(const CGObjectInstance * obj : visitableObjs) + { + if(obj->tempOwner == playerID) + ret.push_back(obj); + } + return ret; +} + +void VCAI::addVisitableObj(const CGObjectInstance * obj) +{ + if(obj->ID == Obj::EVENT) + return; + + visitableObjs.insert(obj); + + // All teleport objects seen automatically assigned to appropriate channels + auto teleportObj = dynamic_cast(obj); + if(teleportObj) + CGTeleport::addToChannel(knownTeleportChannels, teleportObj); +} + +const CGObjectInstance * VCAI::lookForArt(ArtifactID aid) const +{ + for(const CGObjectInstance * obj : ai->visitableObjs) + { + if(obj->ID == Obj::ARTIFACT && dynamic_cast(obj)->getArtifact() == aid) + return obj; + } + + return nullptr; + + //TODO what if more than one artifact is available? return them all or some slection criteria +} + +bool VCAI::isAccessible(const int3 & pos) const +{ + //TODO precalculate for speed + + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(isAccessibleForHero(pos, h)) + return true; + } + + return false; +} + +HeroPtr VCAI::getHeroWithGrail() const +{ + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(h->hasArt(ArtifactID::GRAIL)) + return h; + } + return nullptr; +} + +const CGObjectInstance * VCAI::getUnvisitedObj(const std::function & predicate) +{ + //TODO smarter definition of unvisited + for(const CGObjectInstance * obj : visitableObjs) + { + if(predicate(obj) && !vstd::contains(alreadyVisited, obj)) + return obj; + } + return nullptr; +} + +bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const +{ + // Don't visit tile occupied by allied hero + if(!includeAllies) + { + for(auto obj : cb->getVisitableObjs(pos)) + { + if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES) + { + if(obj != h.get()) + return false; + } + } + } + return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); +} + +bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) +{ + //TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj() + + auto afterMovementCheck = [&]() -> void + { + waitTillFree(); //movement may cause battle or blocking dialog + if(!h) + { + lostHero(h); + teleportChannelProbingList.clear(); + if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off + status.setChannelProbing(false); + throw cannotFulfillGoalException("Hero was lost!"); + } + }; + + logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString()); + int3 startHpos = h->visitablePos(); + bool ret = false; + if(startHpos == dst) + { + //FIXME: this assertion fails also if AI moves onto defeated guarded object + assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object + cb->moveHero(*h, h->convertFromVisitablePos(dst)); + afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? + // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared + teleportChannelProbingList.clear(); + // not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information. + ret = true; + } + else + { + CGPath path; + cb->getPathsInfo(h.get())->getPath(path, dst); + if(path.nodes.empty()) + { + logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString()); + throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h))); + } + int i = (int)path.nodes.size() - 1; + + auto getObj = [&](int3 coord, bool ignoreHero) + { + auto tile = cb->getTile(coord, false); + assert(tile); + return tile->topVisitableObj(ignoreHero); + //return cb->getTile(coord,false)->topVisitableObj(ignoreHero); + }; + + auto isTeleportAction = [&](EPathNodeAction action) -> bool + { + if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT) + { + if(action != EPathNodeAction::TELEPORT_BATTLE) + { + return false; + } + } + + return true; + }; + + auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * + { + if(CGTeleport::isConnected(currentObject, nextObjectTop)) + return nextObjectTop; + if(nextObjectTop && nextObjectTop->ID == Obj::HERO) + { + if(CGTeleport::isConnected(currentObject, nextObject)) + return nextObject; + } + + return nullptr; + }; + + auto doMovement = [&](int3 dst, bool transit) + { + cb->moveHero(*h, h->convertFromVisitablePos(dst), transit); + }; + + auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) + { + destinationTeleport = exitId; + if(exitPos.valid()) + destinationTeleportPos = h->convertFromVisitablePos(exitPos); + cb->moveHero(*h, h->pos); + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + afterMovementCheck(); + }; + + auto doChannelProbing = [&]() -> void + { + auto currentPos = h->visitablePos(); + auto currentExit = getObj(currentPos, true)->id; + + status.setChannelProbing(true); + for(auto exit : teleportChannelProbingList) + doTeleportMovement(exit, int3(-1)); + teleportChannelProbingList.clear(); + status.setChannelProbing(false); + + doTeleportMovement(currentExit, currentPos); + }; + + for(; i > 0; i--) + { + int3 currentCoord = path.nodes[i].coord; + int3 nextCoord = path.nodes[i - 1].coord; + + auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos()); + auto nextObjectTop = getObj(nextCoord, false); + auto nextObject = getObj(nextCoord, true); + auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); + if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr) + { + //we use special login if hero standing on teleporter it's mean we need + doTeleportMovement(destTeleportObj->id, nextCoord); + if(teleportChannelProbingList.size()) + doChannelProbing(); + markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited + + continue; + } + + //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) + if(path.nodes[i - 1].turns) + { + //blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs + break; + } + + int3 endpos = path.nodes[i - 1].coord; + if(endpos == h->visitablePos()) + continue; + + bool isConnected = false; + bool isNextObjectTeleport = false; + // Check there is node after next one; otherwise transit is pointless + if(i - 2 >= 0) + { + isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false)); + isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop); + } + if(isConnected || isNextObjectTeleport) + { + // Hero should be able to go through object if it's allow transit + doMovement(endpos, true); + } + else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR) + { + doMovement(endpos, true); + } + else + { + doMovement(endpos, false); + } + + afterMovementCheck(); + + if(teleportChannelProbingList.size()) + doChannelProbing(); + } + + if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT) + { + ret = h && i == 0; // when we take resource we do not reach its position. We even might not move + } + } + if(h) + { + if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting + { + if(visitedObject != *h) + performObjectInteraction(visitedObject, h); + } + } + if(h) //we could have lost hero after last move + { + completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway + completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h))); + + ret = ret || (dst == h->visitablePos()); + + if(!ret) //reserve object we are heading towards + { + auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst)); + if(obj && obj != *h) + reserveObject(h, obj); + } + + if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target + { + vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move + invalidPathHeroes.insert(h); + throw cannotFulfillGoalException("Invalid path found!"); + } + evaluateGoal(h); //new hero position means new game situation + logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret); + } + return ret; +} + +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()); + cb->buildBuilding(t, building); //just do this; +} + +void VCAI::tryRealize(Goals::Explore & g) +{ + throw cannotFulfillGoalException("EXPLORE is not an elementar goal!"); +} + +void VCAI::tryRealize(Goals::RecruitHero & g) +{ + if(const CGTownInstance * t = findTownWithTavern()) + { + recruitHero(t, true); + //TODO try to free way to blocked town + //TODO: adventure map tavern or prison? + } + else + { + throw cannotFulfillGoalException("No town to recruit hero!"); + } +} + +void VCAI::tryRealize(Goals::VisitTile & g) +{ + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!"); + if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) + { + logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); + throw goalFulfilledException(sptr(g)); + } + if(ai->moveHeroToTile(g.tile, g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } +} + +void VCAI::tryRealize(Goals::VisitObj & g) +{ + auto position = g.tile; + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!"); + if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) + { + logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); + throw goalFulfilledException(sptr(g)); + } + if(ai->moveHeroToTile(position, g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } +} + +void VCAI::tryRealize(Goals::VisitHero & g) +{ + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!"); + + const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid)); + if(obj) + { + if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } + } + else + { + throw cannotFulfillGoalException("Cannot visit hero: object not found!"); + } +} + +void VCAI::tryRealize(Goals::BuildThis & g) +{ + auto b = BuildingID(g.bid); + auto t = g.town; + + if (t) + { + 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()); + cb->buildBuilding(t, b); + throw goalFulfilledException(sptr(g)); + } + } + throw cannotFulfillGoalException("Cannot build a given structure!"); +} + +void VCAI::tryRealize(Goals::DigAtTile & g) +{ + assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? + if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) + { + cb->dig(g.hero.get()); + completeGoal(sptr(g)); // finished digging + } + else + { + ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else + throw cannotFulfillGoalException("A hero can't dig!\n"); + } +} + +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; + if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) + { + if(const IMarket * m = IMarket::castFrom(obj, false)) + { + auto freeRes = ah->freeResources(); //trade only resources which are not reserved + for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) + { + auto res = it->resType; + if(res.getNum() == g.resID) //sell any other resource + continue; + + int toGive, toGet; + m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); + toGive = static_cast(toGive * (it->resVal / toGive)); //round down + //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()); + } + if (ah->freeResources()[g.resID] >= g.value) + throw goalFulfilledException(sptr(g)); //we traded all we needed + } + + throw cannotFulfillGoalException("I cannot get needed resources by trade!"); + } + else + { + throw cannotFulfillGoalException("I don't know how to use this object to raise resources!"); + } + } + else + { + throw cannotFulfillGoalException("No object that could be used to raise resources!"); + } +} + +void VCAI::tryRealize(Goals::BuyArmy & g) +{ + auto t = g.town; + + ui64 valueBought = 0; + //buy the stacks with largest AI value + + makePossibleUpgrades(t); + + while (valueBought < g.value) + { + auto res = ah->allResources(); + std::vector creaturesInDwellings; + + for (int i = 0; i < t->creatures.size(); i++) + { + auto ci = infoFromDC(t->creatures[i]); + + if(!ci.count + || ci.creID == CreatureID::NONE + || (g.objid != -1 && ci.creID.getNum() != g.objid) + || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) + continue; + + vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford + + if(!ci.count) + continue; + + ci.level = i; //this is important for Dungeon Summoning Portal + creaturesInDwellings.push_back(ci); + } + + if (creaturesInDwellings.empty()) + throw cannotFulfillGoalException("Can't buy any more creatures!"); + + creInfo ci = + *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) + { + //max value of creatures we can buy with our res + int value1 = lhs.cre->getAIValue() * lhs.count, + value2 = rhs.cre->getAIValue() * rhs.count; + + return value1 < value2; + }); + + + cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); + valueBought += ci.count * ci.cre->getAIValue(); + } + + throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted +} + +void VCAI::tryRealize(Goals::Invalid & g) +{ + throw cannotFulfillGoalException("I don't know how to fulfill this!"); +} + +void VCAI::tryRealize(Goals::AbstractGoal & g) +{ + logAi->debug("Attempting realizing goal with code %s", g.name()); + throw cannotFulfillGoalException("Unknown type of goal !"); +} + +const CGTownInstance * VCAI::findTownWithTavern() const +{ + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero) + return t; + + return nullptr; +} + +Goals::TSubgoal VCAI::getGoal(HeroPtr h) const +{ + auto it = lockedHeroes.find(h); + if(it != lockedHeroes.end()) + return it->second; + else + return sptr(Goals::Invalid()); +} + + +std::vector VCAI::getUnblockedHeroes() const +{ + std::vector ret; + for(auto h : cb->getHeroesInfo()) + { + //&& !vstd::contains(lockedHeroes, h) + //at this point we assume heroes exhausted their locked goals + if(canAct(h)) + ret.push_back(h); + } + return ret; +} + +bool VCAI::canAct(HeroPtr h) const +{ + auto mission = lockedHeroes.find(h); + if(mission != lockedHeroes.end()) + { + //FIXME: I'm afraid there can be other conditions when heroes can act but not move :? + if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar) + return false; + } + + return h->movementPointsRemaining(); +} + +HeroPtr VCAI::primaryHero() const +{ + auto hs = cb->getHeroesInfo(); + if (hs.empty()) + return nullptr; + else + return *boost::max_element(hs, compareHeroStrength); +} + +void VCAI::endTurn() +{ + logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); + if(!status.haveTurn()) + { + logAi->error("Not having turn at the end of turn???"); + } + logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); + do + { + cb->endTurn(); + } + while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over + + logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); +} + +void VCAI::striveToGoal(Goals::TSubgoal basicGoal) +{ + //TODO: this function is deprecated and should be dropped altogether + + auto goalToDecompose = basicGoal; + Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); + int maxAbstractGoals = 10; + while (!elementarGoal->isElementar && maxAbstractGoals) + { + try + { + elementarGoal = decomposeGoal(goalToDecompose); + } + catch (goalFulfilledException & e) + { + //it is impossible to continue some goals (like exploration, for example) + completeGoal(e.goal); //put in goalsToRemove + logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); + return; + } + catch (std::exception & e) + { + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); + return; + } + if (elementarGoal->isAbstract) //we can decompose it further + { + goalsToAdd.push_back(elementarGoal); + //decompose further now - this is necesssary if we can't add over 10 goals in the pool + goalToDecompose = elementarGoal; + //there is a risk of infinite abstract goal loop, though it indicates failed logic + maxAbstractGoals--; + } + else if (elementarGoal->isElementar) //should be + { + logAi->debug("Found elementar goal %s", elementarGoal->name()); + ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? + break; + } + else //should never be here + throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); + } + + //realize best goal + if (!elementarGoal->invalid()) + { + logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority); + + try + { + boost::this_thread::interruption_point(); + elementarGoal->accept(this); //visitor pattern + boost::this_thread::interruption_point(); + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Player %d: Making turn thread received an interruption!", playerID); + throw; //rethrow, we want to truly end this thread + } + catch (goalFulfilledException & e) + { + //the sub-goal was completed successfully + completeGoal(e.goal); + //local goal was also completed + completeGoal(elementarGoal); + } + catch (std::exception & e) + { + logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name()); + logAi->debug("The error message was: %s", e.what()); + + //erase base goal if we failed to execute decomposed goal + for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal]) + goalsToRemove.push_back(basicGoalToRemove); + } + } +} + +Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) +{ + if(ultimateGoal->isElementar) + { + logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name()); + + return ultimateGoal; + } + + const int searchDepth = 30; + + Goals::TSubgoal goal = ultimateGoal; + logAi->debug("Decomposing goal %s", ultimateGoal->name()); + int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals + while (maxGoals) + { + boost::this_thread::interruption_point(); + + goal = goal->whatToDoToAchieve(); //may throw if decomposition fails + --maxGoals; + if (goal == ultimateGoal) //compare objects by value + if (goal->isElementar == ultimateGoal->isElementar) + throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!") + % ultimateGoal->name()).str()); + if (goal->isAbstract || goal->isElementar) + return goal; + else + logAi->debug("Considering: %s", goal->name()); + } + + throw cannotFulfillGoalException("Too many subgoals, don't know what to do"); +} + +void VCAI::performTypicalActions() +{ + for(auto h : getUnblockedHeroes()) + { + if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn + continue; + + logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining()); + makePossibleUpgrades(*h); + pickBestArtifacts(*h); + try + { + wander(h); + } + catch(std::exception & e) + { + logAi->debug("Cannot use this hero anymore, received exception: %s", e.what()); + continue; + } + } +} + +void VCAI::buildArmyIn(const CGTownInstance * t) +{ + makePossibleUpgrades(t->visitingHero); + makePossibleUpgrades(t); + recruitCreatures(t, t->getUpperArmy()); + moveCreaturesToHero(t); +} + +void VCAI::checkHeroArmy(HeroPtr h) +{ + auto it = lockedHeroes.find(h); + if(it != lockedHeroes.end()) + { + if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength()) + completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h))); + } +} + +void VCAI::recruitHero(const CGTownInstance * t, bool throwing) +{ + logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString()); + + auto heroes = cb->getAvailableHeroes(t); + if(heroes.size()) + { + auto hero = heroes[0]; + if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week + { + if(heroes[1]->getTotalStrength() > hero->getTotalStrength()) + hero = heroes[1]; + } + cb->recruitHero(t, hero); + throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); + } + else if(throwing) + { + throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); + } +} + +void VCAI::finish() +{ + //we want to lock to avoid multiple threads from calling makingTurn->join() at same time + boost::lock_guard multipleCleanupGuard(turnInterruptionMutex); + if(makingTurn) + { + makingTurn->interrupt(); + makingTurn->join(); + makingTurn.reset(); + } +} + +void VCAI::requestActionASAP(std::function whatToDo) +{ + boost::thread newThread([this, whatToDo]() + { + setThreadName("VCAI::requestActionASAP::whatToDo"); + SET_GLOBAL_STATE(this); + boost::shared_lock gsLock(CGameState::mutex); + whatToDo(); + }); + + newThread.detach(); +} + +void VCAI::lostHero(HeroPtr h) +{ + logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name); + + vstd::erase_if_present(lockedHeroes, h); + for(auto obj : reservedHeroesMap[h]) + { + vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero + } + vstd::erase_if_present(reservedHeroesMap, h); + vstd::erase_if_present(visitedHeroes, h); + for (auto heroVec : visitedHeroes) + { + vstd::erase_if_present(heroVec.second, h); + } + + //remove goals with removed hero assigned from main loop + vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool + { + if(x.first->hero == h) + return true; + else + return false; + }); + + auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool + { + if(x->hero == h) + return true; + else + return false; + }; + + vstd::erase_if(basicGoals, removedHeroGoalPredicate); + vstd::erase_if(goalsToAdd, removedHeroGoalPredicate); + vstd::erase_if(goalsToRemove, removedHeroGoalPredicate); + + for(auto goal : ultimateGoalsFromBasic) + vstd::erase_if(goal.second, removedHeroGoalPredicate); +} + +void VCAI::answerQuery(QueryID queryID, int selection) +{ + logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection); + if(queryID != QueryID(-1)) + { + cb->selectionMade(selection, queryID); + } + else + { + logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID); + //do nothing + } +} + +void VCAI::requestSent(const CPackForServer * pack, int requestID) +{ + //BNLOG("I have sent request of type %s", typeid(*pack).name()); + if(auto reply = dynamic_cast(pack)) + { + status.attemptedAnsweringQuery(reply->qid, requestID); + } +} + +std::string VCAI::getBattleAIName() const +{ + if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING) + return settings["server"]["enemyAI"].String(); + else + return "BattleAI"; +} + +void VCAI::validateObject(const CGObjectInstance * obj) +{ + validateObject(obj->id); +} + +void VCAI::validateObject(ObjectIdRef obj) +{ + auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool + { + return hlpObj->id == obj.id; + }; + if(!obj) + { + vstd::erase_if(visitableObjs, matchesId); + + for(auto & p : reservedHeroesMap) + vstd::erase_if(p.second, matchesId); + + vstd::erase_if(reservedObjs, matchesId); + } +} + +AIStatus::AIStatus() +{ + battle = NO_BATTLE; + havingTurn = false; + ongoingHeroMovement = false; + ongoingChannelProbing = false; +} + +AIStatus::~AIStatus() +{ + +} + +void AIStatus::setBattle(BattleState BS) +{ + boost::unique_lock lock(mx); + LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS); + battle = BS; + cv.notify_all(); +} + +BattleState AIStatus::getBattle() +{ + boost::unique_lock lock(mx); + return battle; +} + +void AIStatus::addQuery(QueryID ID, std::string description) +{ + if(ID == QueryID(-1)) + { + logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description); + return; + } + + assert(ID.getNum() >= 0); + boost::unique_lock lock(mx); + + assert(!vstd::contains(remainingQueries, ID)); + + remainingQueries[ID] = description; + + cv.notify_all(); + logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); +} + +void AIStatus::removeQuery(QueryID ID) +{ + boost::unique_lock lock(mx); + assert(vstd::contains(remainingQueries, ID)); + + std::string description = remainingQueries[ID]; + remainingQueries.erase(ID); + + cv.notify_all(); + logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); +} + +int AIStatus::getQueriesCount() +{ + boost::unique_lock lock(mx); + return static_cast(remainingQueries.size()); +} + +void AIStatus::startedTurn() +{ + boost::unique_lock lock(mx); + havingTurn = true; + cv.notify_all(); +} + +void AIStatus::madeTurn() +{ + boost::unique_lock lock(mx); + havingTurn = false; + cv.notify_all(); +} + +void AIStatus::waitTillFree() +{ + boost::unique_lock lock(mx); + while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) + cv.wait_for(lock, boost::chrono::milliseconds(100)); +} + +bool AIStatus::haveTurn() +{ + boost::unique_lock lock(mx); + return havingTurn; +} + +void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID) +{ + boost::unique_lock lock(mx); + assert(vstd::contains(remainingQueries, queryID)); + std::string description = remainingQueries[queryID]; + logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID); + requestToQueryID[answerRequestID] = queryID; +} + +void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result) +{ + QueryID query; + + { + boost::unique_lock lock(mx); + + assert(vstd::contains(requestToQueryID, answerRequestID)); + query = requestToQueryID[answerRequestID]; + assert(vstd::contains(remainingQueries, query)); + requestToQueryID.erase(answerRequestID); + } + + if(result) + { + removeQuery(query); + } + else + { + logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]); + //TODO safely retry + } +} + +void AIStatus::heroVisit(const CGObjectInstance * obj, bool started) +{ + boost::unique_lock lock(mx); + if(started) + { + objectsBeingVisited.push_back(obj); + } + else + { + // There can be more than one object visited at the time (eg. hero visits Subterranean Gate + // causing visit to hero on the other side. + // However, we are guaranteed that start/end visit notification maintain stack order. + assert(!objectsBeingVisited.empty()); + objectsBeingVisited.pop_back(); + } + cv.notify_all(); +} + +void AIStatus::setMove(bool ongoing) +{ + boost::unique_lock lock(mx); + ongoingHeroMovement = ongoing; + cv.notify_all(); +} + +void AIStatus::setChannelProbing(bool ongoing) +{ + boost::unique_lock lock(mx); + ongoingChannelProbing = ongoing; + cv.notify_all(); +} + +bool AIStatus::channelProbing() +{ + return ongoingChannelProbing; +} + + + +bool isWeeklyRevisitable(const CGObjectInstance * obj) +{ + //TODO: allow polling of remaining creatures in dwelling + if(const auto * rewardable = dynamic_cast(obj)) + return rewardable->configuration.getResetDuration() == 7; + + if(dynamic_cast(obj)) + return true; + if(dynamic_cast(obj)) //banks tend to respawn often in mods + return true; + + switch(obj->ID) + { + case Obj::STABLES: + case Obj::MAGIC_WELL: + case Obj::HILL_FORT: + return true; + case Obj::BORDER_GATE: + case Obj::BORDERGUARD: + return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week + } + return false; +} + +bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) +{ + switch(obj->ID) + { + case Obj::TOWN: + case Obj::HERO: //never visit our heroes at random + return obj->tempOwner != h->tempOwner; //do not visit our towns at random + case Obj::BORDER_GATE: + { + for(auto q : ai->myCb->getMyQuests()) + { + if(q.obj == obj) + { + return false; // do not visit guards or gates when wandering + } + } + return true; //we don't have this quest yet + } + case Obj::BORDERGUARD: //open borderguard if possible + return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); + case Obj::SEER_HUT: + case Obj::QUEST_GUARD: + { + for(auto q : ai->myCb->getMyQuests()) + { + if(q.obj == obj) + { + if(q.quest->checkQuest(h.h)) + return true; //we completed the quest + else + return false; //we can't complete this quest + } + } + return true; //we don't have this quest yet + } + case Obj::CREATURE_GENERATOR1: + { + if(obj->tempOwner != h->tempOwner) + return true; //flag just in case + bool canRecruitCreatures = false; + const CGDwelling * d = dynamic_cast(obj); + for(auto level : d->creatures) + { + for(auto c : level.second) + { + if(h->getSlotFor(CreatureID(c)) != SlotID()) + canRecruitCreatures = true; + } + } + return canRecruitCreatures; + } + case Obj::HILL_FORT: + { + for(auto slot : h->Slots()) + { + if(slot.second->type->hasUpgrades()) + return true; //TODO: check price? + } + return false; + } + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_ONE_WAY_EXIT: + case Obj::MONOLITH_TWO_WAY: + case Obj::WHIRLPOOL: + return false; + case Obj::SCHOOL_OF_MAGIC: + case Obj::SCHOOL_OF_WAR: + { + if (ai->ah->freeGold() < 1000) + return false; + break; + } + case Obj::LIBRARY_OF_ENLIGHTENMENT: + if(h->level < 12) + return false; + break; + case Obj::TREE_OF_KNOWLEDGE: + { + TResources myRes = ai->ah->freeResources(); + if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10) + return false; + break; + } + 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); + 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)) + return false; + else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) + return false; + break; + } + case Obj::BOAT: + return false; + //Boats are handled by pathfinder + case Obj::EYE_OF_MAGI: + return false; //this object is useless to visit, but could be visited indefinitely + } + + if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); + return false; + + return true; +} + +std::optional VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 7d57ad681..5f327e5c3 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -1,406 +1,407 @@ -/* - * VCAI.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 "AIUtility.h" -#include "Goals/AbstractGoal.h" -#include "../../lib/AI_Base.h" -#include "../../CCallback.h" - -#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 "../../lib/CondSh.h" -#include "Pathfinding/AIPathfinder.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class AIhelper; - -class AIStatus -{ - boost::mutex mx; - boost::condition_variable cv; - - BattleState battle; - std::map remainingQueries; - std::map requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query) - std::vector objectsBeingVisited; - bool ongoingHeroMovement; - bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits - - bool havingTurn; - -public: - AIStatus(); - ~AIStatus(); - void setBattle(BattleState BS); - void setMove(bool ongoing); - void setChannelProbing(bool ongoing); - bool channelProbing(); - BattleState getBattle(); - void addQuery(QueryID ID, std::string description); - void removeQuery(QueryID ID); - int getQueriesCount(); - void startedTurn(); - void madeTurn(); - void waitTillFree(); - bool haveTurn(); - void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); - void receivedAnswerConfirmation(int answerRequestID, int result); - void heroVisit(const CGObjectInstance * obj, bool started); - - - template void serialize(Handler & h, const int version) - { - h & battle; - h & remainingQueries; - h & requestToQueryID; - h & havingTurn; - } -}; - -class DLL_EXPORT VCAI : public CAdventureAI -{ -public: - - friend class FuzzyHelper; - friend class ResourceManager; - friend class BuildingManager; - - std::map> knownTeleportChannels; - std::map knownSubterraneanGates; - ObjectInstanceID destinationTeleport; - int3 destinationTeleportPos; - std::vector teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored - //std::vector visitedThisWeek; //only OPWs - std::map> townVisitsThisWeek; - - //part of mainLoop, but accessible from outisde - std::vector basicGoals; - Goals::TGoalVec goalsToRemove; - Goals::TGoalVec goalsToAdd; - std::map ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals - - std::set invalidPathHeroes; //FIXME, just a workaround - std::map lockedHeroes; //TODO: allow non-elementar objectives - std::map> reservedHeroesMap; //objects reserved by specific heroes - std::set heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game - - //sets are faster to search, also do not contain duplicates - std::set visitableObjs; - std::set alreadyVisited; - std::set reservedObjs; //to be visited by specific hero - std::map> visitedHeroes; //visited this turn //FIXME: this is just bug workaround - - AIStatus status; - std::string battlename; - - std::shared_ptr myCb; - - std::unique_ptr makingTurn; -private: - boost::mutex turnInterruptionMutex; -public: - ObjectInstanceID selectedObject; - - AIhelper * ah; - - VCAI(); - virtual ~VCAI(); - - //TODO: use only smart pointers? - void tryRealize(Goals::Explore & g); - void tryRealize(Goals::RecruitHero & g); - void tryRealize(Goals::VisitTile & g); - void tryRealize(Goals::VisitObj & g); - void tryRealize(Goals::VisitHero & g); - void tryRealize(Goals::BuildThis & g); - void tryRealize(Goals::DigAtTile & g); - void tryRealize(Goals::Trade & g); - void tryRealize(Goals::BuyArmy & g); - void tryRealize(Goals::Invalid & g); - void tryRealize(Goals::AbstractGoal & g); - - bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved - - std::string getBattleAIName() const override; - - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn() override; - - void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO - void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - 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(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, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading - void finish() override; - - void availableCreaturesChanged(const CGDwelling * town) override; - void heroMoved(const TryMoveHero & details, bool verbose = true) override; - void heroInGarrisonChange(const CGTownInstance * town) override; - void centerView(int3 pos, int focusTime) override; - void tileHidden(const std::unordered_set & pos) override; - void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; - void artifactAssembled(const ArtifactLocation & al) override; - void showTavernWindow(const CGObjectInstance * townOrTavern) override; - void showThievesGuildWindow(const CGObjectInstance * obj) override; - void playerBlocked(int reason, bool start) override; - void showPuzzleMap() override; - void showShipyardDialog(const IShipyard * obj) override; - void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; - void artifactPut(const ArtifactLocation & al) override; - void artifactRemoved(const ArtifactLocation & al) override; - void artifactDisassembled(const ArtifactLocation & al) override; - void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; - void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; - void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; - void tileRevealed(const std::unordered_set & pos) override; - void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; - void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; - void heroMovePointsChanged(const CGHeroInstance * hero) override; - void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; - void newObject(const CGObjectInstance * obj) override; - void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; - void playerBonusChanged(const Bonus & bonus, bool gain) override; - void heroCreated(const CGHeroInstance *) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; - void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; - void requestRealized(PackageApplied * pa) override; - void receivedResource() override; - void objectRemoved(const CGObjectInstance * obj) override; - void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; - void heroManaPointsChanged(const CGHeroInstance * hero) override; - void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; - void battleResultsApplied() override; - void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; - void objectPropertyChanged(const SetObjectProperty * sop) override; - void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; - void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; - void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; - void battleEnd(const BattleResult * br, QueryID queryID) override; - - void makeTurn(); - void mainLoop(); - void performTypicalActions(); - - void buildArmyIn(const CGTownInstance * t); - void striveToGoal(Goals::TSubgoal ultimateGoal); - Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal); - void endTurn(); - void wander(HeroPtr h); - void setGoal(HeroPtr h, Goals::TSubgoal goal); - void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any - void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero - - void recruitHero(const CGTownInstance * t, bool throwing = false); - bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit = std::nullopt); - bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const; - //void recruitCreatures(const CGTownInstance * t); - void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); - void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack - void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr); - void moveCreaturesToHero(const CGTownInstance * t); - void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); - - bool moveHeroToTile(int3 dst, HeroPtr h); - void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager - - void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on) - void waitTillFree(); - - void addVisitableObj(const CGObjectInstance * obj); - void markObjectVisited(const CGObjectInstance * obj); - void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit - void unreserveObject(HeroPtr h, const CGObjectInstance * obj); - - void markHeroUnableToExplore(HeroPtr h); - void markHeroAbleToExplore(HeroPtr h); - bool isAbleToExplore(HeroPtr h); - void clearPathsInfo(); - - void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it - void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it - void validateVisitableObjs(); - void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; - void retrieveVisitableObjs(); - virtual std::vector getFlaggedObjects() const; - - const CGObjectInstance * lookForArt(int aid) const; - bool isAccessible(const int3 & pos) const; - HeroPtr getHeroWithGrail() const; - - const CGObjectInstance * getUnvisitedObj(const std::function & predicate); - bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; - //optimization - use one SM for every hero call - - const CGTownInstance * findTownWithTavern() const; - bool canRecruitAnyHero(const CGTownInstance * t = NULL) const; - - Goals::TSubgoal getGoal(HeroPtr h) const; - bool canAct(HeroPtr h) const; - std::vector getUnblockedHeroes() const; - std::vector getMyHeroes() const; - HeroPtr primaryHero() const; - void checkHeroArmy(HeroPtr h); - - void requestSent(const CPackForServer * pack, int requestID) override; - 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, const int version) - { - 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, version); - } - } - } - } - } - #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 -{ - std::string msg; - -public: - explicit cannotFulfillGoalException(crstring _Message) - : msg(_Message) - { - } - - virtual ~cannotFulfillGoalException() throw () - { - }; - - const char * what() const throw () override - { - return msg.c_str(); - } -}; - -class goalFulfilledException : public std::exception -{ - std::string msg; - -public: - Goals::TSubgoal goal; - - explicit goalFulfilledException(Goals::TSubgoal Goal) - : goal(Goal) - { - msg = goal->name(); - } - - virtual ~goalFulfilledException() throw () - { - }; - - const char * what() const throw () override - { - return msg.c_str(); - } -}; - -void makePossibleUpgrades(const CArmedInstance * obj); +/* + * VCAI.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 "AIUtility.h" +#include "Goals/AbstractGoal.h" +#include "../../lib/AI_Base.h" +#include "../../CCallback.h" + +#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 "../../lib/CondSh.h" +#include "Pathfinding/AIPathfinder.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class AIhelper; + +class AIStatus +{ + boost::mutex mx; + boost::condition_variable cv; + + BattleState battle; + std::map remainingQueries; + std::map requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query) + std::vector objectsBeingVisited; + bool ongoingHeroMovement; + bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits + + bool havingTurn; + +public: + AIStatus(); + ~AIStatus(); + void setBattle(BattleState BS); + void setMove(bool ongoing); + void setChannelProbing(bool ongoing); + bool channelProbing(); + BattleState getBattle(); + void addQuery(QueryID ID, std::string description); + void removeQuery(QueryID ID); + int getQueriesCount(); + void startedTurn(); + void madeTurn(); + void waitTillFree(); + bool haveTurn(); + void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); + void receivedAnswerConfirmation(int answerRequestID, int result); + void heroVisit(const CGObjectInstance * obj, bool started); + + + template void serialize(Handler & h, const int version) + { + h & battle; + h & remainingQueries; + h & requestToQueryID; + h & havingTurn; + } +}; + +class DLL_EXPORT VCAI : public CAdventureAI +{ +public: + + friend class FuzzyHelper; + friend class ResourceManager; + friend class BuildingManager; + + std::map> knownTeleportChannels; + std::map knownSubterraneanGates; + ObjectInstanceID destinationTeleport; + int3 destinationTeleportPos; + std::vector teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored + //std::vector visitedThisWeek; //only OPWs + std::map> townVisitsThisWeek; + + //part of mainLoop, but accessible from outisde + std::vector basicGoals; + Goals::TGoalVec goalsToRemove; + Goals::TGoalVec goalsToAdd; + std::map ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals + + std::set invalidPathHeroes; //FIXME, just a workaround + std::map lockedHeroes; //TODO: allow non-elementar objectives + std::map> reservedHeroesMap; //objects reserved by specific heroes + std::set heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game + + //sets are faster to search, also do not contain duplicates + std::set visitableObjs; + std::set alreadyVisited; + std::set reservedObjs; //to be visited by specific hero + std::map> visitedHeroes; //visited this turn //FIXME: this is just bug workaround + + AIStatus status; + std::string battlename; + + std::shared_ptr myCb; + + std::unique_ptr makingTurn; +private: + boost::mutex turnInterruptionMutex; +public: + ObjectInstanceID selectedObject; + + AIhelper * ah; + + VCAI(); + virtual ~VCAI(); + + //TODO: use only smart pointers? + void tryRealize(Goals::Explore & g); + void tryRealize(Goals::RecruitHero & g); + void tryRealize(Goals::VisitTile & g); + void tryRealize(Goals::VisitObj & g); + void tryRealize(Goals::VisitHero & g); + void tryRealize(Goals::BuildThis & g); + void tryRealize(Goals::DigAtTile & g); + void tryRealize(Goals::Trade & g); + void tryRealize(Goals::BuyArmy & g); + void tryRealize(Goals::Invalid & g); + void tryRealize(Goals::AbstractGoal & g); + + bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved + + std::string getBattleAIName() const override; + + void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + void yourTurn(QueryID queryID) override; + + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO + void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + 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, const int version) override; //saving + void loadGame(BinaryDeserializer & h, const int version) override; //loading + void finish() override; + + void availableCreaturesChanged(const CGDwelling * town) override; + void heroMoved(const TryMoveHero & details, bool verbose = true) override; + void heroInGarrisonChange(const CGTownInstance * town) override; + void centerView(int3 pos, int focusTime) override; + void tileHidden(const std::unordered_set & pos) override; + void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; + void artifactAssembled(const ArtifactLocation & al) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; + void showThievesGuildWindow(const CGObjectInstance * obj) override; + void playerBlocked(int reason, bool start) override; + void showPuzzleMap() override; + void showShipyardDialog(const IShipyard * obj) override; + void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; + void artifactPut(const ArtifactLocation & al) override; + void artifactRemoved(const ArtifactLocation & al) override; + void artifactDisassembled(const ArtifactLocation & al) override; + void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; + void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; + void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; + void tileRevealed(const std::unordered_set & pos) override; + void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; + void heroMovePointsChanged(const CGHeroInstance * hero) override; + void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; + void newObject(const CGObjectInstance * obj) override; + void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; + void playerBonusChanged(const Bonus & bonus, bool gain) override; + void heroCreated(const CGHeroInstance *) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; + void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; + void requestRealized(PackageApplied * pa) override; + void receivedResource() override; + void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; + void heroManaPointsChanged(const CGHeroInstance * hero) override; + void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; + void battleResultsApplied() override; + void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; + void objectPropertyChanged(const SetObjectProperty * sop) override; + void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; + void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; + 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 battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + + void makeTurn(); + void mainLoop(); + void performTypicalActions(); + + void buildArmyIn(const CGTownInstance * t); + void striveToGoal(Goals::TSubgoal ultimateGoal); + Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal); + void endTurn(); + void wander(HeroPtr h); + void setGoal(HeroPtr h, Goals::TSubgoal goal); + void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any + void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero + + void recruitHero(const CGTownInstance * t, bool throwing = false); + bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit = std::nullopt); + bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const; + //void recruitCreatures(const CGTownInstance * t); + void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); + void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack + void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr); + void moveCreaturesToHero(const CGTownInstance * t); + void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); + + bool moveHeroToTile(int3 dst, HeroPtr h); + void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager + + void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on) + void waitTillFree(); + + void addVisitableObj(const CGObjectInstance * obj); + void markObjectVisited(const CGObjectInstance * obj); + void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit + void unreserveObject(HeroPtr h, const CGObjectInstance * obj); + + void markHeroUnableToExplore(HeroPtr h); + void markHeroAbleToExplore(HeroPtr h); + bool isAbleToExplore(HeroPtr h); + void clearPathsInfo(); + + void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it + void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it + void validateVisitableObjs(); + void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; + void retrieveVisitableObjs(); + virtual std::vector getFlaggedObjects() const; + + const CGObjectInstance * lookForArt(ArtifactID aid) const; + bool isAccessible(const int3 & pos) const; + HeroPtr getHeroWithGrail() const; + + const CGObjectInstance * getUnvisitedObj(const std::function & predicate); + bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; + //optimization - use one SM for every hero call + + const CGTownInstance * findTownWithTavern() const; + bool canRecruitAnyHero(const CGTownInstance * t = nullptr) const; + + Goals::TSubgoal getGoal(HeroPtr h) const; + bool canAct(HeroPtr h) const; + std::vector getUnblockedHeroes() const; + std::vector getMyHeroes() const; + HeroPtr primaryHero() const; + void checkHeroArmy(HeroPtr h); + + void requestSent(const CPackForServer * pack, int requestID) override; + 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, const int version) + { + 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, version); + } + } + } + } + } + #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 +{ + std::string msg; + +public: + explicit cannotFulfillGoalException(crstring _Message) + : msg(_Message) + { + } + + virtual ~cannotFulfillGoalException() throw () + { + }; + + const char * what() const throw () override + { + return msg.c_str(); + } +}; + +class goalFulfilledException : public std::exception +{ + std::string msg; + +public: + Goals::TSubgoal goal; + + explicit goalFulfilledException(Goals::TSubgoal Goal) + : goal(Goal) + { + msg = goal->name(); + } + + virtual ~goalFulfilledException() throw () + { + }; + + const char * what() const throw () override + { + return msg.c_str(); + } +}; + +void makePossibleUpgrades(const CArmedInstance * obj); diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 6160a2eb4..000000000 --- a/AUTHORS +++ /dev/null @@ -1,86 +0,0 @@ -VCMI PROJECT CODE CONTRIBUTORS: - -Michał Urbańczyk aka Tow, - * project originator; programming, making releases, website -maintenance, reverse engineering, general support. - -Mateusz B. aka Tow dragon, - * general support, battle support, support for many Heroes 3 config files, reverse engineering, ERM/VERM parser and interpreter - -Stefan Pavlov aka Ste, - * minor fixes in pregame - -Yifeng Sun aka phoebus118, - * a part of .snd handling, minor fixes and updates - -Andrea Palmate aka afxgroup, - * GCC/AmigaOS4 compatibility updates and makefile - -Vadim Glazunov aka neweagle, - * minor GCC/Linux compatibility changes - -Rafal R. aka ambtrip, - * GeniusAI (battles) - -Lukasz Wychrystenko aka tezeriusz, - * minor GCC/Linux compatibility changes, code review - -Xiaomin Ding, - * smack videos player - -Tom Zielinski aka Warmonger, - * game objects, mechanics - -Frank Zago aka ubuntux, <> - * GCC/Linux compatibility changes, sound/music support, video support on Linux - -Trevor Standley aka tstandley, <> - * adventure map part of Genius AI - -Rickard Westerlund aka Onion Knight, - * battle functionality and general support - -Ivan Savenko, - * GCC/Linux support, client development, general support - -Benjamin Gentner aka beegee, <> - * battle support, programming - -Alexey aka Macron1Robot, <> - * minor modding changes - -Alexander Shishkin aka alexvins, - * MinGW platform support, modding related programming - -Arseniy Shestakov aka SXX, - * pathfinding improvements, programming - -Vadim Markovtsev, - * resolving problems with macOS, bug fixes - -Michał Kalinowski, - * refactoring code - -Dydzio, - * Small features, improvements and bug fixes in all VCMI parts - -Piotr Wójcik aka Chocimier, - * Various bug fixes - -Henning Koehler, - * skill modding, bonus updaters - -Andrzej Żak aka godric3 - * minor bug fixes and modding features - -Andrii Danylchenko - * Nullkiller AI, VCAI improvements - -Dmitry Orlov, - * special buildings support in fan towns, new features and bug fixes - -Andrey Cherkas aka nordsoft, - * new terrain support, rmg features, map editor, multiplayer improvements, bug fixes - -Andrey Filipenkov aka kambala-decapitator, - * iOS support, macOS improvements, various bug fixes diff --git a/AUTHORS.h b/AUTHORS.h new file mode 100644 index 000000000..a9ff41e66 --- /dev/null +++ b/AUTHORS.h @@ -0,0 +1,53 @@ +/* + * AUTHORS.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 PROJECT CODE CONTRIBUTORS: +const std::vector> contributors = { +// Task Name Aka E-Mail + { "Idea", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, + { "Idea", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, + + { "Developing", "Andrea Palmate", "afxgroup", "andrea@amigasoft.net" }, + { "Developing", "Alexander Shishkin", "alexvins", "" }, + { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, + { "Developing", "Andrii Danylchenko", "", "" }, + { "Developing", "Benjamin Gentner", "beegee", "" }, + { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, + { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, + { "Developing", "", "Dydzio", "blood990@gmail.com" }, + { "Developing", "Andrzej Żak", "godric3", "" }, + { "Developing", "Henning Koehler", "henningkoehlernz", "henning.koehler.nz@gmail.com" }, + { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, + { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, + { "Developing", "", "krs0", "" }, + { "Developing", "", "Laserlicht", "" }, + { "Developing", "Alexey", "Macron1Robot", "" }, + { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, + { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, + { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, + { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, + { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, + { "Developing", "", "rilian-la-te", "" }, + { "Developing", "", "SoundSSGood", "" }, + { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, + { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, + { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, + { "Developing", "Trevor Standley", "tstandley", "" }, + { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, + { "Developing", "Frank Zago", "ubuntux", "" }, + { "Developing", "", "vmarkovtsev", "" }, + { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, + { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + + { "Testing", "Ben Yan", "by003", "benyan9110@gmail.com," }, + { "Testing", "", "Misiokles", "" }, + { "Testing", "", "Povelitel", "" }, +}; diff --git a/CCallback.cpp b/CCallback.cpp index 8331a8ec2..7b87bc913 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -1,399 +1,424 @@ -/* - * CCallback.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 "CCallback.h" - -#include "lib/CCreatureHandler.h" -#include "lib/gameState/CGameState.h" -#include "client/CPlayerInterface.h" -#include "client/Client.h" -#include "lib/mapping/CMap.h" -#include "lib/CBuildingHandler.h" -#include "lib/CGeneralTextHandler.h" -#include "lib/CHeroHandler.h" -#include "lib/NetPacks.h" -#include "lib/CArtHandler.h" -#include "lib/GameConstants.h" -#include "lib/CPlayerState.h" -#include "lib/UnlockGuard.h" -#include "lib/battle/BattleInfo.h" - -bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) -{ - CastleTeleportHero pack(who->id, where->id, 1); - sendRequest(&pack); - return true; -} - -bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) -{ - MoveHero pack(dst,h->id,transit); - sendRequest(&pack); - return true; -} - -int CCallback::selectionMade(int selection, QueryID queryID) -{ - JsonNode reply(JsonNode::JsonType::DATA_INTEGER); - reply.Integer() = selection; - return sendQueryReply(reply, queryID); -} - -int CCallback::sendQueryReply(const JsonNode & reply, QueryID queryID) -{ - ASSERT_IF_CALLED_WITH_PLAYER - if(queryID == QueryID(-1)) - { - logGlobal->error("Cannot answer the query -1!"); - return -1; - } - - QueryReply pack(queryID, reply); - pack.player = *player; - return sendRequest(&pack); -} - -void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) -{ - // TODO exception for neutral dwellings shouldn't be hardcoded - if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP) - return; - - RecruitCreatures pack(obj->id, dst->id, ID, amount, level); - sendRequest(&pack); -} - -bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) -{ - if((player && obj->tempOwner != player) || (obj->stacksCount()<2 && obj->needsLastStack())) - return false; - - DisbandCreature pack(stackPos,obj->id); - sendRequest(&pack); - return true; -} - -bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) -{ - UpgradeCreature pack(stackPos,obj->id,newID); - sendRequest(&pack); - return false; -} - -void CCallback::endTurn() -{ - logGlobal->trace("Player %d ended his turn.", player->getNum()); - EndTurn 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); - 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); - 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); - return 0; -} - -int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) -{ - BulkMoveArmy pack(srcArmy, destArmy, srcSlot); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) -{ - BulkSplitStack pack(armyId, srcSlot, howMany); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) -{ - BulkSmartSplitStack pack(armyId, srcSlot); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) -{ - BulkMergeStacks pack(armyId, srcSlot); - sendRequest(&pack); - return 0; -} - -bool CCallback::dismissHero(const CGHeroInstance *hero) -{ - if(player!=hero->tempOwner) return false; - - DismissHero pack(hero->id); - sendRequest(&pack); - return true; -} - -bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) -{ - ExchangeArtifacts ea; - ea.src = l1; - ea.dst = l2; - sendRequest(&ea); - return true; -} - -/** - * Assembles or disassembles a combination artifact. - * @param hero Hero holding the artifact(s). - * @param artifactSlot The worn slot ID of the combination- or constituent artifact. - * @param assemble True for assembly operation, false for disassembly. - * @param assembleTo If assemble is true, this represents the artifact ID of the combination - * artifact to assemble to. Otherwise it's not used. - */ -bool CCallback::assembleArtifacts (const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) -{ - if (player != hero->tempOwner) - return false; - - AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); - sendRequest(&aa); - return true; -} - -void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) -{ - BulkExchangeArtifacts bma(srcHero, dstHero, swap); - sendRequest(&bma); -} - -void CCallback::eraseArtifactByClient(const ArtifactLocation & al) -{ - EraseArtifactByClient ea(al); - sendRequest(&ea); -} - -bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) -{ - if(town->tempOwner!=player) - return false; - - if(!canBuildStructure(town, buildingID)) - return false; - - BuildStructure pack(town->id,buildingID); - sendRequest(&pack); - return true; -} - -void CBattleCallback::battleMakeSpellAction(const BattleAction & action) -{ - assert(action.actionType == EActionType::HERO_SPELL); - MakeCustomAction mca(action); - sendRequest(&mca); -} - -int CBattleCallback::sendRequest(const CPackForServer * request) -{ - int requestID = cl->sendRequest(request, *player); - if(waitTillRealize) - { - logGlobal->trace("We'll wait till request %d is answered.\n", requestID); - auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting); - CClient::waitingRequest.waitWhileContains(requestID); - } - - boost::this_thread::interruption_point(); - return requestID; -} - -void CCallback::swapGarrisonHero( const CGTownInstance *town ) -{ - if(town->tempOwner == *player - || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) - { - GarrisonHeroSwap pack(town->id); - sendRequest(&pack); - } -} - -void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) -{ - if(hero->tempOwner != player) return; - - BuyArtifact pack(hero->id,aid); - sendRequest(&pack); -} - -void CCallback::trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) -{ - trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); -} - -void CCallback::trade(const IMarket * market, EMarketMode::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.heroId = hero ? hero->id : ObjectInstanceID(); - pack.mode = mode; - pack.r1 = id1; - pack.r2 = id2; - pack.val = val1; - sendRequest(&pack); -} - -void CCallback::setFormation(const CGHeroInstance * hero, bool tight) -{ - SetFormation pack(hero->id,tight); - sendRequest(&pack); -} - -void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) -{ - assert(townOrTavern); - assert(hero); - - HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); - pack.player = *player; - sendRequest(&pack); -} - -void CCallback::save( const std::string &fname ) -{ - cl->save(fname); -} - - -void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) -{ - ASSERT_IF_CALLED_WITH_PLAYER - PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); - if(player) - pm.player = *player; - sendRequest(&pm); -} - -void CCallback::buildBoat( const IShipyard *obj ) -{ - BuildBoat bb; - bb.objid = dynamic_cast(obj)->id; - sendRequest(&bb); -} - -CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C): - CBattleCallback(Player, C) -{ - gs = GS; - - waitTillRealize = false; - unlockGsWhenWaiting = false; -} - -CCallback::~CCallback() -{ -//trivial, but required. Don`t remove. -} - -bool CCallback::canMoveBetween(const int3 &a, const int3 &b) -{ - //bidirectional - return gs->map->canMoveBetween(a, b); -} - -std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * h) -{ - return cl->getPathsInfo(h); -} - -int3 CCallback::getGuardingCreaturePosition(int3 tile) -{ - if (!gs->map->isInTheMap(tile)) - return int3(-1,-1,-1); - - return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y]; -} - -void CCallback::dig( const CGObjectInstance *hero ) -{ - DigWithHero dwh; - dwh.id = hero->id; - sendRequest(&dwh); -} - -void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) -{ - CastAdvSpell cas; - cas.hid = hero->id; - cas.sid = spellID; - cas.pos = pos; - sendRequest(&cas); -} - -int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) -{ - if(s1->getCreature(p1) == s2->getCreature(p2)) - return mergeStacks(s1, s2, p1, p2); - else - return swapCreatures(s1, s2, p1, p2); -} - -void CCallback::registerBattleInterface(std::shared_ptr battleEvents) -{ - cl->additionalBattleInts[*player].push_back(battleEvents); -} - -void CCallback::unregisterBattleInterface(std::shared_ptr battleEvents) -{ - cl->additionalBattleInts[*player] -= battleEvents; -} - -#if SCRIPTING_ENABLED -scripting::Pool * CBattleCallback::getContextPool() const -{ - return cl->getGlobalContextPool(); -} -#endif - -CBattleCallback::CBattleCallback(std::optional Player, CClient * C) -{ - player = Player; - cl = C; -} - -void CBattleCallback::battleMakeUnitAction(const BattleAction & action) -{ - assert(!cl->gs->curB->tacticDistance); - MakeAction ma; - ma.ba = action; - sendRequest(&ma); -} - -void CBattleCallback::battleMakeTacticAction( const BattleAction & action ) -{ - assert(cl->gs->curB->tacticDistance); - MakeAction ma; - ma.ba = action; - sendRequest(&ma); -} - -std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) -{ - return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleState); -} +/* + * CCallback.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 "CCallback.h" + +#include "lib/CCreatureHandler.h" +#include "lib/gameState/CGameState.h" +#include "client/CPlayerInterface.h" +#include "client/Client.h" +#include "lib/mapping/CMap.h" +#include "lib/mapObjects/CGHeroInstance.h" +#include "lib/CBuildingHandler.h" +#include "lib/CGeneralTextHandler.h" +#include "lib/CHeroHandler.h" +#include "lib/CArtHandler.h" +#include "lib/GameConstants.h" +#include "lib/CPlayerState.h" +#include "lib/UnlockGuard.h" +#include "lib/battle/BattleInfo.h" +#include "lib/networkPacks/PacksForServer.h" + +bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) +{ + CastleTeleportHero pack(who->id, where->id, 1); + sendRequest(&pack); + return true; +} + +bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) +{ + MoveHero pack(dst,h->id,transit); + sendRequest(&pack); + return true; +} + +int CCallback::selectionMade(int selection, QueryID queryID) +{ + return sendQueryReply(selection, queryID); +} + +int CCallback::sendQueryReply(std::optional reply, QueryID queryID) +{ + ASSERT_IF_CALLED_WITH_PLAYER + if(queryID == QueryID(-1)) + { + logGlobal->error("Cannot answer the query -1!"); + return -1; + } + + QueryReply pack(queryID, reply); + pack.player = *player; + return sendRequest(&pack); +} + +void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) +{ + // TODO exception for neutral dwellings shouldn't be hardcoded + if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP) + return; + + RecruitCreatures pack(obj->id, dst->id, ID, amount, level); + sendRequest(&pack); +} + +bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) +{ + if((player && obj->tempOwner != player) || (obj->stacksCount()<2 && obj->needsLastStack())) + return false; + + DisbandCreature pack(stackPos,obj->id); + sendRequest(&pack); + return true; +} + +bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) +{ + UpgradeCreature pack(stackPos,obj->id,newID); + sendRequest(&pack); + return false; +} + +void CCallback::endTurn() +{ + logGlobal->trace("Player %d ended his turn.", player->getNum()); + EndTurn 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); + 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); + 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); + return 0; +} + +int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) +{ + BulkMoveArmy pack(srcArmy, destArmy, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) +{ + BulkSplitStack pack(armyId, srcSlot, howMany); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkSmartSplitStack pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkMergeStacks pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + +bool CCallback::dismissHero(const CGHeroInstance *hero) +{ + if(player!=hero->tempOwner) return false; + + DismissHero pack(hero->id); + sendRequest(&pack); + return true; +} + +bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) +{ + ExchangeArtifacts ea; + ea.src = l1; + ea.dst = l2; + sendRequest(&ea); + return true; +} + +/** + * Assembles or disassembles a combination artifact. + * @param hero Hero holding the artifact(s). + * @param artifactSlot The worn slot ID of the combination- or constituent artifact. + * @param assemble True for assembly operation, false for disassembly. + * @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) +{ + AssembleArtifacts aa(hero->id, 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); +} + +void CCallback::eraseArtifactByClient(const ArtifactLocation & al) +{ + EraseArtifactByClient ea(al); + sendRequest(&ea); +} + +bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) +{ + if(town->tempOwner!=player) + return false; + + if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED) + return false; + + BuildStructure pack(town->id,buildingID); + sendRequest(&pack); + return true; +} + +void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) +{ + assert(action.actionType == EActionType::HERO_SPELL); + MakeAction mca(action); + mca.battleID = battleID; + sendRequest(&mca); +} + +int CBattleCallback::sendRequest(const CPackForServer * request) +{ + int requestID = cl->sendRequest(request, *getPlayerID()); + if(waitTillRealize) + { + logGlobal->trace("We'll wait till request %d is answered.\n", requestID); + auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting); + CClient::waitingRequest.waitWhileContains(requestID); + } + + boost::this_thread::interruption_point(); + return requestID; +} + +void CCallback::swapGarrisonHero( const CGTownInstance *town ) +{ + if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) + { + GarrisonHeroSwap pack(town->id); + sendRequest(&pack); + } +} + +void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) +{ + if(hero->tempOwner != *player) return; + + BuyArtifact pack(hero->id,aid); + sendRequest(&pack); +} + +void CCallback::trade(const IMarket * market, 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); +} + +void CCallback::trade(const IMarket * market, 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.heroId = hero ? hero->id : ObjectInstanceID(); + pack.mode = mode; + pack.r1 = id1; + pack.r2 = id2; + pack.val = val1; + sendRequest(&pack); +} + +void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) +{ + SetFormation pack(hero->id, mode); + sendRequest(&pack); +} + +void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) +{ + assert(townOrTavern); + assert(hero); + + HireHero pack(hero->getHeroType(), townOrTavern->id); + pack.player = *player; + sendRequest(&pack); +} + +void CCallback::save( const std::string &fname ) +{ + cl->save(fname); +} + +void CCallback::gamePause(bool pause) +{ + if(pause) + { + GamePause pack; + pack.player = *player; + sendRequest(&pack); + } + else + { + sendQueryReply(0, QueryID::CLIENT); + } +} + +void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) +{ + ASSERT_IF_CALLED_WITH_PLAYER + PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); + if(player) + pm.player = *player; + sendRequest(&pm); +} + +void CCallback::buildBoat( const IShipyard *obj ) +{ + BuildBoat bb; + bb.objid = dynamic_cast(obj)->id; + sendRequest(&bb); +} + +CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C) + : CBattleCallback(Player, C) +{ + gs = GS; + + waitTillRealize = false; + unlockGsWhenWaiting = false; +} + +CCallback::~CCallback() = default; + +bool CCallback::canMoveBetween(const int3 &a, const int3 &b) +{ + //bidirectional + return gs->map->canMoveBetween(a, b); +} + +std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * h) +{ + return cl->getPathsInfo(h); +} + +std::optional CCallback::getPlayerID() const +{ + return CBattleCallback::getPlayerID(); +} + +int3 CCallback::getGuardingCreaturePosition(int3 tile) +{ + if (!gs->map->isInTheMap(tile)) + return int3(-1,-1,-1); + + return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y]; +} + +void CCallback::dig( const CGObjectInstance *hero ) +{ + DigWithHero dwh; + dwh.id = hero->id; + sendRequest(&dwh); +} + +void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) +{ + CastAdvSpell cas; + cas.hid = hero->id; + cas.sid = spellID; + cas.pos = pos; + sendRequest(&cas); +} + +int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) +{ + if(s1->getCreature(p1) == s2->getCreature(p2)) + return mergeStacks(s1, s2, p1, p2); + else + return swapCreatures(s1, s2, p1, p2); +} + +void CCallback::registerBattleInterface(std::shared_ptr battleEvents) +{ + cl->additionalBattleInts[*player].push_back(battleEvents); +} + +void CCallback::unregisterBattleInterface(std::shared_ptr battleEvents) +{ + cl->additionalBattleInts[*player] -= battleEvents; +} + +CBattleCallback::CBattleCallback(std::optional player, CClient * C): + cl(C), + player(player) +{ +} + +void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) +{ + assert(!cl->gs->getBattle(battleID)->tacticDistance); + MakeAction ma; + ma.ba = action; + ma.battleID = battleID; + sendRequest(&ma); +} + +void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action ) +{ + assert(cl->gs->getBattle(battleID)->tacticDistance); + MakeAction ma; + ma.ba = action; + ma.battleID = battleID; + sendRequest(&ma); +} + +std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState); +} + +std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) +{ + return activeBattles.at(battleID); +} + +std::optional CBattleCallback::getPlayerID() const +{ + return player; +} + +void CBattleCallback::onBattleStarted(const IBattleInfo * info) +{ + activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); +} + +void CBattleCallback::onBattleEnded(const BattleID & battleID) +{ + activeBattles.erase(battleID); +} diff --git a/CCallback.h b/CCallback.h index 637b9b6c9..068ac547b 100644 --- a/CCallback.h +++ b/CCallback.h @@ -1,186 +1,198 @@ -/* - * CCallback.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/CGameInfoCallback.h" -#include "lib/battle/CPlayerBattleCallback.h" -#include "lib/int3.h" // for int3 - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CGameState; -struct CPath; -class CGObjectInstance; -class CArmedInstance; -class BattleAction; -class CGTownInstance; -class IShipyard; -struct CGPathNode; -struct CGPath; -struct CPathsInfo; -class PathfinderConfig; -struct CPack; -struct CPackForServer; -class IBattleEventsReceiver; -class IGameEventsReceiver; -struct ArtifactLocation; -class BattleStateInfoForRetreat; -class IMarket; - -VCMI_LIB_NAMESPACE_END - -// in static AI build this file gets included into libvcmi -#ifdef STATIC_AI -VCMI_LIB_USING_NAMESPACE -#endif - -class CClient; -struct lua_State; - -class IBattleCallback -{ -public: - virtual ~IBattleCallback() = default; - - bool waitTillRealize = false; //if true, request functions will return after they are realized by server - bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! - //battle - virtual void battleMakeSpellAction(const BattleAction & action) = 0; - virtual void battleMakeUnitAction(const BattleAction & action) = 0; - virtual void battleMakeTacticAction(const BattleAction & action) = 0; - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0; -}; - -class IGameActionCallback -{ -public: - //hero - virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile) - virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly - 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)=0; - virtual bool buildBuilding(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 swapGarrisonHero(const CGTownInstance *town)=0; - - virtual void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 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::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; - - virtual int selectionMade(int selection, QueryID queryID) =0; - virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0; - virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! - virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) - virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second - virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack - //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 bool assembleArtifacts(const CGHeroInstance * hero, 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 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, bool tight)=0; - - virtual void save(const std::string &fname) = 0; - virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; - virtual void buildBoat(const IShipyard *obj) = 0; - - // To implement high-level army management bulk actions - virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0; - virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0; - virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0; - virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0; - - - // Moves all artifacts from one hero to another - virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) = 0; -}; - -class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback -{ -protected: - int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) - CClient *cl; - -public: - CBattleCallback(std::optional Player, CClient * C); - void battleMakeSpellAction(const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack - void battleMakeUnitAction(const BattleAction & action) override; - void battleMakeTacticAction(const BattleAction & action) override; // performs tactic phase actions - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; - -#if SCRIPTING_ENABLED - scripting::Pool * getContextPool() const override; -#endif - - friend class CCallback; - friend class CClient; -}; - -class CCallback : public CPlayerSpecificInfoCallback, - public IGameActionCallback, - public CBattleCallback -{ -public: - CCallback(CGameState * GS, std::optional Player, CClient * C); - virtual ~CCallback(); - - //client-specific functionalities (pathfinding) - virtual bool canMoveBetween(const int3 &a, const int3 &b); - virtual int3 getGuardingCreaturePosition(int3 tile); - virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); - - //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. - void registerBattleInterface(std::shared_ptr battleEvents); - void unregisterBattleInterface(std::shared_ptr battleEvents); - -//commands - bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) - bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); - int selectionMade(int selection, QueryID queryID) override; - int sendQueryReply(const JsonNode & reply, QueryID queryID) override; - int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; - int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second - int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second - int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; - int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override; - int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override; - int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override; - int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; - bool dismissHero(const CGHeroInstance * hero) override; - bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; - bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; - void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) override; - void eraseArtifactByClient(const ArtifactLocation & al) override; - bool buildBuilding(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 endTurn() override; - void swapGarrisonHero(const CGTownInstance *town) override; - void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; - void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; - void setFormation(const CGHeroInstance * hero, bool tight) override; - void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; - void save(const std::string &fname) override; - void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; - void buildBoat(const IShipyard *obj) override; - void dig(const CGObjectInstance *hero) override; - void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; - -//friends - friend class CClient; -}; +/* + * CCallback.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/CGameInfoCallback.h" +#include "lib/battle/CPlayerBattleCallback.h" +#include "lib/int3.h" // for int3 +#include "lib/networkPacks/TradeItem.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CGameState; +struct CPath; +class CGObjectInstance; +class CArmedInstance; +class BattleAction; +class CGTownInstance; +class IShipyard; +struct CGPathNode; +struct CGPath; +struct CPathsInfo; +class PathfinderConfig; +struct CPack; +struct CPackForServer; +class IBattleEventsReceiver; +class IGameEventsReceiver; +struct ArtifactLocation; +class BattleStateInfoForRetreat; +class IMarket; + +VCMI_LIB_NAMESPACE_END + +// in static AI build this file gets included into libvcmi +#ifdef STATIC_AI +VCMI_LIB_USING_NAMESPACE +#endif + +class CClient; +struct lua_State; + +class IBattleCallback +{ +public: + virtual ~IBattleCallback() = default; + + bool waitTillRealize = false; //if true, request functions will return after they are realized by server + bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! + //battle + virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; + + virtual std::shared_ptr getBattle(const BattleID & battleID) = 0; + virtual std::optional getPlayerID() const = 0; +}; + +class IGameActionCallback +{ +public: + //hero + virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile) + virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly + 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)=0; + virtual bool buildBuilding(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 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 int selectionMade(int selection, QueryID queryID) =0; + virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; + virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! + virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) + virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second + virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack + //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 assembleArtifacts(const CGHeroInstance * hero, 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 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; + + virtual void save(const std::string &fname) = 0; + virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; + virtual void gamePause(bool pause) = 0; + virtual void buildBoat(const IShipyard *obj) = 0; + + // To implement high-level army management bulk actions + virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0; + virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0; + virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0; + virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0; + + + // Moves all artifacts from one hero to another + virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0; +}; + +class CBattleCallback : public IBattleCallback +{ + std::map> activeBattles; + + std::optional player; + +protected: + int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) + CClient *cl; + +public: + CBattleCallback(std::optional player, CClient * C); + void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack + void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override; + void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + + std::shared_ptr getBattle(const BattleID & battleID) override; + std::optional getPlayerID() const override; + + void onBattleStarted(const IBattleInfo * info); + void onBattleEnded(const BattleID & battleID); + + friend class CCallback; + friend class CClient; +}; + +class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback +{ +public: + CCallback(CGameState * GS, std::optional Player, CClient * C); + virtual ~CCallback(); + + //client-specific functionalities (pathfinding) + virtual bool canMoveBetween(const int3 &a, const int3 &b); + virtual int3 getGuardingCreaturePosition(int3 tile); + virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); + + std::optional getPlayerID() const override; + + //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. + void registerBattleInterface(std::shared_ptr battleEvents); + void unregisterBattleInterface(std::shared_ptr battleEvents); + +//commands + bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) + bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); + int selectionMade(int selection, QueryID queryID) override; + int sendQueryReply(std::optional reply, QueryID queryID) override; + int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; + int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second + int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second + int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; + int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override; + int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override; + int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override; + 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 bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override; + void eraseArtifactByClient(const ArtifactLocation & al) override; + bool buildBuilding(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 endTurn() 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 setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; + void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; + void save(const std::string &fname) override; + void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; + void gamePause(bool pause) override; + void buildBoat(const IShipyard *obj) override; + void dig(const CGObjectInstance *hero) override; + void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; + +//friends + friend class CClient; +}; diff --git a/CI/linux-qt6/before_install.sh b/CI/linux-qt6/before_install.sh index 756b42eb3..689101138 100644 --- a/CI/linux-qt6/before_install.sh +++ b/CI/linux-qt6/before_install.sh @@ -3,9 +3,8 @@ sudo apt-get update # Dependencies -sudo apt-get install libboost-all-dev -sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev -sudo apt-get install qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools -sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev -# Optional dependencies -sudo apt-get install libminizip-dev libfuzzylite-dev +sudo apt-get install libboost-all-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 \ +ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ +libminizip-dev libfuzzylite-dev # Optional dependencies diff --git a/CI/linux-qt6/validate_json.py b/CI/linux-qt6/validate_json.py new file mode 100755 index 000000000..5fbe0ac76 --- /dev/null +++ b/CI/linux-qt6/validate_json.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 + +import re +import sys +from pathlib import Path +from pprint import pprint + +import json5 +import jstyleson +import yaml + +# 'json', 'json5' or 'yaml' +# json: strict, but doesn't preserve line numbers necessarily, since it strips comments before parsing +# json5: strict and preserves line numbers even for files with line comments +# yaml: less strict, allows e.g. leading zeros +VALIDATION_TYPE = "json5" + +errors = [] +for path in sorted(Path(".").glob("**/*.json")): + # because path is an object and not a string + path_str = str(path) + try: + with open(path_str, "r") as file: + if VALIDATION_TYPE == "json": + jstyleson.load(file) + elif VALIDATION_TYPE == "json5": + json5.load(file) + elif VALIDATION_TYPE == "yaml": + file = file.read().replace("\t", " ") + file = file.replace("//", "#") + yaml.safe_load(file) + print(f"Validation of {path_str} succeeded") + except Exception as exc: + print(f"Validation of {path_str} failed") + pprint(exc) + + error_pos = path_str + + # create error position strings for each type of parser + if hasattr(exc, "pos"): + # 'json' + # https://stackoverflow.com/a/72850269/2278742 + error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" + print(error_pos) + elif VALIDATION_TYPE == "json5": + # 'json5' + pos = re.findall(r"\d+", str(exc)) + error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" + elif hasattr(exc, "problem_mark"): + # 'yaml' + mark = exc.problem_mark + error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" + print(error_pos) + + errors.append({"error_pos": error_pos, "error_msg": exc}) + +if errors: + print("The following JSON files are invalid:") + pprint(errors) + sys.exit(1) diff --git a/CI/linux/before_install.sh b/CI/linux/before_install.sh index 8b0c75d59..e08075d7d 100644 --- a/CI/linux/before_install.sh +++ b/CI/linux/before_install.sh @@ -3,9 +3,8 @@ sudo apt-get update # Dependencies -sudo apt-get install libboost-all-dev -sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev -sudo apt-get install qtbase5-dev -sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev -# Optional dependencies -sudo apt-get install libminizip-dev libfuzzylite-dev qttools5-dev +sudo apt-get install libboost-all-dev \ +libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ +qtbase5-dev \ +ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ +libminizip-dev libfuzzylite-dev qttools5-dev # Optional dependencies diff --git a/CI/msvc/before_install.sh b/CI/msvc/before_install.sh index 851cbd4a9..5388e84f8 100644 --- a/CI/msvc/before_install.sh +++ b/CI/msvc/before_install.sh @@ -1,5 +1,5 @@ curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \ - "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 0cf681b8d..b502b3fbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,30 @@ if(NOT APPLE_IOS AND NOT ANDROID) option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF) endif() +# On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. +# The XCode and MSVC builds each require some more configuration, which is enabled by the following option: +if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode")) + option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) +endif() + +if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) + find_program(CCACHE ccache REQUIRED) + if(CCACHE) + # https://stackoverflow.com/a/36515503/2278742 + # Set up wrapper scripts + configure_file(xcode/launch-c.in xcode/launch-c) + configure_file(xcode/launch-cxx.in xcode/launch-cxx) + execute_process(COMMAND chmod a+rx + "${CMAKE_BINARY_DIR}/xcode/launch-c" + "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + # Set Xcode project attributes to route compilation through our scripts + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + endif() +endif() + # Allow to pass package name from Travis CI set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name") set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename") @@ -237,6 +261,23 @@ if(MINGW OR MSVC) set(CMAKE_SHARED_LIBRARY_PREFIX "") if(MSVC) + if(ENABLE_CCACHE) + # https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387 + find_program(CCACHE ccache REQUIRED) + if (CCACHE) + file(COPY_FILE + ${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe + ONLY_IF_DIFFERENT) + + set(CMAKE_VS_GLOBALS + "CLToolExe=cl.exe" + "CLToolPath=${CMAKE_BINARY_DIR}" + "TrackFileAccess=false" + "UseMultiToolTask=true" + ) + endif() + endif() + add_definitions(-DBOOST_ALL_NO_LIB) add_definitions(-DBOOST_ALL_DYN_LINK) set(Boost_USE_STATIC_LIBS OFF) @@ -250,6 +291,7 @@ if(MINGW OR MSVC) # Suppress warnings add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-D_SCL_SECURE_NO_WARNINGS) + add_definitions(-D_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) # Fix gtest and gmock build set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") @@ -587,11 +629,6 @@ if(ANDROID) ) endif() - install(FILES AUTHORS - DESTINATION res/raw - RENAME authors.txt - ) - # zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip install(CODE " cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX @@ -678,6 +715,11 @@ endif(WIN32) # Packaging section # ####################################### +if(MSVC) + SET(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${BIN_DIR}) + Include(InstallRequiredSystemLibraries) +endif() + set(CPACK_PACKAGE_VERSION_MAJOR ${VCMI_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${VCMI_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${VCMI_VERSION_PATCH}) @@ -715,14 +757,21 @@ if(WIN32) else() set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") endif() + if(ENABLE_LAUNCHER) - set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"") + set(VCMI_MAIN_EXECUTABLE "VCMI_launcher") else() - set(CPACK_PACKAGE_EXECUTABLES "VCMI_client;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_client.exe\\\"") + set(VCMI_MAIN_EXECUTABLE "VCMI_client") endif() - set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ") + set(CPACK_PACKAGE_EXECUTABLES "${VCMI_MAIN_EXECUTABLE};VCMI") + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\${VCMI_MAIN_EXECUTABLE}.exe\\\" + ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE + ") + set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " + Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" + ExecShell '' 'netsh' 'advfirewall firewall delete rule name=vcmi_server' SW_HIDE + ") # Strip MinGW CPack target if build configuration without debug info if(MINGW) diff --git a/CMakePresets.json b/CMakePresets.json index 39c752e94..d96a905b0 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -25,7 +25,8 @@ "CMAKE_BUILD_TYPE": "RelWithDebInfo", "ENABLE_TEST": "OFF", "ENABLE_STRICT_COMPILATION": "ON", - "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}" + "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}", + "ENABLE_PCH" : "OFF" } }, { @@ -83,7 +84,6 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "ENABLE_LUA" : "ON", - "ENABLE_PCH" : "OFF", "CMAKE_C_COMPILER": "/usr/bin/gcc", "CMAKE_CXX_COMPILER": "/usr/bin/g++" } @@ -123,6 +123,17 @@ "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "CMAKE_POLICY_DEFAULT_CMP0091": "NEW", "FORCE_BUNDLED_MINIZIP": "ON" + + } + }, + { + "name": "windows-msvc-release-ccache", + "displayName": "Windows x64 RelWithDebInfo with ccache", + "description": "VCMI RelWithDebInfo build with ccache", + "inherits": "windows-msvc-release", + "cacheVariables": { + "ENABLE_CCACHE": "ON" + } }, { @@ -217,7 +228,20 @@ "inherits": [ "base-ios-release", "ios-device-conan" - ] + ], + "cacheVariables": { + "ENABLE_PCH" : "ON" + } + }, + { + "name": "ios-release-conan-ccache", + "displayName": "iOS+Conan release using ccache", + "description": "VCMI iOS release using Conan and ccache", + "inherits": "ios-release-conan", + "cacheVariables": { + "ENABLE_PCH" : "OFF", + "ENABLE_CCACHE": "ON" + } }, { "name": "ios-release-legacy", @@ -303,8 +327,12 @@ { "name": "windows-msvc-release", "configurePreset": "windows-msvc-release", - "inherits": "default-release", - "configuration": "Release" + "inherits": "default-release" + }, + { + "name": "windows-msvc-release-ccache", + "configurePreset": "windows-msvc-release-ccache", + "inherits": "windows-msvc-release" }, { "name": "windows-msvc-relwithdebinfo", @@ -327,6 +355,11 @@ "CODE_SIGNING_ALLOWED_FOR_APPS=NO" ] }, + { + "name": "ios-release-conan-ccache", + "configurePreset": "ios-release-conan-ccache", + "inherits": "ios-release-conan" + }, { "name": "ios-release-legacy", "configurePreset": "ios-release-legacy", diff --git a/ChangeLog.md b/ChangeLog.md index 873ce4147..a9f6efe83 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,2160 +1,2349 @@ -# 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 -* Added option to open hero backpack window in hero screen -* Added detection of misclicks for touch inputs to make hitting small UI elements easier -* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities -* It is no longer possible to stop movement while moving over water with Water Walk -* Game will now automatically update hero path if it was blocked by another hero -* 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 -* Fixed crash on attempting to visit unknown object, such as Market of Time -* Fixed crash on attempting to teleport unit that is immune to a spell -* 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 -* Added option to toggle spell usage by AI in quick combat -* Fixed updating of spell point of enemy hero in game interface after spell cast -* Fixed wrong creature spellcasting shortcut (now set to "F") -* It is now possible to perform melee attack by creatures with spells, especially area spells -* Right-click will now properly end spellcast mode -* Fixed cursor preview when casting spell using touchscreen -* 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 -* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window -* Fixed incorrect color of resource bar in hotseat mode -* Fixed broken toggle map level button in world view mode -* Fixed corrupted interface after opening puzzle window from world view mode -* Fixed blocked interface after attempt to start invalid map -* Add yellow border to selected commander grandmaster ability -* Always use bonus description for commander abilities instead of not provided wog-specific translation -* Fix scrolling when commander has large number of grandmaster abilities -* Fixed corrupted message on another player defeat -* Fixed unavailable Quest Log button on maps with quests -* Fixed incorrect values on a difficulty selector in save load screen -* 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 - -### 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 -* 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 -* Background battlefield obstacles will now appear below creatures -* it is now possible to load save game located inside mod -* Added option to configure reserved screen area in Launcher on iOS -* Fixed border scrolling when game window is maximized - -### 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 -* NKAI: Fixed recruitment of new heroes -* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location -* VCAI: Fixed spellcasting by Archangels - -### 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: -* 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 -* Fixed crash on starting game with outdated mods -* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice -* Fixed crash on leveling up after winning battle as defender -* Fixed possible crash on end of battle opening sound -* Fixed crash on accepting battle result after winning battle as defender -* Fixed possible crash on casting spell in battle by AI -* Fixed multiple possible crashes on managing mods on Android -* Fixed multiple possible crashes on importing data on Android -* Fixed crash on refusing rewards from town building -* Fixed possible crash on threat evaluation by NKAI -* Fixed crash on using haptic feedback on some Android systems -* Fixed crash on right-clicking flags area in RMG setup mode -* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations -* 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 - -### 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 -* Fixed multiple issues with borderless fullscreen mode -* On mobile systems game will now always run at native resolution with configurable UI scaling -* Implemented support for Horn of the Abyss map format -* Implemented option to replay results of quick combat -* Added translations to French and Chinese -* All in-game cheats are now case-insensitive -* Added high-definition icon for Windows -* Fix crash on connecting to server on FreeBSD and Flatpak builds -* Save games now consist of a single file -* Added H3:SOD cheat codes as alternative to vcmi cheats -* Fixed several possible crashes caused by autocombat activation -* Fixed artifact lock icon in localized versions of the game -* Fixed possible crash on changing hardware cursor - -### 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 -* Implemented radial menu for army management, activated via swiping creature icon -* Implemented swipe gesture for scrolling through lists -* All windows that have sliders in UI can now be scrolled using swipe gesture -* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from -* Implemented pinch gesture for zooming adventure map -* Implemented haptic feedback (vibration) for long press gesture - -### 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 -* Patially fixed displaying of download progress for mods -* Fixed potential crash on opening mod information for mods with a changelog -* Added option to configure number of autosaves - -### 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: -* Fixed potential crash on accessing market (VCAI) -* Fixed potentially infinite turns (VCAI) -* Reworked object prioritizing -* Improved town defense against enemy heroes -* Improved town building (mage guild and horde) -* 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 -* It is now possible to access Shipwrecks from coast -* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings -* It is no longer possible to abort movement while hero is flying over water -* Fixed digging for Grail -* Implemented "Survive beyond a time limit" victory condition -* Implemented "Defeat all monsters" victory condition -* 100% damage resistance or damage reduction will make unit immune to a spell -* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic -* Fixed duration of bonuses from visitable object such as Idol of Fortune -* Rescued hero from prison will now correctly reveal map around him -* Lighthouses will no longer give movement bonus on land - -### 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 -* Fixed incorrect starting level of heroes in campaigns -* Game will now play correct music track on scenario selection window -* Dracon woll now correctly start without spellbook in Dragon Slayer campaign -* 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: -* Improved zone placement, shape and connections -* Improved zone passability for better gameplay -* Improved treasure distribution and treasure values to match SoD closely -* Navigation and water-specific spells are now banned on maps without water -* RMG will now respect road settings set in menu -* Tweaked many original templates so they allow new terrains and factions -* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties -* Added "road" property to connections -* Added monster strength "none" -* Support for "wide" connections -* Support for new "fictive" and "repulsive" connections -* RMG will now run faster, utilizing many CPU cores -* Removed random seed number from random map description - -### 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 -* It is now possible to arrange army using status window -* It is now possible to zoom in or out using mouse wheel or pinch gesture -* It is now possible to reset zoom via Backspace hotkey -* Receiving a message in chat will now play sound -* Map grid will now correctly display on map start -* Fixed multiple issues with incorrect updates of save/load game screen -* Fixed missing fortifications level icon in town tooltip -* Fixed positioning of resource label in Blacksmith window -* Status bar on inactive windows will no longer show any tooltip from active window -* Fixed highlighting of possible artifact placements when exchanging with allied hero -* Implemented sound of flying movement (for Fly spell or Angel Wings) -* Last symbol of entered cheat/chat message will no longer trigger hotkey -* Right-clicking map name in scenario selection will now show file name -* Right-clicking save game in save/load screen will now show file name and creation date -* Right-clicking in town fort window will now show creature information popup -* Implemented pasting from clipboard (Ctrl+V) for text input - -### 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 -* 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: -* Implemented initial version of VCMI campaign format -* Implemented spell cast as possible reward for configurable object -* Implemented support for configurable buildings in towns -* Implemented support for placing prison, tavern and heroes on water -* Implemented support for new boat types -* It is now possible for boats to use other movement layers, such as "air" -* It is now possible to use growing artifacts on artifacts that can be used by hero -* It is now possible to configure town moat -* Palette-cycling animation of terrains and rivers can now be configured in json -* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless') -* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades -* It is now possible to configure spells for Shrines -* It is now possible to configure upgrade costs per level for Hill Forts -* It is now possible to configure boat type for Shipyards on adventure map and in town -* Implemented support for HotA-style adventure map images for monsters, with offset -* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype). -* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL -* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance -* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses -* Configurable objects can now be translated -* Fixed loading of custom battlefield identifiers for map objects - -# 1.2.0 -> 1.2.1 - -### GENERAL: -* Implemented spell range overlay for Dimension Door and Scuttle Boat -* Fixed movement cost penalty from terrain -* Fixed empty Black Market on game start -* Fixed bad morale happening after waiting -* Fixed good morale happening after defeating last enemy unit -* Fixed death animation of Efreeti killed by petrification attack -* Fixed crash on leaving to main menu from battle in hotseat mode -* Fixed music playback on switching between towns -* Special months (double growth and plague) will now appear correctly -* Adventure map spells are no longer visible on units in battle -* Attempt to cast spell with no valid targets in hotseat will show appropriate error message -* RMG settings will now show all existing in game templates and not just those suitable for current settings -* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked -* Fixed centering of scenario information window -* Fixed crash on empty save game list after filtering -* Fixed blocked progress in Launcher on language detection failure -* Launcher will now correctly handle selection of Ddata directory in H3 install -* 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 - -### 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 -* Implemented hardware cursor support -* Heroes III language can now be detected automatically -* Increased targeted framerate from 48 to 60 -* Increased performance of UI updates -* 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 -* 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 -* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot -* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp -* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000 -* Fixed oversided message window from Scholar skill that had confirmation button outside game window -* Fixed loading of prebuilt creature hordes from h3m maps -* Fixed volume of ambient sounds when changing game sounds volume -* Fixed might&magic affinities of Dungeon heroes -* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins -* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally -* Default game difficulty is now set to "normal" instead of "easy" -* Fixed crash on missing music files - -### 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: -* 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 -* Added translations to German, Polish, Russian, Spanish, Ukrainian -* Mods tab layout has been adjusted based on feedback from players -* Settings tab layout has been redesigned to support longer texts -* Added button to start map editor directly from Launcher -* Simplified game starting flow from online lobby -* Mod description will now show list of languages supported by mod -* 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: -* 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 -* Increased AI priority of town dwelling upgrades -* AI will now de-prioritize town hall upgrades when low on resources -* Messages from cheats used by AI are now hidden -* Improved army gathering from towns -* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army. -* Improved Pandora handling -* AI takes into account fort level now when evaluating enemy town capturing priority. -* AI can not use allied shipyard now to avoid freeze -* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed. -* 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 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 -* Objects with limited per-map count will be distributed evenly among zones with suitable terrain -* Objects above zone treasure value will not be considered for placement -* RMG will prefer terrain-specific templates for objects placement -* RMG will place Towns and Monoliths first in order to generate long roads across the zone. -* Adjust the position of center town in the zone for better look & feel on S maps. -* Description of random map will correctly show number of levels -* 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: -* 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 -* Status bar will now show movement points information on pressing ALT or after enabling option in settings -* It is now not possible to receive rewards from School of War without required gold amount -* Owned objects, like Mines and Dwellings will always show their owner in status bar -* It is now possible to interact with on-map Shipyard when no hero is selected -* 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 -* Map scrolling is now more graduated and scrolls with pixel-level precision -* Hero movement speed now matches H3 -* Improved performance of adventure map rendering -* Fixed embarking and disembarking sounds -* Fixed selection of "new week" animation for status window -* Object render order now mostly matches H3 -* Fixed movement cost calculation when using "Fly" spell or "Angel Wings" -* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy -* Fixed invalid ambient sound of Whirlpool -* Hero path will now be correctly removed on defeating monsters that are at the end of hero path -* 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 -* 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 -* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine -* Added option to show number of available creatures in place of growth -* Fixed possible interaction with hero / town list from adventure map while in town screen -* 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: -* 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 -* All effects will now wait for battle opening sound before playing -* Hex highlighting will now be disabled during enemy turn -* Fixed incorrect log message when casting spell that kills zero units -* Implemented animated cursor for spellcasting -* Fixed multiple issues related to ordering of creature animations -* Fixed missing flags from hero animations when opening menus -* Fixed rendering order of moat and grid shadow -* Jousting bonus from Champions will now be correctly accounted for in damage estimation -* Building Castle building will now provide walls with additional health point -* Speed of all battle animations should now match H3 -* Fixed missing obstacles on subterranean terrain -* Ballistics mechanics now matches H3 logic -* 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 -* 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 -* Added missing combat log message on resurrecting creatures -* 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 -* 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 -* Fixed visibility of unit creature amount labels and timing of their updates -* Firewall will no longer hit double-wide units twice when passing through -* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance -* Orb of Vulnerability will now negate Resistance skill - -### SPELLS: -* Hero casting animation will play before spell effect -* Fire Shield: added sound effect -* Fire Shield: effect now correctly plays on defending creature -* Earthquake: added sound effect -* Earthquake: spell will not select sections that were already destroyed before cast -* Remove Obstacles: fixed error message when casting on maps without obstacles -* All area-effect spells (e.g. Fireball) will play their effect animation on top -* Summoning spells: added fade-in effect for summoned creatures -* Fixed timing of hit animation for damage-dealing spells -* Obstacle-creating spells: UI is now locked during effect animation -* Obstacle-creating spells: added sound effect -* Added reverse death animation for spells that bring stack back to life -* Bloodlust: implemented visual effect -* Teleport: implemented visual fade-out and fade-in effect for teleporting -* Berserk: Fixed duration of effect -* Frost Ring: Fixed spell effect range -* Fixed several cases where multiple different effects could play at the same time -* 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: -* 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 -* 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 -* 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: -* 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. -* Removed multiple bonuses that can be replaced with another bonus. -* It is now possible to define new hero movement sounds in terrains -* Implemented translation support for mods -* Implemented translation support for .h3m maps and .h3c campaigns -* Translation mods are now automatically disabled if player uses different language -* Files with new Terrains, Roads and Rivers are now validated by game -* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json -* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range -* Battle window and Random Map Tab now have their layout defined in json file -* Implemented code support for alternative actions mod -* Implemented code support for improved random map dialog -* It is now possible to configure number of creature stacks in heroes' starting armies -* It is now possible to configure number of constructed dwellings in towns on map start -* Game settings previously located in defaultMods.json are now loaded directly from mod.json -* 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 - -### 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 -* Fixed potential game freeze on end of music playback -* macOS/iOS: fixed sound glitches -* Android: upgraded version of SDL library -* Android: reworked right click gesture and relative pointer mode -* Improved map loading speed -* Ubuntu PPA: game will no longer crash on assertion failure - -### 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: -* 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: -* Local mods can be disabled or uninstalled -* Fixed styling of Launcher interface - -### MAP EDITOR: -* Fixed saving of roads and rivers -* Fixed placement of heroes on map - -# 1.0.0 -> 1.1.0 - -### 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 -* LUA/ERM libs are not compiled by default -* FFMpeg dependency is optional now -* Conan package manager is supported for MacOS and iOS - -### 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 -* Host can control game session by using console commands -* Control over player is transferred to AI if client escaped the game -* Reconnection mode for crashed client processes -* Playing online is available using proxy server - -### ADVENTURE MAP: -* Fix for digging while opponent's turn -* Supported right click for quick recruit window -* Fixed problem with quests are requiring identical artefacts -* Bulk move and swap artifacts -* Pause & resume for towns and terrains music themes -* 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 missing artifact slot in Artifact Merchant window - -### 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: -* 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: -* Speedup generation of random maps -* Necromancy cannot be learned in Witch Hut on random maps - -### MODS: -* Supported rewardable objects customization -* Battleground obstacles are extendable now with VLC mechanism -* Introduced "compatibility" section into mods settings -* Fixed bonus system for custom advmap spells -* Supported customisable town entrance placement -* Fixed validation of mods with new adventure map objects - -### 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: -* Basic version of Qt-based map editor - -# 0.99 -> 1.0.0 - -### 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 -* Skyship Grail works now immediately after capturing without battle -* 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 - -### 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: -* Implemented cumulative effects for spells - -### 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 -* Added bonus updaters for hero specialties -* Added allOf, anyOf and noneOf qualifiers for bonus limiters -* 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 - -### SOUND: -* Fixed many mising 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 supports water modes (normal, islands) -* Added config randomMap.json with settings for map generator -* Added parameter for template allowedWaterContent -* Extra resource packs appear nearby mines -* Underground can be player starting place for factions allowed to be placed underground -* Improved obstacles placement aesthetics -* Rivers are generated on the random maps -* RMG works more stable, various crashes have been fixed -* Treasures requiring guards are guaranteed to be protected - -### 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 -* AI can gather and save resources on purpose -* AI will only buy army on demand instead of every turn -* AI can distinguish the value of all map objects -* General speed optimizations - -### 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: -* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes -* Fix: Captured town should not be duplicated on the UI - -### LAUNCHER: -* Implemented notifications about updates -* Supported redirection links for downloading mods - -# 0.98 -> 0.99 - -### 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 -* Implemented rumors in tavern window -* New cheat codes: -* * 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 - -### 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: -* 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: -* 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 -* Added Thieves Guild random object (1 per zone) -* 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 - -### GENERAL: -* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) - -### 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: -* Implemented world view -* Added graphical fading effects - -### SPELLS: -* New spells handled: -* * Earthquake -* * View Air -* * View Earth -* * Visions -* * Disguise -* Implemented CURE spell negative dispell effect -* Added LOCATION target for spells castable on any hex with new target modifiers - -### BATTLES: -* Implemented OH3 stack split / upgrade formulas according to AlexSpl - -### 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) - -### 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 -* Multiplayer load support was implemented through command-line options - -### ADVENTURE AI: -* Significantly optimized execution time, AI should be much faster now. - -### 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: -* 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 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: -* Implemented switch to disable intro movies in game - -# 0.95 -> 0.96 (Jul 01 2014) - -### GENERAL: -* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 - -### ADVENTURE AI: -* Optimized speed and removed various bottlenecks. - -### ADVENTURE MAP: -* Heroes auto-level primary and secondary skill levels according to experience - -### BATTLES: -* Wall hit/miss sound will be played when using catapult during siege - -### SPELLS: -* New configuration format - -### 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) - -### 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: -* Added configuring of heroes quantity per player allowed in game - -# 0.94 -> 0.95 (Mar 01 2014) - -### 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 -* 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: -* 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. -* Significantly improved exploration algorithm. -* Locked heroes now try to decompose their goals exhaustively. -* Fixed (common) issue when AI found neutral stacks infinitely strong. -* Improvements for army exchange criteria. -* GatherArmy may include building dwellings in town (experimental). -* 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: -* 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 - -### TOWNS: -* Extended building dependencies support - -### 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) - -### 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 -* command "def2bmp" to convert def into set of frames. -* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus) -* fixed duels, added no-GUI mode for automatic AI testing -* Sir Mullich is available at the start of the game -* Upgrade cost will never be negative. -* support for Chinese fonts (GBK 2-byte encoding) - -### 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: -* autofight implemented -* most of the animations is time-based -* simplified postioning of units in battle, should fix remaining issues with unit positioning -* synchronized attack/defence animation -* spell animation speed uses game settings -* fixed disrupting ray duration -* added logging domain for battle animations -* Fixed crashes on Land Mines / Fire Wall casting. -* UI will be correctly greyed-out during opponent turn -* fixed remaining issues with blit order -* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3) -* Fixed Remove Obstacle. -* defeating hero will yield 500 XP -* Added lots of missing spell immunities from Strategija -* Added stone gaze immunity for Troglodytes (did you know about it?) -* damage done by turrets is properly increased by built buldings -* Wyverns will cast Poison instead of Stone Gaze. - -### TOWN: -* Fixed issue that allowed to build multiple boats in town. -* fix for lookout tower - -# 0.92 -> 0.93 (Jun 01 2013) - -### GENERAL: -* Support for SoD-only installations, WoG becomes optional addition -* New logging framework -* Negative luck support, disabled by default -* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack) -* Fixed stack artifact (and related buttons) not displaying in creature window. -* Fixed crash at month of double population. - -### 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) -* 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: -* Fixed Death Stare of Commanders -* Projectile blitting should be closer to original H3. But still not perfect. -* Fixed missing Mirth effects -* Stack affected by Berserk should not try to attack itself -* Fixed several cases of incorrect positioning of creatures in battles -* Fixed abilities of Efreet. -* Fixed broken again palette in some battle backgrounds - -### 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) - -### 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: -* 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) - -### GENERAL: -* hero crossover between missions in campaigns -* introduction before missions in campaigns - -### MODS: -* Added CREATURE_SPELL_POWER for commanders -* 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 -* Drain Life now has % effect depending on bonus value -* 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. -* support for string subtype id in short bonus format -* primary skill identifiers for bonuses - -# 0.9 -> 0.91 (Feb 01 2013) - -### 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: -* 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 -* 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 - -### BATTLES: -* Support for "enchanted" WoG ability - -### 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) - -### 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. - -### BATTLES: -* New graphics for Stack Queue -* Death Stare works identically to H3 -* No explosion when catapult fails to damage the wall -* Fixed crash when attacking stack dies before counterattack -* 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. - -### 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. -* AI can now exchange armies between heroes. By default, it will pass army to main hero. -* Fixed strange case when AI found allied town extremely dangerous -* Fixed crash when AI tried to "revisit" a Boat -* 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. -* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells - -# 0.88 -> 0.89 (Jun 01 2012) - -### GENERAL: -* Mostly implemented Commanders feature (missing level-up dialog) -* Support for stack artifacts -* New creature window graphics contributed by fishkebab -* Config file may have multiple upgrades for creatures -* CTRL+T will open marketplace window -* G will open thieves guild window if player owns at least one town with tavern -* Implemented restart functionality. CTRL+R will trigger a quick restart -* Save game screen and returning to main menu will work if game was started with --start option -* Simple mechanism for detecting game desynchronization after init -* 1280x800 resolution graphics, contributed by Topas - -### ADVENTURE MAP: -* Fixed monsters regenerating casualties from battle at the start of new week. -* T in adventure map will switch to next town - -### 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. -* Fixed crash when death stare or acid breath activated on stack that was just killed -* First aid tent can heal only creatures that suffered damage -* War machines can't be healed by tent -* Creatures casting spells won't try to cast them during tactic phase -* Console tooltips for first aid tent -* Console tooltips for teleport spell -* Cursor is reset to pointer when action is requested -* Fixed a few other missing or wrong tooltips/cursors -* Implemented opening creature window by l-clicking on stack -* Fixed crash on attacking walls with Cyclop Kings -* Fixed and simplified Teleport casting -* Fixed Remove Obstacle spell -* New spells supported: -* * Chain Lightning -* * Fire Wall -* * Force Field -* * Land Mine -* * Quicksands -* * Sacrifice - -### TOWNS: -* T in castle window will open a tavern window (if available) - -### PREGAME: -* Pregame will use same resolution as main game -* Support for scaling background image -* Customization of graphics with config file. - -### ADVENTURE AI: -* basic rule system for threat evaluation -* new town development logic -* AI can now use external dwellings -* AI will weekly revisit dwellings & mills -* AI will now always pick best stacks from towns -* AI will recruit multiple heroes for exploration -* AI won't try attacking its own heroes - -# 0.87 -> 0.88 (Mar 01 2012) - -* added an initial version of new adventure AI: VCAI -* system settings window allows to change default resolution -* introduced unified JSON-based settings system -* fixed all known localization issues -* Creature Window can handle descriptions of spellcasting abilities -* Support for the clone spell - -# 0.86 -> 0.87 (Dec 01 2011) - -### GENERAL: -* Pathfinder can find way using ships and subterranean gates -* Hero reminder & sleep button - -### PREGAME: -* Credits are implemented - -### 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 - -# 0.85 -> 0.86 (Sep 01 2011) - -### 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 - -### 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 -* New WoG abilities supported -* * Defense Bonus -* * Cast before attack -* * Immunity to direct damage spells -* New spells supported -* * Magic Mirror -* * Titan's Lightning Bolt - -# 0.84 -> 0.85 (Jun 01 2011) - -### GENERAL: -* Support for stack experience -* Implemented original campaign selection screens -* New artifacts supported: -* * Statesman's Medal -* * Diplomat's Ring -* * Ambassador's Sash - -### TOWNS: -* Implemented animation for new town buildings -* It's possible to sell artifacts at Artifact Merchants - -### 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) - -### 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 - -### BATTLES: -* Better animations handling -* Defensive stance is supported - -### HERO: -* New secondary skills supported: -* * Artillery -* * Eagle Eye -* * Tactics - -### AI PLAYER: -* new AI leading neutral creatures in combat, slightly better then previous - -# 0.82 -> 0.83 (Nov 01 2010) - -### 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 -* Town Portal supported -* Vial of Dragon Blood and Statue of Legion supported - -### HERO: -* remaining specialities have been implemented - -### TOWNS: -* town events supported -* Support for new town structures: Deiety of Fire and Escape Tunnel - -### BATTLES: -* blocked retreating from castle - -# 0.81 -> 0.82 (Aug 01 2010) - -### 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 - -### ADVENTURE MAP: -* Neutral armies growth implemented (10% weekly) -* Power rating of neutral stacks -* Favourable Winds reduce sailing cost - -### 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) - -### TOWNS: -* Support for new town structures: -* * Artifact Merchant -* * Aurora Borealis -* * Castle Gates -* * Magic University -* * Portal of Summoning -* * Skeleton transformer -* * Veil of Darkness - -### 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 - -# 0.8 -> 0.81 (Jun 01 2010) - -### 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 - -### ADVENTURE MAP: -* Creatures now guard surrounding tiles -* New adventura map spells supported: -* * Summon Boat -* * Scuttle Boat -* * Dimension Door -* * Fly -* * Water walk - -### BATTLES: -* A number of new creature abilities supported -* First Aid Tent is functional -* Support for distance/wall/melee penalties & no * penalty abilities -* Reworked damage calculation to fit OH3 formula better -* Luck support -* Teleportation spell - -### HERO: -* First Aid secondary skill -* Improved formula for necromancy to match better OH3 - -### TOWNS: -* Sending resources to other players by marketplace -* Support for new town structures: -* * Lighthouse -* * Colossus -* * Freelancer's Guild -* * Guardian Spirit -* * Necromancy Amplifier -* * Soul Prison - -### OBJECTS: -* New object supported: -* * Freelancer's Guild -* * Trading Post -* * War Machine Factory - -# 0.75 -> 0.8 (Mar 01 2010) - -### 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: -* Implemented rivers animations (thx to GrayFace). - -### BATTLES: -* Fire Shield spell (and creature ability) supported -* affecting morale/luck and casting spell after attack creature abilities supported - -### HERO: -* Implementation of Scholar secondary skill - -### 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 - -### OBJECTS: -* New objects supported: -* * 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) - -### 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: -* 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. - -### TOWNS: -* new special town structures supported: -* * Academy of Battle Scholars -* * Cage of Warlords -* * Mana Vortex -* * Stables -* * Skyship (revealing entire map only) - -### 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. - -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) - -### GENERAL: -* Scenario Information window -* Save Game window -* VCMI window should start centered -* support for Necromancy and Ballistics secondary skills -* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining) -* VCMI client has its own icon (thx for graphic to Dikamilo) -* Ellipsis won't be split when breaking text on several lines -* split button will be grayed out when no creature is selected -* 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 -* moving hero with arrow keys / numpad -* fixed Next Hero button behaviour -* fixed Surface/Underground switch button in higher resolutions - -### 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) -* more creatures special abilities supported -* battle settings will be stored -* fixed crashes occurring on attacking two hex creatures from back -* 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 -* 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. -* 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 - -### 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 - -### HERO WINDOW: -* war machines cannot be unequiped - -### 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 -* subtitles for chosen hero/town/bonus in pregame - -### 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 - -# 0.72 -> 0.73 (Aug 01 2009) - -### 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. -* 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 -* giving starting spells for heroes -* pressing enter or escape close spellbook -* removed redundant quotation marks from skills description and artifact events texts -* disabled autosaving on first turn -* bonuses from bonus artifacts -* increased char per line limit for subtitles under components -* corrected some exp/level values -* primary skills cannot be negative -* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity -* fixed timed events reappearing -* saving system options -* saving hero direction -* r-click popups on enemy heroes and towns -* hero leveling formula matches the H3 - -### 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: -* spell resistance supported (secondary skill, artifacts, creature skill) -* corrected damage inflicted by spells and ballista -* added some missing projectile infos -* added native terrain bonuses in battles -* number of units in stack in battle should better fit the box -* 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 - -### 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: -* garrisoned heroes won't be shown on the list -* artifacts will be present on morale/luck bonuses list - -### PREGAME: -* saves are sorted primary by map format, secondary by name -* fixed displaying date of saved game (uses local time, removed square character) - -### 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 - -# 0.71 -> 0.72 (Jun 1 2009) - -### GENERAL: -* many sound effects and music -* autosave (to 5 subsequent files) -* artifacts support (most of them) -* added internal game console (activated on TAB) -* fixed 8 hero limit to check only for wandering heroes (not garrisoned) -* improved randomization -* fixed crash on closing application -* VCMI won't always give all three stacks in the starting armies -* fix for drawing starting army creatures count -* 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 -* no income at the first day - -### 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 -* clicking scrolling arrows in town/hero list won't open town/hero window -* 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: -* many creatures special skills implemented -* battle will end when one side has only war machines -* fixed some problems with handling obstacles info -* fixed bug with defending / waiting while no stack is active -* spellbook button is inactive when hero cannot cast any spell -* obstacles will be placed more properly when resolution is different than 800x600 -* 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 - -### 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: -* 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: -* clicking on "advanced options" a second time now closes the tab instead of refreshing it. -* 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: -* 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 -* support for School of Magic -* support for School of War -* support for Pillar of Fire -* support for Corpse -* support for Lean To -* support for Wagon -* support for Warrior's Tomb -* support for Event -* Corpse (Skeleton) will be accessible from all directions - -# 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 -* 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 problems with 1024x600 screen resolution -* 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: -* added "Next hero" button functionality -* added missing path arrows -* 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 -* 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 -* provisional support for "Save" button in System Options Window -* it's possible to revisit object under hero by pressing Space - -### 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 -* spell books won't be placed in War Machine slots after battle -* 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 -* standing stacks have more static animation -* probably fixed problem with displaying corpses on battlefield -* fixes for two-hex creatures actions -* fixed hero casting spell animation -* corrected stack death animation -* battle settings will be remembered between battles -* improved damage calculation formula -* 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 -* corrected hero spell casting animation -* better support for battle backgrounds -* blocked "save" command during battle -* spellbook displays only spells known by Hero -* New spells supported: -* * Mirth -* * Sorrow -* * Fortune -* * Misfortune - -### TOWN INTERFACE: -* cannot build more than one capitol -* cannot build shipyard if town is not near water -* 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 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: -* added scrolling scenario list with mouse wheel -* fixed mouse slow downs -* 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 - -### AI PLAYER: -* Minor improvements and fixes. - -# 0.64 -> 0.7 (Feb 01 2009) - -### 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) -* /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 - -### 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: -* 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: -* support for the Wait command -* magic arrow *really* works -* war machines support partially added -* queue of stacks narrowed -* spell effect animation displaying improvements -* 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 -* 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 - -### AI PLAYER: -* Genius AI (first VCMI AI) will control computer creatures during the combat. - -### OBJECTS: -* Guardians property for resources is handled -* support for Witch Hut -* support for Arena -* support for Library of Enlightenment - -And a lot of minor fixes - -# 0.63 -> 0.64 (Nov 01 2008) - -### GENERAL: -* sprites from /Sprites folder are handled correctly -* several fixes for pathfinder and path arrows -* better handling disposed/predefined heroes -* heroes regain 1 mana point each turn -* support for mistycisim and intelligence skills -* hero hiring possible -* added support for a number of hotkeys -* it's not possible anymore to leave hero level-up window without selecting secondary skill -* 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 - -### 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: -* partially done tavern window (only hero hiring functionality) - -### BATTLES: -* water elemental will really be treated as 2 hex creature -* potential infinite loop in reverseCreature removed -* 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 - -# 0.62 -> 0.63 (Oct 01 2008) - -### 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 -* partially done spellbook -* Alt+F4 quits the game -* 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 - -### BATTLES: -* very significant optimization of battles -* battle summary window -* 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 -* 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 -* hero's primary skills accounted for damage dealt by creatures in battle - -### TOWNS: -* mostly done marketplace -* fixed crashbug with battles on swamps and rough terrain -* 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: -* setting army formation -* tooltips for artifacts in backpack - -### ADVENTURE INTERFACE: -* fixed bug with disappearing head of a hero in adventure map -* 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.) -* artifacts we pick up go to the appropriate slot (if free) - -# 0.61 -> 0.62 (Sep 01 2008) - -### GENERAL: -* restructured to the server-client model -* support for heroes placed in towns -* upgrading creatures -* working gaining levels for heroes (including dialog with skill selection) -* added graphical cursor -* showing creature amount in the creature info window -* giving starting bonus - -### 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) -* fixed reading forbidden structures -* randomizing spells in towns -* viewing hero window in the town screen -* possibility of moving hero into the garrison -* 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: -* hopefully fixed problems with wrong town defs (village/fort/capitol) - -### HERO WINDOW: -* bugfix: splitting stacks works in hero window -* removed bug causing significant increase of CPU consumption - -### BATTLES: -* shooting -* removed some displaying problems -* showing last group of frames in creature animation won't crash -* added start moving and end moving animations -* fixed moving two-hex creatures -* showing/hiding graphic cursor -* a part of using graphic cursor -* slightly optimized showing of battle interface -* animation of getting hit / death by shooting is displayed when it should be -* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. -* minor optimizations - -### 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: -* support for the Tree of knowledge -* support for Campfires -* added event message when picking artifact - -# 0.6 -> 0.61 (Jun 15 2008) - -### IMPROVEMENTS: -* improved attacking in the battles -* it's possible to kill hostile stack -* animations won't go in the same phase -* Better pathfinder -* "%s" substitutions in Right-click information in town hall -* windmill won't give wood -* hover text for heroes -* support for ZSoft-style PCX files in /Data -* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved -* in the townlist in castle selected town will by placed on the 2nd place (not 3rd) -* stack at the limit of unit's range can now be attacked -* range of unit is now properly displayed -* battle log is scrolled down when new event occurs -* console is closed when application exits - -### 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 -* VCMI won't crash when r-click neutral stack during the battle -* water won't blink behind shipyard in the Castle -* fixed several memory leaks -* 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) - -* partially done attacking in battles -* screen isn't now refreshed while blitting creature info window -* r-click creature info windows in battles -* no more division by 0 in slider -* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working) -* fixed estate problems -* fixed blinking mana vortex -* grail increases creature growths -* new pathfinder -* several minor improvements - -# 0.58 -> 0.59 (May 24 2008 - closed, test release) - -* fixed memory leak in battles -* blitting creature animations to rects in the recruitment window -* fixed wrong creatures def names -* better battle pathfinder and unit reversing -* improved slider ( #58 ) -* fixed problems with horde buildings (won't block original dwellings) -* giving primary skill when hero get level (but there is still no dialog) -* if an upgraded creature is available it'll be shown as the first in a recruitment window -* creature levels not messed in Fortress -* war machines are added to the hero's inventory, not to the garrison -* support for H3-style PCX graphics in Data/ -* VCMI won't crash when is unable to initialize audio system -* fixed displaying wrong town defs -* improvements in recruitment window (slider won't allow to select more creatures than we can afford) -* creature info window (only r-click) -* callback for buttons/lists based on boost::function -* a lot of minor improvements - -# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) - -### TOWNS: -* recruiting creatures -* working creature growths (including castle and horde building influences) -* towns give income -* town hall screen -* building buildings (requirements and cost are handled) -* hints for structures -* updating town infobox - -### GARRISONS: -* merging stacks -* splitting stacks - -### 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: -* a number of minor fixes and improvements - -# 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 -* fix of Pandora's Box handling -* reading disposed/predefined heroes -* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder. -* more detailed logs -* reported problems with hero flags resolved -* heroes cannot occupy the same tile -* hints for most of creature generators -* some minor stuff - -# 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 -* giving random hero on RoE maps -* improved protection against hero duplication -* fixed starting values of primary abilities of random heroes on RoE/AB maps -* right click popups with infoboxes for heroes/towns lists -* new interface coloring (many thanks to GrayFace ;]) -* fixed bug in object flag's coloring -* added hints in town lists -* eliminated square from city hints - -# 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): - -* hopefully the last bugfix of Pandora's Box -* fixed blockmaps of generated heroes -* disposed hero cannot be chosen in scenario settings (unless he is in prison) -* fixed town randomization -* fixed hero randomization -* fixed displaying heroes in preGame -* fixed selecting/deselecting artifact slots in hero window -* much faster pathfinder -* memory usage and load time significantly decreased -* it's impossible to select empty artifact slot in hero window -* fixed problem with FoW displaying on minimap on L-sized maps -* fixed crashbug in hero list connected with heroes dismissing -* mostly done town infobox -* town daily income is properly calculated - -# 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] improved randomization -* [bugfix] pathfinder can't be cheated (what caused errors) - -# 0.5 - 0.51 (Feb 3 2008): - -* close button properly closes (same does 'q' key) -* two players can't have selected same hero -* double click on "Show Available Scenarios" won't reset options -* fixed possible crashbug in town/hero lists -* fixed crashbug in initializing game caused by wrong prisons handling -* fixed crashbug on reading hero's custom artifacts in RoE maps -* fixed crashbug on reading custom Pandora's Box in RoE maps -* fixed crashbug on reading blank Quest Guards -* 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): - -* Main menu and New game screens -* Scenario selection, part of advanced options support -* Partially done adventure map, town and hero interfaces -* Moving hero -* Interactions with several objects (mines, resources, mills, and others) +# 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 +* Completing a campaign will now return player to campaign selection window instead of main menu +* Game will now play video on winning or losing a game +* Game will now correctly check for mod compatibility when loading saved games +* Game client will no longer load conflicting mods if player have both of them enabled +* If some mods fail to load due to missing dependencies or conflicts, game client will display message on opening main menu +* Game will no longer crash on loading save with different mod versions and will show error message instead +* Saved games are now 2-3 times smaller than before +* Added Vietnamese translation +* Failure to connect to a MP game will now show proper error message +* Added VSync support +* Implemented tutorial +* Implemented support for playback of audio from video files +* Windows Installer will now automatically add required firewall rules +* Game audio will now be disabled if game window is not focused +* Fixed formatting of date and time of a savegame on Android +* Added list of VCMI authors to credits screen +* Quick combat is now disabled by default +* 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 +* Added option to pause game timer while on system options window +* Implemented localization support for maps +* Game will now use texts from local player instead of host +* 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 +* Implemented smooth scrolling for adventure map +* Implemented radial wheel to reorder list of owned towns and heroes +* 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 +* "Friendly AI" option will now correctly display current type of friendly AI +* Player can now correctly switch to global chat after disconnect +* "Resolve mods conflicts" button now attempts to fix all mods if nothing is selected +* Implemented support for mention in game lobby +* Implemented support for global and room channels in game lobby +* 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 +* Improved army configuration interface +* 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 +* It is now possible to configure hero placeholders in map editor +* 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 +* Fixed crash on attempting to move all artifacts from hero that has no artifacts +* Fixed crash on attempting to load corrupted .def file +* 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 +* Improved BattleAI performance when selection unit action +* Improved BattleAI spell selection logic +* 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 +* 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) +* Fixed incorrect positioning of total experience text on Altar of Sacrifice +* Game will now show correct video file on battle end +* Game will now correctly loop battle end animation video +* Implemented larger version of spellbooks that displays up to 24 spells at once +* Spell scrolls in hero inventory now show icon of contained spell +* Fixed incorrect hero morale tooltip after visiting adventure map objects +* Fixed incorrect information for skills in hero exchange window +* Confirmation button will now be disabled on automatic server connect dialog +* 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 +* Added support for folders in scenario selection and save/load screens +* Added support for "Show Random Maps" button in random map setup screen +* Added starting hero preview screen +* Added option to change name of player while in map setup screen +* Implemented loading screen with progress bar +* Game will now stay on loading screen while random map generation is in process +* Team Alignments popup in scenario options will no longer show empty teams +* Fixed missing borders on team alignments configuration window in random maps +* Fixed map difficulty icon on save/load screen +* 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 +* Ctrl-click on hero will now select him instead of changing path of active hero +* In selection windows (level up window, treasure chest pickup, etc) it is now possible to select reward via double-click +* Attacking wandering monsters with preconfigured message will now correctly show the message +* Revisit object button will now be blocked if there is no object to revisit +* Fixed missing tooltip for "revisit object" button +* Fixed calculation of fow reveal range for all objects +* Attempt to close game will now ask for confirmation +* Right-clicking previously visited Seer Huts or Quest Guards will show icon with preview of quest goal +* Right-clicking owned dwellings will show amount of creatures available to for recruitment +* Right-clicking previously visited creature banks will show exact guards composition with their portraits +* Right-clicking artifacts on map will show artifact description +* 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 +* Fixed Grail in Crypt on some custom maps +* 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 +* 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 +* Pathfinder will no longer make U-turns when moving onto visitable objects while flying +* Pathfinder will no longer make paths that go over teleporters without actually using them +* Game will now correctly update guard status of tiles that are guarded by multiple wandering monsters +* Moving onto Garrisons and Border Guards entrance tiles that are guarded by wandering monsters will now correctly trigger battle +* It is no longer possible to build second boat in shipyard when shipyard should be blocked by boat with hero +* 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 +* Game will no longer route roads through non-removable treasure objects, such as Corpse +* Fixed placement of treasure piles with non-removable objects, such as Corpse +* Fixed interface no displaying correct random map settings in some cases +* Fixed misleading error "no info for player X found" +* 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 +* Added support for giving arbitrary bonuses to AI players +* Most mods of type "Translation" are now hidden in Launcher +* Added new mod type: "Compatibility". Mods of this type are hidden in Launcher and are always active if they are compatible. +* Added new mod type: "Maps" +* Added new TERRAIN_NATIVE bonus that makes any terrain native to affected units +* SPELL_DURATION now allows subtypes. If set to spell, bonus will only affect specified spell +* Both game client and launcher will now correctly handle dependencies that are not in lower case +* Implemented support for refusable Witch Hut and Scholar +* Added "variables" to configurable objects that are shared between all rewards +* Added visit mode "limiter" for configurable objects. Hero will be considered as "visited this object" if he fulfills provided condition +* Added option to customize text displayed for visited objects, e.g. show "Already learned" instead of "Visited" +* Added option to define custom description of configurable object, accessible via right-click +* Added option to show object content icons on right-click +* Object now allows checking whether hero can learn spell +* 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 + +### 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 +* Added option to open hero backpack window in hero screen +* Added detection of misclicks for touch inputs to make hitting small UI elements easier +* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities +* It is no longer possible to stop movement while moving over water with Water Walk +* Game will now automatically update hero path if it was blocked by another hero +* 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 +* Fixed crash on attempting to visit unknown object, such as Market of Time +* Fixed crash on attempting to teleport unit that is immune to a spell +* 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 +* Added option to toggle spell usage by AI in quick combat +* Fixed updating of spell point of enemy hero in game interface after spell cast +* Fixed wrong creature spellcasting shortcut (now set to "F") +* It is now possible to perform melee attack by creatures with spells, especially area spells +* Right-click will now properly end spellcast mode +* Fixed cursor preview when casting spell using touchscreen +* 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 +* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window +* Fixed incorrect color of resource bar in hotseat mode +* Fixed broken toggle map level button in world view mode +* Fixed corrupted interface after opening puzzle window from world view mode +* Fixed blocked interface after attempt to start invalid map +* Add yellow border to selected commander grandmaster ability +* Always use bonus description for commander abilities instead of not provided wog-specific translation +* Fix scrolling when commander has large number of grandmaster abilities +* Fixed corrupted message on another player defeat +* Fixed unavailable Quest Log button on maps with quests +* Fixed incorrect values on a difficulty selector in save load screen +* 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 + +### 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 +* 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 +* Background battlefield obstacles will now appear below creatures +* it is now possible to load save game located inside mod +* Added option to configure reserved screen area in Launcher on iOS +* Fixed border scrolling when game window is maximized + +### 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 +* NKAI: Fixed recruitment of new heroes +* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location +* VCAI: Fixed spellcasting by Archangels + +### 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: +* 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 +* Fixed crash on starting game with outdated mods +* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice +* Fixed crash on leveling up after winning battle as defender +* Fixed possible crash on end of battle opening sound +* Fixed crash on accepting battle result after winning battle as defender +* Fixed possible crash on casting spell in battle by AI +* Fixed multiple possible crashes on managing mods on Android +* Fixed multiple possible crashes on importing data on Android +* Fixed crash on refusing rewards from town building +* Fixed possible crash on threat evaluation by NKAI +* Fixed crash on using haptic feedback on some Android systems +* Fixed crash on right-clicking flags area in RMG setup mode +* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations +* 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 + +### 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 +* Fixed multiple issues with borderless fullscreen mode +* On mobile systems game will now always run at native resolution with configurable UI scaling +* Implemented support for Horn of the Abyss map format +* Implemented option to replay results of quick combat +* Added translations to French and Chinese +* All in-game cheats are now case-insensitive +* Added high-definition icon for Windows +* Fix crash on connecting to server on FreeBSD and Flatpak builds +* Save games now consist of a single file +* Added H3:SOD cheat codes as alternative to vcmi cheats +* Fixed several possible crashes caused by autocombat activation +* Fixed artifact lock icon in localized versions of the game +* Fixed possible crash on changing hardware cursor + +### 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 +* Implemented radial menu for army management, activated via swiping creature icon +* Implemented swipe gesture for scrolling through lists +* All windows that have sliders in UI can now be scrolled using swipe gesture +* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from +* Implemented pinch gesture for zooming adventure map +* Implemented haptic feedback (vibration) for long press gesture + +### 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 +* Patially fixed displaying of download progress for mods +* Fixed potential crash on opening mod information for mods with a changelog +* Added option to configure number of autosaves + +### 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: +* Fixed potential crash on accessing market (VCAI) +* Fixed potentially infinite turns (VCAI) +* Reworked object prioritizing +* Improved town defense against enemy heroes +* Improved town building (mage guild and horde) +* 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 +* It is now possible to access Shipwrecks from coast +* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings +* It is no longer possible to abort movement while hero is flying over water +* Fixed digging for Grail +* Implemented "Survive beyond a time limit" victory condition +* Implemented "Defeat all monsters" victory condition +* 100% damage resistance or damage reduction will make unit immune to a spell +* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic +* Fixed duration of bonuses from visitable object such as Idol of Fortune +* Rescued hero from prison will now correctly reveal map around him +* Lighthouses will no longer give movement bonus on land + +### 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 +* Fixed incorrect starting level of heroes in campaigns +* Game will now play correct music track on scenario selection window +* Dracon woll now correctly start without spellbook in Dragon Slayer campaign +* 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: +* Improved zone placement, shape and connections +* Improved zone passability for better gameplay +* Improved treasure distribution and treasure values to match SoD closely +* Navigation and water-specific spells are now banned on maps without water +* RMG will now respect road settings set in menu +* Tweaked many original templates so they allow new terrains and factions +* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties +* Added "road" property to connections +* Added monster strength "none" +* Support for "wide" connections +* Support for new "fictive" and "repulsive" connections +* RMG will now run faster, utilizing many CPU cores +* Removed random seed number from random map description + +### 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 +* It is now possible to arrange army using status window +* It is now possible to zoom in or out using mouse wheel or pinch gesture +* It is now possible to reset zoom via Backspace hotkey +* Receiving a message in chat will now play sound +* Map grid will now correctly display on map start +* Fixed multiple issues with incorrect updates of save/load game screen +* Fixed missing fortifications level icon in town tooltip +* Fixed positioning of resource label in Blacksmith window +* Status bar on inactive windows will no longer show any tooltip from active window +* Fixed highlighting of possible artifact placements when exchanging with allied hero +* Implemented sound of flying movement (for Fly spell or Angel Wings) +* Last symbol of entered cheat/chat message will no longer trigger hotkey +* Right-clicking map name in scenario selection will now show file name +* Right-clicking save game in save/load screen will now show file name and creation date +* Right-clicking in town fort window will now show creature information popup +* Implemented pasting from clipboard (Ctrl+V) for text input + +### 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 +* 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: +* Implemented initial version of VCMI campaign format +* Implemented spell cast as possible reward for configurable object +* Implemented support for configurable buildings in towns +* Implemented support for placing prison, tavern and heroes on water +* Implemented support for new boat types +* It is now possible for boats to use other movement layers, such as "air" +* It is now possible to use growing artifacts on artifacts that can be used by hero +* It is now possible to configure town moat +* Palette-cycling animation of terrains and rivers can now be configured in json +* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless') +* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades +* It is now possible to configure spells for Shrines +* It is now possible to configure upgrade costs per level for Hill Forts +* It is now possible to configure boat type for Shipyards on adventure map and in town +* Implemented support for HotA-style adventure map images for monsters, with offset +* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype). +* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL +* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance +* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses +* Configurable objects can now be translated +* Fixed loading of custom battlefield identifiers for map objects + +# 1.2.0 -> 1.2.1 + +### GENERAL: +* Implemented spell range overlay for Dimension Door and Scuttle Boat +* Fixed movement cost penalty from terrain +* Fixed empty Black Market on game start +* Fixed bad morale happening after waiting +* Fixed good morale happening after defeating last enemy unit +* Fixed death animation of Efreeti killed by petrification attack +* Fixed crash on leaving to main menu from battle in hotseat mode +* Fixed music playback on switching between towns +* Special months (double growth and plague) will now appear correctly +* Adventure map spells are no longer visible on units in battle +* Attempt to cast spell with no valid targets in hotseat will show appropriate error message +* RMG settings will now show all existing in game templates and not just those suitable for current settings +* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked +* Fixed centering of scenario information window +* Fixed crash on empty save game list after filtering +* Fixed blocked progress in Launcher on language detection failure +* Launcher will now correctly handle selection of Ddata directory in H3 install +* 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 + +### 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 +* Implemented hardware cursor support +* Heroes III language can now be detected automatically +* Increased targeted framerate from 48 to 60 +* Increased performance of UI updates +* 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 +* 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 +* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot +* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp +* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000 +* Fixed oversided message window from Scholar skill that had confirmation button outside game window +* Fixed loading of prebuilt creature hordes from h3m maps +* Fixed volume of ambient sounds when changing game sounds volume +* Fixed might&magic affinities of Dungeon heroes +* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins +* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally +* Default game difficulty is now set to "normal" instead of "easy" +* Fixed crash on missing music files + +### 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: +* 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 +* Added translations to German, Polish, Russian, Spanish, Ukrainian +* Mods tab layout has been adjusted based on feedback from players +* Settings tab layout has been redesigned to support longer texts +* Added button to start map editor directly from Launcher +* Simplified game starting flow from online lobby +* Mod description will now show list of languages supported by mod +* 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: +* 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 +* Increased AI priority of town dwelling upgrades +* AI will now de-prioritize town hall upgrades when low on resources +* Messages from cheats used by AI are now hidden +* Improved army gathering from towns +* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army. +* Improved Pandora handling +* AI takes into account fort level now when evaluating enemy town capturing priority. +* AI can not use allied shipyard now to avoid freeze +* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed. +* 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 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 +* Objects with limited per-map count will be distributed evenly among zones with suitable terrain +* Objects above zone treasure value will not be considered for placement +* RMG will prefer terrain-specific templates for objects placement +* RMG will place Towns and Monoliths first in order to generate long roads across the zone. +* Adjust the position of center town in the zone for better look & feel on S maps. +* Description of random map will correctly show number of levels +* 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: +* 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 +* Status bar will now show movement points information on pressing ALT or after enabling option in settings +* It is now not possible to receive rewards from School of War without required gold amount +* Owned objects, like Mines and Dwellings will always show their owner in status bar +* It is now possible to interact with on-map Shipyard when no hero is selected +* 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 +* Map scrolling is now more graduated and scrolls with pixel-level precision +* Hero movement speed now matches H3 +* Improved performance of adventure map rendering +* Fixed embarking and disembarking sounds +* Fixed selection of "new week" animation for status window +* Object render order now mostly matches H3 +* Fixed movement cost calculation when using "Fly" spell or "Angel Wings" +* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy +* Fixed invalid ambient sound of Whirlpool +* Hero path will now be correctly removed on defeating monsters that are at the end of hero path +* 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 +* 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 +* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine +* Added option to show number of available creatures in place of growth +* Fixed possible interaction with hero / town list from adventure map while in town screen +* 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: +* 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 +* All effects will now wait for battle opening sound before playing +* Hex highlighting will now be disabled during enemy turn +* Fixed incorrect log message when casting spell that kills zero units +* Implemented animated cursor for spellcasting +* Fixed multiple issues related to ordering of creature animations +* Fixed missing flags from hero animations when opening menus +* Fixed rendering order of moat and grid shadow +* Jousting bonus from Champions will now be correctly accounted for in damage estimation +* Building Castle building will now provide walls with additional health point +* Speed of all battle animations should now match H3 +* Fixed missing obstacles on subterranean terrain +* Ballistics mechanics now matches H3 logic +* 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 +* 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 +* Added missing combat log message on resurrecting creatures +* 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 +* 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 +* Fixed visibility of unit creature amount labels and timing of their updates +* Firewall will no longer hit double-wide units twice when passing through +* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance +* Orb of Vulnerability will now negate Resistance skill + +### SPELLS: +* Hero casting animation will play before spell effect +* Fire Shield: added sound effect +* Fire Shield: effect now correctly plays on defending creature +* Earthquake: added sound effect +* Earthquake: spell will not select sections that were already destroyed before cast +* Remove Obstacles: fixed error message when casting on maps without obstacles +* All area-effect spells (e.g. Fireball) will play their effect animation on top +* Summoning spells: added fade-in effect for summoned creatures +* Fixed timing of hit animation for damage-dealing spells +* Obstacle-creating spells: UI is now locked during effect animation +* Obstacle-creating spells: added sound effect +* Added reverse death animation for spells that bring stack back to life +* Bloodlust: implemented visual effect +* Teleport: implemented visual fade-out and fade-in effect for teleporting +* Berserk: Fixed duration of effect +* Frost Ring: Fixed spell effect range +* Fixed several cases where multiple different effects could play at the same time +* 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: +* 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 +* 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 +* 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: +* 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. +* Removed multiple bonuses that can be replaced with another bonus. +* It is now possible to define new hero movement sounds in terrains +* Implemented translation support for mods +* Implemented translation support for .h3m maps and .h3c campaigns +* Translation mods are now automatically disabled if player uses different language +* Files with new Terrains, Roads and Rivers are now validated by game +* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json +* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range +* Battle window and Random Map Tab now have their layout defined in json file +* Implemented code support for alternative actions mod +* Implemented code support for improved random map dialog +* It is now possible to configure number of creature stacks in heroes' starting armies +* It is now possible to configure number of constructed dwellings in towns on map start +* Game settings previously located in defaultMods.json are now loaded directly from mod.json +* 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 + +### 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 +* Fixed potential game freeze on end of music playback +* macOS/iOS: fixed sound glitches +* Android: upgraded version of SDL library +* Android: reworked right click gesture and relative pointer mode +* Improved map loading speed +* Ubuntu PPA: game will no longer crash on assertion failure + +### 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: +* 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: +* Local mods can be disabled or uninstalled +* Fixed styling of Launcher interface + +### MAP EDITOR: +* Fixed saving of roads and rivers +* Fixed placement of heroes on map + +# 1.0.0 -> 1.1.0 + +### 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 +* LUA/ERM libs are not compiled by default +* FFMpeg dependency is optional now +* Conan package manager is supported for MacOS and iOS + +### 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 +* Host can control game session by using console commands +* Control over player is transferred to AI if client escaped the game +* Reconnection mode for crashed client processes +* Playing online is available using proxy server + +### ADVENTURE MAP: +* Fix for digging while opponent's turn +* Supported right click for quick recruit window +* Fixed problem with quests are requiring identical artefacts +* Bulk move and swap artifacts +* Pause & resume for towns and terrains music themes +* 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 missing artifact slot in Artifact Merchant window + +### 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: +* 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: +* Speedup generation of random maps +* Necromancy cannot be learned in Witch Hut on random maps + +### MODS: +* Supported rewardable objects customization +* Battleground obstacles are extendable now with VLC mechanism +* Introduced "compatibility" section into mods settings +* Fixed bonus system for custom advmap spells +* Supported customisable town entrance placement +* Fixed validation of mods with new adventure map objects + +### 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: +* Basic version of Qt-based map editor + +# 0.99 -> 1.0.0 + +### 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 +* Skyship Grail works now immediately after capturing without battle +* 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 + +### 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: +* Implemented cumulative effects for spells + +### 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 +* Added bonus updaters for hero specialties +* Added allOf, anyOf and noneOf qualifiers for bonus limiters +* 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 + +### SOUND: +* Fixed many mising 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 supports water modes (normal, islands) +* Added config randomMap.json with settings for map generator +* Added parameter for template allowedWaterContent +* Extra resource packs appear nearby mines +* Underground can be player starting place for factions allowed to be placed underground +* Improved obstacles placement aesthetics +* Rivers are generated on the random maps +* RMG works more stable, various crashes have been fixed +* Treasures requiring guards are guaranteed to be protected + +### 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 +* AI can gather and save resources on purpose +* AI will only buy army on demand instead of every turn +* AI can distinguish the value of all map objects +* General speed optimizations + +### 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: +* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes +* Fix: Captured town should not be duplicated on the UI + +### LAUNCHER: +* Implemented notifications about updates +* Supported redirection links for downloading mods + +# 0.98 -> 0.99 + +### 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 +* Implemented rumors in tavern window +* New cheat codes: +* * 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 + +### 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: +* 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: +* 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 +* Added Thieves Guild random object (1 per zone) +* 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 + +### GENERAL: +* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) + +### 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: +* Implemented world view +* Added graphical fading effects + +### SPELLS: +* New spells handled: +* * Earthquake +* * View Air +* * View Earth +* * Visions +* * Disguise +* Implemented CURE spell negative dispell effect +* Added LOCATION target for spells castable on any hex with new target modifiers + +### BATTLES: +* Implemented OH3 stack split / upgrade formulas according to AlexSpl + +### 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) + +### 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 +* Multiplayer load support was implemented through command-line options + +### ADVENTURE AI: +* Significantly optimized execution time, AI should be much faster now. + +### 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: +* 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 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: +* Implemented switch to disable intro movies in game + +# 0.95 -> 0.96 (Jul 01 2014) + +### GENERAL: +* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 + +### ADVENTURE AI: +* Optimized speed and removed various bottlenecks. + +### ADVENTURE MAP: +* Heroes auto-level primary and secondary skill levels according to experience + +### BATTLES: +* Wall hit/miss sound will be played when using catapult during siege + +### SPELLS: +* New configuration format + +### 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) + +### 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: +* Added configuring of heroes quantity per player allowed in game + +# 0.94 -> 0.95 (Mar 01 2014) + +### 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 +* 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: +* 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. +* Significantly improved exploration algorithm. +* Locked heroes now try to decompose their goals exhaustively. +* Fixed (common) issue when AI found neutral stacks infinitely strong. +* Improvements for army exchange criteria. +* GatherArmy may include building dwellings in town (experimental). +* 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: +* 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 + +### TOWNS: +* Extended building dependencies support + +### 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) + +### 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 +* command "def2bmp" to convert def into set of frames. +* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus) +* fixed duels, added no-GUI mode for automatic AI testing +* Sir Mullich is available at the start of the game +* Upgrade cost will never be negative. +* support for Chinese fonts (GBK 2-byte encoding) + +### 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: +* autofight implemented +* most of the animations is time-based +* simplified postioning of units in battle, should fix remaining issues with unit positioning +* synchronized attack/defence animation +* spell animation speed uses game settings +* fixed disrupting ray duration +* added logging domain for battle animations +* Fixed crashes on Land Mines / Fire Wall casting. +* UI will be correctly greyed-out during opponent turn +* fixed remaining issues with blit order +* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3) +* Fixed Remove Obstacle. +* defeating hero will yield 500 XP +* Added lots of missing spell immunities from Strategija +* Added stone gaze immunity for Troglodytes (did you know about it?) +* damage done by turrets is properly increased by built buldings +* Wyverns will cast Poison instead of Stone Gaze. + +### TOWN: +* Fixed issue that allowed to build multiple boats in town. +* fix for lookout tower + +# 0.92 -> 0.93 (Jun 01 2013) + +### GENERAL: +* Support for SoD-only installations, WoG becomes optional addition +* New logging framework +* Negative luck support, disabled by default +* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack) +* Fixed stack artifact (and related buttons) not displaying in creature window. +* Fixed crash at month of double population. + +### 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) +* 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: +* Fixed Death Stare of Commanders +* Projectile blitting should be closer to original H3. But still not perfect. +* Fixed missing Mirth effects +* Stack affected by Berserk should not try to attack itself +* Fixed several cases of incorrect positioning of creatures in battles +* Fixed abilities of Efreet. +* Fixed broken again palette in some battle backgrounds + +### 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) + +### 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: +* 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) + +### GENERAL: +* hero crossover between missions in campaigns +* introduction before missions in campaigns + +### MODS: +* Added CREATURE_SPELL_POWER for commanders +* 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 +* Drain Life now has % effect depending on bonus value +* 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. +* support for string subtype id in short bonus format +* primary skill identifiers for bonuses + +# 0.9 -> 0.91 (Feb 01 2013) + +### 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: +* 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 +* 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 + +### BATTLES: +* Support for "enchanted" WoG ability + +### 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) + +### 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. + +### BATTLES: +* New graphics for Stack Queue +* Death Stare works identically to H3 +* No explosion when catapult fails to damage the wall +* Fixed crash when attacking stack dies before counterattack +* 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. + +### 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. +* AI can now exchange armies between heroes. By default, it will pass army to main hero. +* Fixed strange case when AI found allied town extremely dangerous +* Fixed crash when AI tried to "revisit" a Boat +* 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. +* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells + +# 0.88 -> 0.89 (Jun 01 2012) + +### GENERAL: +* Mostly implemented Commanders feature (missing level-up dialog) +* Support for stack artifacts +* New creature window graphics contributed by fishkebab +* Config file may have multiple upgrades for creatures +* CTRL+T will open marketplace window +* G will open thieves guild window if player owns at least one town with tavern +* Implemented restart functionality. CTRL+R will trigger a quick restart +* Save game screen and returning to main menu will work if game was started with --start option +* Simple mechanism for detecting game desynchronization after init +* 1280x800 resolution graphics, contributed by Topas + +### ADVENTURE MAP: +* Fixed monsters regenerating casualties from battle at the start of new week. +* T in adventure map will switch to next town + +### 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. +* Fixed crash when death stare or acid breath activated on stack that was just killed +* First aid tent can heal only creatures that suffered damage +* War machines can't be healed by tent +* Creatures casting spells won't try to cast them during tactic phase +* Console tooltips for first aid tent +* Console tooltips for teleport spell +* Cursor is reset to pointer when action is requested +* Fixed a few other missing or wrong tooltips/cursors +* Implemented opening creature window by l-clicking on stack +* Fixed crash on attacking walls with Cyclop Kings +* Fixed and simplified Teleport casting +* Fixed Remove Obstacle spell +* New spells supported: +* * Chain Lightning +* * Fire Wall +* * Force Field +* * Land Mine +* * Quicksands +* * Sacrifice + +### TOWNS: +* T in castle window will open a tavern window (if available) + +### PREGAME: +* Pregame will use same resolution as main game +* Support for scaling background image +* Customization of graphics with config file. + +### ADVENTURE AI: +* basic rule system for threat evaluation +* new town development logic +* AI can now use external dwellings +* AI will weekly revisit dwellings & mills +* AI will now always pick best stacks from towns +* AI will recruit multiple heroes for exploration +* AI won't try attacking its own heroes + +# 0.87 -> 0.88 (Mar 01 2012) + +* added an initial version of new adventure AI: VCAI +* system settings window allows to change default resolution +* introduced unified JSON-based settings system +* fixed all known localization issues +* Creature Window can handle descriptions of spellcasting abilities +* Support for the clone spell + +# 0.86 -> 0.87 (Dec 01 2011) + +### GENERAL: +* Pathfinder can find way using ships and subterranean gates +* Hero reminder & sleep button + +### PREGAME: +* Credits are implemented + +### 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 + +# 0.85 -> 0.86 (Sep 01 2011) + +### 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 + +### 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 +* New WoG abilities supported +* * Defense Bonus +* * Cast before attack +* * Immunity to direct damage spells +* New spells supported +* * Magic Mirror +* * Titan's Lightning Bolt + +# 0.84 -> 0.85 (Jun 01 2011) + +### GENERAL: +* Support for stack experience +* Implemented original campaign selection screens +* New artifacts supported: +* * Statesman's Medal +* * Diplomat's Ring +* * Ambassador's Sash + +### TOWNS: +* Implemented animation for new town buildings +* It's possible to sell artifacts at Artifact Merchants + +### 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) + +### 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 + +### BATTLES: +* Better animations handling +* Defensive stance is supported + +### HERO: +* New secondary skills supported: +* * Artillery +* * Eagle Eye +* * Tactics + +### AI PLAYER: +* new AI leading neutral creatures in combat, slightly better then previous + +# 0.82 -> 0.83 (Nov 01 2010) + +### 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 +* Town Portal supported +* Vial of Dragon Blood and Statue of Legion supported + +### HERO: +* remaining specialities have been implemented + +### TOWNS: +* town events supported +* Support for new town structures: Deiety of Fire and Escape Tunnel + +### BATTLES: +* blocked retreating from castle + +# 0.81 -> 0.82 (Aug 01 2010) + +### 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 + +### ADVENTURE MAP: +* Neutral armies growth implemented (10% weekly) +* Power rating of neutral stacks +* Favourable Winds reduce sailing cost + +### 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) + +### TOWNS: +* Support for new town structures: +* * Artifact Merchant +* * Aurora Borealis +* * Castle Gates +* * Magic University +* * Portal of Summoning +* * Skeleton transformer +* * Veil of Darkness + +### 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 + +# 0.8 -> 0.81 (Jun 01 2010) + +### 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 + +### ADVENTURE MAP: +* Creatures now guard surrounding tiles +* New adventura map spells supported: +* * Summon Boat +* * Scuttle Boat +* * Dimension Door +* * Fly +* * Water walk + +### BATTLES: +* A number of new creature abilities supported +* First Aid Tent is functional +* Support for distance/wall/melee penalties & no * penalty abilities +* Reworked damage calculation to fit OH3 formula better +* Luck support +* Teleportation spell + +### HERO: +* First Aid secondary skill +* Improved formula for necromancy to match better OH3 + +### TOWNS: +* Sending resources to other players by marketplace +* Support for new town structures: +* * Lighthouse +* * Colossus +* * Freelancer's Guild +* * Guardian Spirit +* * Necromancy Amplifier +* * Soul Prison + +### OBJECTS: +* New object supported: +* * Freelancer's Guild +* * Trading Post +* * War Machine Factory + +# 0.75 -> 0.8 (Mar 01 2010) + +### 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: +* Implemented rivers animations (thx to GrayFace). + +### BATTLES: +* Fire Shield spell (and creature ability) supported +* affecting morale/luck and casting spell after attack creature abilities supported + +### HERO: +* Implementation of Scholar secondary skill + +### 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 + +### OBJECTS: +* New objects supported: +* * 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) + +### 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: +* 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. + +### TOWNS: +* new special town structures supported: +* * Academy of Battle Scholars +* * Cage of Warlords +* * Mana Vortex +* * Stables +* * Skyship (revealing entire map only) + +### 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. + +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) + +### GENERAL: +* Scenario Information window +* Save Game window +* VCMI window should start centered +* support for Necromancy and Ballistics secondary skills +* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining) +* VCMI client has its own icon (thx for graphic to Dikamilo) +* Ellipsis won't be split when breaking text on several lines +* split button will be grayed out when no creature is selected +* 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 +* moving hero with arrow keys / numpad +* fixed Next Hero button behaviour +* fixed Surface/Underground switch button in higher resolutions + +### 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) +* more creatures special abilities supported +* battle settings will be stored +* fixed crashes occurring on attacking two hex creatures from back +* 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 +* 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. +* 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 + +### 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 + +### HERO WINDOW: +* war machines cannot be unequiped + +### 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 +* subtitles for chosen hero/town/bonus in pregame + +### 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 + +# 0.72 -> 0.73 (Aug 01 2009) + +### 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. +* 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 +* giving starting spells for heroes +* pressing enter or escape close spellbook +* removed redundant quotation marks from skills description and artifact events texts +* disabled autosaving on first turn +* bonuses from bonus artifacts +* increased char per line limit for subtitles under components +* corrected some exp/level values +* primary skills cannot be negative +* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity +* fixed timed events reappearing +* saving system options +* saving hero direction +* r-click popups on enemy heroes and towns +* hero leveling formula matches the H3 + +### 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: +* spell resistance supported (secondary skill, artifacts, creature skill) +* corrected damage inflicted by spells and ballista +* added some missing projectile infos +* added native terrain bonuses in battles +* number of units in stack in battle should better fit the box +* 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 + +### 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: +* garrisoned heroes won't be shown on the list +* artifacts will be present on morale/luck bonuses list + +### PREGAME: +* saves are sorted primary by map format, secondary by name +* fixed displaying date of saved game (uses local time, removed square character) + +### 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 + +# 0.71 -> 0.72 (Jun 1 2009) + +### GENERAL: +* many sound effects and music +* autosave (to 5 subsequent files) +* artifacts support (most of them) +* added internal game console (activated on TAB) +* fixed 8 hero limit to check only for wandering heroes (not garrisoned) +* improved randomization +* fixed crash on closing application +* VCMI won't always give all three stacks in the starting armies +* fix for drawing starting army creatures count +* 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 +* no income at the first day + +### 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 +* clicking scrolling arrows in town/hero list won't open town/hero window +* 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: +* many creatures special skills implemented +* battle will end when one side has only war machines +* fixed some problems with handling obstacles info +* fixed bug with defending / waiting while no stack is active +* spellbook button is inactive when hero cannot cast any spell +* obstacles will be placed more properly when resolution is different than 800x600 +* 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 + +### 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: +* 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: +* clicking on "advanced options" a second time now closes the tab instead of refreshing it. +* 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: +* 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 +* support for School of Magic +* support for School of War +* support for Pillar of Fire +* support for Corpse +* support for Lean To +* support for Wagon +* support for Warrior's Tomb +* support for Event +* Corpse (Skeleton) will be accessible from all directions + +# 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 +* 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 problems with 1024x600 screen resolution +* 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: +* added "Next hero" button functionality +* added missing path arrows +* 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 +* 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 +* provisional support for "Save" button in System Options Window +* it's possible to revisit object under hero by pressing Space + +### 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 +* spell books won't be placed in War Machine slots after battle +* 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 +* standing stacks have more static animation +* probably fixed problem with displaying corpses on battlefield +* fixes for two-hex creatures actions +* fixed hero casting spell animation +* corrected stack death animation +* battle settings will be remembered between battles +* improved damage calculation formula +* 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 +* corrected hero spell casting animation +* better support for battle backgrounds +* blocked "save" command during battle +* spellbook displays only spells known by Hero +* New spells supported: +* * Mirth +* * Sorrow +* * Fortune +* * Misfortune + +### TOWN INTERFACE: +* cannot build more than one capitol +* cannot build shipyard if town is not near water +* 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 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: +* added scrolling scenario list with mouse wheel +* fixed mouse slow downs +* 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 + +### AI PLAYER: +* Minor improvements and fixes. + +# 0.64 -> 0.7 (Feb 01 2009) + +### 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) +* /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 + +### 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: +* 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: +* support for the Wait command +* magic arrow *really* works +* war machines support partially added +* queue of stacks narrowed +* spell effect animation displaying improvements +* 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 +* 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 + +### AI PLAYER: +* Genius AI (first VCMI AI) will control computer creatures during the combat. + +### OBJECTS: +* Guardians property for resources is handled +* support for Witch Hut +* support for Arena +* support for Library of Enlightenment + +And a lot of minor fixes + +# 0.63 -> 0.64 (Nov 01 2008) + +### GENERAL: +* sprites from /Sprites folder are handled correctly +* several fixes for pathfinder and path arrows +* better handling disposed/predefined heroes +* heroes regain 1 mana point each turn +* support for mistycisim and intelligence skills +* hero hiring possible +* added support for a number of hotkeys +* it's not possible anymore to leave hero level-up window without selecting secondary skill +* 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 + +### 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: +* partially done tavern window (only hero hiring functionality) + +### BATTLES: +* water elemental will really be treated as 2 hex creature +* potential infinite loop in reverseCreature removed +* 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 + +# 0.62 -> 0.63 (Oct 01 2008) + +### 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 +* partially done spellbook +* Alt+F4 quits the game +* 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 + +### BATTLES: +* very significant optimization of battles +* battle summary window +* 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 +* 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 +* hero's primary skills accounted for damage dealt by creatures in battle + +### TOWNS: +* mostly done marketplace +* fixed crashbug with battles on swamps and rough terrain +* 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: +* setting army formation +* tooltips for artifacts in backpack + +### ADVENTURE INTERFACE: +* fixed bug with disappearing head of a hero in adventure map +* 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.) +* artifacts we pick up go to the appropriate slot (if free) + +# 0.61 -> 0.62 (Sep 01 2008) + +### GENERAL: +* restructured to the server-client model +* support for heroes placed in towns +* upgrading creatures +* working gaining levels for heroes (including dialog with skill selection) +* added graphical cursor +* showing creature amount in the creature info window +* giving starting bonus + +### 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) +* fixed reading forbidden structures +* randomizing spells in towns +* viewing hero window in the town screen +* possibility of moving hero into the garrison +* 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: +* hopefully fixed problems with wrong town defs (village/fort/capitol) + +### HERO WINDOW: +* bugfix: splitting stacks works in hero window +* removed bug causing significant increase of CPU consumption + +### BATTLES: +* shooting +* removed some displaying problems +* showing last group of frames in creature animation won't crash +* added start moving and end moving animations +* fixed moving two-hex creatures +* showing/hiding graphic cursor +* a part of using graphic cursor +* slightly optimized showing of battle interface +* animation of getting hit / death by shooting is displayed when it should be +* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. +* minor optimizations + +### 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: +* support for the Tree of knowledge +* support for Campfires +* added event message when picking artifact + +# 0.6 -> 0.61 (Jun 15 2008) + +### IMPROVEMENTS: +* improved attacking in the battles +* it's possible to kill hostile stack +* animations won't go in the same phase +* Better pathfinder +* "%s" substitutions in Right-click information in town hall +* windmill won't give wood +* hover text for heroes +* support for ZSoft-style PCX files in /Data +* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved +* in the townlist in castle selected town will by placed on the 2nd place (not 3rd) +* stack at the limit of unit's range can now be attacked +* range of unit is now properly displayed +* battle log is scrolled down when new event occurs +* console is closed when application exits + +### 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 +* VCMI won't crash when r-click neutral stack during the battle +* water won't blink behind shipyard in the Castle +* fixed several memory leaks +* 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) + +* partially done attacking in battles +* screen isn't now refreshed while blitting creature info window +* r-click creature info windows in battles +* no more division by 0 in slider +* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working) +* fixed estate problems +* fixed blinking mana vortex +* grail increases creature growths +* new pathfinder +* several minor improvements + +# 0.58 -> 0.59 (May 24 2008 - closed, test release) + +* fixed memory leak in battles +* blitting creature animations to rects in the recruitment window +* fixed wrong creatures def names +* better battle pathfinder and unit reversing +* improved slider ( #58 ) +* fixed problems with horde buildings (won't block original dwellings) +* giving primary skill when hero get level (but there is still no dialog) +* if an upgraded creature is available it'll be shown as the first in a recruitment window +* creature levels not messed in Fortress +* war machines are added to the hero's inventory, not to the garrison +* support for H3-style PCX graphics in Data/ +* VCMI won't crash when is unable to initialize audio system +* fixed displaying wrong town defs +* improvements in recruitment window (slider won't allow to select more creatures than we can afford) +* creature info window (only r-click) +* callback for buttons/lists based on boost::function +* a lot of minor improvements + +# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) + +### TOWNS: +* recruiting creatures +* working creature growths (including castle and horde building influences) +* towns give income +* town hall screen +* building buildings (requirements and cost are handled) +* hints for structures +* updating town infobox + +### GARRISONS: +* merging stacks +* splitting stacks + +### 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: +* a number of minor fixes and improvements + +# 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 +* fix of Pandora's Box handling +* reading disposed/predefined heroes +* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder. +* more detailed logs +* reported problems with hero flags resolved +* heroes cannot occupy the same tile +* hints for most of creature generators +* some minor stuff + +# 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 +* giving random hero on RoE maps +* improved protection against hero duplication +* fixed starting values of primary abilities of random heroes on RoE/AB maps +* right click popups with infoboxes for heroes/towns lists +* new interface coloring (many thanks to GrayFace ;]) +* fixed bug in object flag's coloring +* added hints in town lists +* eliminated square from city hints + +# 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): + +* hopefully the last bugfix of Pandora's Box +* fixed blockmaps of generated heroes +* disposed hero cannot be chosen in scenario settings (unless he is in prison) +* fixed town randomization +* fixed hero randomization +* fixed displaying heroes in preGame +* fixed selecting/deselecting artifact slots in hero window +* much faster pathfinder +* memory usage and load time significantly decreased +* it's impossible to select empty artifact slot in hero window +* fixed problem with FoW displaying on minimap on L-sized maps +* fixed crashbug in hero list connected with heroes dismissing +* mostly done town infobox +* town daily income is properly calculated + +# 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] improved randomization +* [bugfix] pathfinder can't be cheated (what caused errors) + +# 0.5 - 0.51 (Feb 3 2008): + +* close button properly closes (same does 'q' key) +* two players can't have selected same hero +* double click on "Show Available Scenarios" won't reset options +* fixed possible crashbug in town/hero lists +* fixed crashbug in initializing game caused by wrong prisons handling +* fixed crashbug on reading hero's custom artifacts in RoE maps +* fixed crashbug on reading custom Pandora's Box in RoE maps +* fixed crashbug on reading blank Quest Guards +* 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): + +* Main menu and New game screens +* Scenario selection, part of advanced options support +* Partially done adventure map, town and hero interfaces +* Moving hero +* Interactions with several objects (mines, resources, mills, and others) diff --git a/Global.h b/Global.h index 3c308f54c..e3987bbbe 100644 --- a/Global.h +++ b/Global.h @@ -1,673 +1,692 @@ -/* - * Global.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 - -/* ---------------------------------------------------------------------------- */ -/* Compiler detection */ -/* ---------------------------------------------------------------------------- */ -// Fixed width bool data type is important for serialization -static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); - -/* ---------------------------------------------------------------------------- */ -/* System detection. */ -/* ---------------------------------------------------------------------------- */ -// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/ -// and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor -// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h) -#ifdef _WIN16 // Defined for 16-bit environments -# error "16-bit Windows isn't supported" -#elif defined(_WIN64) // Defined for 64-bit environments -# define VCMI_WINDOWS -# define VCMI_WINDOWS_64 -#elif defined(_WIN32) // Defined for both 32-bit and 64-bit environments -# define VCMI_WINDOWS -# define VCMI_WINDOWS_32 -#elif defined(_WIN32_WCE) -# error "Windows CE isn't supported" -#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux) -# define VCMI_UNIX -# define VCMI_XDG -# if defined(__ANDROID__) || defined(ANDROID) -# define VCMI_ANDROID -# endif -#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_FREEBSD -#elif defined(__HAIKU__) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_HAIKU -#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__)) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_HURD -#elif defined(__APPLE__) && defined(__MACH__) -# define VCMI_UNIX -# define VCMI_APPLE -# include "TargetConditionals.h" -# if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR -# define VCMI_IOS -# define VCMI_IOS_SIM -# elif TARGET_OS_IPHONE -# define VCMI_IOS -# elif TARGET_OS_MAC -# define VCMI_MAC -# else -//# warning "Unknown Apple target."? -# endif -#else -# error "This platform isn't supported" -#endif - -#if defined(VCMI_ANDROID) || defined(VCMI_IOS) -#define VCMI_MOBILE -#endif - -/* ---------------------------------------------------------------------------- */ -/* Commonly used C++, Boost headers */ -/* ---------------------------------------------------------------------------- */ -#ifdef VCMI_WINDOWS -# ifndef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers - delete this line if something is missing. -# endif -# ifndef NOMINMAX -# define NOMINMAX // Exclude min/max macros from . Use std::[min/max] from instead. -# endif -# ifndef _NO_W32_PSEUDO_MODIFIERS -# define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux. -# endif -#endif - -/* ---------------------------------------------------------------------------- */ -/* A macro to force inlining some of our functions */ -/* ---------------------------------------------------------------------------- */ -// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower -#ifdef _MSC_VER -# define STRONG_INLINE __forceinline -#elif __GNUC__ -# define STRONG_INLINE inline __attribute__((always_inline)) -#else -# define STRONG_INLINE inline -#endif - -#define _USE_MATH_DEFINES - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//The only available version is 3, as of Boost 1.50 -#include - -#define BOOST_FILESYSTEM_VERSION 3 -#if BOOST_VERSION > 105000 -# define BOOST_THREAD_VERSION 3 -#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 -#define BOOST_BIND_NO_PLACEHOLDERS - -#if BOOST_VERSION >= 106600 -#define BOOST_ASIO_ENABLE_OLD_SERVICES -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef VCMI_WINDOWS -#include -#endif -#include -#include -#include -#include -#include -#include - -#ifndef M_PI -# define M_PI 3.14159265358979323846 -#endif - -/* ---------------------------------------------------------------------------- */ -/* Usings */ -/* ---------------------------------------------------------------------------- */ -using namespace std::placeholders; -namespace range = boost::range; - -/* ---------------------------------------------------------------------------- */ -/* Typedefs */ -/* ---------------------------------------------------------------------------- */ -// Integral data types -typedef uint64_t ui64; //unsigned int 64 bits (8 bytes) -typedef uint32_t ui32; //unsigned int 32 bits (4 bytes) -typedef uint16_t ui16; //unsigned int 16 bits (2 bytes) -typedef uint8_t ui8; //unsigned int 8 bits (1 byte) -typedef int64_t si64; //signed int 64 bits (8 bytes) -typedef int32_t si32; //signed int 32 bits (4 bytes) -typedef int16_t si16; //signed int 16 bits (2 bytes) -typedef int8_t si8; //signed int 8 bits (1 byte) - -// Lock typedefs -using TLockGuard = std::lock_guard; -using TLockGuardRec = std::lock_guard; - -/* ---------------------------------------------------------------------------- */ -/* Macros */ -/* ---------------------------------------------------------------------------- */ -// Import + Export macro declarations -#ifdef VCMI_WINDOWS -#ifdef VCMI_DLL_STATIC -# define DLL_IMPORT -# define DLL_EXPORT -#elif defined(__GNUC__) -# define DLL_IMPORT __attribute__((dllimport)) -# define DLL_EXPORT __attribute__((dllexport)) -# else -# define DLL_IMPORT __declspec(dllimport) -# define DLL_EXPORT __declspec(dllexport) -# endif -# define ELF_VISIBILITY -#else -# ifdef __GNUC__ -# define DLL_IMPORT __attribute__ ((visibility("default"))) -# define DLL_EXPORT __attribute__ ((visibility("default"))) -# define ELF_VISIBILITY __attribute__ ((visibility("default"))) -# endif -#endif - -#ifdef VCMI_DLL -# define DLL_LINKAGE DLL_EXPORT -#else -# define DLL_LINKAGE DLL_IMPORT -#endif - -#define THROW_FORMAT(message, formatting_elems) throw std::runtime_error(boost::str(boost::format(message) % formatting_elems)) - -// old iOS SDKs compatibility -#ifdef VCMI_IOS -#include - -#ifndef __IPHONE_13_0 -#define __IPHONE_13_0 130000 -#endif -#endif // VCMI_IOS - -// single-process build makes 2 copies of the main lib by wrapping it in a namespace -#ifdef VCMI_LIB_NAMESPACE -#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE { -#define VCMI_LIB_NAMESPACE_END } -#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE; -#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x -#else -#define VCMI_LIB_NAMESPACE_BEGIN -#define VCMI_LIB_NAMESPACE_END -#define VCMI_LIB_USING_NAMESPACE -#define VCMI_LIB_WRAP_NAMESPACE(x) ::x -#endif - -/* ---------------------------------------------------------------------------- */ -/* VCMI standard library */ -/* ---------------------------------------------------------------------------- */ -#include - -VCMI_LIB_NAMESPACE_BEGIN - -namespace vstd -{ - // combine hashes. Present in boost but not in std - template - inline void hash_combine(std::size_t& seed, const T& v) - { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); - } - - //returns true if container c contains item i - template - bool contains(const Container & c, const Item &i) - { - return std::find(std::begin(c), std::end(c),i) != std::end(c); - } - - //returns true if container c contains item i - template - bool contains_if(const Container & c, Pred p) - { - return std::find_if(std::begin(c), std::end(c), p) != std::end(c); - } - - //returns true if map c contains item i - template - bool contains(const std::map & c, const Item2 &i) - { - return c.find(i)!=c.end(); - } - - //returns true if unordered set c contains item i - template - bool contains(const std::unordered_set & c, const Item &i) - { - return c.find(i)!=c.end(); - } - - template - bool contains(const std::unordered_map & c, const Item2 &i) - { - return c.find(i)!=c.end(); - } - - //returns position of first element in vector c equal to s, if there is no such element, -1 is returned - template - int find_pos(const Container & c, const T2 &s) - { - int i=0; - for (auto iter = std::begin(c); iter != std::end(c); iter++, i++) - if(*iter == s) - return i; - return -1; - } - - //Func f tells if element matches - template - int find_pos_if(const Container & c, const Func &f) - { - auto ret = boost::range::find_if(c, f); - if(ret != std::end(c)) - return std::distance(std::begin(c), ret); - - return -1; - } - - //returns iterator to the given element if present in container, end() if not - template - typename Container::iterator find(Container & c, const Item &i) - { - return std::find(c.begin(),c.end(),i); - } - - //returns const iterator to the given element if present in container, end() if not - template - typename Container::const_iterator find(const Container & c, const Item &i) - { - return std::find(c.begin(),c.end(),i); - } - - //returns first key that maps to given value if present, returns success via found if provided - template - Key findKey(const std::map & map, const T & value, bool * found = nullptr) - { - for(auto iter = map.cbegin(); iter != map.cend(); iter++) - { - if(iter->second == value) - { - if(found) - *found = true; - return iter->first; - } - } - if(found) - *found = false; - return Key(); - } - - //removes element i from container c, returns false if c does not contain i - template - typename Container::size_type operator-=(Container &c, const Item &i) - { - typename Container::iterator itr = find(c,i); - if(itr == c.end()) - return false; - c.erase(itr); - return true; - } - - //assigns greater of (a, b) to a and returns maximum of (a, b) - template - t1 &amax(t1 &a, const t2 &b) - { - if(a >= (t1)b) - return a; - else - { - a = t1(b); - return a; - } - } - - //assigns smaller of (a, b) to a and returns minimum of (a, b) - template - t1 &amin(t1 &a, const t2 &b) - { - if(a <= (t1)b) - return a; - else - { - a = t1(b); - return a; - } - } - - //makes a to fit the range - template - void abetween(T &value, const T &min, const T &max) - { - value = std::clamp(value, min, max); - } - - //checks if a is between b and c - template - bool isbetween(const t1 &value, const t2 &min, const t3 &max) - { - return value > (t1)min && value < (t1)max; - } - - //checks if a is within b and c - template - bool iswithin(const t1 &value, const t2 &min, const t3 &max) - { - return value >= (t1)min && value <= (t1)max; - } - - template - struct assigner - { - public: - t1 &op1; - t2 op2; - assigner(t1 &a1, const t2 & a2) - :op1(a1), op2(a2) - {} - void operator()() - { - op1 = op2; - } - }; - - //deleted pointer and sets it to nullptr - template - void clear_pointer(T* &ptr) - { - delete ptr; - ptr = nullptr; - } - - template - typename Container::const_reference circularAt(const Container &r, size_t index) - { - assert(r.size()); - index %= r.size(); - auto itr = std::begin(r); - std::advance(itr, index); - return *itr; - } - - template - void erase(Container &c, const Item &item) - { - c.erase(boost::remove(c, item), c.end()); - } - - template - void erase_if(Range &vec, Predicate pred) - { - vec.erase(boost::remove_if(vec, pred),vec.end()); - } - - template - void erase_if(std::set &setContainer, Predicate pred) - { - auto itr = setContainer.begin(); - auto endItr = setContainer.end(); - while(itr != endItr) - { - auto tmpItr = itr++; - if(pred(*tmpItr)) - setContainer.erase(tmpItr); - } - } - - //works for map and std::map, maybe something else - template - void erase_if(std::map &container, Predicate pred) - { - auto itr = container.begin(); - auto endItr = container.end(); - while(itr != endItr) - { - auto tmpItr = itr++; - if(pred(*tmpItr)) - container.erase(tmpItr); - } - } - - template - OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred) - { - return std::copy_if(std::cbegin(input), std::end(input), result, pred); - } - - template - std::insert_iterator set_inserter(Container &c) - { - return std::inserter(c, c.end()); - } - - //Returns iterator to the element for which the value of ValueFunction is minimal - template - auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) - { - /* Clang crashes when instantiating this function template and having PCH compilation enabled. - * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 - * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype - * directly for both function parameters. - */ - return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool - { - return vf(lhs) < vf(rhs); - }); - } - - //Returns iterator to the element for which the value of ValueFunction is maximal - template - auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) - { - /* Clang crashes when instantiating this function template and having PCH compilation enabled. - * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 - * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype - * directly for both function parameters. - */ - return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool - { - return vf(lhs) < vf(rhs); - }); - } - - /// Increments value by specific delta - /// similar to std::next but works with other types, e.g. enum class - template - T next(const T &obj, int change) - { - return static_cast(static_cast(obj) + change); - } - - template - typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers) - { - if(c.size()) - return c.back(); - else - return typename Container::value_type(); - } - - template - typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers) - { - if(c.size()) - return c.front(); - else - return nullptr; - } - - template - bool isValidIndex(const Container &c, Index i) - { - return i >= 0 && i < c.size(); - } - - template - typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue) - { - if(index < r.size()) - return r[index]; - - return defaultValue; - } - - template - bool erase_if_present(Container &c, const Item &item) - { - auto i = std::find(c.begin(), c.end(), item); - if (i != c.end()) - { - c.erase(i); - return true; - } - - return false; - } - - template - bool erase_if_present(std::map & c, const Item2 &item) - { - auto i = c.find(item); - if (i != c.end()) - { - c.erase(i); - return true; - } - return false; - } - - template - void removeDuplicates(std::vector &vec) - { - std::sort(vec.begin(), vec.end()); - vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); - } - - template - void concatenate(std::vector &dest, const std::vector &src) - { - dest.reserve(dest.size() + src.size()); - dest.insert(dest.end(), src.begin(), src.end()); - } - - template - std::vector intersection(std::vector &v1, std::vector &v2) - { - std::vector v3; - std::sort(v1.begin(), v1.end()); - std::sort(v2.begin(), v2.end()); - std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3)); - return v3; - } - - template - std::set difference(const std::set &s1, const std::set s2) - { - std::set s3; - std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end())); - return s3; - } - - template - bool containsMapping(const std::multimap & map, const std::pair & mapping) - { - auto range = map.equal_range(mapping.first); - for(auto contained = range.first; contained != range.second; contained++) - { - if(mapping.second == contained->second) - return true; - } - return false; - } - - template - typename M::mapped_type & getOrCompute(M & m, const Key & k, F f) - { - typedef typename M::mapped_type V; - - std::pair r = m.insert(typename M::value_type(k, V())); - V & v = r.first->second; - - if(r.second) - f(v); - - return v; - } - - //c++20 feature - template - Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f) - { - return a + (b - a) * f; - } - - ///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr - static constexpr int abs(int i) { - if(i < 0) return -i; - return i; - } - - ///C++23 - template< class Enum > constexpr std::underlying_type_t to_underlying( Enum e ) noexcept - { - return static_cast>(e); - } -} -using vstd::operator-=; - -VCMI_LIB_NAMESPACE_END +/* + * Global.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 + +/* ---------------------------------------------------------------------------- */ +/* Compiler detection */ +/* ---------------------------------------------------------------------------- */ +// Fixed width bool data type is important for serialization +static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); + +/* ---------------------------------------------------------------------------- */ +/* System detection. */ +/* ---------------------------------------------------------------------------- */ +// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/ +// and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor +// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h) +#ifdef _WIN16 // Defined for 16-bit environments +# error "16-bit Windows isn't supported" +#elif defined(_WIN64) // Defined for 64-bit environments +# define VCMI_WINDOWS +# define VCMI_WINDOWS_64 +#elif defined(_WIN32) // Defined for both 32-bit and 64-bit environments +# define VCMI_WINDOWS +# define VCMI_WINDOWS_32 +#elif defined(_WIN32_WCE) +# error "Windows CE isn't supported" +#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux) +# define VCMI_UNIX +# define VCMI_XDG +# if defined(__ANDROID__) || defined(ANDROID) +# define VCMI_ANDROID +# endif +#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_FREEBSD +#elif defined(__HAIKU__) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_HAIKU +#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__)) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_HURD +#elif defined(__APPLE__) && defined(__MACH__) +# define VCMI_UNIX +# define VCMI_APPLE +# include "TargetConditionals.h" +# if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR +# define VCMI_IOS +# define VCMI_IOS_SIM +# elif TARGET_OS_IPHONE +# define VCMI_IOS +# elif TARGET_OS_MAC +# define VCMI_MAC +# else +//# warning "Unknown Apple target."? +# endif +#else +# error "This platform isn't supported" +#endif + +#if defined(VCMI_ANDROID) || defined(VCMI_IOS) +#define VCMI_MOBILE +#endif + +/* ---------------------------------------------------------------------------- */ +/* Commonly used C++, Boost headers */ +/* ---------------------------------------------------------------------------- */ +#ifdef VCMI_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers - delete this line if something is missing. +# endif +# ifndef NOMINMAX +# define NOMINMAX // Exclude min/max macros from . Use std::[min/max] from instead. +# endif +# ifndef _NO_W32_PSEUDO_MODIFIERS +# define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux. +# endif +#endif + +/* ---------------------------------------------------------------------------- */ +/* A macro to force inlining some of our functions */ +/* ---------------------------------------------------------------------------- */ +// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower +#ifdef _MSC_VER +# define STRONG_INLINE __forceinline +#elif __GNUC__ +# define STRONG_INLINE inline __attribute__((always_inline)) +#else +# define STRONG_INLINE inline +#endif + +#define _USE_MATH_DEFINES + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//The only available version is 3, as of Boost 1.50 +#include + +#define BOOST_FILESYSTEM_VERSION 3 +#if BOOST_VERSION > 105000 +# define BOOST_THREAD_VERSION 3 +#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 +#define BOOST_BIND_NO_PLACEHOLDERS + +#if BOOST_VERSION >= 106600 +#define BOOST_ASIO_ENABLE_OLD_SERVICES +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef VCMI_WINDOWS +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +/* ---------------------------------------------------------------------------- */ +/* Usings */ +/* ---------------------------------------------------------------------------- */ +using namespace std::placeholders; +namespace range = boost::range; + +/* ---------------------------------------------------------------------------- */ +/* Typedefs */ +/* ---------------------------------------------------------------------------- */ +// Integral data types +typedef uint64_t ui64; //unsigned int 64 bits (8 bytes) +typedef uint32_t ui32; //unsigned int 32 bits (4 bytes) +typedef uint16_t ui16; //unsigned int 16 bits (2 bytes) +typedef uint8_t ui8; //unsigned int 8 bits (1 byte) +typedef int64_t si64; //signed int 64 bits (8 bytes) +typedef int32_t si32; //signed int 32 bits (4 bytes) +typedef int16_t si16; //signed int 16 bits (2 bytes) +typedef int8_t si8; //signed int 8 bits (1 byte) + +// Lock typedefs +using TLockGuard = std::lock_guard; +using TLockGuardRec = std::lock_guard; + +/* ---------------------------------------------------------------------------- */ +/* Macros */ +/* ---------------------------------------------------------------------------- */ +// Import + Export macro declarations +#ifdef VCMI_WINDOWS +#ifdef VCMI_DLL_STATIC +# define DLL_IMPORT +# define DLL_EXPORT +#elif defined(__GNUC__) +# define DLL_IMPORT __attribute__((dllimport)) +# define DLL_EXPORT __attribute__((dllexport)) +# else +# define DLL_IMPORT __declspec(dllimport) +# define DLL_EXPORT __declspec(dllexport) +# endif +# define ELF_VISIBILITY +#else +# ifdef __GNUC__ +# define DLL_IMPORT __attribute__ ((visibility("default"))) +# define DLL_EXPORT __attribute__ ((visibility("default"))) +# define ELF_VISIBILITY __attribute__ ((visibility("default"))) +# endif +#endif + +#ifdef VCMI_DLL +# define DLL_LINKAGE DLL_EXPORT +#else +# define DLL_LINKAGE DLL_IMPORT +#endif + +#define THROW_FORMAT(message, formatting_elems) throw std::runtime_error(boost::str(boost::format(message) % formatting_elems)) + +// old iOS SDKs compatibility +#ifdef VCMI_IOS +#include + +#ifndef __IPHONE_13_0 +#define __IPHONE_13_0 130000 +#endif +#endif // VCMI_IOS + +// single-process build makes 2 copies of the main lib by wrapping it in a namespace +#ifdef VCMI_LIB_NAMESPACE +#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE { +#define VCMI_LIB_NAMESPACE_END } +#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE; +#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x +#else +#define VCMI_LIB_NAMESPACE_BEGIN +#define VCMI_LIB_NAMESPACE_END +#define VCMI_LIB_USING_NAMESPACE +#define VCMI_LIB_WRAP_NAMESPACE(x) ::x +#endif + +/* ---------------------------------------------------------------------------- */ +/* VCMI standard library */ +/* ---------------------------------------------------------------------------- */ +#include + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + // combine hashes. Present in boost but not in std + template + inline void hash_combine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + } + + //returns true if container c contains item i + template + bool contains(const Container & c, const Item &i) + { + return std::find(std::begin(c), std::end(c),i) != std::end(c); + } + + //returns true if container c contains item i + template + bool contains_if(const Container & c, Pred p) + { + return std::find_if(std::begin(c), std::end(c), p) != std::end(c); + } + + //returns true if map c contains item i + template + bool contains(const std::map & c, const Item2 &i) + { + return c.find(i)!=c.end(); + } + + //returns true if unordered set c contains item i + template + bool contains(const std::unordered_set & c, const Item &i) + { + return c.find(i)!=c.end(); + } + + template + bool contains(const std::unordered_map & c, const Item2 &i) + { + return c.find(i)!=c.end(); + } + + //returns position of first element in vector c equal to s, if there is no such element, -1 is returned + template + int find_pos(const Container & c, const T2 &s) + { + int i=0; + for (auto iter = std::begin(c); iter != std::end(c); iter++, i++) + if(*iter == s) + return i; + return -1; + } + + //Func f tells if element matches + template + int find_pos_if(const Container & c, const Func &f) + { + auto ret = boost::range::find_if(c, f); + if(ret != std::end(c)) + return std::distance(std::begin(c), ret); + + return -1; + } + + //returns iterator to the given element if present in container, end() if not + template + typename Container::iterator find(Container & c, const Item &i) + { + return std::find(c.begin(),c.end(),i); + } + + //returns const iterator to the given element if present in container, end() if not + template + typename Container::const_iterator find(const Container & c, const Item &i) + { + return std::find(c.begin(),c.end(),i); + } + + //returns first key that maps to given value if present, returns success via found if provided + template + Key findKey(const std::map & map, const T & value, bool * found = nullptr) + { + for(auto iter = map.cbegin(); iter != map.cend(); iter++) + { + if(iter->second == value) + { + if(found) + *found = true; + return iter->first; + } + } + if(found) + *found = false; + return Key(); + } + + //removes element i from container c, returns false if c does not contain i + template + typename Container::size_type operator-=(Container &c, const Item &i) + { + typename Container::iterator itr = find(c,i); + if(itr == c.end()) + return false; + c.erase(itr); + return true; + } + + //assigns greater of (a, b) to a and returns maximum of (a, b) + template + t1 &amax(t1 &a, const t2 &b) + { + if(a >= (t1)b) + return a; + else + { + a = t1(b); + return a; + } + } + + //assigns smaller of (a, b) to a and returns minimum of (a, b) + template + t1 &amin(t1 &a, const t2 &b) + { + if(a <= (t1)b) + return a; + else + { + a = t1(b); + return a; + } + } + + //makes a to fit the range + template + void abetween(T &value, const T &min, const T &max) + { + value = std::clamp(value, min, max); + } + + //checks if a is between b and c + template + bool isbetween(const t1 &value, const t2 &min, const t3 &max) + { + return value > (t1)min && value < (t1)max; + } + + //checks if a is within b and c + template + bool iswithin(const t1 &value, const t2 &min, const t3 &max) + { + return value >= (t1)min && value <= (t1)max; + } + + template + struct assigner + { + public: + t1 &op1; + t2 op2; + assigner(t1 &a1, const t2 & a2) + :op1(a1), op2(a2) + {} + void operator()() + { + op1 = op2; + } + }; + + //deleted pointer and sets it to nullptr + template + void clear_pointer(T* &ptr) + { + delete ptr; + ptr = nullptr; + } + + template + typename Container::const_reference circularAt(const Container &r, size_t index) + { + assert(r.size()); + index %= r.size(); + auto itr = std::begin(r); + std::advance(itr, index); + return *itr; + } + + template + void erase(Container &c, const Item &item) + { + c.erase(boost::remove(c, item), c.end()); + } + + template + void erase_if(Range &vec, Predicate pred) + { + vec.erase(boost::remove_if(vec, pred),vec.end()); + } + + template + void erase_if(std::set &setContainer, Predicate pred) + { + auto itr = setContainer.begin(); + auto endItr = setContainer.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + setContainer.erase(tmpItr); + } + } + + template + void erase_if(std::unordered_set &setContainer, Predicate pred) + { + auto itr = setContainer.begin(); + auto endItr = setContainer.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + setContainer.erase(tmpItr); + } + } + + //works for map and std::map, maybe something else + template + void erase_if(std::map &container, Predicate pred) + { + auto itr = container.begin(); + auto endItr = container.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + container.erase(tmpItr); + } + } + + template + OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred) + { + return std::copy_if(std::cbegin(input), std::end(input), result, pred); + } + + template + std::insert_iterator set_inserter(Container &c) + { + return std::inserter(c, c.end()); + } + + //Returns iterator to the element for which the value of ValueFunction is minimal + template + auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) + { + /* Clang crashes when instantiating this function template and having PCH compilation enabled. + * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 + * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype + * directly for both function parameters. + */ + return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool + { + return vf(lhs) < vf(rhs); + }); + } + + //Returns iterator to the element for which the value of ValueFunction is maximal + template + auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) + { + /* Clang crashes when instantiating this function template and having PCH compilation enabled. + * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 + * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype + * directly for both function parameters. + */ + return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool + { + return vf(lhs) < vf(rhs); + }); + } + + /// Increments value by specific delta + /// similar to std::next but works with other types, e.g. enum class + template + T next(const T &obj, int change) + { + return static_cast(static_cast(obj) + change); + } + + template + typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers) + { + if(c.size()) + return c.back(); + else + return typename Container::value_type(); + } + + template + typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers) + { + if(c.size()) + return c.front(); + else + return nullptr; + } + + template + bool isValidIndex(const Container &c, Index i) + { + return i >= 0 && i < c.size(); + } + + template + typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue) + { + if(index < r.size()) + return r[index]; + + return defaultValue; + } + + template + bool erase_if_present(Container &c, const Item &item) + { + auto i = std::find(c.begin(), c.end(), item); + if (i != c.end()) + { + c.erase(i); + return true; + } + + return false; + } + + template + bool erase_if_present(std::map & c, const Item2 &item) + { + auto i = c.find(item); + if (i != c.end()) + { + c.erase(i); + return true; + } + return false; + } + + template + void removeDuplicates(std::vector &vec) + { + std::sort(vec.begin(), vec.end()); + vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); + } + + template + void concatenate(std::vector &dest, const std::vector &src) + { + dest.reserve(dest.size() + src.size()); + dest.insert(dest.end(), src.begin(), src.end()); + } + + template + std::vector intersection(std::vector &v1, std::vector &v2) + { + std::vector v3; + std::sort(v1.begin(), v1.end()); + std::sort(v2.begin(), v2.end()); + std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3)); + return v3; + } + + template + std::set difference(const std::set &s1, const std::set s2) + { + std::set s3; + std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end())); + return s3; + } + + template + bool containsMapping(const std::multimap & map, const std::pair & mapping) + { + auto range = map.equal_range(mapping.first); + for(auto contained = range.first; contained != range.second; contained++) + { + if(mapping.second == contained->second) + return true; + } + return false; + } + + template + typename M::mapped_type & getOrCompute(M & m, const Key & k, F f) + { + typedef typename M::mapped_type V; + + std::pair r = m.insert(typename M::value_type(k, V())); + V & v = r.first->second; + + if(r.second) + f(v); + + return v; + } + + //c++20 feature + template + Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f) + { + return a + (b - a) * f; + } + + ///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr + static constexpr int abs(int i) { + if(i < 0) return -i; + return i; + } + + ///C++23 + template< class Enum > constexpr std::underlying_type_t to_underlying( Enum e ) noexcept + { + return static_cast>(e); + } +} +using vstd::operator-=; + +VCMI_LIB_NAMESPACE_END diff --git a/Mods/vcmi/Data/lobby/iconFolder.png b/Mods/vcmi/Data/lobby/iconFolder.png new file mode 100644 index 000000000..44547ef38 Binary files /dev/null and b/Mods/vcmi/Data/lobby/iconFolder.png differ diff --git a/Mods/vcmi/Data/lobby/townBorderBig.png b/Mods/vcmi/Data/lobby/townBorderBig.png new file mode 100644 index 000000000..847dab8ed Binary files /dev/null and b/Mods/vcmi/Data/lobby/townBorderBig.png differ diff --git a/Mods/vcmi/Data/lobby/townBorderBigActivated.png b/Mods/vcmi/Data/lobby/townBorderBigActivated.png new file mode 100644 index 000000000..0cfdd8a62 Binary files /dev/null and b/Mods/vcmi/Data/lobby/townBorderBigActivated.png differ diff --git a/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png b/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png new file mode 100644 index 000000000..c7b672338 Binary files /dev/null and b/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png differ diff --git a/Mods/vcmi/Data/lobby/townBorderSmallActivated.png b/Mods/vcmi/Data/lobby/townBorderSmallActivated.png new file mode 100644 index 000000000..5aa146d1c Binary files /dev/null and b/Mods/vcmi/Data/lobby/townBorderSmallActivated.png differ diff --git a/Mods/vcmi/Data/radialMenu/altDown.png b/Mods/vcmi/Data/radialMenu/altDown.png new file mode 100644 index 000000000..a3945a500 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/altDown.png differ diff --git a/Mods/vcmi/Data/radialMenu/altDownBottom.png b/Mods/vcmi/Data/radialMenu/altDownBottom.png new file mode 100644 index 000000000..f15747138 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/altDownBottom.png differ diff --git a/Mods/vcmi/Data/radialMenu/altUp.png b/Mods/vcmi/Data/radialMenu/altUp.png new file mode 100644 index 000000000..905617f33 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/altUp.png differ diff --git a/Mods/vcmi/Data/radialMenu/altUpTop.png b/Mods/vcmi/Data/radialMenu/altUpTop.png new file mode 100644 index 000000000..ae34d5775 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/altUpTop.png differ diff --git a/Mods/vcmi/Data/radialMenu/dismissHero.png b/Mods/vcmi/Data/radialMenu/dismissHero.png new file mode 100644 index 000000000..fe196b7d6 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/dismissHero.png differ diff --git a/Mods/vcmi/Data/radialMenu/itemEmptyAlt.png b/Mods/vcmi/Data/radialMenu/itemEmptyAlt.png new file mode 100644 index 000000000..80383f03a Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/itemEmptyAlt.png differ diff --git a/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png b/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png new file mode 100644 index 000000000..631d8f2b5 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png differ diff --git a/Mods/vcmi/Data/radialMenu/moveArtifacts.png b/Mods/vcmi/Data/radialMenu/moveArtifacts.png new file mode 100644 index 000000000..3f2a9b2f4 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/moveArtifacts.png differ diff --git a/Mods/vcmi/Data/radialMenu/moveTroops.png b/Mods/vcmi/Data/radialMenu/moveTroops.png new file mode 100644 index 000000000..710657656 Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/moveTroops.png differ diff --git a/Mods/vcmi/Data/radialMenu/swapArtifacts.png b/Mods/vcmi/Data/radialMenu/swapArtifacts.png new file mode 100644 index 000000000..6e513e94e Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/swapArtifacts.png differ diff --git a/Mods/vcmi/Data/radialMenu/tradeHeroes.png b/Mods/vcmi/Data/radialMenu/tradeHeroes.png new file mode 100644 index 000000000..176b6e97e Binary files /dev/null and b/Mods/vcmi/Data/radialMenu/tradeHeroes.png differ diff --git a/Mods/vcmi/Data/s/std.verm b/Mods/vcmi/Data/s/std.verm index 865997ce7..54c08dc57 100755 --- a/Mods/vcmi/Data/s/std.verm +++ b/Mods/vcmi/Data/s/std.verm @@ -1,40 +1,40 @@ -VERM -; standard verm file, global engine things should be put here - -!?PI; -; example 1 --- Hello World -![print ^Hello world!^] - -; example 2 --- simple arithmetics -![defun add [x y] [+ x y]] -![print [add 2 3]] - -; example 3 --- semantic macros -![defmacro do-n-times [times body] - `[progn - [setq do-counter 0] - [setq do-max ,times] - [do [< do-counter do-max] - [progn - [setq do-counter [+ do-counter 1]] - ,body - ] - ] - ] -] -![do-n-times 4 [print ^tekst\n^]] - - -; example 4 --- conditional expression -![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]] - -; example 5 --- lambda expressions -![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3] - -; example 6 --- resursion -![defun factorial [n] - [if [= n 0] 1 - [* n [factorial [- n 1]]] - ] -] +VERM +; standard verm file, global engine things should be put here + +!?PI; +; example 1 --- Hello World +![print ^Hello world!^] + +; example 2 --- simple arithmetics +![defun add [x y] [+ x y]] +![print [add 2 3]] + +; example 3 --- semantic macros +![defmacro do-n-times [times body] + `[progn + [setq do-counter 0] + [setq do-max ,times] + [do [< do-counter do-max] + [progn + [setq do-counter [+ do-counter 1]] + ,body + ] + ] + ] +] +![do-n-times 4 [print ^tekst\n^]] + + +; example 4 --- conditional expression +![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]] + +; example 5 --- lambda expressions +![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3] + +; example 6 --- resursion +![defun factorial [n] + [if [= n 0] 1 + [* n [factorial [- n 1]]] + ] +] ![print [factorial 8]] \ No newline at end of file diff --git a/Mods/vcmi/Data/s/testy.erm b/Mods/vcmi/Data/s/testy.erm index 082c1891e..051c27a19 100755 --- a/Mods/vcmi/Data/s/testy.erm +++ b/Mods/vcmi/Data/s/testy.erm @@ -1,14 +1,14 @@ -ZVSE -!?PI; - !!VRv2777:S4; - !!DO1/0/5/1&v2777<>1:P0; - -!?FU1; - !!VRv2778:Sx16%2; - !!IF&x16>3:M^Hello world number %X16! To duza liczba^; - !!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^; - !!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^; - -!?PI; - !!VRz10:S^Composed hello ^; +ZVSE +!?PI; + !!VRv2777:S4; + !!DO1/0/5/1&v2777<>1:P0; + +!?FU1; + !!VRv2778:Sx16%2; + !!IF&x16>3:M^Hello world number %X16! To duza liczba^; + !!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^; + !!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^; + +!?PI; + !!VRz10:S^Composed hello ^; !!IF:M^%Z10%%world%%, v2777=%V2777, v2778=%V2778!^; \ No newline at end of file diff --git a/Mods/vcmi/Sounds/we5.wav b/Mods/vcmi/Sounds/we5.wav new file mode 100644 index 000000000..47901221e Binary files /dev/null and b/Mods/vcmi/Sounds/we5.wav differ diff --git a/Mods/vcmi/Sprites/PortraitsLarge.json b/Mods/vcmi/Sprites/PortraitsLarge.json index 9a74e7dc2..44dcc7ad7 100644 --- a/Mods/vcmi/Sprites/PortraitsLarge.json +++ b/Mods/vcmi/Sprites/PortraitsLarge.json @@ -156,13 +156,6 @@ { "frame" : 152, "file" : "HPL009SH.bmp"}, { "frame" : 153, "file" : "HPL008SH.bmp"}, { "frame" : 154, "file" : "HPL001SH.bmp"}, - { "frame" : 155, "file" : "HPL131DM.bmp"}, - { "frame" : 156, "file" : "HPL129MK.bmp"}, - { "frame" : 157, "file" : "HPL002SH.bmp"}, - { "frame" : 158, "file" : "HPL132Wl.bmp"}, - { "frame" : 159, "file" : "HPL133Nc.bmp"}, - { "frame" : 160, "file" : "HPL134Nc.bmp"}, - { "frame" : 161, "file" : "HPL135Wi.bmp"}, - { "frame" : 162, "file" : "HPL136Wi.bmp"} + { "frame" : 155, "file" : "HPL131DM.bmp"} ] } diff --git a/Mods/vcmi/Sprites/PortraitsSmall.json b/Mods/vcmi/Sprites/PortraitsSmall.json index 8064ecf25..701da953a 100644 --- a/Mods/vcmi/Sprites/PortraitsSmall.json +++ b/Mods/vcmi/Sprites/PortraitsSmall.json @@ -155,16 +155,9 @@ { "frame" : 151, "file" : "HPS007SH.bmp"}, { "frame" : 152, "file" : "HPS009SH.bmp"}, { "frame" : 153, "file" : "HPS008SH.bmp"}, - { "frame" : 154, "file" : "HPS001SH.bmp"}, - { "frame" : 155, "file" : "HPS131DM.bmp"}, - { "frame" : 156, "file" : "HPS129MK.bmp"}, - { "frame" : 157, "file" : "HPS002SH.bmp"}, - { "frame" : 158, "file" : "HPS132Wl.bmp"}, - { "frame" : 159, "file" : "HPS133Nc.bmp"}, - { "frame" : 160, "file" : "HPS134Nc.bmp"}, - { "frame" : 161, "file" : "HPS135Wi.bmp"}, - { "frame" : 162, "file" : "HPS136Wi.bmp"}, - { "frame" : 163, "file" : "HPSRAND1.bmp"}, //random hero - { "frame" : 164, "file" : "HPSRAND6.bmp"} //no hero + { "frame" : 154, "file" : "HPS001SH.bmp"}, + { "frame" : 155, "file" : "HPS131DM.bmp"}, + { "frame" : 156, "file" : "HPSRAND1.bmp"}, // random hero + { "frame" : 157, "file" : "HPSRAND6.bmp"} // no hero ] } diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json index a52f145a9..1c6b60d36 100644 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json +++ b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json @@ -2,20 +2,20 @@ "basepath" : "battle/rangeHighlights/green/", "images" : [ - { "frame" : 00, "file" : "empty.png"}, // 000001 -> 00 empty frame + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame // load single edges - { "frame" : 01, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 02, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 03, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 04, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 05, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 06, "file" : "left.png"}, //100000 -> 32 left + { "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" : 07, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 08, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 09, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner + { "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 diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json index 15dac5b75..42350c073 100644 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json +++ b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json @@ -2,20 +2,20 @@ "basepath" : "battle/rangeHighlights/red/", "images" : [ - { "frame" : 00, "file" : "empty.png"}, // 000001 -> 00 empty frame + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame // load single edges - { "frame" : 01, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 02, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 03, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 04, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 05, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 06, "file" : "left.png"}, //100000 -> 32 left + { "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" : 07, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 08, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 09, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner + { "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 diff --git a/Mods/vcmi/Sprites/lobby/checkbox.json b/Mods/vcmi/Sprites/lobby/checkbox.json new file mode 100644 index 000000000..bfb1e6587 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/checkbox.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "checkboxBlueOff.png"}, + { "frame" : 1, "file" : "checkboxBlueOn.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png new file mode 100644 index 000000000..8e611ac95 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png new file mode 100644 index 000000000..580941806 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxOff.png b/Mods/vcmi/Sprites/lobby/checkboxOff.png new file mode 100644 index 000000000..d650018dc Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxOff.png differ diff --git a/Mods/vcmi/Sprites/lobby/checkboxOn.png b/Mods/vcmi/Sprites/lobby/checkboxOn.png new file mode 100644 index 000000000..cd6e46308 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/checkboxOn.png differ diff --git a/Mods/vcmi/Sprites/lobby/dropdown.json b/Mods/vcmi/Sprites/lobby/dropdown.json new file mode 100644 index 000000000..3a6d41203 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/dropdown.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "dropdownNormal.png"}, + { "frame" : 1, "file" : "dropdownPressed.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Sprites/lobby/dropdownNormal.png new file mode 100644 index 000000000..71dc20b3d Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/dropdownNormal.png differ diff --git a/Mods/vcmi/Sprites/lobby/dropdownPressed.png b/Mods/vcmi/Sprites/lobby/dropdownPressed.png new file mode 100644 index 000000000..48ee1358f Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/dropdownPressed.png differ diff --git a/Mods/vcmi/Video/tutorial/AbortSpell.webm b/Mods/vcmi/Video/tutorial/AbortSpell.webm new file mode 100644 index 000000000..6aa7e8b9b Binary files /dev/null and b/Mods/vcmi/Video/tutorial/AbortSpell.webm differ diff --git a/Mods/vcmi/Video/tutorial/BattleDirection.webm b/Mods/vcmi/Video/tutorial/BattleDirection.webm new file mode 100644 index 000000000..67b3407fa Binary files /dev/null and b/Mods/vcmi/Video/tutorial/BattleDirection.webm differ diff --git a/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm b/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm new file mode 100644 index 000000000..ecbf8fbf4 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm differ diff --git a/Mods/vcmi/Video/tutorial/MapPanning.webm b/Mods/vcmi/Video/tutorial/MapPanning.webm new file mode 100644 index 000000000..90ad7da27 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/MapPanning.webm differ diff --git a/Mods/vcmi/Video/tutorial/MapZooming.webm b/Mods/vcmi/Video/tutorial/MapZooming.webm new file mode 100644 index 000000000..8f3a0e3d3 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/MapZooming.webm differ diff --git a/Mods/vcmi/Video/tutorial/RadialWheel.webm b/Mods/vcmi/Video/tutorial/RadialWheel.webm new file mode 100644 index 000000000..f91e87f39 Binary files /dev/null and b/Mods/vcmi/Video/tutorial/RadialWheel.webm differ diff --git a/Mods/vcmi/Video/tutorial/RightClick.webm b/Mods/vcmi/Video/tutorial/RightClick.webm new file mode 100644 index 000000000..bd60061da Binary files /dev/null and b/Mods/vcmi/Video/tutorial/RightClick.webm differ diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 666b2168b..0c5010903 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -1,17 +1,17 @@ { "vcmi.adventureMap.monsterThreat.title" : "\n\n威胁度: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "不值一提", + "vcmi.adventureMap.monsterThreat.levels.0" : "极低", "vcmi.adventureMap.monsterThreat.levels.1" : "很低", "vcmi.adventureMap.monsterThreat.levels.2" : "低", "vcmi.adventureMap.monsterThreat.levels.3" : "较低", - "vcmi.adventureMap.monsterThreat.levels.4" : "势均力敌", + "vcmi.adventureMap.monsterThreat.levels.4" : "中等", "vcmi.adventureMap.monsterThreat.levels.5" : "较高", "vcmi.adventureMap.monsterThreat.levels.6" : "高", "vcmi.adventureMap.monsterThreat.levels.7" : "很高", - "vcmi.adventureMap.monsterThreat.levels.8" : "略有挑战", + "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", - "vcmi.adventureMap.monsterThreat.levels.10" : "自寻死路", - "vcmi.adventureMap.monsterThreat.levels.11" : "天方夜谭", + "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", + "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜", "vcmi.adventureMap.confirmRestartGame" : "你想要重新开始游戏吗?", "vcmi.adventureMap.noTownWithMarket" : "没有足够的市场。", @@ -23,15 +23,38 @@ "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", - "vcmi.capitalColors.2" : "青色", + "vcmi.capitalColors.2" : "褐色", "vcmi.capitalColors.3" : "绿色", "vcmi.capitalColors.4" : "橙色", "vcmi.capitalColors.5" : "紫色", - "vcmi.capitalColors.6" : "褐色", + "vcmi.capitalColors.6" : "青色", "vcmi.capitalColors.7" : "粉色", + "vcmi.heroOverview.startingArmy" : "初始部队", + "vcmi.heroOverview.warMachine" : "战争机器", + "vcmi.heroOverview.secondarySkills" : "初始技能", + "vcmi.heroOverview.spells" : "魔法", + + "vcmi.radialWheel.mergeSameUnit" : "合并相同生物", + "vcmi.radialWheel.showUnitInformation" : "显示生物信息", + "vcmi.radialWheel.splitSingleUnit" : "分割单个生物", + "vcmi.radialWheel.splitUnitEqually" : "平均分配生物", + "vcmi.radialWheel.moveUnit" : "将生物移动到部队", + "vcmi.radialWheel.splitUnit" : "分割生物到其他空位", + + "vcmi.mainMenu.serverConnecting" : "连接中...", + "vcmi.mainMenu.serverAddressEnter" : "使用地址:", + "vcmi.mainMenu.serverClosing" : "关闭中...", + "vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏", + "vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏", + "vcmi.mainMenu.playerName" : "Player", + + "vcmi.lobby.filename" : "文件名", + "vcmi.lobby.creationDate" : "创建时间", + "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", - "vcmi.server.errors.modsIncompatibility" : "需要加载的MOD列表:", + "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", + "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", "vcmi.settingsMainWindow.generalTab.hover" : "常规", @@ -46,15 +69,29 @@ "vcmi.systemOptions.otherGroup" : "其他设置", // unused right now "vcmi.systemOptions.townsGroup" : "城镇画面", - "vcmi.systemOptions.fullscreenButton.hover" : "全屏", - "vcmi.systemOptions.fullscreenButton.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", + "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", + "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (边框)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", "vcmi.systemOptions.resolutionButton.hover" : "分辨率", "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。", "vcmi.systemOptions.resolutionMenu.hover" : "分辨率选择", "vcmi.systemOptions.resolutionMenu.help" : "修改游戏运行时的分辨率。", - "vcmi.systemOptions.fullscreenFailed" : "{全屏}\n\n选择切换到全屏模式失败!当前显示器不支持该分辨率!", + "vcmi.systemOptions.scalingButton.hover" : "界面大小: %p%", + "vcmi.systemOptions.scalingButton.help" : "{界面大小}\n\n改变界面的大小", + "vcmi.systemOptions.scalingMenu.hover" : "选择界面大小", + "vcmi.systemOptions.scalingMenu.help" : "改变游戏界面大小。", + "vcmi.systemOptions.longTouchButton.hover" : "触控间距: %d 毫秒", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{触控间距}\n\n使用触摸屏时,触摸屏幕指定持续时间(以毫秒为单位)后将出现弹出窗口。", + "vcmi.systemOptions.longTouchMenu.hover" : "选择触控间距", + "vcmi.systemOptions.longTouchMenu.help" : "改变触控间距。", + "vcmi.systemOptions.longTouchMenu.entry" : "%d 毫秒", "vcmi.systemOptions.framerateButton.hover" : "显示FPS", "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "触觉反馈", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{触觉反馈}\n\n切换触摸输入的触觉反馈。", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "界面增强", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。", "vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息", "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", @@ -64,6 +101,12 @@ "vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。", "vcmi.adventureOptions.showGrid.hover" : "显示网格", "vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。", + "vcmi.adventureOptions.borderScroll.hover" : "滚动边界", + "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.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -88,10 +131,16 @@ "vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为即刻", "vcmi.battleOptions.movementHighlightOnHover.hover": "鼠标悬停高亮单位移动范围", "vcmi.battleOptions.movementHighlightOnHover.help": "{鼠标悬停高亮单位移动范围}\n\n当你的鼠标悬停在单位上时高亮他的行动范围。", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "显示生物射程限制", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{显示生物射程限制}\n\n当您将鼠标悬停在其上时,显示射手的射程限制。", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "显示英雄统计数据窗口", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。", + "vcmi.battleOptions.enableAutocombatSpells.hover": "魔法", + "vcmi.battleOptions.enableAutocombatSpells.help": "{魔法}\n\n快速战斗时不会使用魔法。", "vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐", "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", - "vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗", + "vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗", "vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE).", @@ -122,16 +171,18 @@ "vcmi.townHall.greetingDefence" : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。", "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", - "vcmi.townHall.greetingCustomBonus" : "%s 赋予你 +%d %s%s", + "vcmi.townHall.greetingCustomBonus" : "当你的英雄访问%s 时,这个神奇的建筑使你的英雄 +%d %s%s。", "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", "vcmi.logicalExpressions.anyOf" : "以下任一前提:", "vcmi.logicalExpressions.allOf" : "以下所有前提:", - "vcmi.logicalExpressions.noneOf" : "无前提:", + "vcmi.logicalExpressions.noneOf" : "与此建筑冲突:", "vcmi.heroWindow.openCommander.hover" : "开启指挥官界面", "vcmi.heroWindow.openCommander.help" : "显示该英雄指挥官详细信息", + "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", + "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", "vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?", @@ -145,12 +196,30 @@ "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", - "vcmi.randomMapTab.widgets.randomTemplate" : "(随机)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "默认", "vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", "vcmi.randomMapTab.widgets.roadTypesLabel" : "道路类型", - + + "vcmi.optionsTab.chessFieldBase.hover" : "额外计时器", + "vcmi.optionsTab.chessFieldTurn.hover" : "转动计时器", + "vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器", + "vcmi.optionsTab.chessFieldUnit.hover" : "堆栈计时器", + "vcmi.optionsTab.chessFieldBase.help" : "当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", + "vcmi.optionsTab.chessFieldTurn.help" : "当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", + "vcmi.optionsTab.chessFieldBattle.help" : "战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", + "vcmi.optionsTab.chessFieldUnit.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "祝贺你,你依然存活,取得了胜利!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "敌人消灭了所有怪物,取得了胜利!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "祝贺你!你消灭了所有怪物,取得了胜利!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "获得所有三件宝物", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "祝贺你!你取得了天使联盟且消灭了所有敌人,取得了胜利!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "击败所有敌人并取得天使联盟", + // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i", "vcmi.stackExperience.rank.0" : "新兵 1级", @@ -164,9 +233,9 @@ "vcmi.stackExperience.rank.8" : "少校 9级", "vcmi.stackExperience.rank.9" : "中校 10级", "vcmi.stackExperience.rank.10" : "上校 11级", - - "core.bonus.ADDITIONAL_ATTACK.name": "双重攻击", - "core.bonus.ADDITIONAL_ATTACK.description": "攻击两次", + + "core.bonus.ADDITIONAL_ATTACK.name": "双击", + "core.bonus.ADDITIONAL_ATTACK.description": "生物可以攻击两次", "core.bonus.ADDITIONAL_RETALIATION.name": "额外反击", "core.bonus.ADDITIONAL_RETALIATION.description": "每回合额外获得${val}次反击机会", "core.bonus.AIR_IMMUNITY.name": "气系免疫", @@ -185,11 +254,11 @@ "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "敌方施法消耗的魔法值增加${val}点", "core.bonus.CHARGE_IMMUNITY.name": "免疫冲锋", "core.bonus.CHARGE_IMMUNITY.description": "对冲锋特技的额外伤害免疫", - "core.bonus.DARKNESS.name": "阴影庇体", + "core.bonus.DARKNESS.name": "黑幕", "core.bonus.DARKNESS.description": "创建${val}半径黑幕", "core.bonus.DEATH_STARE.name": "死亡凝视 (${val}%)", "core.bonus.DEATH_STARE.description": "${val}%几率直接杀死一单位生物", - "core.bonus.DEFENSIVE_STANCE.name": "防御增效", + "core.bonus.DEFENSIVE_STANCE.name": "防御奖励", "core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val}防御力", "core.bonus.DESTRUCTION.name": "毁灭", "core.bonus.DESTRUCTION.description": "${val}%几率在攻击后追加消灭数量", @@ -199,9 +268,9 @@ "core.bonus.DRAGON_NATURE.description": "生物拥有龙的特性", "core.bonus.EARTH_IMMUNITY.name": "土系免疫", "core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法", - "core.bonus.ENCHANTER.name": "强化师", + "core.bonus.ENCHANTER.name": "施法者", "core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}", - "core.bonus.ENCHANTED.name": "魔法附身", + "core.bonus.ENCHANTED.name": "法术加持", "core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。", @@ -209,29 +278,29 @@ "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法", "core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)", "core.bonus.FIRE_SHIELD.description": "反弹部分受到的近战伤害", - "core.bonus.FIRST_STRIKE.name": "抢先攻击", + "core.bonus.FIRST_STRIKE.name": "抢先反击", "core.bonus.FIRST_STRIKE.description": "该生物的反击将会在被攻击前进行", "core.bonus.FEAR.name": "恐惧", "core.bonus.FEAR.description": "使得敌方一只部队恐惧", "core.bonus.FEARLESS.name": "无惧", "core.bonus.FEARLESS.description": "免疫恐惧特质", - "core.bonus.FLYING.name": "飞行", + "core.bonus.FLYING.name": "飞行兵种", "core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)", "core.bonus.FREE_SHOOTING.name": "近身射击", "core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击", - "core.bonus.GARGOYLE.name": "石像鬼", + "core.bonus.GARGOYLE.name": "石像鬼属性", "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.description": "对${subtype.creature}造成额外${val}%伤害", - "core.bonus.HEALER.name": "治疗者", + "core.bonus.HEALER.name": "治疗", "core.bonus.HEALER.description": "可以治疗友军单位", "core.bonus.HP_REGENERATION.name": "再生", - "core.bonus.HP_REGENERATION.description": "每回合恢复${SHval}点生命值", - "core.bonus.JOUSTING.name": "英勇冲锋", + "core.bonus.HP_REGENERATION.description": "每回合恢复${val}点生命值", + "core.bonus.JOUSTING.name": "冲锋", "core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害", - "core.bonus.KING.name": "王牌", + "core.bonus.KING.name": "顶级怪物", "core.bonus.KING.description": "受${val}级或更高级屠戮成性影响", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法", @@ -239,7 +308,7 @@ "core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标", "core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)", "core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身", - "core.bonus.MANA_CHANNELING.name": "魔法通道${val}%", + "core.bonus.MANA_CHANNELING.name": "魔法虹吸${val}%", "core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值", "core.bonus.MANA_DRAIN.name": "吸取魔力", "core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值", @@ -256,14 +325,14 @@ "core.bonus.NO_MORALE.name": "无士气", "core.bonus.NO_MORALE.description": "生物不受士气影响", "core.bonus.NO_WALL_PENALTY.name": "无城墙影响", - "core.bonus.NO_WALL_PENALTY.description": "攻城战中被城墙阻挡造成全额伤害", + "core.bonus.NO_WALL_PENALTY.description": "攻城战中不被城墙阻挡造成全额伤害", "core.bonus.NON_LIVING.name": "无生命", "core.bonus.NON_LIVING.description": "免疫大多数的效果", "core.bonus.RANDOM_SPELLCASTER.name": "随机施法", "core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法", "core.bonus.RANGED_RETALIATION.name": "远程反击", "core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击", - "core.bonus.RECEPTIVE.name": "适应", + "core.bonus.RECEPTIVE.name": "接受", "core.bonus.RECEPTIVE.description": "不会免疫有益魔法", "core.bonus.REBIRTH.name": "复生 (${val}%)", "core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活", @@ -295,12 +364,12 @@ "core.bonus.SYNERGY_TARGET.description": "生物受到协助攻击的影响", "core.bonus.TWO_HEX_ATTACK_BREATH.name": "吐息", "core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击(2格范围)", - "core.bonus.THREE_HEADED_ATTACK.name": "三头攻击", + "core.bonus.THREE_HEADED_ATTACK.name": "半环攻击", "core.bonus.THREE_HEADED_ATTACK.description": "攻击三格邻接单位", "core.bonus.TRANSMUTATION.name": "变形术", "core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物", - "core.bonus.UNDEAD.name": "亡灵", - "core.bonus.UNDEAD.description": "该生物属于亡灵", + "core.bonus.UNDEAD.name": "不死生物", + "core.bonus.UNDEAD.description": "该生物属于丧尸", "core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击", "core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人", "core.bonus.WATER_IMMUNITY.name": "水系免疫", diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index dcbb8ccaf..fbe240eb8 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", - "vcmi.mainMenu.highscoresNotImplemented" : "Omlouvám se, menu nejvyšší skóre ještě není implementováno\n", "vcmi.mainMenu.serverConnecting" : "Připojování...", "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", "vcmi.mainMenu.serverClosing" : "Zavírání...", @@ -45,12 +44,12 @@ "vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP", "vcmi.mainMenu.playerName" : "Hráč", - "vcmi.lobby.filename" : "Název souboru", + "vcmi.lobby.filepath" : "Název souboru", "vcmi.lobby.creationDate" : "Datum vytvoření", - "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", - "vcmi.server.errors.modsIncompatibility" : "Následující modifikace jsou nutné pro načtení hry:", - "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", + "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.confirmReconnect" : "Chcete se připojit k poslední relaci?", "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", @@ -193,15 +192,10 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Druhy cest", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", "vcmi.optionsTab.widgets.labelTimer" : "Časovač", "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač", "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač", - // 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!", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 315011511..242099ee7 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -30,35 +30,59 @@ "vcmi.capitalColors.6" : "Teal", "vcmi.capitalColors.7" : "Pink", + "vcmi.heroOverview.startingArmy" : "Starting Units", + "vcmi.heroOverview.warMachine" : "War Machines", + "vcmi.heroOverview.secondarySkills" : "Secondary Skills", + "vcmi.heroOverview.spells" : "Spells", + "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", "vcmi.radialWheel.splitUnitEqually" : "Split creatures equally", "vcmi.radialWheel.moveUnit" : "Move creatures to another army", "vcmi.radialWheel.splitUnit" : "Split creature to another slot", + + "vcmi.radialWheel.heroGetArmy" : "Get army from other hero", + "vcmi.radialWheel.heroSwapArmy" : "Swap army with other hero", + "vcmi.radialWheel.heroExchange" : "Open hero exchange", + "vcmi.radialWheel.heroGetArtifacts" : "Get artifacts from other hero", + "vcmi.radialWheel.heroSwapArtifacts" : "Swap artifacts with other hero", + "vcmi.radialWheel.heroDismiss" : "Dismiss hero", + + "vcmi.radialWheel.moveTop" : "Move to top", + "vcmi.radialWheel.moveUp" : "Move up", + "vcmi.radialWheel.moveDown" : "Move down", + "vcmi.radialWheel.moveBottom" : "Move to bottom", - "vcmi.mainMenu.tutorialNotImplemented" : "Sorry, tutorial is not implemented yet\n", - "vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n", "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", + "vcmi.mainMenu.serverConnectionFailed" : "Failed to connect", "vcmi.mainMenu.serverClosing" : "Closing...", "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", "vcmi.mainMenu.playerName" : "Player", - "vcmi.lobby.filename" : "Filename", + "vcmi.lobby.filepath" : "File path", "vcmi.lobby.creationDate" : "Creation date", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Map preview", + "vcmi.lobby.noPreview" : "no preview", + "vcmi.lobby.noUnderground" : "no underground", - "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", - "vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:", - "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "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.confirmReconnect" : "Do you want to reconnect to the last session?", + "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.settingsMainWindow.generalTab.hover" : "General", - "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior", + "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", "vcmi.settingsMainWindow.battleTab.hover" : "Battle", - "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles", + "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles.", "vcmi.settingsMainWindow.adventureTab.hover" : "Adventure Map", - "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes)", + "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes).", "vcmi.systemOptions.videoGroup" : "Video Settings", "vcmi.systemOptions.audioGroup" : "Audio Settings", @@ -74,38 +98,42 @@ "vcmi.systemOptions.resolutionMenu.hover" : "Select Resolution", "vcmi.systemOptions.resolutionMenu.help" : "Change in-game screen resolution.", "vcmi.systemOptions.scalingButton.hover" : "Interface Scaling: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface", + "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface.", "vcmi.systemOptions.scalingMenu.hover" : "Select Interface Scaling", "vcmi.systemOptions.scalingMenu.help" : "Change in-game interface scaling.", "vcmi.systemOptions.longTouchButton.hover" : "Long Touch Interval: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds", + "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds.", "vcmi.systemOptions.longTouchMenu.hover" : "Select Long Touch Interval", "vcmi.systemOptions.longTouchMenu.help" : "Change duration of long touch interval.", "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", "vcmi.systemOptions.framerateButton.hover" : "Show FPS", - "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window", + "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Enhancements", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a backpack button etc. Disable to have a more classic experience.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Large Spell Book", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Large Spell Book}\n\nEnables larger spell book that fits more spells per page. Spell book page change animation does not work with this setting enabled.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", "vcmi.adventureOptions.numericQuantities.hover" : "Numeric Creature Quantities", "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nShow the approximate quantities of enemy creatures in the numeric A-B format.", "vcmi.adventureOptions.forceMovementInfo.hover" : "Always Show Movement Cost", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information. (Instead of viewing it only while you hold down ALT key)", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information (Instead of viewing it only while you hold down ALT key).", "vcmi.adventureOptions.showGrid.hover" : "Show Grid", "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nShow the grid overlay, highlighting the borders between adventure map tiles.", "vcmi.adventureOptions.borderScroll.hover" : "Border Scrolling", "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.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.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.", "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", @@ -113,16 +141,16 @@ "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", "vcmi.battleOptions.queueSizeSmallButton.hover": "SMALL", "vcmi.battleOptions.queueSizeBigButton.hover": "BIG", - "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue", - "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL", - "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels)", + "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL.", + "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow", - "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast", - "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous", + "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow.", + "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast.", + "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous.", "vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover", "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Show range limits for shooters", @@ -130,7 +158,10 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows", "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", + + "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.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", @@ -146,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", + "vcmi.tutorialWindow.title" : "Touchscreen Introduction", + "vcmi.tutorialWindow.decription.RightClick" : "Touch and hold the element on which you want to right-click. Touch the free area to close.", + "vcmi.tutorialWindow.decription.MapPanning" : "Touch and drag with one finger to move the map.", + "vcmi.tutorialWindow.decription.MapZooming" : "Pinch with two fingers to change the map zoom.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Swiping opens radial wheel for various actions, such as creature/hero management and town ordering.", + "vcmi.tutorialWindow.decription.BattleDirection" : "To attack from a particular direction, swipe in the direction from which the attack is to be made.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "The attack direction gesture can be cancelled if the finger is far enough away.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Touch and hold to cancel a spell.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", @@ -172,21 +212,21 @@ "vcmi.logicalExpressions.noneOf" : "None of the following:", "vcmi.heroWindow.openCommander.hover" : "Open commander info window", - "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero", + "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.openBackpack.help" : "Opens window that allows easier artifact backpack management.", "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", - "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander", + "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", - "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander", + "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", "vcmi.creatureWindow.returnArtifact.hover" : "Return artifact", - "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack", + "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack.", "vcmi.questLog.hideComplete.hover" : "Hide complete quests", - "vcmi.questLog.hideComplete.help" : "Hide all completed quests", + "vcmi.questLog.hideComplete.help" : "Hide all completed quests.", "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", @@ -194,6 +234,68 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", + "vcmi.optionsTab.turnOptions.hover" : "Turn Options", + "vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options", + "vcmi.optionsTab.selectPreset" : "Preset", + + "vcmi.optionsTab.chessFieldBase.hover" : "Base timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Unit timer", + "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Any unspent time is lost", + "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost", + + "vcmi.optionsTab.accumulate" : "Accumulate", + + "vcmi.optionsTab.simturnsTitle" : "Simultaneous turns", + "vcmi.optionsTab.simturnsMin.hover" : "At least for", + "vcmi.optionsTab.simturnsMax.hover" : "At most for", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", + "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked", + "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player", + "vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.", + + "vcmi.optionsTab.turnTime.select" : "Select turn timer preset", + "vcmi.optionsTab.turnTime.unlimited" : "Unlimited turn time", + "vcmi.optionsTab.turnTime.classic.1" : "Classic timer: 1 minute", + "vcmi.optionsTab.turnTime.classic.2" : "Classic timer: 2 minutes", + "vcmi.optionsTab.turnTime.classic.5" : "Classic timer: 5 minutes", + "vcmi.optionsTab.turnTime.classic.10" : "Classic timer: 10 minutes", + "vcmi.optionsTab.turnTime.classic.20" : "Classic timer: 20 minutes", + "vcmi.optionsTab.turnTime.classic.30" : "Classic timer: 30 minutes", + "vcmi.optionsTab.turnTime.chess.20" : "Chess: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Chess: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Chess: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Chess: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Chess: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Chess: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Select simultaneous turns preset", + "vcmi.optionsTab.simturns.none" : "No simultaneous turns", + "vcmi.optionsTab.simturns.tillContactMax" : "Simturns: Until contact", + "vcmi.optionsTab.simturns.tillContact1" : "Simturns: 1 week, break on contact", + "vcmi.optionsTab.simturns.tillContact2" : "Simturns: 2 weeks, break on contact", + "vcmi.optionsTab.simturns.tillContact4" : "Simturns: 1 month, break on contact", + "vcmi.optionsTab.simturns.blocked1" : "Simturns: 1 week, contacts blocked", + "vcmi.optionsTab.simturns.blocked2" : "Simturns: 2 weeks, contacts blocked", + "vcmi.optionsTab.simturns.blocked4" : "Simturns: 1 month, contacts blocked", + + // 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 days", + "vcmi.optionsTab.simturns.days.1" : " %d day", + "vcmi.optionsTab.simturns.days.2" : " %d days", + "vcmi.optionsTab.simturns.weeks.0" : " %d weeks", + "vcmi.optionsTab.simturns.weeks.1" : " %d week", + "vcmi.optionsTab.simturns.weeks.2" : " %d weeks", + "vcmi.optionsTab.simturns.months.0" : " %d months", + "vcmi.optionsTab.simturns.months.1" : " %d month", + "vcmi.optionsTab.simturns.months.2" : " %d months", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 1597ab88d..377f3da83 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -30,8 +30,6 @@ "vcmi.capitalColors.6" : "Turquoise", "vcmi.capitalColors.7" : "Rose", - "vcmi.mainMenu.tutorialNotImplemented" : "Désolé, le didacticiel n'est pas encore implémenté\n", - "vcmi.mainMenu.highscoresNotImplemented" : "Désolé, le menu des meilleurs scores n'est pas encore implémenté\n", "vcmi.mainMenu.serverConnecting" : "Connexion...", "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", "vcmi.mainMenu.serverClosing" : "Fermeture...", @@ -39,9 +37,9 @@ "vcmi.mainMenu.joinTCP" : "Rejoindre TCP/IP jeu", "vcmi.mainMenu.playerName" : "Joueur", - "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", - "vcmi.server.errors.modsIncompatibility" : "Les mods suivants sont nécessaires pour charger le jeu :", - "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", + "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", + "vcmi.server.errors.modsToEnable" : "{Les mods suivants sont nécessaires pour charger le jeu}", + "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", "vcmi.settingsMainWindow.generalTab.hover" : "Général", "vcmi.settingsMainWindow.generalTab.help" : "Passe à l'onglet Options générales, qui contient des paramètres liés au comportement général du client de jeu", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index b2a92ce4b..07d13f06d 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -1,359 +1,461 @@ -{ - "vcmi.adventureMap.monsterThreat.title" : "\n\n Bedrohung: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "Mühelos", - "vcmi.adventureMap.monsterThreat.levels.1" : "Sehr schwach", - "vcmi.adventureMap.monsterThreat.levels.2" : "Schwach", - "vcmi.adventureMap.monsterThreat.levels.3" : "Ein bisschen schwächer", - "vcmi.adventureMap.monsterThreat.levels.4" : "Gleichauf", - "vcmi.adventureMap.monsterThreat.levels.5" : "Ein bisschen stärker", - "vcmi.adventureMap.monsterThreat.levels.6" : "Stark", - "vcmi.adventureMap.monsterThreat.levels.7" : "Sehr Stark", - "vcmi.adventureMap.monsterThreat.levels.8" : "Herausfordernd", - "vcmi.adventureMap.monsterThreat.levels.9" : "Überwältigend", - "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", - "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", - - "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", - "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", - "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", - "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", - "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.capitalColors.0" : "Rot", - "vcmi.capitalColors.1" : "Blau", - "vcmi.capitalColors.2" : "Braun", - "vcmi.capitalColors.3" : "Grün", - "vcmi.capitalColors.4" : "Orange", - "vcmi.capitalColors.5" : "Violett", - "vcmi.capitalColors.6" : "Türkis", - "vcmi.capitalColors.7" : "Rosa", - - "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", - "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", - "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", - "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", - "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", - - "vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n", - "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", - "vcmi.mainMenu.serverConnecting" : "Verbinde...", - "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", - "vcmi.mainMenu.serverClosing" : "Trenne...", - "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", - "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", - "vcmi.mainMenu.playerName" : "Spieler", - - "vcmi.lobby.filename" : "Dateiname", - "vcmi.lobby.creationDate" : "Erstellungsdatum", - - "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", - "vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:", - "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", - - "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", - "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", - "vcmi.settingsMainWindow.battleTab.hover" : "Kampf", - "vcmi.settingsMainWindow.battleTab.help" : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.", - "vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte", - "vcmi.settingsMainWindow.adventureTab.help" : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.", - - "vcmi.systemOptions.videoGroup" : "Video-Einstellungen", - "vcmi.systemOptions.audioGroup" : "Audio-Einstellungen", - "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now - "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", - - "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)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im exklusiven Vollbildmodus ausgeführt. In diesem Modus ändert das Spiel die Auflösung des Monitors auf die ausgewählte Auflösung.", - "vcmi.systemOptions.resolutionButton.hover" : "Auflösung: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Wähle Auflösung}\n\n Ändert die Spielauflösung.", - "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.scalingMenu.hover" : "Skalierung des Interfaces 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.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.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", - - "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", - "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", - "vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen", - "vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.", - "vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.", - "vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen", - "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", - "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", - "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.mapScrollSpeed1.hover": "", - "vcmi.adventureOptions.mapScrollSpeed5.hover": "", - "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen", - - "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", - "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", - "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN", - "vcmi.battleOptions.queueSizeBigButton.hover": "GROß", - "vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf", - "vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein", - "vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)", - "vcmi.battleOptions.animationsSpeed1.hover": "", - "vcmi.battleOptions.animationsSpeed5.hover": "", - "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Setzt die Animationsgeschwindigkeit auf sehr langsam", - "vcmi.battleOptions.animationsSpeed5.help": "Setzt die Animationsgeschwindigkeit auf sehr schnell", - "vcmi.battleOptions.animationsSpeed6.help": "Setzt die Animationsgeschwindigkeit auf sofort", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Hervorhebung der Bewegung bei Hover", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", - "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.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", - "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend", - "vcmi.battleWindow.damageEstimation.damage" : "%d Schaden", - "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", - "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", - - "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", - - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Wöchentliches Wachstum der Kreaturen anzeigen}\n\n Zeigt das wöchentliche Wachstum der Kreaturen anstelle der verfügbaren Menge in der Stadtübersicht (unten links).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakte Kreatur-Infos", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakte Kreatur-Infos}\n\n Kleinere Stadt-Kreaturen Informationen in der Stadtübersicht.", - - "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.logicalExpressions.anyOf" : "Eines der folgenden:", - "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", - "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", - - "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", - "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", - - "vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", - - "vcmi.creatureWindow.showBonuses.hover" : "Wechsle zur Bonus-Ansicht", - "vcmi.creatureWindow.showBonuses.help" : "Zeige alle aktiven Boni des Kommandanten", - "vcmi.creatureWindow.showSkills.hover" : "Wechsle zur Fertigkeits-Ansicht", - "vcmi.creatureWindow.showSkills.help" : "Zeige alle erlernten Fertigkeiten des Kommandanten", - "vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben", - "vcmi.creatureWindow.returnArtifact.help" : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben", - - "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", - "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", - - "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)", - "vcmi.randomMapTab.widgets.templateLabel" : "Template", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", - "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", - - // Custom victory conditions for H3 campaigns and HotA maps - "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", - "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!", - "vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte", - "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz", - - // few strings from WoG used by vcmi - "vcmi.stackExperience.description" : "» D e t a i l s z u r S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i", - "vcmi.stackExperience.rank.0" : "Grundlagen", - "vcmi.stackExperience.rank.1" : "Neuling", - "vcmi.stackExperience.rank.2" : "Ausgebildet", - "vcmi.stackExperience.rank.3" : "Kompetent", - "vcmi.stackExperience.rank.4" : "Bewährt", - "vcmi.stackExperience.rank.5" : "Veteran", - "vcmi.stackExperience.rank.6" : "Gekonnt", - "vcmi.stackExperience.rank.7" : "Experte", - "vcmi.stackExperience.rank.8" : "Elite", - "vcmi.stackExperience.rank.9" : "Meister", - "vcmi.stackExperience.rank.10" : "Ass", - - "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", - "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", - "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", - "core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten", - "core.bonus.AIR_IMMUNITY.name": "Luftimmunität", - "core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an", - "core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung", - "core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten", - "core.bonus.CATAPULT.name": "Katapult", - "core.bonus.CATAPULT.description": "Greift Belagerungsmauern an", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern", - "core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung", - "core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung", - "core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit", - "core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu", - "core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)", - "core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten", - "core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen", - "core.bonus.DESTRUCTION.name": "Zerstörung", - "core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% Chance auf doppelten Schaden", - "core.bonus.DRAGON_NATURE.name": "Drache", - "core.bonus.DRAGON_NATURE.description": "Kreatur hat eine Drachennatur", - "core.bonus.EARTH_IMMUNITY.name": "Erdimmunität", - "core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule", - "core.bonus.ENCHANTER.name": "Verzauberer", - "core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern", - "core.bonus.ENCHANTED.name": "Verzaubert", - "core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff", - "core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität", - "core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers", - "core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens", - "core.bonus.FIRST_STRIKE.name": "Erstschlag", - "core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten", - "core.bonus.FEAR.name": "Furcht", - "core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel", - "core.bonus.FEARLESS.name": "Furchtlos", - "core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht", - "core.bonus.FLYING.name": "Fliegen", - "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", - "core.bonus.FREE_SHOOTING.name": "Nah schießen", - "core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen", - "core.bonus.GARGOYLE.name": "Gargoyle", - "core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf", - "core.bonus.HATE.name": "Hasst ${subtype.creature}", - "core.bonus.HATE.description": "Macht ${val}% mehr Schaden", - "core.bonus.HEALER.name": "Heiler", - "core.bonus.HEALER.description": "Heilt verbündete Einheiten", - "core.bonus.HP_REGENERATION.name": "Regeneration", - "core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde", - "core.bonus.JOUSTING.name": "Champion Charge", - "core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld", - "core.bonus.KING.name": "König", - "core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind", - "core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens", - "core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%", - "core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird", - "core.bonus.MANA_DRAIN.name": "Mana-Entzug", - "core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde", - "core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken", - "core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen", - "core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität", - "core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist", - "core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe", - "core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung", - "core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe", - "core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus", - "core.bonus.NO_MORALE.name": "Neutrale Moral", - "core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte", - "core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe", - "core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung", - "core.bonus.NON_LIVING.name": "Nicht lebend", - "core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte", - "core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker", - "core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken", - "core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung", - "core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen", - "core.bonus.RECEPTIVE.name": "Empfänglich", - "core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber", - "core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)", - "core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen", - "core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr", - "core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück", - "core.bonus.SHOOTER.name": "Fernkämpfer", - "core.bonus.SHOOTER.description": "Kreatur kann schießen", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich", - "core.bonus.SOUL_STEAL.name": "Seelenraub", - "core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner", - "core.bonus.SPELLCASTER.name": "Zauberer", - "core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern", - "core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern", - "core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff", - "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität", - "core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff", - "core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand", - "core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören", - "core.bonus.SUMMON_GUARDIANS.description": "Beschwört bei Kampfbeginn ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergierbar", - "core.bonus.SYNERGY_TARGET.description": "Diese Kreatur ist anfällig für Synergieeffekte", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Atem", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Atem-Angriff (2-Hex-Bereich)", - "core.bonus.THREE_HEADED_ATTACK.name": "Dreiköpfiger Angriff", - "core.bonus.THREE_HEADED_ATTACK.description": "Greift drei benachbarte Einheiten an", - "core.bonus.TRANSMUTATION.name": "Transmutation", - "core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln", - "core.bonus.UNDEAD.name": "Untot", - "core.bonus.UNDEAD.description": "Kreatur ist untot", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen", - "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)" -} +{ + "vcmi.adventureMap.monsterThreat.title" : "\n\n Bedrohung: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Mühelos", + "vcmi.adventureMap.monsterThreat.levels.1" : "Sehr schwach", + "vcmi.adventureMap.monsterThreat.levels.2" : "Schwach", + "vcmi.adventureMap.monsterThreat.levels.3" : "Ein bisschen schwächer", + "vcmi.adventureMap.monsterThreat.levels.4" : "Gleichauf", + "vcmi.adventureMap.monsterThreat.levels.5" : "Ein bisschen stärker", + "vcmi.adventureMap.monsterThreat.levels.6" : "Stark", + "vcmi.adventureMap.monsterThreat.levels.7" : "Sehr Stark", + "vcmi.adventureMap.monsterThreat.levels.8" : "Herausfordernd", + "vcmi.adventureMap.monsterThreat.levels.9" : "Überwältigend", + "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", + "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", + + "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", + "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", + "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", + "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", + "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.capitalColors.0" : "Rot", + "vcmi.capitalColors.1" : "Blau", + "vcmi.capitalColors.2" : "Braun", + "vcmi.capitalColors.3" : "Grün", + "vcmi.capitalColors.4" : "Orange", + "vcmi.capitalColors.5" : "Violett", + "vcmi.capitalColors.6" : "Türkis", + "vcmi.capitalColors.7" : "Rosa", + + "vcmi.heroOverview.startingArmy" : "Starteinheiten", + "vcmi.heroOverview.warMachine" : "Kriegsmaschinen", + "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", + "vcmi.heroOverview.spells" : "Zaubersprüche", + + "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", + "vcmi.radialWheel.fillSingleUnit" : "Füllen mit einzelnen Kreaturen", + "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", + "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", + "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", + "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", + + "vcmi.radialWheel.heroGetArmy" : "Armee von anderem Helden erhalten", + "vcmi.radialWheel.heroSwapArmy" : "Tausche Armee mit anderem Helden", + "vcmi.radialWheel.heroExchange" : "Öffne Tausch-Menü", + "vcmi.radialWheel.heroGetArtifacts" : "Artefakte von anderen Helden erhalten", + "vcmi.radialWheel.heroSwapArtifacts" : "Tausche Artefakte mit anderen Helden", + "vcmi.radialWheel.heroDismiss" : "Held entlassen", + + "vcmi.radialWheel.moveTop" : "Ganz nach oben bewegen", + "vcmi.radialWheel.moveUp" : "Nach oben bewegen", + "vcmi.radialWheel.moveDown" : "Nach unten bewegen", + "vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen", + + "vcmi.mainMenu.serverConnecting" : "Verbinde...", + "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", + "vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen", + "vcmi.mainMenu.serverClosing" : "Trenne...", + "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", + "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", + "vcmi.mainMenu.playerName" : "Spieler", + + "vcmi.lobby.filepath" : "Dateipfad", + "vcmi.lobby.creationDate" : "Erstellungsdatum", + "vcmi.lobby.scenarioName" : "Szenario-Name", + "vcmi.lobby.mapPreview" : "Kartenvorschau", + "vcmi.lobby.noPreview" : "Keine Vorschau", + "vcmi.lobby.noUnderground" : "Kein Untergrund", + + "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.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + "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.settingsMainWindow.generalTab.hover" : "Allgemein", + "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", + "vcmi.settingsMainWindow.battleTab.hover" : "Kampf", + "vcmi.settingsMainWindow.battleTab.help" : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.", + "vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte", + "vcmi.settingsMainWindow.adventureTab.help" : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.", + + "vcmi.systemOptions.videoGroup" : "Video-Einstellungen", + "vcmi.systemOptions.audioGroup" : "Audio-Einstellungen", + "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now + "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", + + "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)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im exklusiven Vollbildmodus ausgeführt. In diesem Modus ändert das Spiel die Auflösung des Monitors auf die ausgewählte Auflösung.", + "vcmi.systemOptions.resolutionButton.hover" : "Auflösung: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Wähle Auflösung}\n\n Ändert die Spielauflösung.", + "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.scalingMenu.hover" : "Skalierung des Interfaces 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.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.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein Rucksack-Button, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Großes Zauberbuch", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Großes Zauberbuch}\n\nErmöglicht ein größeres Zauberbuch, in das mehr Zaubersprüche pro Seite passen. Die Animation des Seitenwechsels im Zauberbuch funktioniert nicht, wenn diese Einstellung aktiviert ist.", + + "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", + "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", + "vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen", + "vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.", + "vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen", + "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", + "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", + "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.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen", + + "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", + "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", + "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN", + "vcmi.battleOptions.queueSizeBigButton.hover": "GROß", + "vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf", + "vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)", + "vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein", + "vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Setzt die Animationsgeschwindigkeit auf sehr langsam", + "vcmi.battleOptions.animationsSpeed5.help": "Setzt die Animationsgeschwindigkeit auf sehr schnell", + "vcmi.battleOptions.animationsSpeed6.help": "Setzt die Animationsgeschwindigkeit auf sofort", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Hervorhebung der Bewegung bei Hover", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", + "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.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.", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", + "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend", + "vcmi.battleWindow.damageEstimation.damage" : "%d Schaden", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", + "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", + + "vcmi.tutorialWindow.title" : "Touchscreen Einführung", + "vcmi.tutorialWindow.decription.RightClick" : "Berührt und haltet das Element, auf das mit der rechten Maustaste geklickt werden soll. Berührt den freien Bereich, um zu schließen.", + "vcmi.tutorialWindow.decription.MapPanning" : "Berührt und zieht mit einem Finger, um die Karte zu verschieben.", + "vcmi.tutorialWindow.decription.MapZooming" : "Berührt mit zwei Fingern, um den Kartenzoom zu ändern.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Durch Streichen öffnet sich das Radialrad für verschiedene Aktionen, wie z.B. Kreaturen-/Heldenverwaltung und Stadtreihenfolge.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Um aus einer bestimmten Richtung anzugreifen, wischt in die Richtung, aus der der Angriff erfolgen soll.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Die Geste für die Angriffsrichtung kann abgebrochen werden, wenn der Finger weit genug entfernt ist.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Berühren und halten, um einen Zauber abzubrechen.", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Wöchentliches Wachstum der Kreaturen anzeigen}\n\n Zeigt das wöchentliche Wachstum der Kreaturen anstelle der verfügbaren Menge in der Stadtübersicht (unten links).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakte Kreatur-Infos", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakte Kreatur-Infos}\n\n Kleinere Stadt-Kreaturen Informationen in der Stadtübersicht.", + + "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.logicalExpressions.anyOf" : "Eines der folgenden:", + "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", + "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", + + "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", + "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.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", + + "vcmi.creatureWindow.showBonuses.hover" : "Wechsle zur Bonus-Ansicht", + "vcmi.creatureWindow.showBonuses.help" : "Zeige alle aktiven Boni des Kommandanten", + "vcmi.creatureWindow.showSkills.hover" : "Wechsle zur Fertigkeits-Ansicht", + "vcmi.creatureWindow.showSkills.help" : "Zeige alle erlernten Fertigkeiten des Kommandanten", + "vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben", + "vcmi.creatureWindow.returnArtifact.help" : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben", + + "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", + "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)", + "vcmi.randomMapTab.widgets.templateLabel" : "Template", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", + + "vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen", + "vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen", + + "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Einheiten-Timer", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Der Rest wird am Ende des Zuges der Einheit zum {Kampf-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn des Zuges jeder Einheit zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + + "vcmi.optionsTab.accumulate" : "Akkumulieren", + + "vcmi.optionsTab.turnTime.select" : "Spielzug-Timer-Voreinst. wählen", + "vcmi.optionsTab.turnTime.unlimited" : "Unbegrenzter Spielzug-Timer", + "vcmi.optionsTab.turnTime.classic.1" : "Klassischer Timer: 1 Minute", + "vcmi.optionsTab.turnTime.classic.2" : "Klassischer Timer: 2 Minuten", + "vcmi.optionsTab.turnTime.classic.5" : "Klassischer Timer: 5 Minuten", + "vcmi.optionsTab.turnTime.classic.10" : "Klassischer Timer: 10 Minuten", + "vcmi.optionsTab.turnTime.classic.20" : "Klassischer Timer: 20 Minuten", + "vcmi.optionsTab.turnTime.classic.30" : "Klassischer Timer: 30 Minuten", + "vcmi.optionsTab.turnTime.chess.20" : "Schach: 20:00 10:00 02:00 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Schach: 16:00 08:00 01:30 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Schach: 08:00 04:00 01:00 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Schach: 04:00 02:00 00:30 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Schach: 02:00 01:00 00:15 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Schach: 01:00 01:00 00:00 00:00", + + "vcmi.optionsTab.simturns.select" : "Voreinst. für simultane Züge wählen", + "vcmi.optionsTab.simturns.none" : "Keine simultanen Züge", + "vcmi.optionsTab.simturns.tillContactMax" : "Simzüge: Bis zum Kontakt", + "vcmi.optionsTab.simturns.tillContact1" : "Simzüge: 1 Woche, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact2" : "Simzüge: 2 Wochen, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact4" : "Simzüge: 1 Monat, Stop bei Kontakt", + "vcmi.optionsTab.simturns.blocked1" : "Simzüge: 1 Woche, Kontakte block.", + "vcmi.optionsTab.simturns.blocked2" : "Simzüge: 2 Wochen, Kontakte block.", + "vcmi.optionsTab.simturns.blocked4" : "Simzüge: 1 Monat, Kontakte block.", + + "vcmi.optionsTab.simturnsTitle" : "Simultane Züge", + "vcmi.optionsTab.simturnsMin.hover" : "Zumindest für", + "vcmi.optionsTab.simturnsMax.hover" : "Höchstens für", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge", + "vcmi.optionsTab.simturnsMin.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen. Die Kontakte zwischen den Spielern sind während dieser Zeit blockiert", + "vcmi.optionsTab.simturnsMax.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen oder bis zum Kontakt mit einem anderen Spieler", + "vcmi.optionsTab.simturnsAI.help" : "{Simultane KI Züge}\nExperimentelle Option. Ermöglicht es den KI-Spielern, gleichzeitig mit dem menschlichen Spieler zu agieren, wenn simultane Spielzüge aktiviert sind.", + + // 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 Tage", + "vcmi.optionsTab.simturns.days.1" : "%d Tag", + "vcmi.optionsTab.simturns.days.2" : "%d Tage", + "vcmi.optionsTab.simturns.weeks.0" : "%d Wochen", + "vcmi.optionsTab.simturns.weeks.1" : "%d Woche", + "vcmi.optionsTab.simturns.weeks.2" : "%d Wochen", + "vcmi.optionsTab.simturns.months.0" : "%d Monate", + "vcmi.optionsTab.simturns.months.1" : "%d Monat", + "vcmi.optionsTab.simturns.months.2" : "%d Monate", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» D e t a i l s z u r S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i", + "vcmi.stackExperience.rank.0" : "Grundlagen", + "vcmi.stackExperience.rank.1" : "Neuling", + "vcmi.stackExperience.rank.2" : "Ausgebildet", + "vcmi.stackExperience.rank.3" : "Kompetent", + "vcmi.stackExperience.rank.4" : "Bewährt", + "vcmi.stackExperience.rank.5" : "Veteran", + "vcmi.stackExperience.rank.6" : "Gekonnt", + "vcmi.stackExperience.rank.7" : "Experte", + "vcmi.stackExperience.rank.8" : "Elite", + "vcmi.stackExperience.rank.9" : "Meister", + "vcmi.stackExperience.rank.10" : "Ass", + + "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", + "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", + "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", + "core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten", + "core.bonus.AIR_IMMUNITY.name": "Luftimmunität", + "core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an", + "core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung", + "core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten", + "core.bonus.CATAPULT.name": "Katapult", + "core.bonus.CATAPULT.description": "Greift Belagerungsmauern an", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern", + "core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung", + "core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung", + "core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit", + "core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu", + "core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)", + "core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten", + "core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen", + "core.bonus.DESTRUCTION.name": "Zerstörung", + "core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% Chance auf doppelten Schaden", + "core.bonus.DRAGON_NATURE.name": "Drache", + "core.bonus.DRAGON_NATURE.description": "Kreatur hat eine Drachennatur", + "core.bonus.EARTH_IMMUNITY.name": "Erdimmunität", + "core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule", + "core.bonus.ENCHANTER.name": "Verzauberer", + "core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern", + "core.bonus.ENCHANTED.name": "Verzaubert", + "core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff", + "core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität", + "core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers", + "core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens", + "core.bonus.FIRST_STRIKE.name": "Erstschlag", + "core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten", + "core.bonus.FEAR.name": "Furcht", + "core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel", + "core.bonus.FEARLESS.name": "Furchtlos", + "core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht", + "core.bonus.FLYING.name": "Fliegen", + "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", + "core.bonus.FREE_SHOOTING.name": "Nah schießen", + "core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf", + "core.bonus.HATE.name": "Hasst ${subtype.creature}", + "core.bonus.HATE.description": "Macht ${val}% mehr Schaden", + "core.bonus.HEALER.name": "Heiler", + "core.bonus.HEALER.description": "Heilt verbündete Einheiten", + "core.bonus.HP_REGENERATION.name": "Regeneration", + "core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde", + "core.bonus.JOUSTING.name": "Champion Charge", + "core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld", + "core.bonus.KING.name": "König", + "core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind", + "core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens", + "core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%", + "core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird", + "core.bonus.MANA_DRAIN.name": "Mana-Entzug", + "core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde", + "core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken", + "core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen", + "core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität", + "core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist", + "core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe", + "core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung", + "core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe", + "core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus", + "core.bonus.NO_MORALE.name": "Neutrale Moral", + "core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte", + "core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe", + "core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung", + "core.bonus.NON_LIVING.name": "Nicht lebend", + "core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte", + "core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker", + "core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken", + "core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung", + "core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen", + "core.bonus.RECEPTIVE.name": "Empfänglich", + "core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber", + "core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)", + "core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen", + "core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr", + "core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück", + "core.bonus.SHOOTER.name": "Fernkämpfer", + "core.bonus.SHOOTER.description": "Kreatur kann schießen", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich", + "core.bonus.SOUL_STEAL.name": "Seelenraub", + "core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner", + "core.bonus.SPELLCASTER.name": "Zauberer", + "core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern", + "core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern", + "core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.", + "core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität", + "core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff", + "core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand", + "core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören", + "core.bonus.SUMMON_GUARDIANS.description": "Beschwört bei Kampfbeginn ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Synergierbar", + "core.bonus.SYNERGY_TARGET.description": "Diese Kreatur ist anfällig für Synergieeffekte", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Atem", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Atem-Angriff (2-Hex-Bereich)", + "core.bonus.THREE_HEADED_ATTACK.name": "Dreiköpfiger Angriff", + "core.bonus.THREE_HEADED_ATTACK.description": "Greift drei benachbarte Einheiten an", + "core.bonus.TRANSMUTATION.name": "Transmutation", + "core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln", + "core.bonus.UNDEAD.name": "Untot", + "core.bonus.UNDEAD.description": "Kreatur ist untot", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen", + "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)" +} diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 9f175e83e..1028d8349 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -30,27 +30,52 @@ "vcmi.capitalColors.6" : "Jasnoniebieski", "vcmi.capitalColors.7" : "Różowy", + "vcmi.heroOverview.startingArmy" : "Jednostki startowe", + "vcmi.heroOverview.warMachine" : "Machiny wojenne", + "vcmi.heroOverview.secondarySkills" : "Umiejętności drugorzędne", + "vcmi.heroOverview.spells" : "Zaklęcia", + "vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia", + "vcmi.radialWheel.fillSingleUnit" : "Wypełnij pojedynczymi stworzeniami", "vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie", "vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo", "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", - "vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n", - "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", + "vcmi.radialWheel.heroGetArmy" : "Weź armię z innego bohatera", + "vcmi.radialWheel.heroSwapArmy" : "Zamień armię z innym bohaterem", + "vcmi.radialWheel.heroExchange" : "Rozpocznij wymianę między bohaterami", + "vcmi.radialWheel.heroGetArtifacts" : "Weź artefakty z innego bohatera", + "vcmi.radialWheel.heroSwapArtifacts" : "Zamień artefakty z innym bohaterem", + "vcmi.radialWheel.heroDismiss" : "Dymisja bohatera", + + "vcmi.radialWheel.moveTop" : "Przenieś na początek", + "vcmi.radialWheel.moveUp" : "Przenieś w górę", + "vcmi.radialWheel.moveDown" : "Przenieś w dół", + "vcmi.radialWheel.moveBottom" : "Przenieś na spód", + "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", + "vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się", "vcmi.mainMenu.serverClosing" : "Zamykanie...", "vcmi.mainMenu.hostTCP" : "Hostuj grę TCP/IP", "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", "vcmi.mainMenu.playerName" : "Gracz", - "vcmi.lobby.filename" : "Nazwa pliku", + "vcmi.lobby.filepath" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", + "vcmi.lobby.scenarioName" : "Nazwa scenariusza", + "vcmi.lobby.mapPreview" : "Podgląd mapy", + "vcmi.lobby.noPreview" : "brak podglądu", + "vcmi.lobby.noUnderground" : "brak podziemi", - "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", - "vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:", - "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", + "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.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.settingsMainWindow.generalTab.hover" : "Ogólne", "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", @@ -85,6 +110,10 @@ "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Ulepszenia interfejsu", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Duża księga zaklęć", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", @@ -131,6 +160,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe", "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.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.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo", "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", @@ -145,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Zatwierdź wynik bitwy", + "vcmi.tutorialWindow.title" : "Samouczek ekranu dotykowego", + "vcmi.tutorialWindow.decription.RightClick" : "Dotknij i przytrzymaj palec na elemencie na którym chcesz wykonać akcję prawego przycisku myszy. Dotknij wolny obszar by zamknąć.", + "vcmi.tutorialWindow.decription.MapPanning" : "Dotknij i przeciągnij jednym palcem by przesunąć mapę.", + "vcmi.tutorialWindow.decription.MapZooming" : "Za pomocą gestu szczypania / rozwierania dwóch palców możesz zmieniać powiększenie mapy.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Przeciąganie palcem otwiera kołowe menu dla różnych akcji takich jak zarządzanie stworzeniami/bohaterami i sortowanie miast.", + "vcmi.tutorialWindow.decription.BattleDirection" : "By zaatakować z określonego kierunku przeciągnij palcem w stronę kierunku z którego chcesz zaatakować.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Atak z wyborem kierunku może zostać anulowany jeśli palec znajdzie się wystarczająco daleko.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Naciśnij i przytrzymaj by anulować rzucenie zaklęcia.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń", @@ -193,6 +234,68 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Typy dróg", + "vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur", + "vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu (timery) oraz tury równoczesne", + "vcmi.optionsTab.selectPreset" : "Szablonowe ustawienie", + + "vcmi.optionsTab.chessFieldBase.hover" : "Timer startowy", + "vcmi.optionsTab.chessFieldTurn.hover" : "Timer tury", + "vcmi.optionsTab.chessFieldBattle.hover" : "Timer bitwy", + "vcmi.optionsTab.chessFieldUnit.hover" : "Timer jednostki", + "vcmi.optionsTab.chessFieldBase.help" : "Używany gdy {Timer tury} osiągnie 0. Ustawiany raz przy starcie gry. Gdy osiągnie 0 tura się kończy, a trwająca bitwa zostanie przegrana.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Nadwyżka czasu dodaje się do {Timera startowego} pod koniec tury.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Niewykorzystany czas zostaje utracony.", + "vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Timer jednostki} się wyczerpie w bitwie pomiędzy graczami. Odnawia się przy starcie każdej bitwy.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Nadwyżka czasu dodaje się do {Timera bitwy}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Resetuje się przy rozpoczęciu akcji jednostką.", + + "vcmi.optionsTab.accumulate" : "Akumuluj", + + "vcmi.optionsTab.simturnsTitle" : "Tury równoczesne / symultaniczne", + "vcmi.optionsTab.simturnsMin.hover" : "Co najmniej przez", + "vcmi.optionsTab.simturnsMax.hover" : "Maks. przez", + "vcmi.optionsTab.simturnsAI.hover" : "(Eksperymentalne) Równoczesne tury graczy AI", + "vcmi.optionsTab.simturnsMin.help" : "Graj równocześnie przez określoną liczbę dni. Kontakt pomiędzy graczami do tego czasu jest zablokowany.", + "vcmi.optionsTab.simturnsMax.help" : "Graj równocześnie przez określoną liczbę dni lub do momentu napotkania innego gracza.", + "vcmi.optionsTab.simturnsAI.help" : "{Równoczesne tury graczy AI}\nOpcja eksperymentalna. Pozwala graczom sterowanym przez komputer wykonywać akcje w tym samym czasie co gracz ludzki gdy jednoczesne tury są włączone.", + + "vcmi.optionsTab.turnTime.select" : "Predefiniowane schematy zegarów", + "vcmi.optionsTab.turnTime.unlimited" : "Nieograniczony czas tury", + "vcmi.optionsTab.turnTime.classic.1" : "Klasyczny: 1 minuta", + "vcmi.optionsTab.turnTime.classic.2" : "Klasyczny: 2 minuty", + "vcmi.optionsTab.turnTime.classic.5" : "Klasyczny: 5 minut", + "vcmi.optionsTab.turnTime.classic.10" : "Klasyczny: 10 minut", + "vcmi.optionsTab.turnTime.classic.20" : "Klasyczny: 20 minut", + "vcmi.optionsTab.turnTime.classic.30" : "Klasyczny: 30 minut", + "vcmi.optionsTab.turnTime.chess.20" : "Szach: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Szach: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Szach: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Szach: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Szach: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Szach: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Predefiniowane schematy tur sym.", + "vcmi.optionsTab.simturns.none" : "Brak tur symultanicznych / równocz.", + "vcmi.optionsTab.simturns.tillContactMax" : "Tury sym.: Do kontaktu", + "vcmi.optionsTab.simturns.tillContact1" : "Tury sym.: 1 tydz., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.tillContact2" : "Tury sym.: 2 tyg., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.tillContact4" : "Tury sym.: 1 mies., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.blocked1" : "Tury sym.: 1 tydz., kontakt zablokowany", + "vcmi.optionsTab.simturns.blocked2" : "Tury sym.: 2 tyg., kontakt zablokowany", + "vcmi.optionsTab.simturns.blocked4" : "Tury sym.: 1 mies., kontakt zablokowany", + + // 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 dni", + "vcmi.optionsTab.simturns.days.1" : " %d dzień", + "vcmi.optionsTab.simturns.days.2" : " %d dni", + "vcmi.optionsTab.simturns.weeks.0" : " %d tygodni", + "vcmi.optionsTab.simturns.weeks.1" : " %d tydzień", + "vcmi.optionsTab.simturns.weeks.2" : " %d tygodnie", + "vcmi.optionsTab.simturns.months.0" : " %d miesięcy", + "vcmi.optionsTab.simturns.months.1" : " %d miesiąc", + "vcmi.optionsTab.simturns.months.2" : " %d miesiące", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Wróg dał radę przetrwać do dzisiejszego dnia. Zwycięstwo należy do niego!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulacje! Dałeś radę przetrwać. Zwycięstwo jest twoje!", diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON index 738cf04c2..f98ee579f 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON @@ -1,188 +1,188 @@ -{ - "Around A Marsh" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 9500, "density" : 1 }, - { "min" : 3500, "max" : 5500, "density" : 3 }, - { "min" : 350, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 28000, "density" : 1 }, - { "min" : 6000, "max" : 9500, "density" : 7 }, - { "min" : 350, "max" : 2000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "mercury" : 1, "crystal" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 120000, "density" : 6 }, - { "min" : 9000, "max" : 9500, "density" : 1 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "3", "b" : "7", "guard" : 5000 }, - { "a" : "4", "b" : "8", "guard" : 5000 }, - { "a" : "5", "b" : "6", "guard" : 25000 }, - { "a" : "6", "b" : "7", "guard" : 35000 }, - { "a" : "7", "b" : "8", "guard" : 25000 }, - { "a" : "5", "b" : "8", "guard" : 35000 }, - - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "11", "b" : "12", "guard" : 0 }, - { "a" : "11", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 } - ] - } -} +{ + "Around A Marsh" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 9500, "density" : 1 }, + { "min" : 3500, "max" : 5500, "density" : 3 }, + { "min" : 350, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 28000, "density" : 1 }, + { "min" : 6000, "max" : 9500, "density" : 7 }, + { "min" : 350, "max" : 2000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "mercury" : 1, "crystal" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 120000, "density" : 6 }, + { "min" : 9000, "max" : 9500, "density" : 1 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "3", "b" : "7", "guard" : 5000 }, + { "a" : "4", "b" : "8", "guard" : 5000 }, + { "a" : "5", "b" : "6", "guard" : 25000 }, + { "a" : "6", "b" : "7", "guard" : 35000 }, + { "a" : "7", "b" : "8", "guard" : 25000 }, + { "a" : "5", "b" : "8", "guard" : 35000 }, + + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "11", "b" : "12", "guard" : 0 }, + { "a" : "11", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON index f8d95f0cb..30e62b040 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON @@ -1,532 +1,532 @@ -{ - "Balance M" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 7 }, - { "min" : 8000, "max" : 9500, "density" : 7 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 5 }, - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 6000, "max" : 8000, "density" : 7 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "2", "b" : "9", "guard" : 5000 }, - { "a" : "6", "b" : "3", "guard" : 8000 }, - { "a" : "7", "b" : "4", "guard" : 9000 }, - { "a" : "8", "b" : "3", "guard" : 8000 }, - { "a" : "9", "b" : "4", "guard" : 9000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 20000 }, - { "a" : "2", "b" : "5", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 14000 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - }, - "Balance L" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 4900, "density" : 6 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "mercury" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 7 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 3 }, - { "min" : 8000, "max" : 9500, "density" : 5 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6500 }, - { "a" : "6", "b" : "3", "guard" : 11000 }, - { "a" : "7", "b" : "4", "guard" : 12000 }, - { "a" : "8", "b" : "3", "guard" : 11000 }, - { "a" : "9", "b" : "4", "guard" : 12000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 30000 }, - { "a" : "2", "b" : "5", "guard" : 30000 }, - { "a" : "5", "b" : "10", "guard" : 22000 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - }, - "Balance XL" : - { - "minSize" : "xl", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 4900, "density" : 6 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "mercury" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 6 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 3 }, - { "min" : 8000, "max" : 9500, "density" : 5 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 5 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 11 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6500 }, - { "a" : "1", "b" : "7", "guard" : 7000 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "9", "guard" : 7000 }, - { "a" : "6", "b" : "3", "guard" : 12000 }, - { "a" : "7", "b" : "4", "guard" : 13000 }, - { "a" : "8", "b" : "3", "guard" : 12000 }, - { "a" : "9", "b" : "4", "guard" : 13000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 40000 }, - { "a" : "2", "b" : "5", "guard" : 40000 }, - { "a" : "5", "b" : "10", "guard" : 30000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "1", "b" : "11", "guard" : 6500 }, - { "a" : "2", "b" : "12", "guard" : 6500 }, - { "a" : "11", "b" : "3", "guard" : 12000 }, - { "a" : "12", "b" : "3", "guard" : 12000 }, - { "a" : "11", "b" : "4", "guard" : 13000 }, - { "a" : "12", "b" : "4", "guard" : 13000 } - ] - } -} +{ + "Balance M" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 7 }, + { "min" : 8000, "max" : 9500, "density" : 7 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 5 }, + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 6000, "max" : 8000, "density" : 7 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "2", "b" : "9", "guard" : 5000 }, + { "a" : "6", "b" : "3", "guard" : 8000 }, + { "a" : "7", "b" : "4", "guard" : 9000 }, + { "a" : "8", "b" : "3", "guard" : 8000 }, + { "a" : "9", "b" : "4", "guard" : 9000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 20000 }, + { "a" : "2", "b" : "5", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 14000 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + }, + "Balance L" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 4900, "density" : 6 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "mercury" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 7 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 3 }, + { "min" : 8000, "max" : 9500, "density" : 5 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6500 }, + { "a" : "6", "b" : "3", "guard" : 11000 }, + { "a" : "7", "b" : "4", "guard" : 12000 }, + { "a" : "8", "b" : "3", "guard" : 11000 }, + { "a" : "9", "b" : "4", "guard" : 12000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 30000 }, + { "a" : "2", "b" : "5", "guard" : 30000 }, + { "a" : "5", "b" : "10", "guard" : 22000 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + }, + "Balance XL" : + { + "minSize" : "xl", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 4900, "density" : 6 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "mercury" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 6 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 3 }, + { "min" : 8000, "max" : 9500, "density" : 5 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 5 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 11 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6500 }, + { "a" : "1", "b" : "7", "guard" : 7000 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "9", "guard" : 7000 }, + { "a" : "6", "b" : "3", "guard" : 12000 }, + { "a" : "7", "b" : "4", "guard" : 13000 }, + { "a" : "8", "b" : "3", "guard" : 12000 }, + { "a" : "9", "b" : "4", "guard" : 13000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 40000 }, + { "a" : "2", "b" : "5", "guard" : 40000 }, + { "a" : "5", "b" : "10", "guard" : 30000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "1", "b" : "11", "guard" : 6500 }, + { "a" : "2", "b" : "12", "guard" : 6500 }, + { "a" : "11", "b" : "3", "guard" : 12000 }, + { "a" : "12", "b" : "3", "guard" : 12000 }, + { "a" : "11", "b" : "4", "guard" : 13000 }, + { "a" : "12", "b" : "4", "guard" : 13000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON index ff5c7d684..0cccafbb0 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON @@ -1,372 +1,372 @@ -{ - "Blockbuster M" : - //(ban fly/DD, 2 player, 15-Jun-03, midnight design) - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 6000, "density" : 4 }, - { "min" : 800, "max" : 2000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9800, "density" : 3 }, - { "min" : 3500, "max" : 8000, "density" : 10 }, - { "min" : 800, "max" : 1200, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 9000, "max" : 9800, "density" : 2 }, - { "min" : 3500, "max" : 8999, "density" : 8 }, - { "min" : 800, "max" : 1200, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 18000, "max" : 25000, "density" : 2 }, - { "min" : 9000, "max" : 9800, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 50000 }, - { "a" : "1", "b" : "3", "guard" : 4000 }, - { "a" : "1", "b" : "5", "guard" : 8000 }, - { "a" : "2", "b" : "4", "guard" : 4000 }, - { "a" : "2", "b" : "6", "guard" : 8000 }, - { "a" : "5", "b" : "7", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 } - ] - }, - "Blockbuster L" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 5500, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 320, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9100, "density" : 4 }, - { "min" : 3500, "max" : 8000, "density" : 5 }, - { "min" : 800, "max" : 2000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 6000, "max" : 9300, "density" : 8 }, - { "min" : 800, "max" : 1200, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 18000, "max" : 25000, "density" : 2 }, - { "min" : 0, "max" : 45000, "density" : 6 }, - { "min" : 8000, "max" : 9300, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 7, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 110000 }, - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "1", "b" : "5", "guard" : 15000 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "2", "b" : "6", "guard" : 15000 }, - { "a" : "5", "b" : "7", "guard" : 24000 }, - { "a" : "6", "b" : "8", "guard" : 24000 }, - { "a" : "7", "b" : "8", "guard" : 40000 } - ] - }, - "Blockbuster XL" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 5500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 3 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9100, "density" : 3 }, - { "min" : 3500, "max" : 8000, "density" : 4 }, - { "min" : 800, "max" : 2000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 6000, "max" : 9200, "density" : 6 }, - { "min" : 800, "max" : 2000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 28000, "max" : 29000, "density" : 1 }, - { "min" : 0, "max" : 50000, "density" : 5 }, - { "min" : 7500, "max" : 9200, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 7, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 140000 }, - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 17000 }, - { "a" : "2", "b" : "4", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 17000 }, - { "a" : "5", "b" : "7", "guard" : 30000 }, - { "a" : "6", "b" : "8", "guard" : 30000 }, - { "a" : "7", "b" : "8", "guard" : 50000 } - ] - } -} +{ + "Blockbuster M" : + //(ban fly/DD, 2 player, 15-Jun-03, midnight design) + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 6000, "density" : 4 }, + { "min" : 800, "max" : 2000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9800, "density" : 3 }, + { "min" : 3500, "max" : 8000, "density" : 10 }, + { "min" : 800, "max" : 1200, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 9000, "max" : 9800, "density" : 2 }, + { "min" : 3500, "max" : 8999, "density" : 8 }, + { "min" : 800, "max" : 1200, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 18000, "max" : 25000, "density" : 2 }, + { "min" : 9000, "max" : 9800, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 50000 }, + { "a" : "1", "b" : "3", "guard" : 4000 }, + { "a" : "1", "b" : "5", "guard" : 8000 }, + { "a" : "2", "b" : "4", "guard" : 4000 }, + { "a" : "2", "b" : "6", "guard" : 8000 }, + { "a" : "5", "b" : "7", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 } + ] + }, + "Blockbuster L" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 5500, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 320, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9100, "density" : 4 }, + { "min" : 3500, "max" : 8000, "density" : 5 }, + { "min" : 800, "max" : 2000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 6000, "max" : 9300, "density" : 8 }, + { "min" : 800, "max" : 1200, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 18000, "max" : 25000, "density" : 2 }, + { "min" : 0, "max" : 45000, "density" : 6 }, + { "min" : 8000, "max" : 9300, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 7, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 110000 }, + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "1", "b" : "5", "guard" : 15000 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "2", "b" : "6", "guard" : 15000 }, + { "a" : "5", "b" : "7", "guard" : 24000 }, + { "a" : "6", "b" : "8", "guard" : 24000 }, + { "a" : "7", "b" : "8", "guard" : 40000 } + ] + }, + "Blockbuster XL" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 5500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 3 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9100, "density" : 3 }, + { "min" : 3500, "max" : 8000, "density" : 4 }, + { "min" : 800, "max" : 2000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 6000, "max" : 9200, "density" : 6 }, + { "min" : 800, "max" : 2000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 28000, "max" : 29000, "density" : 1 }, + { "min" : 0, "max" : 50000, "density" : 5 }, + { "min" : 7500, "max" : 9200, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 7, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 140000 }, + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 17000 }, + { "a" : "2", "b" : "4", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 17000 }, + { "a" : "5", "b" : "7", "guard" : 30000 }, + { "a" : "6", "b" : "8", "guard" : 30000 }, + { "a" : "7", "b" : "8", "guard" : 50000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json index d05d2aa2d..d62bdde01 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json @@ -1,239 +1,239 @@ -{ - "Coldshadow's Fantasy": - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "4-8", "cpu" : "3-6", - "zones": - { - "1": - { - "type" : "playerStart", "size" : 30, "owner" : 1, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "normal", - "mines" : {"wood" : 2, "ore" : 2, "gems" : 1, "crystal" : 1, "sulfur" : 1, "mercury" : 1, "gold" : 1}, - "treasure" : [ - {"min" : 7500, "max": 25000, "density": 4}, - {"min" : 3000, "max": 9000, "density": 6}, - {"min" : 300, "max": 3000, "density": 8} - ] - }, - "2": - { - "type" : "cpuStart", "size" : 30, "owner" : 2, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3": - { - "type" : "cpuStart", "size" : 30, "owner" : 3, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4": - { - "type" : "cpuStart", "size" : 30, "owner" : 4, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5": - { - "type" : "playerStart", "size" : 30, "owner" : 5, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6": - { - "type" : "cpuStart", "size" : 30, "owner" : 6, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7": - { - "type" : "cpuStart", "size" : 30, "owner" : 7, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8": - { - "type" : "cpuStart", "size" : 30, "owner" : 8, - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "monsters" : "weak", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9": - { - "type" : "treasure", "size" : 15, - "terrainTypes" : ["subterra"], "matchTerrainToTown" : false, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "strong", - "mines" : {"gems" : 1, "sulfur" : 1, "mercury" : 1, "crystal" : 1}, - "treasure" : [ - {"min" : 45000, "max": 75000, "density": 3}, - {"min" : 15000, "max": 50000, "density": 3}, - {"min" : 3080, "max": 12500, "density": 4} - ] - }, - "10": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "strong", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "15": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "16": - { - "type" : "treasure", "size" : 15, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "normal", - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "17": - { - "type" : "junction", "size" : 30, - "terrainTypeLikeZone" : 9, - "allowedTowns" : ["neutral"], - "monsters" : "strong", - "mines" : {"gold" : 1}, - "treasure" : [ - {"min" : 65000, "max": 100000, "density": 3}, - {"min" : 50000, "max": 100000, "density": 3}, - {"min" : 10000, "max": 15000, "density": 3} - ] - }, - "18": - { - "type" : "junction", "size" : 30, - "terrainTypeLikeZone" : 9, - "allowedTowns" : ["neutral"], - "monsters" : "strong", - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "19": - { - "type" : "junction", "size" : 30, - "terrainTypeLikeZone" : 9, - "allowedTowns" : ["neutral"], - "monsters" : "strong", - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "20": - { - "type" : "junction", "size" : 30, - "terrainTypeLikeZone" : 9, - "allowedTowns" : ["neutral"], - "monsters" : "strong", - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "21": - { - "type" : "treasure", "size" : 20, - "terrainTypeLikeZone" : 9, - "neutralTowns" : { "castles" : 1 }, - "monsters" : "strong", - "treasure" : [ - {"min" : 100000, "max": 130000, "density": 3}, - {"min" : 100000, "max": 150000, "density": 3}, - {"min" : 20000, "max": 60000, "density": 3} - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 36000 }, - { "a" : "2", "b" : "10", "guard" : 12000 }, - { "a" : "3", "b" : "11", "guard" : 12000 }, - { "a" : "4", "b" : "12", "guard" : 12000 }, - { "a" : "5", "b" : "13", "guard" : 36000 }, - { "a" : "6", "b" : "14", "guard" : 12000 }, - { "a" : "7", "b" : "15", "guard" : 12000 }, - { "a" : "8", "b" : "16", "guard" : 12000 }, - { "a" : "9", "b" : "17", "guard" : 75000 }, - { "a" : "10", "b" : "17", "guard" : 25000 }, - { "a" : "11", "b" : "18", "guard" : 25000 }, - { "a" : "12", "b" : "18", "guard" : 25000 }, - { "a" : "13", "b" : "19", "guard" : 75000 }, - { "a" : "14", "b" : "19", "guard" : 25000 }, - { "a" : "15", "b" : "20", "guard" : 25000 }, - { "a" : "16", "b" : "20", "guard" : 25000 }, - { "a" : "17", "b" : "18", "guard" : 50000 }, - { "a" : "19", "b" : "20", "guard" : 50000 }, - { "a" : "17", "b" : "21", "guard" : 60000 }, - { "a" : "18", "b" : "21", "guard" : 60000 }, - { "a" : "19", "b" : "21", "guard" : 60000 }, - { "a" : "20", "b" : "21", "guard" : 60000 } - ] - } -} +{ + "Coldshadow's Fantasy": + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "4-8", "humans" : "3-6", + "zones": + { + "1": + { + "type" : "playerStart", "size" : 30, "owner" : 1, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "normal", + "mines" : {"wood" : 2, "ore" : 2, "gems" : 1, "crystal" : 1, "sulfur" : 1, "mercury" : 1, "gold" : 1}, + "treasure" : [ + {"min" : 7500, "max": 25000, "density": 4}, + {"min" : 3000, "max": 9000, "density": 6}, + {"min" : 300, "max": 3000, "density": 8} + ] + }, + "2": + { + "type" : "cpuStart", "size" : 30, "owner" : 2, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3": + { + "type" : "cpuStart", "size" : 30, "owner" : 3, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4": + { + "type" : "cpuStart", "size" : 30, "owner" : 4, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5": + { + "type" : "playerStart", "size" : 30, "owner" : 5, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6": + { + "type" : "cpuStart", "size" : 30, "owner" : 6, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7": + { + "type" : "cpuStart", "size" : 30, "owner" : 7, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8": + { + "type" : "cpuStart", "size" : 30, "owner" : 8, + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "monsters" : "weak", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9": + { + "type" : "treasure", "size" : 15, + "terrainTypes" : ["subterra"], "matchTerrainToTown" : false, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "strong", + "mines" : {"gems" : 1, "sulfur" : 1, "mercury" : 1, "crystal" : 1}, + "treasure" : [ + {"min" : 45000, "max": 75000, "density": 3}, + {"min" : 15000, "max": 50000, "density": 3}, + {"min" : 3080, "max": 12500, "density": 4} + ] + }, + "10": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "strong", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "15": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "16": + { + "type" : "treasure", "size" : 15, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "normal", + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "17": + { + "type" : "junction", "size" : 30, + "terrainTypeLikeZone" : 9, + "allowedTowns" : ["neutral"], + "monsters" : "strong", + "mines" : {"gold" : 1}, + "treasure" : [ + {"min" : 65000, "max": 100000, "density": 3}, + {"min" : 50000, "max": 100000, "density": 3}, + {"min" : 10000, "max": 15000, "density": 3} + ] + }, + "18": + { + "type" : "junction", "size" : 30, + "terrainTypeLikeZone" : 9, + "allowedTowns" : ["neutral"], + "monsters" : "strong", + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "19": + { + "type" : "junction", "size" : 30, + "terrainTypeLikeZone" : 9, + "allowedTowns" : ["neutral"], + "monsters" : "strong", + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "20": + { + "type" : "junction", "size" : 30, + "terrainTypeLikeZone" : 9, + "allowedTowns" : ["neutral"], + "monsters" : "strong", + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "21": + { + "type" : "treasure", "size" : 20, + "terrainTypeLikeZone" : 9, + "neutralTowns" : { "castles" : 1 }, + "monsters" : "strong", + "treasure" : [ + {"min" : 100000, "max": 130000, "density": 3}, + {"min" : 100000, "max": 150000, "density": 3}, + {"min" : 20000, "max": 60000, "density": 3} + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 36000 }, + { "a" : "2", "b" : "10", "guard" : 12000 }, + { "a" : "3", "b" : "11", "guard" : 12000 }, + { "a" : "4", "b" : "12", "guard" : 12000 }, + { "a" : "5", "b" : "13", "guard" : 36000 }, + { "a" : "6", "b" : "14", "guard" : 12000 }, + { "a" : "7", "b" : "15", "guard" : 12000 }, + { "a" : "8", "b" : "16", "guard" : 12000 }, + { "a" : "9", "b" : "17", "guard" : 75000 }, + { "a" : "10", "b" : "17", "guard" : 25000 }, + { "a" : "11", "b" : "18", "guard" : 25000 }, + { "a" : "12", "b" : "18", "guard" : 25000 }, + { "a" : "13", "b" : "19", "guard" : 75000 }, + { "a" : "14", "b" : "19", "guard" : 25000 }, + { "a" : "15", "b" : "20", "guard" : 25000 }, + { "a" : "16", "b" : "20", "guard" : 25000 }, + { "a" : "17", "b" : "18", "guard" : 50000 }, + { "a" : "19", "b" : "20", "guard" : 50000 }, + { "a" : "17", "b" : "21", "guard" : 60000 }, + { "a" : "18", "b" : "21", "guard" : 60000 }, + { "a" : "19", "b" : "21", "guard" : 60000 }, + { "a" : "20", "b" : "21", "guard" : 60000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON index 642165c63..70baf74fd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON @@ -1,117 +1,117 @@ -{ - "Cube" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9200, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 6 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 9800, "density" : 12 }, - { "min" : 1500, "max" : 2000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "1", "b" : "4", "guard" : 4500 }, - { "a" : "1", "b" : "5", "guard" : 4500 }, - { "a" : "2", "b" : "6", "guard" : 4500 }, - { "a" : "2", "b" : "7", "guard" : 4500 }, - { "a" : "2", "b" : "8", "guard" : 4500 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "8", "guard" : 11000 }, - { "a" : "5", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "8", "guard" : 11000 } - ] - } -} +{ + "Cube" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9200, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 6 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 9800, "density" : 12 }, + { "min" : 1500, "max" : 2000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "1", "b" : "4", "guard" : 4500 }, + { "a" : "1", "b" : "5", "guard" : 4500 }, + { "a" : "2", "b" : "6", "guard" : 4500 }, + { "a" : "2", "b" : "7", "guard" : 4500 }, + { "a" : "2", "b" : "8", "guard" : 4500 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "8", "guard" : 11000 }, + { "a" : "5", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "8", "guard" : 11000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON index ae3989c17..c83a506c4 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON @@ -1,228 +1,228 @@ -{ - "Diamond" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 8999, "density" : 3 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 12 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 3 }, - { "min" : 9000, "max" : 20000, "density" : 9 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "gems" : 2 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 6000 }, - { "a" : "5", "b" : "10", "guard" : 6000 }, - { "a" : "3", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "15", "guard" : 6000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "8", "b" : "15", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "4", "b" : "13", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "4", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "16", "guard" : 6000 }, - { "a" : "3", "b" : "16", "guard" : 6000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "2", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "13", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "16", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 } - ] - } -} +{ + "Diamond" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 8999, "density" : 3 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 12 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 3 }, + { "min" : 9000, "max" : 20000, "density" : 9 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "gems" : 2 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 6000 }, + { "a" : "5", "b" : "10", "guard" : 6000 }, + { "a" : "3", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "15", "guard" : 6000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "8", "b" : "15", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "4", "b" : "13", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "4", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "16", "guard" : 6000 }, + { "a" : "3", "b" : "16", "guard" : 6000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "2", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "13", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "16", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON index 37fc0f1ea..bac5bc7bc 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON @@ -1,362 +1,362 @@ -{ - "Extreme L" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3400, "max" : 3500, "density" : 3 }, - { "min" : 1000, "max" : 2000, "density" : 10 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3500, "max" : 9600, "density" : 8 }, - { "min" : 300, "max" : 1000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1 }, - "treasure" : - [ - { "min" : 40000, "max" : 42000, "density" : 1 }, - { "min" : 25000, "max" : 27000, "density" : 2 }, - { "min" : 6000, "max" : 15000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 29000, "density" : 2 }, - { "min" : 3500, "max" : 20000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 1 }, - { "min" : 50000, "max" : 70000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 15000 }, - { "a" : "3", "b" : "7", "guard" : 20000 }, - { "a" : "4", "b" : "6", "guard" : 15000 }, - { "a" : "4", "b" : "8", "guard" : 20000 }, - { "a" : "3", "b" : "11", "guard" : 50000 }, - { "a" : "4", "b" : "12", "guard" : 50000 }, - { "a" : "5", "b" : "9", "guard" : 40000 }, - { "a" : "7", "b" : "11", "guard" : 40000 }, - { "a" : "6", "b" : "10", "guard" : 40000 }, - { "a" : "8", "b" : "12", "guard" : 40000 }, - { "a" : "1", "b" : "9", "guard" : 50000 }, - { "a" : "2", "b" : "10", "guard" : 50000 }, - { "a" : "9", "b" : "12", "guard" : 100000 }, - { "a" : "10", "b" : "11", "guard" : 100000 } - ] - }, - "Extreme XL" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xh", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3400, "max" : 3500, "density" : 3 }, - { "min" : 1000, "max" : 2000, "density" : 6 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3500, "max" : 9500, "density" : 5 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1 }, - "treasure" : - [ - { "min" : 40000, "max" : 42000, "density" : 1 }, - { "min" : 25000, "max" : 27000, "density" : 1 }, - { "min" : 6000, "max" : 15000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 3500, "max" : 20000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 1 }, - { "min" : 50000, "max" : 80000, "density" : 6 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 18000 }, - { "a" : "3", "b" : "7", "guard" : 22000 }, - { "a" : "4", "b" : "6", "guard" : 18000 }, - { "a" : "4", "b" : "8", "guard" : 22000 }, - { "a" : "3", "b" : "11", "guard" : 60000 }, - { "a" : "4", "b" : "12", "guard" : 60000 }, - { "a" : "5", "b" : "9", "guard" : 50000 }, - { "a" : "7", "b" : "11", "guard" : 50000 }, - { "a" : "6", "b" : "10", "guard" : 50000 }, - { "a" : "8", "b" : "12", "guard" : 50000 }, - { "a" : "1", "b" : "9", "guard" : 60000 }, - { "a" : "2", "b" : "10", "guard" : 60000 }, - { "a" : "9", "b" : "12", "guard" : 140000 }, - { "a" : "10", "b" : "11", "guard" : 140000 } - ] - } -} +{ + "Extreme L" : + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3400, "max" : 3500, "density" : 3 }, + { "min" : 1000, "max" : 2000, "density" : 10 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3500, "max" : 9600, "density" : 8 }, + { "min" : 300, "max" : 1000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1 }, + "treasure" : + [ + { "min" : 40000, "max" : 42000, "density" : 1 }, + { "min" : 25000, "max" : 27000, "density" : 2 }, + { "min" : 6000, "max" : 15000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 29000, "density" : 2 }, + { "min" : 3500, "max" : 20000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 1 }, + { "min" : 50000, "max" : 70000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 15000 }, + { "a" : "3", "b" : "7", "guard" : 20000 }, + { "a" : "4", "b" : "6", "guard" : 15000 }, + { "a" : "4", "b" : "8", "guard" : 20000 }, + { "a" : "3", "b" : "11", "guard" : 50000 }, + { "a" : "4", "b" : "12", "guard" : 50000 }, + { "a" : "5", "b" : "9", "guard" : 40000 }, + { "a" : "7", "b" : "11", "guard" : 40000 }, + { "a" : "6", "b" : "10", "guard" : 40000 }, + { "a" : "8", "b" : "12", "guard" : 40000 }, + { "a" : "1", "b" : "9", "guard" : 50000 }, + { "a" : "2", "b" : "10", "guard" : 50000 }, + { "a" : "9", "b" : "12", "guard" : 100000 }, + { "a" : "10", "b" : "11", "guard" : 100000 } + ] + }, + "Extreme XL" : + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xh", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3400, "max" : 3500, "density" : 3 }, + { "min" : 1000, "max" : 2000, "density" : 6 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3500, "max" : 9500, "density" : 5 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1 }, + "treasure" : + [ + { "min" : 40000, "max" : 42000, "density" : 1 }, + { "min" : 25000, "max" : 27000, "density" : 1 }, + { "min" : 6000, "max" : 15000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 3500, "max" : 20000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 1 }, + { "min" : 50000, "max" : 80000, "density" : 6 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 18000 }, + { "a" : "3", "b" : "7", "guard" : 22000 }, + { "a" : "4", "b" : "6", "guard" : 18000 }, + { "a" : "4", "b" : "8", "guard" : 22000 }, + { "a" : "3", "b" : "11", "guard" : 60000 }, + { "a" : "4", "b" : "12", "guard" : 60000 }, + { "a" : "5", "b" : "9", "guard" : 50000 }, + { "a" : "7", "b" : "11", "guard" : 50000 }, + { "a" : "6", "b" : "10", "guard" : 50000 }, + { "a" : "8", "b" : "12", "guard" : 50000 }, + { "a" : "1", "b" : "9", "guard" : 60000 }, + { "a" : "2", "b" : "10", "guard" : 60000 }, + { "a" : "9", "b" : "12", "guard" : 140000 }, + { "a" : "10", "b" : "11", "guard" : 140000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON index 965cba95c..62006c85e 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON @@ -1,310 +1,310 @@ -{ - "Extreme II L": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)" - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 120, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 16000, "max" : 90000, "density" : 1 }, - { "min" : 300, "max" : 16000, "density" : 2 }, - { "min" : 370, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 120, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 22, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 300, "max" : 1000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 22, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 70000, "max" : 90000, "density" : 6 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasure" : - [ - { "min" : 90000, "max" : 120000, "density" : 6 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 65000 }, - { "a" : "3", "b" : "7", "guard" : 65000 }, - { "a" : "4", "b" : "6", "guard" : 65000 }, - { "a" : "4", "b" : "8", "guard" : 65000 }, - { "a" : "5", "b" : "9", "guard" : 135000 }, - { "a" : "6", "b" : "9", "guard" : 135000 }, - { "a" : "7", "b" : "10", "guard" : 135000 }, - { "a" : "8", "b" : "10", "guard" : 135000 }, - { "a" : "3", "b" : "5", "guard" : 60000 }, - { "a" : "3", "b" : "7", "guard" : 60000 }, - { "a" : "4", "b" : "6", "guard" : 60000 }, - { "a" : "4", "b" : "8", "guard" : 60000 } - ] - }, - "Extreme II XL": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xh", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 90, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 16000, "max" : 120000, "density" : 1 }, - { "min" : 300, "max" : 16000, "density" : 2 }, - { "min" : 370, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 90, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 300, "max" : 1000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 85000, "max" : 100000, "density" : 4 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 4 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 80000 }, - { "a" : "3", "b" : "7", "guard" : 80000 }, - { "a" : "4", "b" : "6", "guard" : 80000 }, - { "a" : "4", "b" : "8", "guard" : 80000 }, - { "a" : "5", "b" : "9", "guard" : 160000 }, - { "a" : "6", "b" : "9", "guard" : 160000 }, - { "a" : "7", "b" : "10", "guard" : 160000 }, - { "a" : "8", "b" : "10", "guard" : 160000 }, - { "a" : "3", "b" : "5", "guard" : 70000 }, - { "a" : "3", "b" : "7", "guard" : 70000 }, - { "a" : "4", "b" : "6", "guard" : 70000 }, - { "a" : "4", "b" : "8", "guard" : 70000 } - ] - } -} +{ + "Extreme II L": + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)" + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 120, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 16000, "max" : 90000, "density" : 1 }, + { "min" : 300, "max" : 16000, "density" : 2 }, + { "min" : 370, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 120, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 22, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 300, "max" : 1000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 22, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 70000, "max" : 90000, "density" : 6 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasure" : + [ + { "min" : 90000, "max" : 120000, "density" : 6 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 65000 }, + { "a" : "3", "b" : "7", "guard" : 65000 }, + { "a" : "4", "b" : "6", "guard" : 65000 }, + { "a" : "4", "b" : "8", "guard" : 65000 }, + { "a" : "5", "b" : "9", "guard" : 135000 }, + { "a" : "6", "b" : "9", "guard" : 135000 }, + { "a" : "7", "b" : "10", "guard" : 135000 }, + { "a" : "8", "b" : "10", "guard" : 135000 }, + { "a" : "3", "b" : "5", "guard" : 60000 }, + { "a" : "3", "b" : "7", "guard" : 60000 }, + { "a" : "4", "b" : "6", "guard" : 60000 }, + { "a" : "4", "b" : "8", "guard" : 60000 } + ] + }, + "Extreme II XL": + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xh", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 90, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 16000, "max" : 120000, "density" : 1 }, + { "min" : 300, "max" : 16000, "density" : 2 }, + { "min" : 370, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 90, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 300, "max" : 1000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 85000, "max" : 100000, "density" : 4 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 4 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 80000 }, + { "a" : "3", "b" : "7", "guard" : 80000 }, + { "a" : "4", "b" : "6", "guard" : 80000 }, + { "a" : "4", "b" : "8", "guard" : 80000 }, + { "a" : "5", "b" : "9", "guard" : 160000 }, + { "a" : "6", "b" : "9", "guard" : 160000 }, + { "a" : "7", "b" : "10", "guard" : 160000 }, + { "a" : "8", "b" : "10", "guard" : 160000 }, + { "a" : "3", "b" : "5", "guard" : 70000 }, + { "a" : "3", "b" : "7", "guard" : 70000 }, + { "a" : "4", "b" : "6", "guard" : 70000 }, + { "a" : "4", "b" : "8", "guard" : 70000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON index b31dac257..798115f21 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON @@ -1,183 +1,183 @@ -{ - "Fear" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 2000, "density" : 18 }, - { "min" : 3000, "max" : 5000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 12500, "density" : 2 }, - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 300, "max" : 2000, "density" : 12 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 2, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 2 - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 1400, "density" : 17 }, - { "min" : 3000, "max" : 4000, "density" : 5 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "normal", - "terrainTypeLikeZone" : 4, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 25 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 800, "max" : 4000, "density" : 8 }, - { "min" : 5000, "max" : 8000, "density" : 12 }, - { "min" : 10000, "max" : 15000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 10 }, - { "min" : 20000, "max" : 35000, "density" : 5 } - ] - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 300, "max" : 2000, "density" : 15 }, - { "min" : 3000, "max" : 5000, "density" : 6 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 3, - "treasureLikeZone" : 2 - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 100, "max" : 1400, "density" : 18 }, - { "min" : 3000, "max" : 4000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 6 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 0 }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "5", "b" : "11", "guard" : 0 }, - { "a" : "11", "b" : "8", "guard" : 0 }, - { "a" : "2", "b" : "6", "guard" : 0 }, - { "a" : "3", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "9", "b" : "8", "guard" : 0 }, - { "a" : "10", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 } - ] - } -} +{ + "Fear" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 2000, "density" : 18 }, + { "min" : 3000, "max" : 5000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 12500, "density" : 2 }, + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 300, "max" : 2000, "density" : 12 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 2, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 2 + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 1400, "density" : 17 }, + { "min" : 3000, "max" : 4000, "density" : 5 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "normal", + "terrainTypeLikeZone" : 4, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 25 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 800, "max" : 4000, "density" : 8 }, + { "min" : 5000, "max" : 8000, "density" : 12 }, + { "min" : 10000, "max" : 15000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 10 }, + { "min" : 20000, "max" : 35000, "density" : 5 } + ] + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 300, "max" : 2000, "density" : 15 }, + { "min" : 3000, "max" : 5000, "density" : 6 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 3, + "treasureLikeZone" : 2 + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 100, "max" : 1400, "density" : 18 }, + { "min" : 3000, "max" : 4000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 6 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 0 }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "5", "b" : "11", "guard" : 0 }, + { "a" : "11", "b" : "8", "guard" : 0 }, + { "a" : "2", "b" : "6", "guard" : 0 }, + { "a" : "3", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "9", "b" : "8", "guard" : 0 }, + { "a" : "10", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON index ae231c0f7..8adfb2478 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON @@ -1,255 +1,255 @@ -{ - "Frozen Dragons" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 5 }, - { "min" : 2500, "max" : 3000, "density" : 3 }, - { "min" : 17000, "max" : 20000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 4 }, - { "min" : 3500, "max" : 7000, "density" : 4 }, - { "min" : 9000, "max" : 9300, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "tower", "stronghold", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 4 }, - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 15000, "max" : 19000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "tower", "stronghold", "conflux" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 350, "max" : 9000, "density" : 3 }, - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 25000, "max" : 28000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 19000, "density" : 4 }, - { "min" : 110000, "max" : 120000, "density" : 2 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "mines" : { "wood" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3500, "density" : 1 }, - { "min" : 9000, "max" : 30000, "density" : 2 }, - { "min" : 80000, "max" : 80000, "density" : 6 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 11000, "density" : 2 }, - { "min" : 80000, "max" : 80000, "density" : 6 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasure" : - [ - { "min" : 80000, "max" : 80000, "density" : 7 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3500 }, - { "a" : "2", "b" : "4", "guard" : 3500 }, - { "a" : "3", "b" : "5", "guard" : 5000 }, - { "a" : "4", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "5", "b" : "7", "guard" : 15000 }, - { "a" : "6", "b" : "8", "guard" : 15000 }, - { "a" : "7", "b" : "9", "guard" : 30000 }, - { "a" : "8", "b" : "10", "guard" : 30000 }, - { "a" : "9", "b" : "11", "guard" : 60000 }, - { "a" : "9", "b" : "11", "guard" : 60000 }, - { "a" : "10", "b" : "12", "guard" : 60000 }, - { "a" : "10", "b" : "12", "guard" : 60000 }, - { "a" : "11", "b" : "13", "guard" : 0 }, - { "a" : "11", "b" : "14", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "12", "b" : "14", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 240000 }, - { "a" : "11", "b" : "13", "guard" : 0 }, - { "a" : "11", "b" : "14", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "12", "b" : "14", "guard" : 0 }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 }, - { "a" : "15", "b" : "16", "guard" : 140000 } - ] - } -} +{ + "Frozen Dragons" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 5 }, + { "min" : 2500, "max" : 3000, "density" : 3 }, + { "min" : 17000, "max" : 20000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 4 }, + { "min" : 3500, "max" : 7000, "density" : 4 }, + { "min" : 9000, "max" : 9300, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "tower", "stronghold", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 4 }, + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 15000, "max" : 19000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "tower", "stronghold", "conflux" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 350, "max" : 9000, "density" : 3 }, + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 25000, "max" : 28000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 19000, "density" : 4 }, + { "min" : 110000, "max" : 120000, "density" : 2 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "mines" : { "wood" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3500, "density" : 1 }, + { "min" : 9000, "max" : 30000, "density" : 2 }, + { "min" : 80000, "max" : 80000, "density" : 6 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 11000, "density" : 2 }, + { "min" : 80000, "max" : 80000, "density" : 6 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasure" : + [ + { "min" : 80000, "max" : 80000, "density" : 7 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3500 }, + { "a" : "2", "b" : "4", "guard" : 3500 }, + { "a" : "3", "b" : "5", "guard" : 5000 }, + { "a" : "4", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "5", "b" : "7", "guard" : 15000 }, + { "a" : "6", "b" : "8", "guard" : 15000 }, + { "a" : "7", "b" : "9", "guard" : 30000 }, + { "a" : "8", "b" : "10", "guard" : 30000 }, + { "a" : "9", "b" : "11", "guard" : 60000 }, + { "a" : "9", "b" : "11", "guard" : 60000 }, + { "a" : "10", "b" : "12", "guard" : 60000 }, + { "a" : "10", "b" : "12", "guard" : 60000 }, + { "a" : "11", "b" : "13", "guard" : 0 }, + { "a" : "11", "b" : "14", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "12", "b" : "14", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 240000 }, + { "a" : "11", "b" : "13", "guard" : 0 }, + { "a" : "11", "b" : "14", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "12", "b" : "14", "guard" : 0 }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 }, + { "a" : "15", "b" : "16", "guard" : 140000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON index 214ffd059..0b4573168 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON @@ -1,102 +1,102 @@ -{ - "Gimlis Revenge" : - { - "minSize" : "m", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "junction", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 12000 }, - { "a" : "6", "b" : "8", "guard" : 12000 }, - { "a" : "5", "b" : "8", "guard" : 12000 }, - { "a" : "4", "b" : "7", "guard" : 12000 } - ] - } -} +{ + "Gimlis Revenge" : + { + "minSize" : "m", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "junction", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 12000 }, + { "a" : "6", "b" : "8", "guard" : 12000 }, + { "a" : "5", "b" : "8", "guard" : 12000 }, + { "a" : "4", "b" : "7", "guard" : 12000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON index cbe0a1c48..55f601301 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON @@ -1,332 +1,332 @@ -{ - "Guerilla" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "10", "b" : "13", "guard" : 3000 }, - { "a" : "11", "b" : "13", "guard" : 3000 }, - { "a" : "11", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "1", "b" : "16", "guard" : 3000 }, - { "a" : "14", "b" : "20", "guard" : 6000 }, - { "a" : "13", "b" : "20", "guard" : 6000 }, - { "a" : "16", "b" : "17", "guard" : 6000 }, - { "a" : "6", "b" : "17", "guard" : 6000 }, - { "a" : "6", "b" : "18", "guard" : 6000 }, - { "a" : "7", "b" : "18", "guard" : 6000 }, - { "a" : "7", "b" : "24", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 6000 }, - { "a" : "5", "b" : "23", "guard" : 6000 }, - { "a" : "12", "b" : "23", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "13", "b" : "19", "guard" : 6000 }, - { "a" : "14", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "21", "guard" : 6000 }, - { "a" : "16", "b" : "21", "guard" : 6000 }, - { "a" : "21", "b" : "25", "guard" : 12500 }, - { "a" : "22", "b" : "28", "guard" : 12500 }, - { "a" : "17", "b" : "25", "guard" : 12500 }, - { "a" : "18", "b" : "26", "guard" : 12500 }, - { "a" : "24", "b" : "26", "guard" : 12500 }, - { "a" : "23", "b" : "27", "guard" : 12500 }, - { "a" : "19", "b" : "27", "guard" : 12500 }, - { "a" : "20", "b" : "28", "guard" : 12500 }, - { "a" : "27", "b" : "28", "guard" : 12500 }, - { "a" : "26", "b" : "27", "guard" : 12500 }, - { "a" : "25", "b" : "28", "guard" : 12500 }, - { "a" : "25", "b" : "26", "guard" : 12500 } - ] - } -} +{ + "Guerilla" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "10", "b" : "13", "guard" : 3000 }, + { "a" : "11", "b" : "13", "guard" : 3000 }, + { "a" : "11", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "1", "b" : "16", "guard" : 3000 }, + { "a" : "14", "b" : "20", "guard" : 6000 }, + { "a" : "13", "b" : "20", "guard" : 6000 }, + { "a" : "16", "b" : "17", "guard" : 6000 }, + { "a" : "6", "b" : "17", "guard" : 6000 }, + { "a" : "6", "b" : "18", "guard" : 6000 }, + { "a" : "7", "b" : "18", "guard" : 6000 }, + { "a" : "7", "b" : "24", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 6000 }, + { "a" : "5", "b" : "23", "guard" : 6000 }, + { "a" : "12", "b" : "23", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "13", "b" : "19", "guard" : 6000 }, + { "a" : "14", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "21", "guard" : 6000 }, + { "a" : "16", "b" : "21", "guard" : 6000 }, + { "a" : "21", "b" : "25", "guard" : 12500 }, + { "a" : "22", "b" : "28", "guard" : 12500 }, + { "a" : "17", "b" : "25", "guard" : 12500 }, + { "a" : "18", "b" : "26", "guard" : 12500 }, + { "a" : "24", "b" : "26", "guard" : 12500 }, + { "a" : "23", "b" : "27", "guard" : 12500 }, + { "a" : "19", "b" : "27", "guard" : 12500 }, + { "a" : "20", "b" : "28", "guard" : 12500 }, + { "a" : "27", "b" : "28", "guard" : 12500 }, + { "a" : "26", "b" : "27", "guard" : 12500 }, + { "a" : "25", "b" : "28", "guard" : 12500 }, + { "a" : "25", "b" : "26", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON index 6a236597b..2e2b1cfe0 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON @@ -1,158 +1,158 @@ -{ - "Headquarters" : - { - "minSize" : "l", "maxSize" : "l+u", - "players" : "2-7", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "fortress", "neutral" ], - "mines" : { "wood" : 2, "mercury" : 1, "ore" : 2, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 12000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 16000, "density" : 6 }, - { "min" : 300, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "tower" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "dungeon" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "allowedMonsters" : [ "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 20 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 2 }, - "allowedTowns" : [ "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "mines" : { "wood" : 2, "ore" : 2, "gold" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 3 }, - { "min" : 25000, "max" : 28000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 45000 }, - { "a" : "2", "b" : "9", "guard" : 45000 }, - { "a" : "3", "b" : "9", "guard" : 45000 }, - { "a" : "4", "b" : "9", "guard" : 45000 }, - { "a" : "5", "b" : "9", "guard" : 45000 }, - { "a" : "6", "b" : "9", "guard" : 45000 }, - { "a" : "7", "b" : "9", "guard" : 45000 }, - { "a" : "8", "b" : "9", "guard" : 45000 }, - { "a" : "1", "b" : "8", "guard" : 45000 }, - { "a" : "2", "b" : "8", "guard" : 45000 }, - { "a" : "3", "b" : "8", "guard" : 45000 }, - { "a" : "4", "b" : "8", "guard" : 45000 }, - { "a" : "5", "b" : "8", "guard" : 45000 }, - { "a" : "6", "b" : "8", "guard" : 45000 }, - { "a" : "7", "b" : "8", "guard" : 45000 } - ] - } -} +{ + "Headquarters" : + { + "minSize" : "l", "maxSize" : "l+u", + "players" : "2-7", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "fortress", "neutral" ], + "mines" : { "wood" : 2, "mercury" : 1, "ore" : 2, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 12000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 16000, "density" : 6 }, + { "min" : 300, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "tower" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "dungeon" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "allowedMonsters" : [ "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 20 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 2 }, + "allowedTowns" : [ "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "mines" : { "wood" : 2, "ore" : 2, "gold" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 3 }, + { "min" : 25000, "max" : 28000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 45000 }, + { "a" : "2", "b" : "9", "guard" : 45000 }, + { "a" : "3", "b" : "9", "guard" : 45000 }, + { "a" : "4", "b" : "9", "guard" : 45000 }, + { "a" : "5", "b" : "9", "guard" : 45000 }, + { "a" : "6", "b" : "9", "guard" : 45000 }, + { "a" : "7", "b" : "9", "guard" : 45000 }, + { "a" : "8", "b" : "9", "guard" : 45000 }, + { "a" : "1", "b" : "8", "guard" : 45000 }, + { "a" : "2", "b" : "8", "guard" : 45000 }, + { "a" : "3", "b" : "8", "guard" : 45000 }, + { "a" : "4", "b" : "8", "guard" : 45000 }, + { "a" : "5", "b" : "8", "guard" : 45000 }, + { "a" : "6", "b" : "8", "guard" : 45000 }, + { "a" : "7", "b" : "8", "guard" : 45000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON index e4ecf08dc..6443f0d6b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON @@ -1,230 +1,230 @@ -{ - "HyperCube" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "8", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "1", "b" : "17", "guard" : 3000 }, - { "a" : "7", "b" : "17", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "8", "b" : "20", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "3", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "19", "guard" : 3000 } - ] - } -} +{ + "HyperCube" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "8", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "1", "b" : "17", "guard" : 3000 }, + { "a" : "7", "b" : "17", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "8", "b" : "20", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "3", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "19", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON index 0d9780a74..036052ff2 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON @@ -1,291 +1,291 @@ -{ - "Long Run M" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "subterra" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 30 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, - "treasure" : - [ - { "min" : 20000, "max" : 21000, "density" : 6 }, - { "min" : 12000, "max" : 16000, "density" : 5 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra" ], - "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 2 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 }, - "treasure" : - [ - { "min" : 30000, "max" : 90000, "density" : 25 } - ] - }, - "6" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "8" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 5000 }, - { "a" : "2", "b" : "3", "guard" : 10500 }, - { "a" : "3", "b" : "4", "guard" : 21000 }, - { "a" : "4", "b" : "5", "guard" : 45000 }, - { "a" : "5", "b" : "9", "guard" : 45000 }, - { "a" : "6", "b" : "7", "guard" : 5000 }, - { "a" : "7", "b" : "8", "guard" : 10500 }, - { "a" : "8", "b" : "9", "guard" : 21000 } - ] - }, - "Long Run XL" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 70, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, - "treasure" : - [ - { "min" : 20000, "max" : 21000, "density" : 6 }, - { "min" : 12000, "max" : 16000, "density" : 5 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 80, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 5 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 2, - "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 }, - "treasure" : - [ - { "min" : 30000, "max" : 90000, "density" : 25 } - ] - }, - "6" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "8" : - { - "type" : "treasure", - "size" : 70, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 80, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 7000 }, - { "a" : "2", "b" : "3", "guard" : 14000 }, - { "a" : "3", "b" : "4", "guard" : 28000 }, - { "a" : "4", "b" : "5", "guard" : 60000 }, - { "a" : "5", "b" : "9", "guard" : 60000 }, - { "a" : "6", "b" : "7", "guard" : 7000 }, - { "a" : "7", "b" : "8", "guard" : 14000 }, - { "a" : "8", "b" : "9", "guard" : 28000 } - ] - } -} +{ + "Long Run M" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 3000, "density" : 12 }, + { "min" : 5000, "max" : 9000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "subterra" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 30 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, + "treasure" : + [ + { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra" ], + "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 2 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 10 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 }, + "treasure" : + [ + { "min" : 30000, "max" : 90000, "density" : 25 } + ] + }, + "6" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "8" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 5000 }, + { "a" : "2", "b" : "3", "guard" : 10500 }, + { "a" : "3", "b" : "4", "guard" : 21000 }, + { "a" : "4", "b" : "5", "guard" : 45000 }, + { "a" : "5", "b" : "9", "guard" : 45000 }, + { "a" : "6", "b" : "7", "guard" : 5000 }, + { "a" : "7", "b" : "8", "guard" : 10500 }, + { "a" : "8", "b" : "9", "guard" : 21000 } + ] + }, + "Long Run XL" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 3000, "density" : 12 }, + { "min" : 5000, "max" : 9000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 70, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, + "treasure" : + [ + { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 80, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 5 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 10 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 2, + "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 }, + "treasure" : + [ + { "min" : 30000, "max" : 90000, "density" : 25 } + ] + }, + "6" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "8" : + { + "type" : "treasure", + "size" : 70, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 80, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 7000 }, + { "a" : "2", "b" : "3", "guard" : 14000 }, + { "a" : "3", "b" : "4", "guard" : 28000 }, + { "a" : "4", "b" : "5", "guard" : 60000 }, + { "a" : "5", "b" : "9", "guard" : 60000 }, + { "a" : "6", "b" : "7", "guard" : 7000 }, + { "a" : "7", "b" : "8", "guard" : 14000 }, + { "a" : "8", "b" : "9", "guard" : 28000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON index b68c54f06..cf9e8e970 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON @@ -1,639 +1,639 @@ -{ - "Marathon XL" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 5000, "density" : 4 }, - { "min" : 300, "max" : 2000, "density" : 7 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : [ "castle", "conflux"], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9600, "density" : 7 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : [ "rampart", "conflux" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 8000, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "stronghold", "conflux"], - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 10000, "density" : 1 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "conflux"], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["rampart", "conflux"], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "stronghold", "conflux"], - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 8000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "minesLikeZone" : 10, - "treasureLikeZone" : 10 - }, - "14" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow", "swamp", "rough" ], - "treasure" : - [ - { "min" : 35000, "max" : 45000, "density" : 1 }, - { "min" : 20000, "max" : 22000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 7 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "21" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 80000, "max" : 120000, "density" : 2 }, - { "min" : 40000, "max" : 70000, "density" : 1 }, - { "min" : 10000, "max" : 29000, "density" : 1 } - ] - }, - "22" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - }, - "23" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 7000 }, - { "a" : "2", "b" : "6", "guard" : 7000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 18000 }, - { "a" : "4", "b" : "9", "guard" : 23000 }, - { "a" : "4", "b" : "10", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 23000 }, - { "a" : "5", "b" : "11", "guard" : 16000 }, - { "a" : "3", "b" : "11", "guard" : 27000 }, - { "a" : "6", "b" : "12", "guard" : 18000 }, - { "a" : "7", "b" : "12", "guard" : 23000 }, - { "a" : "8", "b" : "13", "guard" : 23000 }, - { "a" : "8", "b" : "14", "guard" : 18000 }, - { "a" : "7", "b" : "13", "guard" : 20000 }, - { "a" : "6", "b" : "14", "guard" : 27000 }, - { "a" : "9", "b" : "15", "guard" : 40000 }, - { "a" : "10", "b" : "16", "guard" : 45000 }, - { "a" : "11", "b" : "17", "guard" : 35000 }, - { "a" : "12", "b" : "18", "guard" : 40000 }, - { "a" : "13", "b" : "19", "guard" : 45000 }, - { "a" : "14", "b" : "20", "guard" : 35000 }, - { "a" : "15", "b" : "21", "guard" : 60000 }, - { "a" : "16", "b" : "22", "guard" : 80000 }, - { "a" : "17", "b" : "23", "guard" : 70000 }, - { "a" : "18", "b" : "21", "guard" : 70000 }, - { "a" : "19", "b" : "22", "guard" : 80000 }, - { "a" : "20", "b" : "23", "guard" : 60000 }, - { "a" : "21", "b" : "22", "guard" : 280000 }, - { "a" : "22", "b" : "23", "guard" : 280000 }, - { "a" : "23", "b" : "21", "guard" : 280000 } - ] - }, - "Marathon XXL": - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 300, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9600, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 8000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 10000, "density" : 1 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 8000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "minesLikeZone" : 10, - "treasureLikeZone" : 10 - }, - "14" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow", "swamp", "rough" ], - "treasure" : - [ - { "min" : 35000, "max" : 45000, "density" : 1 }, - { "min" : 20000, "max" : 22000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 5 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "17" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "18" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "19" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "20" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "21" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 110000, "max" : 120000, "density" : 2 }, - { "min" : 60000, "max" : 70000, "density" : 1 }, - { "min" : 30000, "max" : 60000, "density" : 1 } - ] - }, - "22" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - }, - "23" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 7000 }, - { "a" : "2", "b" : "6", "guard" : 7000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 18000 }, - { "a" : "4", "b" : "9", "guard" : 25000 }, - { "a" : "4", "b" : "10", "guard" : 22000 }, - { "a" : "5", "b" : "10", "guard" : 23000 }, - { "a" : "5", "b" : "11", "guard" : 16000 }, - { "a" : "3", "b" : "11", "guard" : 29000 }, - { "a" : "6", "b" : "12", "guard" : 18000 }, - { "a" : "7", "b" : "12", "guard" : 25000 }, - { "a" : "7", "b" : "13", "guard" : 22000 }, - { "a" : "8", "b" : "13", "guard" : 23000 }, - { "a" : "8", "b" : "14", "guard" : 18000 }, - { "a" : "6", "b" : "14", "guard" : 29000 }, - { "a" : "9", "b" : "15", "guard" : 45000 }, - { "a" : "10", "b" : "16", "guard" : 50000 }, - { "a" : "11", "b" : "17", "guard" : 40000 }, - { "a" : "12", "b" : "18", "guard" : 45000 }, - { "a" : "13", "b" : "19", "guard" : 50000 }, - { "a" : "14", "b" : "20", "guard" : 40000 }, - { "a" : "15", "b" : "21", "guard" : 70000 }, - { "a" : "16", "b" : "22", "guard" : 90000 }, - { "a" : "17", "b" : "23", "guard" : 80000 }, - { "a" : "18", "b" : "21", "guard" : 80000 }, - { "a" : "19", "b" : "22", "guard" : 90000 }, - { "a" : "20", "b" : "23", "guard" : 70000 }, - { "a" : "21", "b" : "22", "guard" : 350000 }, - { "a" : "22", "b" : "23", "guard" : 350000 }, - { "a" : "23", "b" : "21", "guard" : 350000 } - ] - } -} +{ + "Marathon XL" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 5000, "density" : 4 }, + { "min" : 300, "max" : 2000, "density" : 7 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : [ "castle", "conflux"], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9600, "density" : 7 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : [ "rampart", "conflux" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 8000, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "stronghold", "conflux"], + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 10000, "density" : 1 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "conflux"], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["rampart", "conflux"], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "stronghold", "conflux"], + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 8000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "minesLikeZone" : 10, + "treasureLikeZone" : 10 + }, + "14" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow", "swamp", "rough" ], + "treasure" : + [ + { "min" : 35000, "max" : 45000, "density" : 1 }, + { "min" : 20000, "max" : 22000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 7 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "21" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 80000, "max" : 120000, "density" : 2 }, + { "min" : 40000, "max" : 70000, "density" : 1 }, + { "min" : 10000, "max" : 29000, "density" : 1 } + ] + }, + "22" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + }, + "23" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 7000 }, + { "a" : "2", "b" : "6", "guard" : 7000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 18000 }, + { "a" : "4", "b" : "9", "guard" : 23000 }, + { "a" : "4", "b" : "10", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 23000 }, + { "a" : "5", "b" : "11", "guard" : 16000 }, + { "a" : "3", "b" : "11", "guard" : 27000 }, + { "a" : "6", "b" : "12", "guard" : 18000 }, + { "a" : "7", "b" : "12", "guard" : 23000 }, + { "a" : "8", "b" : "13", "guard" : 23000 }, + { "a" : "8", "b" : "14", "guard" : 18000 }, + { "a" : "7", "b" : "13", "guard" : 20000 }, + { "a" : "6", "b" : "14", "guard" : 27000 }, + { "a" : "9", "b" : "15", "guard" : 40000 }, + { "a" : "10", "b" : "16", "guard" : 45000 }, + { "a" : "11", "b" : "17", "guard" : 35000 }, + { "a" : "12", "b" : "18", "guard" : 40000 }, + { "a" : "13", "b" : "19", "guard" : 45000 }, + { "a" : "14", "b" : "20", "guard" : 35000 }, + { "a" : "15", "b" : "21", "guard" : 60000 }, + { "a" : "16", "b" : "22", "guard" : 80000 }, + { "a" : "17", "b" : "23", "guard" : 70000 }, + { "a" : "18", "b" : "21", "guard" : 70000 }, + { "a" : "19", "b" : "22", "guard" : 80000 }, + { "a" : "20", "b" : "23", "guard" : 60000 }, + { "a" : "21", "b" : "22", "guard" : 280000 }, + { "a" : "22", "b" : "23", "guard" : 280000 }, + { "a" : "23", "b" : "21", "guard" : 280000 } + ] + }, + "Marathon XXL": + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 300, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9600, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 8000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 10000, "density" : 1 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 8000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "minesLikeZone" : 10, + "treasureLikeZone" : 10 + }, + "14" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow", "swamp", "rough" ], + "treasure" : + [ + { "min" : 35000, "max" : 45000, "density" : 1 }, + { "min" : 20000, "max" : 22000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 5 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "17" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "18" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "19" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "20" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "21" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 110000, "max" : 120000, "density" : 2 }, + { "min" : 60000, "max" : 70000, "density" : 1 }, + { "min" : 30000, "max" : 60000, "density" : 1 } + ] + }, + "22" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + }, + "23" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 7000 }, + { "a" : "2", "b" : "6", "guard" : 7000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 18000 }, + { "a" : "4", "b" : "9", "guard" : 25000 }, + { "a" : "4", "b" : "10", "guard" : 22000 }, + { "a" : "5", "b" : "10", "guard" : 23000 }, + { "a" : "5", "b" : "11", "guard" : 16000 }, + { "a" : "3", "b" : "11", "guard" : 29000 }, + { "a" : "6", "b" : "12", "guard" : 18000 }, + { "a" : "7", "b" : "12", "guard" : 25000 }, + { "a" : "7", "b" : "13", "guard" : 22000 }, + { "a" : "8", "b" : "13", "guard" : 23000 }, + { "a" : "8", "b" : "14", "guard" : 18000 }, + { "a" : "6", "b" : "14", "guard" : 29000 }, + { "a" : "9", "b" : "15", "guard" : 45000 }, + { "a" : "10", "b" : "16", "guard" : 50000 }, + { "a" : "11", "b" : "17", "guard" : 40000 }, + { "a" : "12", "b" : "18", "guard" : 45000 }, + { "a" : "13", "b" : "19", "guard" : 50000 }, + { "a" : "14", "b" : "20", "guard" : 40000 }, + { "a" : "15", "b" : "21", "guard" : 70000 }, + { "a" : "16", "b" : "22", "guard" : 90000 }, + { "a" : "17", "b" : "23", "guard" : 80000 }, + { "a" : "18", "b" : "21", "guard" : 80000 }, + { "a" : "19", "b" : "22", "guard" : 90000 }, + { "a" : "20", "b" : "23", "guard" : 70000 }, + { "a" : "21", "b" : "22", "guard" : 350000 }, + { "a" : "22", "b" : "23", "guard" : 350000 }, + { "a" : "23", "b" : "21", "guard" : 350000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON index d4cf5ac3b..ee48447b3 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON @@ -1,179 +1,179 @@ -{ - "Mini Nostalgia" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "11" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "14" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "15" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "3", "guard" : 45000 }, - { "a" : "5", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 } - ] - } -} +{ + "Mini Nostalgia" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "11" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "14" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "15" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "3", "guard" : 45000 }, + { "a" : "5", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON index 1703a3ba1..22ea8d9da 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON @@ -1,292 +1,292 @@ -{ - "Nostalgia" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 17000, "max" : 20000, "density" : 3 }, - { "min" : 10000, "max" : 17000, "density" : 9 }, - { "min" : 6000, "max" : 8999, "density" : 9 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "18" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "19" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "20" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "21" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "22" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "23" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "24" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "8", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "9", "b" : "14", "guard" : 3000 }, - { "a" : "10", "b" : "17", "guard" : 6000 }, - { "a" : "15", "b" : "17", "guard" : 6000 }, - { "a" : "12", "b" : "16", "guard" : 6000 }, - { "a" : "13", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "18", "guard" : 6000 }, - { "a" : "12", "b" : "18", "guard" : 6000 }, - { "a" : "13", "b" : "21", "guard" : 6000 }, - { "a" : "14", "b" : "21", "guard" : 6000 }, - { "a" : "15", "b" : "20", "guard" : 6000 }, - { "a" : "14", "b" : "20", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "15", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "24", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 } - ] - } -} +{ + "Nostalgia" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 17000, "max" : 20000, "density" : 3 }, + { "min" : 10000, "max" : 17000, "density" : 9 }, + { "min" : 6000, "max" : 8999, "density" : 9 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "18" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "19" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "20" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "21" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "22" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "23" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "24" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "8", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "9", "b" : "14", "guard" : 3000 }, + { "a" : "10", "b" : "17", "guard" : 6000 }, + { "a" : "15", "b" : "17", "guard" : 6000 }, + { "a" : "12", "b" : "16", "guard" : 6000 }, + { "a" : "13", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "18", "guard" : 6000 }, + { "a" : "12", "b" : "18", "guard" : 6000 }, + { "a" : "13", "b" : "21", "guard" : 6000 }, + { "a" : "14", "b" : "21", "guard" : 6000 }, + { "a" : "15", "b" : "20", "guard" : 6000 }, + { "a" : "14", "b" : "20", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "15", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "24", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON index 56bc94559..ddf7cd326 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON @@ -1,149 +1,149 @@ -{ - "Oceans Eleven" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "grass" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 6000, "density" : 1 }, - { "min" : 4000, "max" : 5000, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 6000, "density" : 1 }, - { "min" : 4000, "max" : 6000, "density" : 10 }, - { "min" : 2000, "max" : 3000, "density" : 9 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 7000, "max" : 8000, "density" : 1 }, - { "min" : 6000, "max" : 8000, "density" : 9 }, - { "min" : 2000, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 9000, "max" : 11000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 9 }, - { "min" : 3000, "max" : 4000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "lava" ], - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 6000, "max" : 8000, "density" : 8 }, - { "min" : 2000, "max" : 3000, "density" : 10 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "grass" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 5000 }, - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 7000 }, - { "a" : "3", "b" : "5", "guard" : 7000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 9000 }, - { "a" : "6", "b" : "8", "guard" : 9000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 7000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "10", "b" : "11", "guard" : 5000 } - ] - } -} +{ + "Oceans Eleven" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "grass" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 6000, "density" : 1 }, + { "min" : 4000, "max" : 5000, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 6000, "density" : 1 }, + { "min" : 4000, "max" : 6000, "density" : 10 }, + { "min" : 2000, "max" : 3000, "density" : 9 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 7000, "max" : 8000, "density" : 1 }, + { "min" : 6000, "max" : 8000, "density" : 9 }, + { "min" : 2000, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 9000, "max" : 11000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 9 }, + { "min" : 3000, "max" : 4000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "lava" ], + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 6000, "max" : 8000, "density" : 8 }, + { "min" : 2000, "max" : 3000, "density" : 10 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "grass" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 5000 }, + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 7000 }, + { "a" : "3", "b" : "5", "guard" : 7000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 9000 }, + { "a" : "6", "b" : "8", "guard" : 9000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 7000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "10", "b" : "11", "guard" : 5000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON index 937fba84f..773b68625 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON @@ -1,173 +1,173 @@ -{ - "Panic" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 6000, "density" : 2 }, - { "min" : 3500, "max" : 4500, "density" : 5 }, - { "min" : 300, "max" : 2000, "density" : 4 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 7000, "density" : 7 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 300, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 6000, "max" : 8000, "density" : 6 }, - { "min" : 3500, "max" : 6000, "density" : 3 }, - { "min" : 800, "max" : 800, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9500, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 3 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "12" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "13" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 10500 }, - { "a" : "5", "b" : "6", "guard" : 20000 }, - { "a" : "6", "b" : "7", "guard" : 20000 }, - { "a" : "7", "b" : "4", "guard" : 10500 }, - { "a" : "3", "b" : "8", "guard" : 10500 }, - { "a" : "8", "b" : "9", "guard" : 20000 }, - { "a" : "9", "b" : "10", "guard" : 20000 }, - { "a" : "10", "b" : "4", "guard" : 10500 }, - { "a" : "3", "b" : "11", "guard" : 10500 }, - { "a" : "11", "b" : "12", "guard" : 20000 }, - { "a" : "12", "b" : "13", "guard" : 20000 }, - { "a" : "13", "b" : "4", "guard" : 10500 }, - { "a" : "5", "b" : "7", "guard" : 10500 }, - { "a" : "8", "b" : "10", "guard" : 10500 }, - { "a" : "11", "b" : "13", "guard" : 10500 }, - { "a" : "3", "b" : "4", "guard" : 70000 } - ] - } -} +{ + "Panic" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 6000, "density" : 2 }, + { "min" : 3500, "max" : 4500, "density" : 5 }, + { "min" : 300, "max" : 2000, "density" : 4 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 7000, "density" : 7 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 300, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 6000, "max" : 8000, "density" : 6 }, + { "min" : 3500, "max" : 6000, "density" : 3 }, + { "min" : 800, "max" : 800, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9500, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 3 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "12" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "13" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 10500 }, + { "a" : "5", "b" : "6", "guard" : 20000 }, + { "a" : "6", "b" : "7", "guard" : 20000 }, + { "a" : "7", "b" : "4", "guard" : 10500 }, + { "a" : "3", "b" : "8", "guard" : 10500 }, + { "a" : "8", "b" : "9", "guard" : 20000 }, + { "a" : "9", "b" : "10", "guard" : 20000 }, + { "a" : "10", "b" : "4", "guard" : 10500 }, + { "a" : "3", "b" : "11", "guard" : 10500 }, + { "a" : "11", "b" : "12", "guard" : 20000 }, + { "a" : "12", "b" : "13", "guard" : 20000 }, + { "a" : "13", "b" : "4", "guard" : 10500 }, + { "a" : "5", "b" : "7", "guard" : 10500 }, + { "a" : "8", "b" : "10", "guard" : 10500 }, + { "a" : "11", "b" : "13", "guard" : 10500 }, + { "a" : "3", "b" : "4", "guard" : 70000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON index d04595322..3113a91dd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON @@ -1,83 +1,83 @@ -{ - "Poor Jebus" : - //(made by Bjorn190, modified by Maretti and Angelito) - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 12000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 16000, "density" : 6 }, - { "min" : 400, "max" : 3000, "density" : 4 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "castles" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 45000 }, - { "a" : "2", "b" : "5", "guard" : 45000 }, - { "a" : "3", "b" : "5", "guard" : 45000 }, - { "a" : "4", "b" : "5", "guard" : 45000 } - ] - } -} +{ + "Poor Jebus" : + //(made by Bjorn190, modified by Maretti and Angelito) + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 12000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 16000, "density" : 6 }, + { "min" : 400, "max" : 3000, "density" : 4 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "castles" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 45000 }, + { "a" : "2", "b" : "5", "guard" : 45000 }, + { "a" : "3", "b" : "5", "guard" : 45000 }, + { "a" : "4", "b" : "5", "guard" : 45000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON index 82911eafd..f41c68cd3 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON @@ -1,140 +1,140 @@ -{ - "Reckless" : - //(2 player, 6-Jan-03, midnight design) - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 9800, "density" : 1 }, - { "min" : 3500, "max" : 4500, "density" : 5 }, - { "min" : 1500, "max" : 2000, "density" : 7 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 25000, "density" : 1 }, - { "min" : 3500, "max" : 9800, "density" : 8 }, - { "min" : 800, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "castles" : 4 }, - "bannedTowns" : ["necropolis", "conflux"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 2, "ore" : 2, "gold" : 3 }, - "treasure" : - [ - { "min" : 40000, "max" : 60000, "density" : 1 }, - { "min" : 500, "max" : 29000, "density" : 12 }, - { "min" : 800, "max" : 2000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "2", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 11000 }, - { "a" : "4", "b" : "9", "guard" : 11000 }, - { "a" : "5", "b" : "9", "guard" : 11000 }, - { "a" : "6", "b" : "9", "guard" : 11000 }, - { "a" : "7", "b" : "9", "guard" : 11000 }, - { "a" : "8", "b" : "9", "guard" : 11000 } - ] - } -} +{ + "Reckless" : + //(2 player, 6-Jan-03, midnight design) + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 9800, "density" : 1 }, + { "min" : 3500, "max" : 4500, "density" : 5 }, + { "min" : 1500, "max" : 2000, "density" : 7 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 25000, "density" : 1 }, + { "min" : 3500, "max" : 9800, "density" : 8 }, + { "min" : 800, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "castles" : 4 }, + "bannedTowns" : ["necropolis", "conflux"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 2, "ore" : 2, "gold" : 3 }, + "treasure" : + [ + { "min" : 40000, "max" : 60000, "density" : 1 }, + { "min" : 500, "max" : 29000, "density" : 12 }, + { "min" : 800, "max" : 2000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "2", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 11000 }, + { "a" : "4", "b" : "9", "guard" : 11000 }, + { "a" : "5", "b" : "9", "guard" : 11000 }, + { "a" : "6", "b" : "9", "guard" : 11000 }, + { "a" : "7", "b" : "9", "guard" : 11000 }, + { "a" : "8", "b" : "9", "guard" : 11000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON index 4167424c0..063ec0b1f 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON @@ -1,139 +1,139 @@ -{ - "Roadrunner" : - //(ban fly/DD, 2 player, 31-May-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 6000, "density" : 4 }, - { "min" : 300, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9500, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 45000, "max" : 50000, "density" : 1 }, - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 8000, "max" : 9200, "density" : 12 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 100000, "max" : 120000, "density" : 1 }, - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 8000, "max" : 9200, "density" : 12 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 } - ] - } -} +{ + "Roadrunner" : + //(ban fly/DD, 2 player, 31-May-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 6000, "density" : 4 }, + { "min" : 300, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9500, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 45000, "max" : 50000, "density" : 1 }, + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 8000, "max" : 9200, "density" : 12 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 100000, "max" : 120000, "density" : 1 }, + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 8000, "max" : 9200, "density" : 12 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON index 9acc81b5c..cee02bf8a 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON @@ -1,61 +1,61 @@ -{ - "Schaafworld" : - { - "minSize" : "l", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 14000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 5, - "owner" : 3, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 14000, "max" : 20000, "density" : 3 }, - { "min" : 5000, "max" : 8000, "density" : 6 }, - { "min" : 1000, "max" : 5000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 80000 }, - { "a" : "2", "b" : "3", "guard" : 80000 } - ] - } -} +{ + "Schaafworld" : + { + "minSize" : "l", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 14000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 5, + "owner" : 3, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 14000, "max" : 20000, "density" : 3 }, + { "min" : 5000, "max" : 8000, "density" : 6 }, + { "min" : 1000, "max" : 5000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 80000 }, + { "a" : "2", "b" : "3", "guard" : 80000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON index 11cf29d22..99080a813 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON @@ -1,233 +1,233 @@ -{ - "Skirmish M" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 7 }, - { "min" : 330, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 40000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 }, - { "min" : 350, "max" : 2000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 9000 }, - { "a" : "3", "b" : "6", "guard" : 9000 }, - { "a" : "3", "b" : "7", "guard" : 9000 }, - { "a" : "4", "b" : "5", "guard" : 9000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 14000 }, - { "a" : "6", "b" : "7", "guard" : 14000 }, - { "a" : "7", "b" : "5", "guard" : 14000 } - ] - }, - "Skirmish L" : - { - "minSize" : "m+u", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 }, - { "min" : 330, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 60000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 11000 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "5", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "6", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 }, - { "a" : "7", "b" : "5", "guard" : 16000 } - ] - } -} +{ + "Skirmish M" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 7 }, + { "min" : 330, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 40000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 }, + { "min" : 350, "max" : 2000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 9000 }, + { "a" : "3", "b" : "6", "guard" : 9000 }, + { "a" : "3", "b" : "7", "guard" : 9000 }, + { "a" : "4", "b" : "5", "guard" : 9000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 14000 }, + { "a" : "6", "b" : "7", "guard" : 14000 }, + { "a" : "7", "b" : "5", "guard" : 14000 } + ] + }, + "Skirmish L" : + { + "minSize" : "m+u", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 }, + { "min" : 330, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 60000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 11000 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "5", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "6", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 }, + { "a" : "7", "b" : "5", "guard" : 16000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON index 6bf29f7b0..7d275252b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON @@ -1,108 +1,108 @@ -{ - "Speed 1" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6500, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "weak", - "neutralTowns" : { "castles" : 2 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "subterra" ], - "mines" : { "gold" : 3 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "7", "guard" : 17500 }, - { "a" : "1", "b" : "7", "guard" : 17500 } - ] - } -} +{ + "Speed 1" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6500, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "weak", + "neutralTowns" : { "castles" : 2 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "subterra" ], + "mines" : { "gold" : 3 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "7", "guard" : 17500 }, + { "a" : "1", "b" : "7", "guard" : 17500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON index b503b3ae0..804e833a5 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON @@ -1,93 +1,93 @@ -{ - "Speed 2" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 1000, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 1500, "density" : 6 }, - { "min" : 2000, "max" : 3000, "density" : 8 }, - { "min" : 5000, "max" : 9000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 4500 }, - { "a" : "1", "b" : "2", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 4500 }, - { "a" : "1", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "Speed 2" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 1000, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 1500, "density" : 6 }, + { "min" : 2000, "max" : 3000, "density" : 8 }, + { "min" : 5000, "max" : 9000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 4500 }, + { "a" : "1", "b" : "2", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 4500 }, + { "a" : "1", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON index f7e257f48..ca7125434 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON @@ -1,287 +1,287 @@ -{ - "Spider" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9999, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 2 }, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 13, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 2 }, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 15, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 16000, "max" : 20000, "density" : 3 }, - { "min" : 10000, "max" : 16000, "density" : 9 }, - { "min" : 6000, "max" : 8999, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "5", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "16", "guard" : 3000 }, - { "a" : "7", "b" : "11", "guard" : 3000 }, - { "a" : "7", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "12", "guard" : 3000 }, - { "a" : "8", "b" : "14", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "9", "b" : "17", "guard" : 12500 }, - { "a" : "9", "b" : "17", "guard" : 12500 }, - { "a" : "10", "b" : "18", "guard" : 12500 }, - { "a" : "10", "b" : "18", "guard" : 12500 }, - { "a" : "11", "b" : "19", "guard" : 12500 }, - { "a" : "11", "b" : "19", "guard" : 12500 }, - { "a" : "12", "b" : "20", "guard" : 12500 }, - { "a" : "12", "b" : "20", "guard" : 12500 }, - { "a" : "13", "b" : "24", "guard" : 12500 }, - { "a" : "13", "b" : "24", "guard" : 12500 }, - { "a" : "15", "b" : "23", "guard" : 12500 }, - { "a" : "15", "b" : "23", "guard" : 12500 }, - { "a" : "14", "b" : "21", "guard" : 12500 }, - { "a" : "14", "b" : "21", "guard" : 12500 }, - { "a" : "16", "b" : "22", "guard" : 12500 }, - { "a" : "16", "b" : "22", "guard" : 12500 } - ] - } -} +{ + "Spider" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9999, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 2 }, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 13, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 2 }, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 15, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 16000, "max" : 20000, "density" : 3 }, + { "min" : 10000, "max" : 16000, "density" : 9 }, + { "min" : 6000, "max" : 8999, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "5", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "16", "guard" : 3000 }, + { "a" : "7", "b" : "11", "guard" : 3000 }, + { "a" : "7", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "12", "guard" : 3000 }, + { "a" : "8", "b" : "14", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "9", "b" : "17", "guard" : 12500 }, + { "a" : "9", "b" : "17", "guard" : 12500 }, + { "a" : "10", "b" : "18", "guard" : 12500 }, + { "a" : "10", "b" : "18", "guard" : 12500 }, + { "a" : "11", "b" : "19", "guard" : 12500 }, + { "a" : "11", "b" : "19", "guard" : 12500 }, + { "a" : "12", "b" : "20", "guard" : 12500 }, + { "a" : "12", "b" : "20", "guard" : 12500 }, + { "a" : "13", "b" : "24", "guard" : 12500 }, + { "a" : "13", "b" : "24", "guard" : 12500 }, + { "a" : "15", "b" : "23", "guard" : 12500 }, + { "a" : "15", "b" : "23", "guard" : 12500 }, + { "a" : "14", "b" : "21", "guard" : 12500 }, + { "a" : "14", "b" : "21", "guard" : 12500 }, + { "a" : "16", "b" : "22", "guard" : 12500 }, + { "a" : "16", "b" : "22", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON index 19d3922e9..0bad27b1e 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON @@ -1,111 +1,111 @@ -{ - "SuperSlam" : - //(2 player, Large or XL no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules - { - "minSize" : "l", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 400, "max" : 2000, "density" : 6 }, - { "min" : 3500, "max" : 5000, "density" : 5 }, - { "min" : 2100, "max" : 3000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9500, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "mercury" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 5 }, - { "min" : 10000, "max" : 22000, "density" : 3 }, - { "min" : 1000, "max" : 1700, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "minesLikeZone" : 5, - "treasure" : - [ - { "min" : 10000, "max" : 22000, "density" : 3 }, - { "min" : 25000, "max" : 29000, "density" : 5 }, - { "min" : 1000, "max" : 1700, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 10 }, - { "a" : "2", "b" : "4", "guard" : 10 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 120000 }, - { "a" : "3", "b" : "4", "guard" : 120000 } - ] - } -} +{ + "SuperSlam" : + //(2 player, Large or XL no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules + { + "minSize" : "l", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 400, "max" : 2000, "density" : 6 }, + { "min" : 3500, "max" : 5000, "density" : 5 }, + { "min" : 2100, "max" : 3000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9500, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "mercury" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 5 }, + { "min" : 10000, "max" : 22000, "density" : 3 }, + { "min" : 1000, "max" : 1700, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "minesLikeZone" : 5, + "treasure" : + [ + { "min" : 10000, "max" : 22000, "density" : 3 }, + { "min" : 25000, "max" : 29000, "density" : 5 }, + { "min" : 1000, "max" : 1700, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 10 }, + { "a" : "2", "b" : "4", "guard" : 10 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 120000 }, + { "a" : "3", "b" : "4", "guard" : 120000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON index 039075c55..6dcd06d10 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON @@ -1,165 +1,165 @@ -{ - "Balance (sc2tv tourney edition)" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 7 }, - { "min" : 8000, "max" : 9500, "density" : 7 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 5 }, - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 6000, "max" : 8000, "density" : 7 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle"], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle"], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "2", "b" : "9", "guard" : 5000 }, - { "a" : "6", "b" : "3", "guard" : 11000 }, - { "a" : "7", "b" : "4", "guard" : 12000 }, - { "a" : "8", "b" : "3", "guard" : 11000 }, - { "a" : "9", "b" : "4", "guard" : 12000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 20000 }, - { "a" : "2", "b" : "5", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 16000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - } -} +{ + "Balance (sc2tv tourney edition)" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 7 }, + { "min" : 8000, "max" : 9500, "density" : 7 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 5 }, + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 6000, "max" : 8000, "density" : 7 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle"], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle"], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "2", "b" : "9", "guard" : 5000 }, + { "a" : "6", "b" : "3", "guard" : 11000 }, + { "a" : "7", "b" : "4", "guard" : 12000 }, + { "a" : "8", "b" : "3", "guard" : 11000 }, + { "a" : "9", "b" : "4", "guard" : 12000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 20000 }, + { "a" : "2", "b" : "5", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 16000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON index 33af217a1..015cf2168 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON @@ -5117,7 +5117,7 @@ "Midnight Mix 34" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "6", "cpu" : "2", + "players" : "6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON index 12447741e..11c9a025d 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON @@ -1,111 +1,111 @@ -{ - "Skirmish (sc2tv tourney edition)" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 7 }, - { "min" : 330, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 40000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 }, - { "min" : 350, "max" : 2000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 11000 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "5", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "6", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 }, - { "a" : "7", "b" : "5", "guard" : 16000 } - ] - } -} +{ + "Skirmish (sc2tv tourney edition)" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 7 }, + { "min" : 330, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 40000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 }, + { "min" : 350, "max" : 2000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 11000 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "5", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "6", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 }, + { "a" : "7", "b" : "5", "guard" : 16000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON index 1457bf7ea..83b828e4b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON @@ -1,708 +1,708 @@ -{ - "True Random" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 6 }, - { "min" : 4000, "max" : 5500, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 7 }, - { "min" : 15000, "max" : 18000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 7 }, - { "min" : 50000, "max" : 70000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 12 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 3 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 12 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 8000 }, - { "a" : "10", "b" : "14", "guard" : 8000 }, - { "a" : "13", "b" : "14", "guard" : 12000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - }, - "True Random_2" : - { - "minSize" : "xl", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 4000, "max" : 5500, "density" : 5 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 15000, "max" : 18000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 60000, "max" : 80000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 4000 }, - { "a" : "2", "b" : "12", "guard" : 4000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 9000 }, - { "a" : "10", "b" : "14", "guard" : 9000 }, - { "a" : "13", "b" : "14", "guard" : 14000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - }, - "True Random_3" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 4000, "max" : 5500, "density" : 5 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 15000, "max" : 18000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 70000, "max" : 100000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 5 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 4500 }, - { "a" : "2", "b" : "12", "guard" : 4500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 9000 }, - { "a" : "10", "b" : "14", "guard" : 9000 }, - { "a" : "13", "b" : "14", "guard" : 15000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - } -} +{ + "True Random" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 6 }, + { "min" : 4000, "max" : 5500, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 7 }, + { "min" : 15000, "max" : 18000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 7 }, + { "min" : 50000, "max" : 70000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 12 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 3 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 12 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 8000 }, + { "a" : "10", "b" : "14", "guard" : 8000 }, + { "a" : "13", "b" : "14", "guard" : 12000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + }, + "True Random_2" : + { + "minSize" : "xl", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 4000, "max" : 5500, "density" : 5 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 15000, "max" : 18000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 60000, "max" : 80000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 4000 }, + { "a" : "2", "b" : "12", "guard" : 4000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 9000 }, + { "a" : "10", "b" : "14", "guard" : 9000 }, + { "a" : "13", "b" : "14", "guard" : 14000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + }, + "True Random_3" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 4000, "max" : 5500, "density" : 5 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 15000, "max" : 18000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 70000, "max" : 100000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 5 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 4500 }, + { "a" : "2", "b" : "12", "guard" : 4500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 9000 }, + { "a" : "10", "b" : "14", "guard" : 9000 }, + { "a" : "13", "b" : "14", "guard" : 15000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON index 57849e428..8a4802b01 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON @@ -1,226 +1,226 @@ -{ - "Dwarven Tunnels" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "9" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "12" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 8 - }, - "15" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "16" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 8 - }, - "18" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 9000 }, - { "a" : "2", "b" : "8", "guard" : 9000 }, - { "a" : "3", "b" : "9", "guard" : 9000 }, - { "a" : "4", "b" : "17", "guard" : 9000 }, - - { "a" : "12", "b" : "5", "guard" : 9000 }, - { "a" : "12", "b" : "9", "guard" : 12500 }, - { "a" : "12", "b" : "11", "guard" : 12500 }, - { "a" : "12", "b" : "13", "guard" : 15000 }, - { "a" : "12", "b" : "16", "guard" : 15000 }, - { "a" : "12", "b" : "17", "guard" : 12500 }, - { "a" : "12", "b" : "18", "guard" : 12500 }, - - { "a" : "14", "b" : "6", "guard" : 9000 }, - { "a" : "14", "b" : "7", "guard" : 9000 }, - { "a" : "14", "b" : "11", "guard" : 12500 }, - { "a" : "14", "b" : "15", "guard" : 12500 }, - - { "a" : "8", "b" : "18", "guard" : 12500 }, - { "a" : "11", "b" : "18", "guard" : 12500 }, - - { "a" : "8", "b" : "9", "guard" : 12500 }, - { "a" : "8", "b" : "11", "guard" : 12500 }, - { "a" : "15", "b" : "11", "guard" : 12500 }, - { "a" : "15", "b" : "17", "guard" : 12500 }, - { "a" : "10", "b" : "11", "guard" : 15000 } - ] - } -} +{ + "Dwarven Tunnels" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "9" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "12" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 8 + }, + "15" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "16" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 8 + }, + "18" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 9000 }, + { "a" : "2", "b" : "8", "guard" : 9000 }, + { "a" : "3", "b" : "9", "guard" : 9000 }, + { "a" : "4", "b" : "17", "guard" : 9000 }, + + { "a" : "12", "b" : "5", "guard" : 9000 }, + { "a" : "12", "b" : "9", "guard" : 12500 }, + { "a" : "12", "b" : "11", "guard" : 12500 }, + { "a" : "12", "b" : "13", "guard" : 15000 }, + { "a" : "12", "b" : "16", "guard" : 15000 }, + { "a" : "12", "b" : "17", "guard" : 12500 }, + { "a" : "12", "b" : "18", "guard" : 12500 }, + + { "a" : "14", "b" : "6", "guard" : 9000 }, + { "a" : "14", "b" : "7", "guard" : 9000 }, + { "a" : "14", "b" : "11", "guard" : 12500 }, + { "a" : "14", "b" : "15", "guard" : 12500 }, + + { "a" : "8", "b" : "18", "guard" : 12500 }, + { "a" : "11", "b" : "18", "guard" : 12500 }, + + { "a" : "8", "b" : "9", "guard" : 12500 }, + { "a" : "8", "b" : "11", "guard" : 12500 }, + { "a" : "15", "b" : "11", "guard" : 12500 }, + { "a" : "15", "b" : "17", "guard" : 12500 }, + { "a" : "10", "b" : "11", "guard" : 15000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON index bada8c1cf..84ff216ce 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON @@ -1,109 +1,109 @@ -{ - "Golems Aplenty" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 1000, "max" : 4000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "tower" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "snow" ], - "treasure" : - [ - { "min" : 3000, "max" : 9000, "density" : 10 }, - { "min" : 5000, "max" : 18000, "density" : 6 }, - { "min" : 9000, "max" : 60000, "density" : 4 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra" ], - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "treasureLikeZone" : 6 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 8000 }, - { "a" : "1", "b" : "6", "guard" : 10000 }, - { "a" : "2", "b" : "5", "guard" : 8000 }, - { "a" : "3", "b" : "5", "guard" : 8000 }, - { "a" : "3", "b" : "7", "guard" : 10000 }, - { "a" : "4", "b" : "5", "guard" : 8000 }, - { "a" : "5", "b" : "6", "guard" : 7000 }, - { "a" : "5", "b" : "7", "guard" : 7000 } - ] - } -} +{ + "Golems Aplenty" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 1000, "max" : 4000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "tower" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "snow" ], + "treasure" : + [ + { "min" : 3000, "max" : 9000, "density" : 10 }, + { "min" : 5000, "max" : 18000, "density" : 6 }, + { "min" : 9000, "max" : 60000, "density" : 4 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra" ], + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "treasureLikeZone" : 6 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 8000 }, + { "a" : "1", "b" : "6", "guard" : 10000 }, + { "a" : "2", "b" : "5", "guard" : 8000 }, + { "a" : "3", "b" : "5", "guard" : 8000 }, + { "a" : "3", "b" : "7", "guard" : 10000 }, + { "a" : "4", "b" : "5", "guard" : 8000 }, + { "a" : "5", "b" : "6", "guard" : 7000 }, + { "a" : "5", "b" : "7", "guard" : 7000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON index d88b64e4a..189124ae2 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON @@ -2,7 +2,7 @@ "Meeting in Muzgob" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", "cpu" : "2", + "players" : "2-6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON index 87e64680c..e78b47d5c 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON @@ -1,139 +1,139 @@ -{ - "Monk's Retreat" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 10 }, - { "min" : 6000, "max" : 9000, "density" : 8 }, - { "min" : 9000, "max" : 30000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp" ], - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - - { "a" : "1", "b" : "9", "guard" : 18000 }, - { "a" : "2", "b" : "9", "guard" : 18000 }, - { "a" : "3", "b" : "8", "guard" : 18000 }, - { "a" : "4", "b" : "8", "guard" : 18000 }, - { "a" : "5", "b" : "7", "guard" : 18000 }, - { "a" : "6", "b" : "7", "guard" : 18000 }, - - { "a" : "8", "b" : "7", "guard" : 12000 }, - { "a" : "8", "b" : "9", "guard" : 12000 } - ] - } -} +{ + "Monk's Retreat" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 10 }, + { "min" : 6000, "max" : 9000, "density" : 8 }, + { "min" : 9000, "max" : 30000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp" ], + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + + { "a" : "1", "b" : "9", "guard" : 18000 }, + { "a" : "2", "b" : "9", "guard" : 18000 }, + { "a" : "3", "b" : "8", "guard" : 18000 }, + { "a" : "4", "b" : "8", "guard" : 18000 }, + { "a" : "5", "b" : "7", "guard" : 18000 }, + { "a" : "6", "b" : "7", "guard" : 18000 }, + + { "a" : "8", "b" : "7", "guard" : 12000 }, + { "a" : "8", "b" : "9", "guard" : 12000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON index 1c18e5574..7593292e7 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON @@ -1,177 +1,177 @@ -{ - "Newcomers" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-3", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 6000, "max" : 15000, "density" : 6 }, - { "min" : 15000, "max" : 20000, "density" : 1 } - ] - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - } - }, - "connections" : - [ - { "a" : "12", "b" : "1", "guard" : 6000 }, - { "a" : "12", "b" : "3", "guard" : 6000 }, - { "a" : "12", "b" : "7", "guard" : 6000 }, - { "a" : "12", "b" : "9", "guard" : 6000 }, - - { "a" : "13", "b" : "2", "guard" : 6000 }, - { "a" : "13", "b" : "5", "guard" : 6000 }, - { "a" : "13", "b" : "7", "guard" : 6000 }, - { "a" : "13", "b" : "8", "guard" : 6000 }, - { "a" : "13", "b" : "10", "guard" : 6000 }, - - { "a" : "14", "b" : "4", "guard" : 6000 }, - { "a" : "14", "b" : "6", "guard" : 6000 }, - { "a" : "14", "b" : "8", "guard" : 6000 }, - { "a" : "14", "b" : "11", "guard" : 6000 } - ] - } -} +{ + "Newcomers" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-3", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 6000, "max" : 15000, "density" : 6 }, + { "min" : 15000, "max" : 20000, "density" : 1 } + ] + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + } + }, + "connections" : + [ + { "a" : "12", "b" : "1", "guard" : 6000 }, + { "a" : "12", "b" : "3", "guard" : 6000 }, + { "a" : "12", "b" : "7", "guard" : 6000 }, + { "a" : "12", "b" : "9", "guard" : 6000 }, + + { "a" : "13", "b" : "2", "guard" : 6000 }, + { "a" : "13", "b" : "5", "guard" : 6000 }, + { "a" : "13", "b" : "7", "guard" : 6000 }, + { "a" : "13", "b" : "8", "guard" : 6000 }, + { "a" : "13", "b" : "10", "guard" : 6000 }, + + { "a" : "14", "b" : "4", "guard" : 6000 }, + { "a" : "14", "b" : "6", "guard" : 6000 }, + { "a" : "14", "b" : "8", "guard" : 6000 }, + { "a" : "14", "b" : "11", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON index 07a3d8391..e7bef4fe1 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON @@ -1,146 +1,146 @@ -{ - "Ready or Not" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "inferno" ], - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "rampart" ], - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "rampart" ], - "allowedMonsters" : [ "inferno", "fortress", "neutral" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "fortress" ], - "allowedMonsters" : [ "rampart", "inferno", "neutral" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "inferno" ], - "allowedMonsters" : [ "rampart", "fortress", "neutral" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "terrainTypeLikeZone" : 8, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "terrainTypeLikeZone" : 8, - "treasureLikeZone" : 8 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "6", "b" : "5", "guard" : 0 }, - { "a" : "7", "b" : "5", "guard" : 0 }, - { "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster - { "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster - { "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster - ] - } -} +{ + "Ready or Not" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "inferno" ], + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "rampart" ], + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "rampart" ], + "allowedMonsters" : [ "inferno", "fortress", "neutral" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "fortress" ], + "allowedMonsters" : [ "rampart", "inferno", "neutral" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "inferno" ], + "allowedMonsters" : [ "rampart", "fortress", "neutral" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "terrainTypeLikeZone" : 8, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "terrainTypeLikeZone" : 8, + "treasureLikeZone" : 8 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "6", "b" : "5", "guard" : 0 }, + { "a" : "7", "b" : "5", "guard" : 0 }, + { "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster + { "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster + { "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON index f2ac532b6..2f41adc27 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON @@ -1,110 +1,110 @@ -{ - "Small Ring" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 4500 }, - { "a" : "2", "b" : "3", "guard" : 4500 }, - { "a" : "3", "b" : "4", "guard" : 4500 }, - { "a" : "4", "b" : "5", "guard" : 4500 }, - { "a" : "5", "b" : "6", "guard" : 4500 }, - { "a" : "6", "b" : "7", "guard" : 4500 }, - { "a" : "7", "b" : "8", "guard" : 4500 }, - { "a" : "8", "b" : "1", "guard" : 4500 }, - { "a" : "4", "b" : "1", "guard" : 4500 }, - { "a" : "5", "b" : "1", "guard" : 4500 }, - { "a" : "6", "b" : "1", "guard" : 4500 }, - { "a" : "7", "b" : "1", "guard" : 4500 } - ] - } -} +{ + "Small Ring" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 4500 }, + { "a" : "2", "b" : "3", "guard" : 4500 }, + { "a" : "3", "b" : "4", "guard" : 4500 }, + { "a" : "4", "b" : "5", "guard" : 4500 }, + { "a" : "5", "b" : "6", "guard" : 4500 }, + { "a" : "6", "b" : "7", "guard" : 4500 }, + { "a" : "7", "b" : "8", "guard" : 4500 }, + { "a" : "8", "b" : "1", "guard" : 4500 }, + { "a" : "4", "b" : "1", "guard" : 4500 }, + { "a" : "5", "b" : "1", "guard" : 4500 }, + { "a" : "6", "b" : "1", "guard" : 4500 }, + { "a" : "7", "b" : "1", "guard" : 4500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON index 2be3522d0..c93d0fb96 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON @@ -1,165 +1,165 @@ -{ - "South of Hell" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 35, - "owner" : 3, - "monsters" : "strong", - "playerTowns" : { "towns" : 2, "castles" : 3 }, - "allowedTowns" : [ "inferno" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "lava" ], - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 12 }, - { "min" : 3000, "max" : 9000, "density" : 5 }, - { "min" : 9000, "max" : 30000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 1000, "max" : 4000, "density" : 12 }, - { "min" : 4000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 40000, "density" : 1 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 4 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 2 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "9" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 3 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "3", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 8000 }, - { "a" : "2", "b" : "9", "guard" : 8000 }, - { "a" : "3", "b" : "10", "guard" : 10000 }, - { "a" : "8", "b" : "11", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "7", "guard" : 6000 } - ] - } -} +{ + "South of Hell" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 35, + "owner" : 3, + "monsters" : "strong", + "playerTowns" : { "towns" : 2, "castles" : 3 }, + "allowedTowns" : [ "inferno" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "lava" ], + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 12 }, + { "min" : 3000, "max" : 9000, "density" : 5 }, + { "min" : 9000, "max" : 30000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 1000, "max" : 4000, "density" : 12 }, + { "min" : 4000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 40000, "density" : 1 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 4 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 2 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "9" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 3 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "3", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 8000 }, + { "a" : "2", "b" : "9", "guard" : 8000 }, + { "a" : "3", "b" : "10", "guard" : 10000 }, + { "a" : "8", "b" : "11", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "7", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON index 1e2ebb553..d3f603b66 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON @@ -1,232 +1,232 @@ -{ - "Worlds at War" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", "cpu" : "3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 8000, "density" : 8 }, - { "min" : 3000, "max" : 15000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 15000, "max" : 20000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 3 }, - { "min" : 10000, "max" : 20000, "density" : 2 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 10000, "max" : 20000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 2 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 10 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 4 }, - { "min" : 10000, "max" : 20000, "density" : 3 } - ] - }, - "13" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 4 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "12", "guard" : 4500 }, - { "a" : "4", "b" : "14", "guard" : 4500 }, - { "a" : "5", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "15", "guard" : 3000 }, - { "a" : "5", "b" : "16", "guard" : 3000 }, - { "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "6", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "10", "guard" : 4500 }, - { "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "8", "b" : "11", "guard" : 4500 }, - { "a" : "9", "b" : "13", "guard" : 6000 }, - { "a" : "12", "b" : "13", "guard" : 4500 }, - { "a" : "13", "b" : "14", "guard" : 4500 }, - { "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 } - ] - } -} +{ + "Worlds at War" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2", "humans" : "3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 8000, "density" : 8 }, + { "min" : 3000, "max" : 15000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 15000, "max" : 20000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 3 }, + { "min" : 10000, "max" : 20000, "density" : 2 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 10000, "max" : 20000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 2 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 10 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 4 }, + { "min" : 10000, "max" : 20000, "density" : 3 } + ] + }, + "13" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 4 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "12", "guard" : 4500 }, + { "a" : "4", "b" : "14", "guard" : 4500 }, + { "a" : "5", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "15", "guard" : 3000 }, + { "a" : "5", "b" : "16", "guard" : 3000 }, + { "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "6", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "10", "guard" : 4500 }, + { "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "8", "b" : "11", "guard" : 4500 }, + { "a" : "9", "b" : "13", "guard" : 6000 }, + { "a" : "12", "b" : "13", "guard" : 4500 }, + { "a" : "13", "b" : "14", "guard" : 4500 }, + { "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON index d5478386c..a71dc2ae4 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON @@ -1,361 +1,361 @@ -{ - "Gauntlet" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "1", "cpu" : "5", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "treasure" : - [ - { "min" : 5000, "max" : 8000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 2 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "treasure" : - [ - { "min" : 10000, "max" : 14000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "mines" : { "wood" : 1 }, - "treasureLikeZone" : 2 - }, - "5" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "rough" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 5, - "mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "treasure" : - [ - { "min" : 12000, "max" : 18000, "density" : 1 }, - { "min" : 5000, "max" : 10000, "density" : 5 }, - { "min" : 1000, "max" : 4000, "density" : 7 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 7, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 6 }, - { "min" : 1000, "max" : 4000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 8000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "11" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 4 }, - { "min" : 1000, "max" : 4000, "density" : 8 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "14" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "minesLikeZone" : 6, - "treasure" : - [ - { "min" : 4000, "max" : 8000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "15" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 11 - }, - "16" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasure" : - [ - { "min" : 10000, "max" : 14000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 7 } - ] - }, - "17" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 14 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "19" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "20" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 18 - }, - "21" : - { - "type" : "cpuStart", - "size" : 8, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "minesLikeZone" : 10, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "22" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "23" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "minesLikeZone" : 18, - "treasure" : - [ - { "min" : 8000, "max" : 12000, "density" : 2 }, - { "min" : 6000, "max" : 10000, "density" : 4 }, - { "min" : 1000, "max" : 5000, "density" : 8 } - ] - }, - "24" : - { - "type" : "cpuStart", - "size" : 8, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "minesLikeZone" : 17, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 1000, "max" : 4000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster - { "a" : "1", "b" : "15", "guard" : 0 }, - { "a" : "2", "b" : "3", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "8", "b" : "9", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "13", "b" : "14", "guard" : 0 }, - { "a" : "14", "b" : "15", "guard" : 0 }, - { "a" : "15", "b" : "16", "guard" : 0 }, - { "a" : "16", "b" : "17", "guard" : 0 }, - { "a" : "17", "b" : "18", "guard" : 0 }, - { "a" : "18", "b" : "19", "guard" : 0 }, - { "a" : "18", "b" : "22", "guard" : 0 }, - { "a" : "19", "b" : "20", "guard" : 0 }, - { "a" : "20", "b" : "21", "guard" : 0 }, - { "a" : "22", "b" : "23", "guard" : 0 }, - { "a" : "23", "b" : "24", "guard" : 0 } - ] - } -} +{ + "Gauntlet" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "1", "humans" : "5", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "treasure" : + [ + { "min" : 5000, "max" : 8000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 2 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "treasure" : + [ + { "min" : 10000, "max" : 14000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "mines" : { "wood" : 1 }, + "treasureLikeZone" : 2 + }, + "5" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "rough" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 5, + "mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "treasure" : + [ + { "min" : 12000, "max" : 18000, "density" : 1 }, + { "min" : 5000, "max" : 10000, "density" : 5 }, + { "min" : 1000, "max" : 4000, "density" : 7 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 7, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 6 }, + { "min" : 1000, "max" : 4000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 8000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "11" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 4 }, + { "min" : 1000, "max" : 4000, "density" : 8 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "14" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "minesLikeZone" : 6, + "treasure" : + [ + { "min" : 4000, "max" : 8000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "15" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 11 + }, + "16" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasure" : + [ + { "min" : 10000, "max" : 14000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 7 } + ] + }, + "17" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 14 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "19" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "20" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 18 + }, + "21" : + { + "type" : "cpuStart", + "size" : 8, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "minesLikeZone" : 10, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "22" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "23" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "minesLikeZone" : 18, + "treasure" : + [ + { "min" : 8000, "max" : 12000, "density" : 2 }, + { "min" : 6000, "max" : 10000, "density" : 4 }, + { "min" : 1000, "max" : 5000, "density" : 8 } + ] + }, + "24" : + { + "type" : "cpuStart", + "size" : 8, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "minesLikeZone" : 17, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 1000, "max" : 4000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster + { "a" : "1", "b" : "15", "guard" : 0 }, + { "a" : "2", "b" : "3", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "8", "b" : "9", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "13", "b" : "14", "guard" : 0 }, + { "a" : "14", "b" : "15", "guard" : 0 }, + { "a" : "15", "b" : "16", "guard" : 0 }, + { "a" : "16", "b" : "17", "guard" : 0 }, + { "a" : "17", "b" : "18", "guard" : 0 }, + { "a" : "18", "b" : "19", "guard" : 0 }, + { "a" : "18", "b" : "22", "guard" : 0 }, + { "a" : "19", "b" : "20", "guard" : 0 }, + { "a" : "20", "b" : "21", "guard" : 0 }, + { "a" : "22", "b" : "23", "guard" : 0 }, + { "a" : "23", "b" : "24", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON index 6df0e8501..3e63bdc95 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON @@ -1,205 +1,205 @@ -{ - "Ring" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 25000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 7 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 7500, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "16" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "type": "wide" }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 2000 }, - { "a" : "1", "b" : "6", "guard" : 2000 }, - { "a" : "1", "b" : "9", "guard" : 2000 }, - { "a" : "1", "b" : "10", "guard" : 2000 }, - { "a" : "1", "b" : "13", "guard" : 2000 }, - { "a" : "1", "b" : "14", "guard" : 2000 }, - { "a" : "2", "b" : "3", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "5", "guard" : 2000 }, - { "a" : "5", "b" : "6", "type": "wide" }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "5", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "9", "guard" : 2000 }, - { "a" : "9", "b" : "10", "type": "wide" }, - { "a" : "9", "b" : "11", "guard" : 0 }, - { "a" : "9", "b" : "12", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "12", "guard" : 0 }, - { "a" : "10", "b" : "13", "guard" : 2000 }, - { "a" : "13", "b" : "14", "type": "wide" }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "13", "b" : "16", "guard" : 0 }, - { "a" : "14", "b" : "15", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 } - ] - } -} +{ + "Ring" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 25000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 7 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 7500, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "16" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "type": "wide" }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 2000 }, + { "a" : "1", "b" : "6", "guard" : 2000 }, + { "a" : "1", "b" : "9", "guard" : 2000 }, + { "a" : "1", "b" : "10", "guard" : 2000 }, + { "a" : "1", "b" : "13", "guard" : 2000 }, + { "a" : "1", "b" : "14", "guard" : 2000 }, + { "a" : "2", "b" : "3", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "5", "guard" : 2000 }, + { "a" : "5", "b" : "6", "type": "wide" }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "5", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "9", "guard" : 2000 }, + { "a" : "9", "b" : "10", "type": "wide" }, + { "a" : "9", "b" : "11", "guard" : 0 }, + { "a" : "9", "b" : "12", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "12", "guard" : 0 }, + { "a" : "10", "b" : "13", "guard" : 2000 }, + { "a" : "13", "b" : "14", "type": "wide" }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "13", "b" : "16", "guard" : 0 }, + { "a" : "14", "b" : "15", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON index 7c193b03a..0616fc083 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON @@ -1,194 +1,194 @@ -{ - "Rise of Phoenix" : - { - "minSize" : "l", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 6000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 9000, "density" : 8 }, - { "min" : 9000, "max" : 18000, "density" : 3 }, - { "min" : 12000, "max" : 30000, "density" : 2 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "inferno", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough", "lava" ], - "treasure" : - [ - { "min" : 9000, "max" : 18000, "density" : 3 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "inferno" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "lava" ], - "treasure" : - [ - { "min" : 25000, "max" : 40000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 12000 }, - { "a" : "2", "b" : "6", "guard" : 12000 }, - { "a" : "3", "b" : "7", "guard" : 12000 }, - { "a" : "4", "b" : "8", "guard" : 12000 }, - - { "a" : "5", "b" : "10", "guard" : 9000 }, - { "a" : "6", "b" : "11", "guard" : 9000 }, - { "a" : "7", "b" : "12", "guard" : 9000 }, - { "a" : "8", "b" : "9", "guard" : 9000 }, - - { "a" : "9", "b" : "13", "guard" : 20000 }, - { "a" : "10", "b" : "13", "guard" : 20000 }, - { "a" : "11", "b" : "13", "guard" : 20000 }, - { "a" : "12", "b" : "13", "guard" : 12000 }, - { "a" : "13", "b" : "14", "guard" : 24000 } - ] - } -} +{ + "Rise of Phoenix" : + { + "minSize" : "l", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 6000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 9000, "density" : 8 }, + { "min" : 9000, "max" : 18000, "density" : 3 }, + { "min" : 12000, "max" : 30000, "density" : 2 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "inferno", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough", "lava" ], + "treasure" : + [ + { "min" : 9000, "max" : 18000, "density" : 3 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "inferno" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "lava" ], + "treasure" : + [ + { "min" : 25000, "max" : 40000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 12000 }, + { "a" : "2", "b" : "6", "guard" : 12000 }, + { "a" : "3", "b" : "7", "guard" : 12000 }, + { "a" : "4", "b" : "8", "guard" : 12000 }, + + { "a" : "5", "b" : "10", "guard" : 9000 }, + { "a" : "6", "b" : "11", "guard" : 9000 }, + { "a" : "7", "b" : "12", "guard" : 9000 }, + { "a" : "8", "b" : "9", "guard" : 9000 }, + + { "a" : "9", "b" : "13", "guard" : 20000 }, + { "a" : "10", "b" : "13", "guard" : 20000 }, + { "a" : "11", "b" : "13", "guard" : 20000 }, + { "a" : "12", "b" : "13", "guard" : 12000 }, + { "a" : "13", "b" : "14", "guard" : 24000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON index 5a8c3ca18..885c1676a 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON @@ -1,105 +1,105 @@ -{ - "2SM0k" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "type": "wide" }, - { "a" : "1", "b" : "7", "type": "wide" }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "type": "wide" }, - { "a" : "2", "b" : "8", "type": "wide" }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM0k" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "humans" : "1-2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "type": "wide" }, + { "a" : "1", "b" : "7", "type": "wide" }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "type": "wide" }, + { "a" : "2", "b" : "8", "type": "wide" }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON index b0baf73d1..20bc458c2 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON @@ -1,67 +1,67 @@ -{ - "2SM2a" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 12500 }, - { "a" : "2", "b" : "3", "guard" : 12500 } - ] - } -} +{ + "2SM2a" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 12500 }, + { "a" : "2", "b" : "3", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON index 5dd44d974..104baeabc 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON @@ -1,87 +1,87 @@ -{ - "2SM2b(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 12500 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 12500 }, - { "a" : "2", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "2SM2b(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 12500 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 12500 }, + { "a" : "2", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON index cb7aecb4b..575860e39 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON @@ -1,87 +1,87 @@ -{ - "2SM2b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 12500 }, - { "a" : "2", "b" : "3", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "2SM2b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 12500 }, + { "a" : "2", "b" : "3", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON index 9dbe1aa17..8f153ed12 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON @@ -1,96 +1,96 @@ -{ - "2SM2c" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 12500 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2c" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 12500 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON index 9f2997591..946ca05d7 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON @@ -1,86 +1,86 @@ -{ - "2SM2f(2)" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2f(2)" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON index 5acae7bfd..35f06217f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON @@ -1,84 +1,84 @@ -{ - "2SM2f" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2f" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON index bd98c40a2..6acaba717 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON @@ -1,80 +1,80 @@ -{ - "2SM2h(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2h(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON index acda62106..e40139fa5 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON @@ -1,77 +1,77 @@ -{ - "2SM2h" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2h" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index b93afd0c0..18dc90ed9 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -1,75 +1,76 @@ -{ - "2SM2i(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 } - ] - } -} +{ + "2SM2i(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "humans": "1-2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON index 0ffee6aab..818386a00 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON @@ -1,76 +1,76 @@ -{ - "2SM2i" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2i" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON index 49ad8bc09..872fe17da 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON @@ -1,134 +1,134 @@ -{ - "2SM4d(2)" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d(2)" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON index 52d4e8c2d..01ee4ad1f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON @@ -1,134 +1,134 @@ -{ - "2SM4d(3)" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d(3)" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON index 566664a2a..1959a32e0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON @@ -1,134 +1,134 @@ -{ - "2SM4d" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON index 61b8fd0aa..7411144d4 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON @@ -1,105 +1,105 @@ -{ - "3SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "3SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON index 47647e043..fea84faa8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON @@ -1,123 +1,123 @@ -{ - "3SB0c" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "7", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "8", "b" : "6", "guard" : 3000 }, - { "a" : "8", "b" : "10", "guard" : 3000 }, - { "a" : "7", "b" : "9", "guard" : 3000 }, - { "a" : "9", "b" : "10", "guard" : 3000 } - ] - } -} +{ + "3SB0c" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "7", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "8", "b" : "6", "guard" : 3000 }, + { "a" : "8", "b" : "10", "guard" : 3000 }, + { "a" : "7", "b" : "9", "guard" : 3000 }, + { "a" : "9", "b" : "10", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON index a4c75f4be..740da0b96 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON @@ -1,164 +1,164 @@ -{ - "3SM3d" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 100, "max" : 1000, "density" : 10 }, - { "min" : 1000, "max" : 3000, "density" : 1 } - ] - }, - "12" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - }, - "14" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "8", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "13", "guard" : 3000 }, - { "a" : "10", "b" : "14", "guard" : 3000 } - ] - } -} +{ + "3SM3d" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 100, "max" : 1000, "density" : 10 }, + { "min" : 1000, "max" : 3000, "density" : 1 } + ] + }, + "12" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + }, + "14" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "8", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "13", "guard" : 3000 }, + { "a" : "10", "b" : "14", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON index cd41be625..fb4cb46c5 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON @@ -1,120 +1,120 @@ -{ - "4SM0d" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "4SM0d" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON index 26216b071..b443fa584 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON @@ -1,107 +1,107 @@ -{ - "4SM0f" : - { - "minSize" : "s", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "4SM0f" : + { + "minSize" : "s", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON index 795f9dd98..85ef75807 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON @@ -1,111 +1,111 @@ -{ - "4SM0g" : - { - "minSize" : "s", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 } - ] - } -} +{ + "4SM0g" : + { + "minSize" : "s", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON index a425806e6..0a9a38af3 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON @@ -1,150 +1,150 @@ -{ - "4SM4e" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 6 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "7", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "10", "guard" : 6000 }, - { "a" : "10", "b" : "11", "guard" : 6000 }, - { "a" : "9", "b" : "12", "guard" : 6000 }, - { "a" : "11", "b" : "12", "guard" : 6000 }, - { "a" : "6", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "11", "guard" : 6000 }, - { "a" : "9", "b" : "11", "guard" : 6000 } - ] - } -} +{ + "4SM4e" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 6 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "7", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "10", "guard" : 6000 }, + { "a" : "10", "b" : "11", "guard" : 6000 }, + { "a" : "9", "b" : "12", "guard" : 6000 }, + { "a" : "11", "b" : "12", "guard" : 6000 }, + { "a" : "6", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "11", "guard" : 6000 }, + { "a" : "9", "b" : "11", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON index 774a10cfa..da29fda5b 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON @@ -1,121 +1,121 @@ -{ - "5SB0a" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 4 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "8", "guard" : 9000 }, - { "a" : "5", "b" : "9", "guard" : 9000 } - ] - } -} +{ + "5SB0a" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "humans" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 4 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "8", "guard" : 9000 }, + { "a" : "5", "b" : "9", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON index cae17f7f8..6d7d63f5f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON @@ -1,97 +1,97 @@ -{ - "5SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 } - ] - } -} +{ + "5SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "humans" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON index 4eb00d2d0..6ed8fcbb8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON @@ -1,306 +1,306 @@ -{ - "6LM10" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "15", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - { "a" : "21", "b" : "22", "type": "wide" }, - { "a" : "24", "b" : "25", "type": "wide" } - ] - } -} +{ + "6LM10" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "15", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + { "a" : "21", "b" : "22", "type": "wide" }, + { "a" : "24", "b" : "25", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON index b0d44cf41..1df537fd3 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON @@ -1,317 +1,317 @@ -{ - "6LM10a" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 8, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "13", "b" : "14", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "25", "guard" : 6000 }, - - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - - { "a" : "21", "b" : "22", "type": "wide" }, - { "a" : "24", "b" : "25", "type": "wide" } - ] - } -} +{ + "6LM10a" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 8, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "13", "b" : "14", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "25", "guard" : 6000 }, + + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + + { "a" : "21", "b" : "22", "type": "wide" }, + { "a" : "24", "b" : "25", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON index dab2c71b1..c7813cc65 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON @@ -1,132 +1,132 @@ -{ - "6SM0b" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 } - ] - } -} +{ + "6SM0b" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON index ab7ae0c6a..911a70020 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON @@ -1,85 +1,85 @@ -{ - "6SM0d" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "6SM0d" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON index a8ffe28ba..ae12f4d89 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON @@ -1,175 +1,175 @@ -{ - "6SM0e" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "13", "b" : "14", "guard" : 3000 } - ] - } -} +{ + "6SM0e" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "13", "b" : "14", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON index eb3c34ba6..10d05291e 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON @@ -1,105 +1,105 @@ -{ - "7SB0b" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "4", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0b" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2-6", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "4", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index 978ea9453..266d0ba57 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -1,101 +1,101 @@ -{ - "7SB0c" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0c" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-7", "humans" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON index 14f30b43b..6bdf4b3a8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON @@ -1,128 +1,128 @@ -{ - "8MM0e" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "7", "guard" : 3000 }, - { "a" : "8", "b" : "1", "guard" : 3000 }, - { "a" : "9", "b" : "1", "guard" : 6000 }, - { "a" : "9", "b" : "2", "guard" : 6000 }, - { "a" : "9", "b" : "3", "guard" : 6000 }, - { "a" : "9", "b" : "4", "guard" : 6000 }, - { "a" : "9", "b" : "5", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "9", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "8MM0e" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "7", "guard" : 3000 }, + { "a" : "8", "b" : "1", "guard" : 3000 }, + { "a" : "9", "b" : "1", "guard" : 6000 }, + { "a" : "9", "b" : "2", "guard" : 6000 }, + { "a" : "9", "b" : "3", "guard" : 6000 }, + { "a" : "9", "b" : "4", "guard" : 6000 }, + { "a" : "9", "b" : "5", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "9", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON index ccf6c43c9..c02038abe 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON @@ -1,307 +1,307 @@ -{ - "8MM6" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "19", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "4", "b" : "20", "guard" : 3000 }, - { "a" : "5", "b" : "21", "guard" : 3000 }, - { "a" : "5", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "22", "guard" : 3000 }, - { "a" : "6", "b" : "26", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 3000 }, - { "a" : "7", "b" : "25", "guard" : 3000 }, - { "a" : "8", "b" : "22", "guard" : 3000 }, - { "a" : "8", "b" : "26", "guard" : 3000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 } - ] - } -} +{ + "8MM6" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "19", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "4", "b" : "20", "guard" : 3000 }, + { "a" : "5", "b" : "21", "guard" : 3000 }, + { "a" : "5", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "22", "guard" : 3000 }, + { "a" : "6", "b" : "26", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 3000 }, + { "a" : "7", "b" : "25", "guard" : 3000 }, + { "a" : "8", "b" : "22", "guard" : 3000 }, + { "a" : "8", "b" : "26", "guard" : 3000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON index fd05ad3a2..0876a97a2 100755 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON @@ -1,311 +1,311 @@ -{ - "8MM6a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "19", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "4", "b" : "20", "guard" : 3000 }, - { "a" : "5", "b" : "21", "guard" : 3000 }, - { "a" : "5", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "22", "guard" : 3000 }, - { "a" : "6", "b" : "26", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 3000 }, - { "a" : "7", "b" : "25", "guard" : 3000 }, - { "a" : "8", "b" : "22", "guard" : 3000 }, - { "a" : "8", "b" : "26", "guard" : 3000 }, - - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "21", "b" : "23", "guard" : 12500 }, - { "a" : "21", "b" : "23", "guard" : 12500 }, - { "a" : "22", "b" : "24", "guard" : 12500 }, - { "a" : "22", "b" : "24", "guard" : 12500 } - ] - } -} +{ + "8MM6a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "19", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "4", "b" : "20", "guard" : 3000 }, + { "a" : "5", "b" : "21", "guard" : 3000 }, + { "a" : "5", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "22", "guard" : 3000 }, + { "a" : "6", "b" : "26", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 3000 }, + { "a" : "7", "b" : "25", "guard" : 3000 }, + { "a" : "8", "b" : "22", "guard" : 3000 }, + { "a" : "8", "b" : "26", "guard" : 3000 }, + + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "21", "b" : "23", "guard" : 12500 }, + { "a" : "21", "b" : "23", "guard" : 12500 }, + { "a" : "22", "b" : "24", "guard" : 12500 }, + { "a" : "22", "b" : "24", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON index 6796e7df0..7e0323041 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON @@ -1,168 +1,168 @@ -{ - "8SM0c" : - { - "minSize" : "m", "maxSize" : "l", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "7", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "12", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "8", "b" : "9", "guard" : 3000 }, - { "a" : "8", "b" : "11", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 3000 }, - { "a" : "9", "b" : "10", "type": "wide" }, - { "a" : "9", "b" : "12", "type": "wide" }, - { "a" : "10", "b" : "13", "type": "wide" }, - { "a" : "12", "b" : "13", "type": "wide" } - ] - } -} +{ + "8SM0c" : + { + "minSize" : "m", "maxSize" : "l", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "7", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "12", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "8", "b" : "9", "guard" : 3000 }, + { "a" : "8", "b" : "11", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 3000 }, + { "a" : "9", "b" : "10", "type": "wide" }, + { "a" : "9", "b" : "12", "type": "wide" }, + { "a" : "10", "b" : "13", "type": "wide" }, + { "a" : "12", "b" : "13", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON index 43b7211ed..1461f43c1 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON @@ -1,151 +1,151 @@ -{ - "8SM0f" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "9", "guard" : 6000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "12", "guard" : 6000 }, - { "a" : "8", "b" : "10", "guard" : 6000 } - ] - } -} +{ + "8SM0f" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "9", "guard" : 6000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "12", "guard" : 6000 }, + { "a" : "8", "b" : "10", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON index a6a5dd19b..4c00d0ab0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON @@ -1,403 +1,403 @@ -{ - "8XM12" : - { - "minSize" : "xl", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 9000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 9000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 9000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 9000 }, - - { "a" : "21", "b" : "24", "guard" : 3000 }, - { "a" : "23", "b" : "25", "guard" : 3000 }, - { "a" : "28", "b" : "30", "guard" : 3000 }, - { "a" : "29", "b" : "32", "guard" : 3000 } - ] - } -} +{ + "8XM12" : + { + "minSize" : "xl", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 9000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 9000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 9000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 9000 }, + + { "a" : "21", "b" : "24", "guard" : 3000 }, + { "a" : "23", "b" : "25", "guard" : 3000 }, + { "a" : "28", "b" : "30", "guard" : 3000 }, + { "a" : "29", "b" : "32", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON index 4ae30574c..dccafef07 100755 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON @@ -1,377 +1,377 @@ -{ - "8XM12a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 9000 }, - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 9000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 9000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 9000 }, - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - { "a" : "24", "b" : "26", "guard" : 3000 }, - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "21", "b" : "22", "guard" : 3000 }, - { "a" : "26", "b" : "28", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - { "a" : "30", "b" : "31", "guard" : 3000 }, - { "a" : "22", "b" : "23", "guard" : 3000 }, - { "a" : "25", "b" : "27", "guard" : 3000 }, - { "a" : "31", "b" : "32", "guard" : 3000 }, - { "a" : "27", "b" : "29", "guard" : 3000 } - ] - } -} +{ + "8XM12a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 9000 }, + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 9000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 9000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 9000 }, + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + { "a" : "24", "b" : "26", "guard" : 3000 }, + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "21", "b" : "22", "guard" : 3000 }, + { "a" : "26", "b" : "28", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + { "a" : "30", "b" : "31", "guard" : 3000 }, + { "a" : "22", "b" : "23", "guard" : 3000 }, + { "a" : "25", "b" : "27", "guard" : 3000 }, + { "a" : "31", "b" : "32", "guard" : 3000 }, + { "a" : "27", "b" : "29", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON index 5ce959c15..a06befaa9 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON @@ -1,515 +1,515 @@ -{ - "8XM8" : - { - "minSize" : "l", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 12000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 7500, "max" : 10000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "35" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "36" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "43" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "17", "guard" : 3000 }, - { "a" : "1", "b" : "18", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "24", "guard" : 3000 }, - { "a" : "3", "b" : "27", "guard" : 3000 }, - { "a" : "3", "b" : "28", "guard" : 3000 }, - { "a" : "4", "b" : "29", "guard" : 3000 }, - { "a" : "4", "b" : "30", "guard" : 3000 }, - { "a" : "5", "b" : "35", "guard" : 3000 }, - { "a" : "5", "b" : "36", "guard" : 3000 }, - { "a" : "6", "b" : "37", "guard" : 3000 }, - { "a" : "6", "b" : "38", "guard" : 3000 }, - { "a" : "7", "b" : "41", "guard" : 3000 }, - { "a" : "7", "b" : "42", "guard" : 3000 }, - { "a" : "8", "b" : "47", "guard" : 3000 }, - { "a" : "8", "b" : "48", "guard" : 3000 }, - { "a" : "9", "b" : "19", "guard" : 6000 }, - { "a" : "9", "b" : "20", "guard" : 6000 }, - { "a" : "10", "b" : "21", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "11", "b" : "25", "guard" : 6000 }, - { "a" : "11", "b" : "26", "guard" : 6000 }, - { "a" : "12", "b" : "31", "guard" : 6000 }, - { "a" : "12", "b" : "32", "guard" : 6000 }, - { "a" : "13", "b" : "33", "guard" : 6000 }, - { "a" : "13", "b" : "34", "guard" : 6000 }, - { "a" : "14", "b" : "39", "guard" : 6000 }, - { "a" : "14", "b" : "40", "guard" : 6000 }, - { "a" : "15", "b" : "43", "guard" : 6000 }, - { "a" : "15", "b" : "44", "guard" : 6000 }, - { "a" : "16", "b" : "45", "guard" : 6000 }, - { "a" : "16", "b" : "46", "guard" : 6000 }, - { "a" : "17", "b" : "25", "guard" : 3000 }, - { "a" : "18", "b" : "19", "guard" : 3000 }, - { "a" : "20", "b" : "21", "guard" : 3000 }, - { "a" : "20", "b" : "28", "guard" : 3000 }, - { "a" : "21", "b" : "29", "guard" : 3000 }, - { "a" : "22", "b" : "23", "guard" : 3000 }, - { "a" : "24", "b" : "32", "guard" : 3000 }, - { "a" : "26", "b" : "27", "guard" : 3000 }, - { "a" : "26", "b" : "34", "guard" : 3000 }, - { "a" : "30", "b" : "31", "guard" : 3000 }, - { "a" : "31", "b" : "39", "guard" : 3000 }, - { "a" : "33", "b" : "41", "guard" : 3000 }, - { "a" : "34", "b" : "35", "guard" : 3000 }, - { "a" : "36", "b" : "44", "guard" : 3000 }, - { "a" : "37", "b" : "45", "guard" : 3000 }, - { "a" : "38", "b" : "39", "guard" : 3000 }, - { "a" : "40", "b" : "48", "guard" : 3000 }, - { "a" : "42", "b" : "43", "guard" : 3000 }, - { "a" : "44", "b" : "45", "guard" : 3000 }, - { "a" : "46", "b" : "47", "guard" : 3000 } - ] - } -} +{ + "8XM8" : + { + "minSize" : "l", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 12000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 7500, "max" : 10000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "35" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "36" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "43" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "17", "guard" : 3000 }, + { "a" : "1", "b" : "18", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "24", "guard" : 3000 }, + { "a" : "3", "b" : "27", "guard" : 3000 }, + { "a" : "3", "b" : "28", "guard" : 3000 }, + { "a" : "4", "b" : "29", "guard" : 3000 }, + { "a" : "4", "b" : "30", "guard" : 3000 }, + { "a" : "5", "b" : "35", "guard" : 3000 }, + { "a" : "5", "b" : "36", "guard" : 3000 }, + { "a" : "6", "b" : "37", "guard" : 3000 }, + { "a" : "6", "b" : "38", "guard" : 3000 }, + { "a" : "7", "b" : "41", "guard" : 3000 }, + { "a" : "7", "b" : "42", "guard" : 3000 }, + { "a" : "8", "b" : "47", "guard" : 3000 }, + { "a" : "8", "b" : "48", "guard" : 3000 }, + { "a" : "9", "b" : "19", "guard" : 6000 }, + { "a" : "9", "b" : "20", "guard" : 6000 }, + { "a" : "10", "b" : "21", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "11", "b" : "25", "guard" : 6000 }, + { "a" : "11", "b" : "26", "guard" : 6000 }, + { "a" : "12", "b" : "31", "guard" : 6000 }, + { "a" : "12", "b" : "32", "guard" : 6000 }, + { "a" : "13", "b" : "33", "guard" : 6000 }, + { "a" : "13", "b" : "34", "guard" : 6000 }, + { "a" : "14", "b" : "39", "guard" : 6000 }, + { "a" : "14", "b" : "40", "guard" : 6000 }, + { "a" : "15", "b" : "43", "guard" : 6000 }, + { "a" : "15", "b" : "44", "guard" : 6000 }, + { "a" : "16", "b" : "45", "guard" : 6000 }, + { "a" : "16", "b" : "46", "guard" : 6000 }, + { "a" : "17", "b" : "25", "guard" : 3000 }, + { "a" : "18", "b" : "19", "guard" : 3000 }, + { "a" : "20", "b" : "21", "guard" : 3000 }, + { "a" : "20", "b" : "28", "guard" : 3000 }, + { "a" : "21", "b" : "29", "guard" : 3000 }, + { "a" : "22", "b" : "23", "guard" : 3000 }, + { "a" : "24", "b" : "32", "guard" : 3000 }, + { "a" : "26", "b" : "27", "guard" : 3000 }, + { "a" : "26", "b" : "34", "guard" : 3000 }, + { "a" : "30", "b" : "31", "guard" : 3000 }, + { "a" : "31", "b" : "39", "guard" : 3000 }, + { "a" : "33", "b" : "41", "guard" : 3000 }, + { "a" : "34", "b" : "35", "guard" : 3000 }, + { "a" : "36", "b" : "44", "guard" : 3000 }, + { "a" : "37", "b" : "45", "guard" : 3000 }, + { "a" : "38", "b" : "39", "guard" : 3000 }, + { "a" : "40", "b" : "48", "guard" : 3000 }, + { "a" : "42", "b" : "43", "guard" : 3000 }, + { "a" : "44", "b" : "45", "guard" : 3000 }, + { "a" : "46", "b" : "47", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON index 7a2e687c1..d3acb2674 100644 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON @@ -1,87 +1,87 @@ -{ - "4MM2h" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "4MM2h" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON index 3a324306e..64deddcf1 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON @@ -1,235 +1,235 @@ -{ - "2x2sm4d(3)" : - { - "minSize" : "xl", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 500, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "16" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "17" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "18" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "19" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "20" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "1", "b" : "12", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 45000 }, - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "2", "guard" : 2000 }, - { "a" : "3", "b" : "4", "guard" : 2000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "18", "guard" : 6000 }, - { "a" : "10", "b" : "18", "guard" : 6000 }, - { "a" : "10", "b" : "17", "guard" : 6000 }, - { "a" : "9", "b" : "17", "guard" : 6000 }, - { "a" : "9", "b" : "16", "guard" : 6000 }, - { "a" : "8", "b" : "16", "guard" : 6000 }, - { "a" : "8", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "20", "guard" : 6000 }, - { "a" : "12", "b" : "20", "guard" : 6000 } - ] - } -} +{ + "2x2sm4d(3)" : + { + "minSize" : "xl", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 500, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "16" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "17" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "18" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "19" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "20" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "1", "b" : "12", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 45000 }, + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "2", "guard" : 2000 }, + { "a" : "3", "b" : "4", "guard" : 2000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "18", "guard" : 6000 }, + { "a" : "10", "b" : "18", "guard" : 6000 }, + { "a" : "10", "b" : "17", "guard" : 6000 }, + { "a" : "9", "b" : "17", "guard" : 6000 }, + { "a" : "9", "b" : "16", "guard" : 6000 }, + { "a" : "8", "b" : "16", "guard" : 6000 }, + { "a" : "8", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "20", "guard" : 6000 }, + { "a" : "12", "b" : "20", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON index 7a2e687c1..d3acb2674 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON @@ -1,87 +1,87 @@ -{ - "4MM2h" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "4MM2h" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON index 5fb9be1d6..5779f8cf4 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON @@ -1,116 +1,116 @@ -{ - "4SM3i" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "6", "b" : "7", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 6000 } - ] - } -} +{ + "4SM3i" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "6", "b" : "7", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON index 122659efb..f8fa0d969 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON @@ -1,308 +1,308 @@ -{ - "6LM10a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 8, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "13", "b" : "13", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 3000 }, - { "a" : "15", "b" : "24", "guard" : 3000 }, - { "a" : "16", "b" : "25", "guard" : 3000 }, - { "a" : "16", "b" : "25", "guard" : 3000 }, - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - { "a" : "21", "b" : "22", "guard" : 0 }, - { "a" : "24", "b" : "25", "guard" : 0 }, - { "a" : "13", "b" : "14", "guard" : 6000 }, - { "a" : "14", "b" : "25", "guard" : 6000 } - ] - } -} +{ + "6LM10a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 8, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "13", "b" : "13", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 3000 }, + { "a" : "15", "b" : "24", "guard" : 3000 }, + { "a" : "16", "b" : "25", "guard" : 3000 }, + { "a" : "16", "b" : "25", "guard" : 3000 }, + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + { "a" : "21", "b" : "22", "guard" : 0 }, + { "a" : "24", "b" : "25", "guard" : 0 }, + { "a" : "13", "b" : "14", "guard" : 6000 }, + { "a" : "14", "b" : "25", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON index a2ed87446..b18fd8795 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON @@ -1,381 +1,381 @@ -{ - "8XM12 Huge" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - { "a" : "11", "b" : "3", "guard" : 3000 }, - { "a" : "11", "b" : "4", "guard" : 3000 }, - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 12500 }, - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - { "a" : "14", "b" : "3", "guard" : 3000 }, - { "a" : "14", "b" : "5", "guard" : 3000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 12500 }, - { "a" : "15", "b" : "4", "guard" : 3000 }, - { "a" : "15", "b" : "6", "guard" : 3000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 12500 }, - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - { "a" : "18", "b" : "5", "guard" : 3000 }, - { "a" : "18", "b" : "6", "guard" : 3000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 12500 }, - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - { "a" : "21", "b" : "24", "guard" : 3000 }, - { "a" : "23", "b" : "25", "guard" : 3000 }, - { "a" : "28", "b" : "30", "guard" : 3000 }, - { "a" : "29", "b" : "32", "guard" : 3000 } - ] - } -} +{ + "8XM12 Huge" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + { "a" : "11", "b" : "3", "guard" : 3000 }, + { "a" : "11", "b" : "4", "guard" : 3000 }, + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 12500 }, + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + { "a" : "14", "b" : "3", "guard" : 3000 }, + { "a" : "14", "b" : "5", "guard" : 3000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 12500 }, + { "a" : "15", "b" : "4", "guard" : 3000 }, + { "a" : "15", "b" : "6", "guard" : 3000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 12500 }, + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + { "a" : "18", "b" : "5", "guard" : 3000 }, + { "a" : "18", "b" : "6", "guard" : 3000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 12500 }, + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + { "a" : "21", "b" : "24", "guard" : 3000 }, + { "a" : "23", "b" : "25", "guard" : 3000 }, + { "a" : "28", "b" : "30", "guard" : 3000 }, + { "a" : "29", "b" : "32", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON index 88bac0686..0d6d07223 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON @@ -1,515 +1,515 @@ -{ - "8XM8 Huge" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 12000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "35" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "36" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "43" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "17", "guard" : 6000 }, - { "a" : "1", "b" : "18", "guard" : 6000 }, - { "a" : "2", "b" : "23", "guard" : 6000 }, - { "a" : "2", "b" : "24", "guard" : 6000 }, - { "a" : "3", "b" : "27", "guard" : 6000 }, - { "a" : "3", "b" : "28", "guard" : 6000 }, - { "a" : "4", "b" : "29", "guard" : 6000 }, - { "a" : "4", "b" : "30", "guard" : 6000 }, - { "a" : "5", "b" : "35", "guard" : 6000 }, - { "a" : "5", "b" : "36", "guard" : 6000 }, - { "a" : "6", "b" : "37", "guard" : 6000 }, - { "a" : "6", "b" : "38", "guard" : 6000 }, - { "a" : "7", "b" : "41", "guard" : 6000 }, - { "a" : "7", "b" : "42", "guard" : 6000 }, - { "a" : "8", "b" : "47", "guard" : 6000 }, - { "a" : "8", "b" : "48", "guard" : 6000 }, - { "a" : "9", "b" : "19", "guard" : 12500 }, - { "a" : "9", "b" : "20", "guard" : 12500 }, - { "a" : "10", "b" : "21", "guard" : 12500 }, - { "a" : "10", "b" : "22", "guard" : 12500 }, - { "a" : "11", "b" : "25", "guard" : 12500 }, - { "a" : "11", "b" : "26", "guard" : 12500 }, - { "a" : "12", "b" : "31", "guard" : 12500 }, - { "a" : "12", "b" : "32", "guard" : 12500 }, - { "a" : "13", "b" : "33", "guard" : 12500 }, - { "a" : "13", "b" : "34", "guard" : 12500 }, - { "a" : "14", "b" : "39", "guard" : 12500 }, - { "a" : "14", "b" : "40", "guard" : 12500 }, - { "a" : "15", "b" : "43", "guard" : 12500 }, - { "a" : "15", "b" : "44", "guard" : 12500 }, - { "a" : "16", "b" : "45", "guard" : 12500 }, - { "a" : "16", "b" : "46", "guard" : 12500 }, - { "a" : "17", "b" : "25", "guard" : 6000 }, - { "a" : "18", "b" : "19", "guard" : 6000 }, - { "a" : "20", "b" : "21", "guard" : 6000 }, - { "a" : "20", "b" : "28", "guard" : 6000 }, - { "a" : "21", "b" : "29", "guard" : 6000 }, - { "a" : "22", "b" : "23", "guard" : 6000 }, - { "a" : "24", "b" : "32", "guard" : 6000 }, - { "a" : "26", "b" : "27", "guard" : 6000 }, - { "a" : "26", "b" : "34", "guard" : 6000 }, - { "a" : "30", "b" : "31", "guard" : 6000 }, - { "a" : "31", "b" : "39", "guard" : 6000 }, - { "a" : "33", "b" : "41", "guard" : 6000 }, - { "a" : "34", "b" : "35", "guard" : 6000 }, - { "a" : "36", "b" : "44", "guard" : 6000 }, - { "a" : "37", "b" : "45", "guard" : 6000 }, - { "a" : "38", "b" : "39", "guard" : 6000 }, - { "a" : "40", "b" : "48", "guard" : 6000 }, - { "a" : "42", "b" : "43", "guard" : 6000 }, - { "a" : "44", "b" : "45", "guard" : 6000 }, - { "a" : "46", "b" : "47", "guard" : 6000 } - ] - } -} +{ + "8XM8 Huge" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 12000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "35" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "36" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "43" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "17", "guard" : 6000 }, + { "a" : "1", "b" : "18", "guard" : 6000 }, + { "a" : "2", "b" : "23", "guard" : 6000 }, + { "a" : "2", "b" : "24", "guard" : 6000 }, + { "a" : "3", "b" : "27", "guard" : 6000 }, + { "a" : "3", "b" : "28", "guard" : 6000 }, + { "a" : "4", "b" : "29", "guard" : 6000 }, + { "a" : "4", "b" : "30", "guard" : 6000 }, + { "a" : "5", "b" : "35", "guard" : 6000 }, + { "a" : "5", "b" : "36", "guard" : 6000 }, + { "a" : "6", "b" : "37", "guard" : 6000 }, + { "a" : "6", "b" : "38", "guard" : 6000 }, + { "a" : "7", "b" : "41", "guard" : 6000 }, + { "a" : "7", "b" : "42", "guard" : 6000 }, + { "a" : "8", "b" : "47", "guard" : 6000 }, + { "a" : "8", "b" : "48", "guard" : 6000 }, + { "a" : "9", "b" : "19", "guard" : 12500 }, + { "a" : "9", "b" : "20", "guard" : 12500 }, + { "a" : "10", "b" : "21", "guard" : 12500 }, + { "a" : "10", "b" : "22", "guard" : 12500 }, + { "a" : "11", "b" : "25", "guard" : 12500 }, + { "a" : "11", "b" : "26", "guard" : 12500 }, + { "a" : "12", "b" : "31", "guard" : 12500 }, + { "a" : "12", "b" : "32", "guard" : 12500 }, + { "a" : "13", "b" : "33", "guard" : 12500 }, + { "a" : "13", "b" : "34", "guard" : 12500 }, + { "a" : "14", "b" : "39", "guard" : 12500 }, + { "a" : "14", "b" : "40", "guard" : 12500 }, + { "a" : "15", "b" : "43", "guard" : 12500 }, + { "a" : "15", "b" : "44", "guard" : 12500 }, + { "a" : "16", "b" : "45", "guard" : 12500 }, + { "a" : "16", "b" : "46", "guard" : 12500 }, + { "a" : "17", "b" : "25", "guard" : 6000 }, + { "a" : "18", "b" : "19", "guard" : 6000 }, + { "a" : "20", "b" : "21", "guard" : 6000 }, + { "a" : "20", "b" : "28", "guard" : 6000 }, + { "a" : "21", "b" : "29", "guard" : 6000 }, + { "a" : "22", "b" : "23", "guard" : 6000 }, + { "a" : "24", "b" : "32", "guard" : 6000 }, + { "a" : "26", "b" : "27", "guard" : 6000 }, + { "a" : "26", "b" : "34", "guard" : 6000 }, + { "a" : "30", "b" : "31", "guard" : 6000 }, + { "a" : "31", "b" : "39", "guard" : 6000 }, + { "a" : "33", "b" : "41", "guard" : 6000 }, + { "a" : "34", "b" : "35", "guard" : 6000 }, + { "a" : "36", "b" : "44", "guard" : 6000 }, + { "a" : "37", "b" : "45", "guard" : 6000 }, + { "a" : "38", "b" : "39", "guard" : 6000 }, + { "a" : "40", "b" : "48", "guard" : 6000 }, + { "a" : "42", "b" : "43", "guard" : 6000 }, + { "a" : "44", "b" : "45", "guard" : 6000 }, + { "a" : "46", "b" : "47", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON index 601790fef..d50253961 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON @@ -1,188 +1,188 @@ -{ - "Cross" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - }, - "13" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 25000, "density" : 9 }, - { "min" : 20000, "max" : 30000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "8", "b" : "13", "guard" : 12500 }, - { "a" : "5", "b" : "13", "guard" : 12500 }, - { "a" : "7", "b" : "13", "guard" : 12500 }, - { "a" : "6", "b" : "13", "guard" : 12500 } - ] - } -} +{ + "Cross" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + }, + "13" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 25000, "density" : 9 }, + { "min" : 20000, "max" : 30000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "8", "b" : "13", "guard" : 12500 }, + { "a" : "5", "b" : "13", "guard" : 12500 }, + { "a" : "7", "b" : "13", "guard" : 12500 }, + { "a" : "6", "b" : "13", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON index 1ea221acc..214b283ba 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON @@ -1,223 +1,223 @@ -{ - "Cross 2" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - }, - "18" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "13", "b" : "14", "guard" : 3000 }, - { "a" : "11", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "8", "b" : "17", "guard" : 6000 }, - { "a" : "7", "b" : "17", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "12", "b" : "16", "guard" : 6000 }, - { "a" : "14", "b" : "18", "guard" : 6000 }, - { "a" : "13", "b" : "18", "guard" : 6000 } - ] - } -} +{ + "Cross 2" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + }, + "18" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "13", "b" : "14", "guard" : 3000 }, + { "a" : "11", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "8", "b" : "17", "guard" : 6000 }, + { "a" : "7", "b" : "17", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "12", "b" : "16", "guard" : 6000 }, + { "a" : "14", "b" : "18", "guard" : 6000 }, + { "a" : "13", "b" : "18", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON index 58ec239e2..31eb2cf18 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON @@ -1,340 +1,340 @@ -{ - "Cross 3_1" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 } - ] - }, - "Cross 3_2" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "Cross 3_1" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 } + ] + }, + "Cross 3_2" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON index f726a3b53..c822ed62a 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON @@ -1,536 +1,536 @@ -{ - "Doubled 8MM6" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone": 11, - "treasureLikeZone" : 3 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone": 12, - "treasureLikeZone" : 3 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "31" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 3 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 3 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 3 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 3 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "43" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 18 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "49" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "50" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "51" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "52" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 6000 }, - { "a" : "1", "b" : "19", "guard" : 6000 }, - { "a" : "2", "b" : "16", "guard" : 6000 }, - { "a" : "2", "b" : "20", "guard" : 6000 }, - { "a" : "3", "b" : "15", "guard" : 6000 }, - { "a" : "3", "b" : "19", "guard" : 6000 }, - { "a" : "4", "b" : "16", "guard" : 6000 }, - { "a" : "4", "b" : "20", "guard" : 6000 }, - { "a" : "5", "b" : "21", "guard" : 6000 }, - { "a" : "5", "b" : "25", "guard" : 6000 }, - { "a" : "6", "b" : "22", "guard" : 6000 }, - { "a" : "6", "b" : "26", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "25", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "26", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 9000 }, - { "a" : "11", "b" : "20", "guard" : 9000 }, - { "a" : "11", "b" : "22", "guard" : 9000 }, - { "a" : "12", "b" : "19", "guard" : 9000 }, - { "a" : "12", "b" : "21", "guard" : 9000 }, - { "a" : "12", "b" : "22", "guard" : 9000 }, - { "a" : "13", "b" : "25", "guard" : 9000 }, - { "a" : "14", "b" : "26", "guard" : 9000 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 }, - { "a" : "27", "b" : "41", "guard" : 6000 }, - { "a" : "27", "b" : "45", "guard" : 6000 }, - { "a" : "28", "b" : "42", "guard" : 6000 }, - { "a" : "28", "b" : "46", "guard" : 6000 }, - { "a" : "29", "b" : "41", "guard" : 6000 }, - { "a" : "29", "b" : "45", "guard" : 6000 }, - { "a" : "30", "b" : "42", "guard" : 6000 }, - { "a" : "30", "b" : "46", "guard" : 6000 }, - { "a" : "31", "b" : "47", "guard" : 6000 }, - { "a" : "31", "b" : "51", "guard" : 6000 }, - { "a" : "32", "b" : "48", "guard" : 6000 }, - { "a" : "32", "b" : "52", "guard" : 6000 }, - { "a" : "33", "b" : "47", "guard" : 6000 }, - { "a" : "33", "b" : "51", "guard" : 6000 }, - { "a" : "34", "b" : "48", "guard" : 6000 }, - { "a" : "34", "b" : "52", "guard" : 6000 }, - { "a" : "37", "b" : "45", "guard" : 9000 }, - { "a" : "37", "b" : "46", "guard" : 9000 }, - { "a" : "37", "b" : "48", "guard" : 9000 }, - { "a" : "38", "b" : "45", "guard" : 9000 }, - { "a" : "38", "b" : "47", "guard" : 9000 }, - { "a" : "38", "b" : "48", "guard" : 9000 }, - { "a" : "39", "b" : "51", "guard" : 9000 }, - { "a" : "40", "b" : "52", "guard" : 9000 }, - { "a" : "43", "b" : "45", "guard" : 12500 }, - { "a" : "44", "b" : "46", "guard" : 12500 }, - { "a" : "49", "b" : "47", "guard" : 12500 }, - { "a" : "50", "b" : "48", "guard" : 12500 }, - { "a" : "3", "b" : "29", "guard" : 12500 }, - { "a" : "4", "b" : "30", "guard" : 12500 }, - { "a" : "7", "b" : "33", "guard" : 12500 }, - { "a" : "8", "b" : "34", "guard" : 12500 } - ] - } -} +{ + "Doubled 8MM6" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone": 11, + "treasureLikeZone" : 3 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone": 12, + "treasureLikeZone" : 3 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "31" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 3 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 3 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 3 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 3 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "43" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 18 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "49" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "50" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "51" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "52" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 6000 }, + { "a" : "1", "b" : "19", "guard" : 6000 }, + { "a" : "2", "b" : "16", "guard" : 6000 }, + { "a" : "2", "b" : "20", "guard" : 6000 }, + { "a" : "3", "b" : "15", "guard" : 6000 }, + { "a" : "3", "b" : "19", "guard" : 6000 }, + { "a" : "4", "b" : "16", "guard" : 6000 }, + { "a" : "4", "b" : "20", "guard" : 6000 }, + { "a" : "5", "b" : "21", "guard" : 6000 }, + { "a" : "5", "b" : "25", "guard" : 6000 }, + { "a" : "6", "b" : "22", "guard" : 6000 }, + { "a" : "6", "b" : "26", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "25", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "26", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 9000 }, + { "a" : "11", "b" : "20", "guard" : 9000 }, + { "a" : "11", "b" : "22", "guard" : 9000 }, + { "a" : "12", "b" : "19", "guard" : 9000 }, + { "a" : "12", "b" : "21", "guard" : 9000 }, + { "a" : "12", "b" : "22", "guard" : 9000 }, + { "a" : "13", "b" : "25", "guard" : 9000 }, + { "a" : "14", "b" : "26", "guard" : 9000 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 }, + { "a" : "27", "b" : "41", "guard" : 6000 }, + { "a" : "27", "b" : "45", "guard" : 6000 }, + { "a" : "28", "b" : "42", "guard" : 6000 }, + { "a" : "28", "b" : "46", "guard" : 6000 }, + { "a" : "29", "b" : "41", "guard" : 6000 }, + { "a" : "29", "b" : "45", "guard" : 6000 }, + { "a" : "30", "b" : "42", "guard" : 6000 }, + { "a" : "30", "b" : "46", "guard" : 6000 }, + { "a" : "31", "b" : "47", "guard" : 6000 }, + { "a" : "31", "b" : "51", "guard" : 6000 }, + { "a" : "32", "b" : "48", "guard" : 6000 }, + { "a" : "32", "b" : "52", "guard" : 6000 }, + { "a" : "33", "b" : "47", "guard" : 6000 }, + { "a" : "33", "b" : "51", "guard" : 6000 }, + { "a" : "34", "b" : "48", "guard" : 6000 }, + { "a" : "34", "b" : "52", "guard" : 6000 }, + { "a" : "37", "b" : "45", "guard" : 9000 }, + { "a" : "37", "b" : "46", "guard" : 9000 }, + { "a" : "37", "b" : "48", "guard" : 9000 }, + { "a" : "38", "b" : "45", "guard" : 9000 }, + { "a" : "38", "b" : "47", "guard" : 9000 }, + { "a" : "38", "b" : "48", "guard" : 9000 }, + { "a" : "39", "b" : "51", "guard" : 9000 }, + { "a" : "40", "b" : "52", "guard" : 9000 }, + { "a" : "43", "b" : "45", "guard" : 12500 }, + { "a" : "44", "b" : "46", "guard" : 12500 }, + { "a" : "49", "b" : "47", "guard" : 12500 }, + { "a" : "50", "b" : "48", "guard" : 12500 }, + { "a" : "3", "b" : "29", "guard" : 12500 }, + { "a" : "4", "b" : "30", "guard" : 12500 }, + { "a" : "7", "b" : "33", "guard" : 12500 }, + { "a" : "8", "b" : "34", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON index a0aa22d07..46a5df473 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON @@ -1,168 +1,168 @@ -{ - "Elka" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "3", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "3", "b" : "12", "guard" : 3000 } - ] - } -} +{ + "Elka" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "3", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "3", "b" : "12", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON index 42b00a3b5..f0409ac22 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON @@ -1,321 +1,321 @@ -{ - "GreatSands XL 2-8" : - { - "minSize" : "xl", "maxSize" : "h", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 1600, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "conflux" ], - "allowedMonsters" : [ "neutral", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 6000, "max" : 10000, "density" : 5 }, - { "min" : 4000, "max" : 6000, "density" : 5 }, - { "min" : 2000, "max" : 4000, "density" : 5 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 3 }, - { "min" : 3000, "max" : 5000, "density" : 4 }, - { "min" : 1000, "max" : 3000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "14" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "16" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "17" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "18" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "21" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 11 - }, - "22" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "23" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "24" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "25" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "26" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "27" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "28" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "31" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 2600, "max" : 4000, "density" : 2 }, - { "min" : 1600, "max" : 2500, "density" : 2 }, - { "min" : 100, "max" : 1500, "density" : 8 } - ] - }, - "32" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "33" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "34" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "35" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "36" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "37" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "38" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - } - }, - "connections" : - [ - { "a" : "11", "b" : "1", "guard" : 8000 }, - { "a" : "12", "b" : "1", "guard" : 8000 }, - { "a" : "13", "b" : "1", "guard" : 8000 }, - { "a" : "14", "b" : "1", "guard" : 8000 }, - { "a" : "15", "b" : "1", "guard" : 8000 }, - { "a" : "16", "b" : "1", "guard" : 8000 }, - { "a" : "17", "b" : "1", "guard" : 8000 }, - { "a" : "18", "b" : "1", "guard" : 8000 }, - { "a" : "21", "b" : "1", "guard" : 8000 }, - { "a" : "22", "b" : "1", "guard" : 8000 }, - { "a" : "23", "b" : "1", "guard" : 8000 }, - { "a" : "24", "b" : "1", "guard" : 8000 }, - { "a" : "25", "b" : "1", "guard" : 8000 }, - { "a" : "26", "b" : "1", "guard" : 8000 }, - { "a" : "27", "b" : "1", "guard" : 8000 }, - { "a" : "28", "b" : "1", "guard" : 8000 }, - { "a" : "31", "b" : "11", "guard" : 5000 }, - { "a" : "32", "b" : "12", "guard" : 5000 }, - { "a" : "33", "b" : "13", "guard" : 5000 }, - { "a" : "34", "b" : "14", "guard" : 5000 }, - { "a" : "35", "b" : "15", "guard" : 5000 }, - { "a" : "36", "b" : "16", "guard" : 5000 }, - { "a" : "37", "b" : "17", "guard" : 5000 }, - { "a" : "38", "b" : "18", "guard" : 5000 }, - { "a" : "31", "b" : "21", "guard" : 5000 }, - { "a" : "32", "b" : "22", "guard" : 5000 }, - { "a" : "33", "b" : "23", "guard" : 5000 }, - { "a" : "34", "b" : "24", "guard" : 5000 }, - { "a" : "35", "b" : "25", "guard" : 5000 }, - { "a" : "36", "b" : "26", "guard" : 5000 }, - { "a" : "37", "b" : "27", "guard" : 5000 }, - { "a" : "38", "b" : "28", "guard" : 5000 } - ] - } -} +{ + "GreatSands XL 2-8" : + { + "minSize" : "xl", "maxSize" : "h", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 1600, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "conflux" ], + "allowedMonsters" : [ "neutral", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 6000, "max" : 10000, "density" : 5 }, + { "min" : 4000, "max" : 6000, "density" : 5 }, + { "min" : 2000, "max" : 4000, "density" : 5 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 3 }, + { "min" : 3000, "max" : 5000, "density" : 4 }, + { "min" : 1000, "max" : 3000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "14" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "16" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "17" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "18" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "21" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 11 + }, + "22" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "23" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "24" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "25" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "26" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "27" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "28" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "31" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 2600, "max" : 4000, "density" : 2 }, + { "min" : 1600, "max" : 2500, "density" : 2 }, + { "min" : 100, "max" : 1500, "density" : 8 } + ] + }, + "32" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "33" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "34" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "35" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "36" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "37" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "38" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + } + }, + "connections" : + [ + { "a" : "11", "b" : "1", "guard" : 8000 }, + { "a" : "12", "b" : "1", "guard" : 8000 }, + { "a" : "13", "b" : "1", "guard" : 8000 }, + { "a" : "14", "b" : "1", "guard" : 8000 }, + { "a" : "15", "b" : "1", "guard" : 8000 }, + { "a" : "16", "b" : "1", "guard" : 8000 }, + { "a" : "17", "b" : "1", "guard" : 8000 }, + { "a" : "18", "b" : "1", "guard" : 8000 }, + { "a" : "21", "b" : "1", "guard" : 8000 }, + { "a" : "22", "b" : "1", "guard" : 8000 }, + { "a" : "23", "b" : "1", "guard" : 8000 }, + { "a" : "24", "b" : "1", "guard" : 8000 }, + { "a" : "25", "b" : "1", "guard" : 8000 }, + { "a" : "26", "b" : "1", "guard" : 8000 }, + { "a" : "27", "b" : "1", "guard" : 8000 }, + { "a" : "28", "b" : "1", "guard" : 8000 }, + { "a" : "31", "b" : "11", "guard" : 5000 }, + { "a" : "32", "b" : "12", "guard" : 5000 }, + { "a" : "33", "b" : "13", "guard" : 5000 }, + { "a" : "34", "b" : "14", "guard" : 5000 }, + { "a" : "35", "b" : "15", "guard" : 5000 }, + { "a" : "36", "b" : "16", "guard" : 5000 }, + { "a" : "37", "b" : "17", "guard" : 5000 }, + { "a" : "38", "b" : "18", "guard" : 5000 }, + { "a" : "31", "b" : "21", "guard" : 5000 }, + { "a" : "32", "b" : "22", "guard" : 5000 }, + { "a" : "33", "b" : "23", "guard" : 5000 }, + { "a" : "34", "b" : "24", "guard" : 5000 }, + { "a" : "35", "b" : "25", "guard" : 5000 }, + { "a" : "36", "b" : "26", "guard" : 5000 }, + { "a" : "37", "b" : "27", "guard" : 5000 }, + { "a" : "38", "b" : "28", "guard" : 5000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON index 9431b5355..131927e8a 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON @@ -1,141 +1,141 @@ -{ - "Kite" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "10" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 6000 }, - { "a" : "1", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 12500 }, - { "a" : "4", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "10", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "4", "b" : "9", "guard" : 6000 } - ] - } -} +{ + "Kite" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "10" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 6000 }, + { "a" : "1", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 12500 }, + { "a" : "4", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "10", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "4", "b" : "9", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON index 609193652..acaf04086 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON @@ -1,154 +1,154 @@ -{ - "Wheel" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 100, "max" : 500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 3000 }, - { "a" : "9", "b" : "13", "guard" : 10000 }, - { "a" : "9", "b" : "12", "guard" : 10000 }, - { "a" : "9", "b" : "11", "guard" : 10000 }, - { "a" : "9", "b" : "10", "guard" : 10000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "5", "b" : "12", "guard" : 6000 } - ] - } -} +{ + "Wheel" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 100, "max" : 500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 3000 }, + { "a" : "9", "b" : "13", "guard" : 10000 }, + { "a" : "9", "b" : "12", "guard" : 10000 }, + { "a" : "9", "b" : "11", "guard" : 10000 }, + { "a" : "9", "b" : "10", "guard" : 10000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "5", "b" : "12", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 51d13ac0d..636d36d7c 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -21,9 +21,42 @@ "vcmi.adventureMap.moveCostDetails" : "Очки движения - Стоимость: %TURNS ходов + %POINTS очков, Останется: %REMAINING очков", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки движения - Стоимость: %POINTS очков, Останется: %REMAINING очков", - "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", - "vcmi.server.errors.modsIncompatibility" : "Требуемые моды для загрузки игры:", - "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", + "vcmi.capitalColors.0" : "Красный", + "vcmi.capitalColors.1" : "Синий", + "vcmi.capitalColors.2" : "Коричневый", + "vcmi.capitalColors.3" : "Зеленый", + "vcmi.capitalColors.4" : "Оранжевый", + "vcmi.capitalColors.5" : "Пурпурный", + "vcmi.capitalColors.6" : "Бирюзовый", + "vcmi.capitalColors.7" : "Розовый", + + "vcmi.heroOverview.startingArmy" : "Начальная армия", + "vcmi.heroOverview.warMachine" : "Боевые машины", + "vcmi.heroOverview.secondarySkills" : "Вторичные навыки", + "vcmi.heroOverview.spells" : "Заклинания", + + "vcmi.radialWheel.mergeSameUnit" : "Объединить одинковые существа", + "vcmi.radialWheel.fillSingleUnit" : "Заполнить свободные слоты единицами", + "vcmi.radialWheel.splitSingleUnit" : "Отделить единицу в отдельный слот", + "vcmi.radialWheel.splitUnitEqually" : "Разделить отряд поровну", + "vcmi.radialWheel.moveUnit" : "Перенести отряд в другую армию", + "vcmi.radialWheel.splitUnit" : "Разделить отряд в другой слот", + + "vcmi.mainMenu.serverConnecting" : "Подключение...", + "vcmi.mainMenu.serverAddressEnter" : "Введите адрес:", + "vcmi.mainMenu.serverConnectionFailed" : "Ошибка соединения", + "vcmi.mainMenu.serverClosing" : "Завершение...", + "vcmi.mainMenu.hostTCP" : "Создать игру по TCP/IP", + "vcmi.mainMenu.joinTCP" : "Присединиться к игре по TCP/IP", + "vcmi.mainMenu.playerName" : "Игрок", + + "vcmi.lobby.filename" : "Имя файла", + "vcmi.lobby.creationDate" : "Дата создания", + + "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", + "vcmi.server.errors.modsToEnable" : "{Требуемые моды для загрузки игры}", + "vcmi.server.errors.modsToDisable" : "{Необходимо отключить следующие моды}", + "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", "vcmi.settingsMainWindow.generalTab.hover" : "Общее", "vcmi.settingsMainWindow.generalTab.help" : "Переключиться на вкладку \"Общее\", содержащее общие настройки клиента игры", @@ -37,12 +70,29 @@ "vcmi.systemOptions.otherGroup" : "Иное", // unused right now "vcmi.systemOptions.townsGroup" : "Экран города", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Полноэкранный режим (в окне)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Полноэкранный режим (в окне)}\n\nЕсли выбрано, VCMI будет работать в режиме безрамочного окна, растянутого на весь экран. В данном режиме игра использует текущее разрешение экрана, не меняя его. ", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Полноэкранный режим (без окна)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Полноэкранный режим (без окна)}\n\nЕсли выбрано, VCMI будет работать в полноэкранном режиме. Теущее разрешение экрана будет изменено на выбранное.", "vcmi.systemOptions.resolutionButton.hover" : "Разрешение %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Разрешение экрана}\n\n Изменение разрешения экрана. Для применения нового разрешения требуется перезапуск игры.", + "vcmi.systemOptions.resolutionButton.help" : "{Разрешение экрана}\n\n Изменение разрешения экрана.", "vcmi.systemOptions.resolutionMenu.hover" : "Выбрать разрешения экрана", "vcmi.systemOptions.resolutionMenu.help" : "Изменение разрешения экрана в игре.", + "vcmi.systemOptions.scalingButton.hover" : "Масштаб интерфейса: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Масштаб интерфейса}\n\nИзменить масштаб игрового интерфеса.", + "vcmi.systemOptions.scalingMenu.hover" : "Выбрать масштаб интерфеса", + "vcmi.systemOptions.scalingMenu.help" : "Изменить масштаб игрового интерфеса.", + "vcmi.systemOptions.longTouchButton.hover" : "Интервал длительного касания: %d мс", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Интервал длительного касания}\n\nПри использовании сенсорного экрана, всплывающие окна будут показаны после длительного нажатия указанной продолжительности, в миллисекундах.", + "vcmi.systemOptions.longTouchMenu.hover" : "Выбрать интервал длительного касания:", + "vcmi.systemOptions.longTouchMenu.help" : "Изменить интервал длительного касания.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d миллисекунд", "vcmi.systemOptions.framerateButton.hover" : "Показывать частоту кадров", - "vcmi.systemOptions.framerateButton.help" : "{Показывать частоту кадров}\n\n Включить счетчик частоты кадров в углу игрового клиента", + "vcmi.systemOptions.framerateButton.help" : "{Показывать частоту кадров}\n\nЕсли выбрано, частота кадров в секунду будет показана в верхнем левом углу игрового экрана.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильный отклик", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильный отклик}\n\nиспользовать вибрацию при использовании сенсорного экрана.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Расширенные функции интерфейса", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Расширенные функции интерфейса}\n\nРазличные улучшения и дополнительные функции интерфейса. Например, большая книга заклинаний, рюкзак и т.д. Отключите для классического интерфейса.", "vcmi.adventureOptions.infoBarPick.hover" : "Сообщения в информационной панели", "vcmi.adventureOptions.infoBarPick.help" : "{Сообщения в информационной панели}\n\n Если сообщения помещаются, то показывать их в информационной панели (только на интерфейсе карты).", @@ -52,6 +102,12 @@ "vcmi.adventureOptions.forceMovementInfo.help" : "{Всегда показывать стоимость перемещения}\n\n Заменить информацию в статусной строке на информацию о перемещении без необходимости нажатия {ALT}", "vcmi.adventureOptions.showGrid.hover" : "Сетка", "vcmi.adventureOptions.showGrid.help" : "{Сетка}\n\n Показывать сетку на видимой части карты.", + "vcmi.adventureOptions.borderScroll.hover" : "Прокрутка карты по краю", + "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.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -74,10 +130,28 @@ "vcmi.battleOptions.animationsSpeed1.help": "Очень медленная скорость анимации.", "vcmi.battleOptions.animationsSpeed5.help": "Очень быстрая скорость анимации.", "vcmi.battleOptions.animationsSpeed6.help": "Мгновенная скорость анимации.", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Показывать область перемещения существ", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Показывать область перемещения существ}\n\nПодсвечивать область перемещения существ при наведении курсора.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Показывать область полного урона стрелков", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Показывать область полного урона стрелков}\n\nПоказывать область полного урона стреляющих существ при наведении курсора.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Показывать характеристики героев", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Показывать характеристики героев}\n\nПоказывать характеристики героев и очки магии все время.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускать вступительную музыку", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускать вступительную музыку}\n\n Пропускать музыку, которая проигрывается в начале каждой битвы. Также может быть пропущена по нажатию {ESC}", "vcmi.battleWindow.pressKeyToSkipIntro" : "Нажмите любую клавишу для пропуска вступительной музыки", + "vcmi.battleWindow.damageEstimation.melee" : "Атаковать %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Атаковать %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Стрелять в %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Стрелять в %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "Осталось %d выстрелов", + "vcmi.battleWindow.damageEstimation.shots.1" : "Остался %d выстрел", + "vcmi.battleWindow.damageEstimation.damage" : "%d единиц урона", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d единица урона", + "vcmi.battleWindow.damageEstimation.kills" : "%d погибнут", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d погибнет", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Принять результаты боя", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показывать доступных существ", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показывать доступных существ}\n\n Показывать число доступных существ вместо прироста на экране города (в левом нижнем углу).", @@ -106,6 +180,8 @@ "vcmi.heroWindow.openCommander.hover" : "Открыть экран командира", "vcmi.heroWindow.openCommander.help" : "Показать информацию о командире у данного героя", + "vcmi.heroWindow.openBackpack.hover" : "Открыть рюкзак артефактов", + "vcmi.heroWindow.openBackpack.help" : "Рюкзак артефактов упрощает управление артефактами", "vcmi.commanderWindow.artifactMessage" : "Вы хотите отдать артефакт назад герою?", @@ -125,6 +201,15 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Виды дорог", + "vcmi.optionsTab.chessFieldBase.hover" : "Время игрока", + "vcmi.optionsTab.chessFieldTurn.hover" : "Время на ход", + "vcmi.optionsTab.chessFieldBattle.hover" : "Время на битву", + "vcmi.optionsTab.chessFieldUnit.hover" : "Время на отряд", + "vcmi.optionsTab.chessFieldBase.help" : "Обратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", + "vcmi.optionsTab.chessFieldTurn.help" : "Обратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", + "vcmi.optionsTab.chessFieldBattle.help" : "Обратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", + "vcmi.optionsTab.chessFieldUnit.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", + "mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов", "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", "mapObject.core.creatureBank.dwarvenTreasury.name" : "Сокровищница гномов", @@ -148,6 +233,15 @@ "spell.core.strongholdMoatTrigger.name" : "Стена шипов", "spell.core.fortressMoatTrigger.name" : "Кипящая смола", + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Врагу удалось выжить до сегодняшнего дня. Он одержал победу!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Поздравляем! Вам удалось остаться в живых. Победа за вами!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Враг одержал победу над всеми монстрами, заполонившими эту землю, и празднует победу!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Поздравляем! Вы одержали победу над всеми монстрами, заполонившими эту землю, и можете отпраздновать победу!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Получить три артефакта", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Поздравляем! Все ваши враги одержаны победой, и у вас есть Альянс Ангелов! Победа ваша!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Одержите победу над всеми врагами и создайте Альянс Ангелов", + // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» О п ы т с у щ е с т в «\n\nТип существа ................... : %s\nРанг опыта ................. : %s (%i)\nОчки опыта ............... : %i\nДо следующего .. : %i\nМаксимум за битву ... : %i%% (%i)\nЧисло в отряде .... : %i\nМаксимум новичков\n без потери ранга .... : %i\nМножитель опыта ........... : %.2f\nМножитель улучшения .......... : %.2f\nОпыт после 10 ранга ........ : %i\nМаксимум новичков для сохранения\n ранга 10 при максимальном опыте : %i", "vcmi.stackExperience.rank.0" : "Рекрут", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index ffc43609b..e18986ebc 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -30,9 +30,9 @@ "vcmi.capitalColors.6" : "Turquesa", "vcmi.capitalColors.7" : "Rosa", - "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", - "vcmi.server.errors.modsIncompatibility" : "Mods necesarios para cargar el juego:", - "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", + "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", + "vcmi.server.errors.modsToEnable" : "{Mods necesarios para cargar el juego}", + "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index b41fe68cc..6c57e0d19 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -30,6 +30,11 @@ "vcmi.capitalColors.6" : "Сизий", "vcmi.capitalColors.7" : "Рожевий", + "vcmi.heroOverview.startingArmy" : "Початкові загони", + "vcmi.heroOverview.warMachine" : "Бойові машини", + "vcmi.heroOverview.secondarySkills" : "Навички", + "vcmi.heroOverview.spells" : "Закляття", + "vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот", "vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами", "vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту", @@ -37,21 +42,40 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", - "vcmi.mainMenu.tutorialNotImplemented" : "Вибачте, навчання ще не реалізовано\n", - "vcmi.mainMenu.highscoresNotImplemented" : "Вибачте, таблицю рекордів ще не реалізовано\n", + "vcmi.radialWheel.heroGetArmy" : "Отримати армію іншого героя", + "vcmi.radialWheel.heroSwapArmy" : "Обміняти армії героїв", + "vcmi.radialWheel.heroExchange" : "Відкрити вікно обміну", + "vcmi.radialWheel.heroGetArtifacts" : "Отримати артефакти іншого героя", + "vcmi.radialWheel.heroSwapArtifacts" : "Обміняти артефакти героїв", + "vcmi.radialWheel.heroDismiss" : "Звільнити цього героя", + + "vcmi.radialWheel.moveTop" : "Перемістити на початок", + "vcmi.radialWheel.moveUp" : "Перемістити вгору", + "vcmi.radialWheel.moveDown" : "Перемістити вниз", + "vcmi.radialWheel.moveBottom" : "Перемістити у кінець", + "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", + "vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання", "vcmi.mainMenu.serverClosing" : "Завершення...", "vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру", "vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри", "vcmi.mainMenu.playerName" : "Гравець", - "vcmi.lobby.filename" : "Назва файлу", + "vcmi.lobby.filepath" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Огляд мапи", + "vcmi.lobby.noPreview" : "огляд недоступний", + "vcmi.lobby.noUnderground" : "немає підземелля", - "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", - "vcmi.server.errors.modsIncompatibility" : "Потрібні модифікації для завантаження гри:", - "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", + "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.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", @@ -86,6 +110,10 @@ "vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Розширення інтерфейсу", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Велика книга заклять", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Велика книга заклять}\n\nВмикає більшу книгу заклять, яка вміщує більше заклять на сторінці. Якщо цей параметр увімкнено, анімація зміни сторінок книги заклять не буде відображатися.", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", @@ -132,6 +160,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", + "vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт", + "vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій", "vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).", @@ -146,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою", + "vcmi.tutorialWindow.title" : "Використання Сенсорного Екрану", + "vcmi.tutorialWindow.decription.RightClick" : "Торкніться і утримуйте елемент, на якому ви хочете натиснути правою кнопкою миші. Торкніться вільної області, щоб закрити.", + "vcmi.tutorialWindow.decription.MapPanning" : "Торкніться і перетягніть одним пальцем, щоб перемістити мапу.", + "vcmi.tutorialWindow.decription.MapZooming" : "Торкніться двома пальцями, щоб змінити масштаб мапи.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Проводячи пальцем, ви відкриваєте радіальне колесо для різних дій, таких як управління істотами/героями та порядком міст/героїв.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Для того, щоб атакувати з певного напрямку, проведіть пальцем у напрямку, звідки буде здійснено атаку.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Атаку можна скасувати, відвівши палець достатньо далеко.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Щоб скасувати заклинання, торкніться і утримуйте палець.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Показувати приріст істот", @@ -166,7 +206,7 @@ "vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s", "vcmi.townHall.greetingCustomUntil" : " до наступної битви.", "vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.", - + "vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:", "vcmi.logicalExpressions.allOf" : "Все з перерахованого:", "vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:", @@ -194,6 +234,67 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Види доріг", + "vcmi.optionsTab.turnOptions.hover" : "Параметри ходів", + "vcmi.optionsTab.turnOptions.help" : "Виберіть опції таймера ходів та одночасних ходів", + + "vcmi.optionsTab.chessFieldBase.hover" : "Основний таймер", + "vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу", + "vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви", + "vcmi.optionsTab.chessFieldUnit.hover" : "Таймер загону", + "vcmi.optionsTab.chessFieldBase.help" : "Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок додається до {основного таймеру}", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок часу буде втрачено", + "vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок додається до {таймеру битви}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок часу буде втрачено.", + + "vcmi.optionsTab.accumulate" : "Накопичувати", + + "vcmi.optionsTab.simturnsTitle" : "Одночасні ходи", + "vcmi.optionsTab.simturnsMin.hover" : "Щонайменше", + "vcmi.optionsTab.simturnsMax.hover" : "Щонайбільше", + "vcmi.optionsTab.simturnsAI.hover" : "(Експериментально) Одночасні ходи ШІ", + "vcmi.optionsTab.simturnsMin.help" : "Грати одночасно обрану кількість днів. Контакти між гравцями у цей період заблоковані", + "vcmi.optionsTab.simturnsMax.help" : "Грати одночасно обрану кількість днів чи до першого контакту з іншим гравцем", + "vcmi.optionsTab.simturnsAI.help" : "{Одночасні ходи ШІ}\nЕкспериментальна опція. Дозволяє гравцям-ШІ діяти одночасно с гравцями-людьми якщо одночасні ходи увімкнені.", + + "vcmi.optionsTab.turnTime.select" : "Типові налаштування таймерів", + "vcmi.optionsTab.turnTime.unlimited" : "Необмежений час ходу", + "vcmi.optionsTab.turnTime.classic.1" : "Класичний таймер: 1 хвилина", + "vcmi.optionsTab.turnTime.classic.2" : "Класичний таймер: 2 хвилини", + "vcmi.optionsTab.turnTime.classic.5" : "Класичний таймер: 5 хвилин", + "vcmi.optionsTab.turnTime.classic.10" : "Класичний таймер: 10 хвилин", + "vcmi.optionsTab.turnTime.classic.20" : "Класичний таймер: 20 хвилин", + "vcmi.optionsTab.turnTime.classic.30" : "Класичний таймер: 30 хвилин", + "vcmi.optionsTab.turnTime.chess.20" : "Шахи: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Шахи: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Шахи: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Шахи: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Шахи: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Шахи: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Типові налаштування одночасних ходів", + "vcmi.optionsTab.simturns.none" : "Без одночасних ходів", + "vcmi.optionsTab.simturns.tillContactMax" : "Одночасно: До контакту", + "vcmi.optionsTab.simturns.tillContact1" : "Одночасно: 1 тиждень, до контакту", + "vcmi.optionsTab.simturns.tillContact2" : "Одночасно: 2 тижні, до контакту", + "vcmi.optionsTab.simturns.tillContact4" : "Одночасно: 1 місяць, до контакту", + "vcmi.optionsTab.simturns.blocked1" : "Одночасно: 1 тиждень, без контактів", + "vcmi.optionsTab.simturns.blocked2" : "Одночасно: 2 тижні, без контактів", + "vcmi.optionsTab.simturns.blocked4" : "Одночасно: 1 місяць, без контактів", + + // 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 днів", + "vcmi.optionsTab.simturns.days.1" : " %d день", + "vcmi.optionsTab.simturns.days.2" : " %d дні", + "vcmi.optionsTab.simturns.weeks.0" : " %d тижнів", + "vcmi.optionsTab.simturns.weeks.1" : " %d тиждень", + "vcmi.optionsTab.simturns.weeks.2" : " %d тижні", + "vcmi.optionsTab.simturns.months.0" : " %d місяців", + "vcmi.optionsTab.simturns.months.1" : " %d місяць", + "vcmi.optionsTab.simturns.months.2" : " %d місяці", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!", diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json new file mode 100644 index 000000000..5939c1db0 --- /dev/null +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -0,0 +1,373 @@ +{ + "vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ", + "vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng", + "vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu", + "vcmi.adventureMap.monsterThreat.levels.2": "Yếu", + "vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn", + "vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng", + "vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn", + "vcmi.adventureMap.monsterThreat.levels.6": "Mạnh", + "vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh", + "vcmi.adventureMap.monsterThreat.levels.8": "Thách thức", + "vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo", + "vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc", + "vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt", + + "vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?", + "vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!", + "vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!", + "vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.", + "vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s", + "vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING", + + "vcmi.capitalColors.0": "Đỏ", + "vcmi.capitalColors.1": "Xanh dương", + "vcmi.capitalColors.2": "Nâu", + "vcmi.capitalColors.3": "Xanh lá", + "vcmi.capitalColors.4": "Cam", + "vcmi.capitalColors.5": "Tím", + "vcmi.capitalColors.6": "Xanh đậm", + "vcmi.capitalColors.7": "Hồng", + + "vcmi.heroOverview.startingArmy": "Lính ban đầu", + "vcmi.heroOverview.warMachine": "Chiến cơ", + "vcmi.heroOverview.secondarySkills": "Kĩ năng", + "vcmi.heroOverview.spells": "Phép", + + "vcmi.radialWheel.mergeSameUnit": "Sáp nhập cùng loài", + "vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài", + "vcmi.radialWheel.splitSingleUnit": "Tách 1 loài", + "vcmi.radialWheel.splitUnitEqually": "Chia quái bằng nhau", + "vcmi.radialWheel.moveUnit": "Di chuyển quái đến đội khác", + "vcmi.radialWheel.splitUnit": "Chia quái đến ô khác", + + "vcmi.mainMenu.highscoresNotImplemented": "Xin lỗi, bảng xếp hạng chưa được làm đầy đủ\n", + "vcmi.mainMenu.serverConnecting": "Đang kết nối...", + "vcmi.mainMenu.serverAddressEnter": "Nhập địa chỉ:", + "vcmi.mainMenu.serverClosing": "Đang hủy kết nối...", + "vcmi.mainMenu.hostTCP": "Chủ phòng TCP/IP", + "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP", + "vcmi.mainMenu.playerName": "Người chơi", + + "vcmi.lobby.filepath": "Tên tập tin", + "vcmi.lobby.creationDate": "Ngày tạo", + + "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới", + "vcmi.server.errors.modsIncompatibility": "Các bản sửa đổi cần để tải trò chơi:", + "vcmi.server.confirmReconnect": "Bạn có muốn kết nối lại phiên trước?", + + "vcmi.settingsMainWindow.generalTab.hover": "Chung", + "vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi", + "vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu", + "vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh", + "vcmi.settingsMainWindow.adventureTab.hover": "Phiêu lưu", + "vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Phiêu lưu (bản đồ phiêu lưu là nơi mà người chơi di chuyển tướng của họ)", + + "vcmi.systemOptions.videoGroup": "Thiết lập phim ảnh", + "vcmi.systemOptions.audioGroup": "Thiết lập âm thanh", + "vcmi.systemOptions.otherGroup": "Thiết lập khác", + "vcmi.systemOptions.townsGroup": "Thành phố", + + "vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)", + "vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.", + "vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)", + "vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.", + "vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h", + "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải", + "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%", + "vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.", + "vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện", + "vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.", + "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", + "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.", + "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ", + "vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.", + "vcmi.systemOptions.longTouchMenu.entry": "%d mili giây", + "vcmi.systemOptions.framerateButton.hover": "Hiện FPS", + "vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi", + "vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm", + "vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.", + + "vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin", + "vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.", + "vcmi.adventureOptions.numericQuantities.hover": "Số lượng quái", + "vcmi.adventureOptions.numericQuantities.help": "{Số lượng quái}\n\nHiện lượng quái đối phương dạng số A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển", + "vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím ALT)", + "vcmi.adventureOptions.showGrid.hover": "Hiện ô kẻ", + "vcmi.adventureOptions.showGrid.help": "{Hiện ô kẻ}\n\nHiện đường biên giữa các ô trên bản đồ phiêu lưu.", + "vcmi.adventureOptions.borderScroll.hover": "Cuộn ở biên", + "vcmi.adventureOptions.borderScroll.help": "{Cuộn ở biên}\n\nCuộn bản đồ phiêu lưu ở biên. Nhấn giữ phím CTRL để tắt chức năng.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quái ở bảng thông tin", + "vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quái ở bảng thông tin}\n\nCho phép sắp xếp quái ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.", + "vcmi.adventureOptions.leftButtonDrag.hover": "Chuột trái kéo bản đồ", + "vcmi.adventureOptions.leftButtonDrag.help": "{Chuột trái kéo bản đồ}\n\nGiữ chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.", + "vcmi.adventureOptions.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.", + + "vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt", + "vcmi.battleOptions.queueSizeNoneButton.hover": "TẮT", + "vcmi.battleOptions.queueSizeAutoButton.hover": "TỰ ĐỘNG", + "vcmi.battleOptions.queueSizeSmallButton.hover": "NHỎ", + "vcmi.battleOptions.queueSizeBigButton.hover": "LỚN", + "vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (NHỎ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng LỚN).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang NHỎ.", + "vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang LỚN (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hoạt ảnh sang rất chậm", + "vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hoạt ảnh sang rất nhanh", + "vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hoạt ảnh sang tức thời", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quái khi di chuột lên chúng.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh", + + "vcmi.battleWindow.pressKeyToSkipIntro": "Nhấn phím bất kì để bắt đầu trận đánh", + "vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.damage": "%d sát thương", + "vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương", + "vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt", + "vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt", + + "vcmi.battleResultsWindow.applyResultsLabel": "Dùng kết quả trận đánh", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quái được mua", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quái được mua}\n\nHiện quái được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quái hàng tuần", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quái hàng tuần}\n\nHiện sinh trưởng quái thay vì lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quái", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quái}\n\nHiện thông tin quái nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).", + + "vcmi.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:", + "vcmi.logicalExpressions.noneOf": "Không có những cái sau:", + + "vcmi.heroWindow.openCommander.hover": "Mở cửa sổ thông tin chỉ huy", + "vcmi.heroWindow.openCommander.help": "Hiện chi tiết về chỉ huy tướng này", + "vcmi.heroWindow.openBackpack.hover": "Mở hành lí", + "vcmi.heroWindow.openBackpack.help": "Hành lí cho phép quản lí vật phẩm dễ dàng hơn.", + + "vcmi.commanderWindow.artifactMessage": "Bạn muốn trả lại vật phẩm này cho tướng?", + + "vcmi.creatureWindow.showBonuses.hover": "Chuyển sang phần tăng thêm", + "vcmi.creatureWindow.showBonuses.help": "Hiện thuộc tính tăng thêm của chỉ huy", + "vcmi.creatureWindow.showSkills.hover": "Chuyển sang phần kĩ năng", + "vcmi.creatureWindow.showSkills.help": "Hiện kĩ năng đã học của chỉ huy", + "vcmi.creatureWindow.returnArtifact.hover": "Trả vật phẩm", + "vcmi.creatureWindow.returnArtifact.help": "Nhấn nút này để trả vật phẩm cho tướng", + + "vcmi.questLog.hideComplete.hover": "Ẩn nhiệm vụ đã làm", + "vcmi.questLog.hideComplete.help": "Ẩn các nhiệm vụ đã hoàn thành", + + "vcmi.randomMapTab.widgets.defaultTemplate": "(mặc định)", + "vcmi.randomMapTab.widgets.templateLabel": "Mẫu", + "vcmi.randomMapTab.widgets.teamAlignmentsButton": "Cài đặt...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội", + "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá", + + "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", + "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", + "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh", + "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian lính", + "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", + "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", + "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", + "vcmi.optionsTab.chessFieldUnit.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", + + "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", + "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers": "Đối thủ đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf": "Chúc mừng! Bạn đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", + "vcmi.map.victoryCondition.collectArtifacts.message": "Đoạt 3 vật phẩm", + "vcmi.map.victoryCondition.angelicAlliance.toSelf": "Chúc mừng! Tất cả đối thủ bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.angelicAlliance.message": "Đánh bại tất cả đối thủ và tạo Angelic Alliance", + + "vcmi.stackExperience.description": "» K I N H N G H I Ệ M «\n\nLoại Quái ................... : %s\nCấp Kinh Nghiệm ................. : %s (%i)\nĐiểm Kinh Nghiệm ............... : %i\nĐiểm Kinh Nghiệm Để Lên Cấp .. : %i\nKinh Nghiệm Tối Đa Mỗi Trận Đánh ... : %i%% (%i)\nSố Lượng Quái .... : %i\nTối Đa Mua Mới\n không bị giảm cấp .... : %i\nHệ Số Kinh Nghiệm ........... : %.2f\nHệ Số Nâng Cấp .............. : %.2f\nKinh Nghiệm Sau Cấp 10 ........ : %i\nTối Đa Mua Mới để vẫn ở\n Mức Tối Đa Kinh Nghiệm Cấp 10 : %i", + "vcmi.stackExperience.rank.0": "Lính Mới", + "vcmi.stackExperience.rank.1": "Tập Sự", + "vcmi.stackExperience.rank.2": "Lành Nghề", + "vcmi.stackExperience.rank.3": "Khéo Léo", + "vcmi.stackExperience.rank.4": "Thông Thạo", + "vcmi.stackExperience.rank.5": "Kì Cựu", + "vcmi.stackExperience.rank.6": "Lão Luyện", + "vcmi.stackExperience.rank.7": "Chuyên Gia", + "vcmi.stackExperience.rank.8": "Tinh Hoa", + "vcmi.stackExperience.rank.9": "Bậc Thầy", + "vcmi.stackExperience.rank.10": "Thiên Tài", + + "core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần", + "core.bonus.ADDITIONAL_ATTACK.description": "Tấn công 2 lần", + "core.bonus.ADDITIONAL_RETALIATION.name": "Thêm phản công", + "core.bonus.ADDITIONAL_RETALIATION.description": "Phản công thêm ${val} lần", + "core.bonus.AIR_IMMUNITY.name": "Kháng Khí", + "core.bonus.AIR_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Khí", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả đối phương xung quanh", + "core.bonus.BLOCKS_RETALIATION.name": "Ngăn phản công", + "core.bonus.BLOCKS_RETALIATION.description": "Đối phương không thể phản công", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Ngăn bắn phản công", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Đối phương không thể bắn phản công", + "core.bonus.CATAPULT.name": "Công thành", + "core.bonus.CATAPULT.description": "Tấn công tường thành", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm yêu cầu năng lượng (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng cần làm phép", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tăng yêu cầu năng lượng (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tăng ${val} năng lượng đối phương cần làm phép", + "core.bonus.CHARGE_IMMUNITY.name": "Kháng đột kích", + "core.bonus.CHARGE_IMMUNITY.description": "Kháng đột kích của Cavalier và Champion", + "core.bonus.DARKNESS.name": "Màn tối", + "core.bonus.DARKNESS.description": "Tạo màn bóng tối bán kính ${val}", + "core.bonus.DEATH_STARE.name": "Cái nhìn chết chóc (${val}%)", + "core.bonus.DEATH_STARE.description": "${val}% cơ hội diệt thêm quái", + "core.bonus.DEFENSIVE_STANCE.name": "Tăng Thủ", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Thủ khi đang thế thủ", + "core.bonus.DESTRUCTION.name": "Hủy diệt", + "core.bonus.DESTRUCTION.description": "${val}% cơ hội diệt thêm quái", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% cơ hội nhân đôi sát thương khi tấn công", + "core.bonus.DRAGON_NATURE.name": "Rồng", + "core.bonus.DRAGON_NATURE.description": "Quái có chất Rồng", + "core.bonus.EARTH_IMMUNITY.name": "Kháng Đất", + "core.bonus.EARTH_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Đất", + "core.bonus.ENCHANTER.name": "Bùa chú", + "core.bonus.ENCHANTER.description": "Ếm ${subtype.spell} mỗi lượt", + "core.bonus.ENCHANTED.name": "Chúc phúc", + "core.bonus.ENCHANTED.description": "Nhận phép vĩnh cửu ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Xuyên giáp (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, bỏ qua ${val}% Thủ", + "core.bonus.FIRE_IMMUNITY.name": "Kháng Lửa", + "core.bonus.FIRE_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Lửa", + "core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Phản ${val}% sát thương cận chiến", + "core.bonus.FIRST_STRIKE.name": "Đánh trước", + "core.bonus.FIRST_STRIKE.description": "Quái phản công trước khi bị tấn công", + "core.bonus.FEAR.name": "Hãi hùng", + "core.bonus.FEAR.description": "Gây Hoảng Sợ lên quái đối phương", + "core.bonus.FEARLESS.name": "Can đảm", + "core.bonus.FEARLESS.description": "Không bị hoảng sợ", + "core.bonus.FLYING.name": "Bay", + "core.bonus.FLYING.description": "Vượt chướng ngại vật", + "core.bonus.FREE_SHOOTING.name": "Bắn gần", + "core.bonus.FREE_SHOOTING.description": "Bắn kể cả khi cận chiến", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Không thể hồi sinh", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm sát thương (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương vật lí", + "core.bonus.HATE.name": "Ghét ${subtype.creature}", + "core.bonus.HATE.description": "Tăng thêm ${val}% sát thương cho ${subtype.creature}", + "core.bonus.HEALER.name": "Hồi máu", + "core.bonus.HEALER.description": "Hồi máu đồng đội", + "core.bonus.HP_REGENERATION.name": "Tự hồi máu", + "core.bonus.HP_REGENERATION.description": "Hồi ${SHval} máu mỗi lượt", + "core.bonus.JOUSTING.name": "Đột kích", + "core.bonus.JOUSTING.description": "+${val}% sát thương cho mỗi ô đi qua", + "core.bonus.KING.name": "Khổng lồ", + "core.bonus.KING.description": "Dễ tổn thương bởi Diệt Khổng Lồ cấp ${val} hoặc cao hơn", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1 - ${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name": "Tầm bắn", + "core.bonus.LIMITED_SHOOTING_RANGE.description": "Không thể nhắm bắn quái xa hơn ${val} ô", + "core.bonus.LIFE_DRAIN.name": "Hút máu (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Hồi máu ${val}% sát thương gây ra", + "core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%", + "core.bonus.MANA_CHANNELING.description": "Cho tướng ${val}% năng lượng dùng bởi đối phương", + "core.bonus.MANA_DRAIN.name": "Hút năng lượng", + "core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt", + "core.bonus.MAGIC_MIRROR.name": "Phản phép (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "${val}% cơ hội phản phép tấn công đến quái đối phương", + "core.bonus.MAGIC_RESISTANCE.name": "Né phép (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "${val}% cơ hội tránh phép của đối phương", + "core.bonus.MIND_IMMUNITY.name": "Kháng phép tinh thần", + "core.bonus.MIND_IMMUNITY.description": "Kháng ma thuật về tinh thần", + "core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa", + "core.bonus.NO_DISTANCE_PENALTY.description": "Gây trọn sát thương ở bất kì khoảng cách nào", + "core.bonus.NO_MELEE_PENALTY.name": "Đánh gần", + "core.bonus.NO_MELEE_PENALTY.description": "Quái không bị giảm sát thương khi cận chiến", + "core.bonus.NO_MORALE.name": "Bình tĩnh", + "core.bonus.NO_MORALE.description": "Quái không ảnh hưởng bởi sĩ khí", + "core.bonus.NO_WALL_PENALTY.name": "Bỏ qua tường", + "core.bonus.NO_WALL_PENALTY.description": "Gây trọn sát thương khi công thành", + "core.bonus.NON_LIVING.name": "Vô sinh", + "core.bonus.NON_LIVING.description": "Kháng nhiều hiệu ứng", + "core.bonus.RANDOM_SPELLCASTER.name": "Ếm ngẫu nhiên", + "core.bonus.RANDOM_SPELLCASTER.description": "Ếm phép ngẫu nhiên", + "core.bonus.RANGED_RETALIATION.name": "Phản công tầm xa", + "core.bonus.RANGED_RETALIATION.description": "Phản công khi bị bắn", + "core.bonus.RECEPTIVE.name": "Tiếp thu", + "core.bonus.RECEPTIVE.description": "Không kháng phép có lợi", + "core.bonus.REBIRTH.name": "Tái sinh (${val}%)", + "core.bonus.REBIRTH.description": "${val}% số lượng sẽ hồi sinh sau khi chết", + "core.bonus.RETURN_AFTER_STRIKE.name": "Du kích", + "core.bonus.RETURN_AFTER_STRIKE.description": "Trở về sau khi đánh", + "core.bonus.SHOOTER.name": "Cung thủ", + "core.bonus.SHOOTER.description": "Quái có thể tấn công tầm xa", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả quái trong phạm vi nhỏ", + "core.bonus.SOUL_STEAL.name": "Hút hồn", + "core.bonus.SOUL_STEAL.description": "Tăng ${val} quái mới với mỗi quái đối phương bị diệt", + "core.bonus.SPELLCASTER.name": "Pháp sư", + "core.bonus.SPELLCASTER.description": "Có thể ếm phép ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Ếm sau khi đánh", + "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} sau khi tấn công", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Ếm trước khi đánh", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} trước khi tấn công", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Sát thương phép giảm ${val}%", + "core.bonus.SPELL_IMMUNITY.name": "Miễn dịch", + "core.bonus.SPELL_IMMUNITY.description": "Miễn dịch với phép ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "Đánh phép", + "core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Quái ở gần nhận ${val}% kháng ma thuật", + "core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ", + "core.bonus.SUMMON_GUARDIANS.description": "Đầu trận gọi quái ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Hợp lực", + "core.bonus.SYNERGY_TARGET.description": "Quái này dễ bị ảnh hưởng hợp lực", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Hơi thở", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công 2 ô", + "core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu", + "core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quái liền kề mục tiêu", + "core.bonus.TRANSMUTATION.name": "Biến đổi", + "core.bonus.TRANSMUTATION.description": "${val}% cơ hội biến đổi quái mục tiêu thành dạng khác", + "core.bonus.UNDEAD.name": "Thây ma", + "core.bonus.UNDEAD.description": "Quái là thây ma", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Phản công vô hạn", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Không giới hạn số lần phản công", + "core.bonus.WATER_IMMUNITY.name": "Kháng Nước", + "core.bonus.WATER_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Nước", + "core.bonus.WIDE_BREATH.name": "Hơi thở sâu", + "core.bonus.WIDE_BREATH.description": "Tấn công nhiều ô" +} diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index cf65ef8df..cedf5bf5b 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -5,7 +5,7 @@ "chinese" : { "name" : "VCMI essential files", "description" : "Essential files required for VCMI to run correctly", - + "skipValidation" : true, "translations" : [ "config/vcmi/chinese.json" @@ -15,7 +15,7 @@ "czech" : { "name" : "Nezbytné soubory VCMI", "description" : "Nezbytné soubory pro správný běh VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/czech.json" @@ -32,23 +32,23 @@ "config/vcmi/french.json" ] }, - + "german" : { "name" : "VCMI - grundlegende Dateien", "description" : "Grundlegende Dateien, die für die korrekte Ausführung von VCMI erforderlich sind", "author" : "VCMI-Team", - + "skipValidation" : true, "translations" : [ "config/vcmi/german.json" ] }, - + "polish" : { "name" : "Podstawowe pliki VCMI", "description" : "Dodatkowe pliki wymagane do prawidłowego działania VCMI", "author" : "Zespół VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/polish.json" @@ -70,7 +70,7 @@ "name" : "VCMI - ключові файли", "description" : "Ключові файли необхідні для повноцінної роботи VCMI", "author" : "Команда VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/ukrainian.json" @@ -88,18 +88,28 @@ ] }, + "vietnamese": { + "name": "VCMI essential files", + "description": "Các tập tin cần thiết để chạy VCMI", + "author": "Vũ Đắc Hoàng Ân", + "skipValidation": true, + "translations": [ + "config/vcmi/vietnamese.json" + ] + }, + "version" : "1.3", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", - + "factions" : [ "config/vcmi/towerFactions" ], "creatures" : [ "config/vcmi/towerCreature" ], - + "translations" : [ "config/vcmi/english.json" ], - + "templates" : [ "config/vcmi/rmg/hdmod/aroundamarsh.JSON", "config/vcmi/rmg/hdmod/balance.JSON", @@ -200,6 +210,14 @@ "MAPS/": [ {"type" : "dir", "path" : "/Maps"} + ], + "SOUNDS/": + [ + {"type" : "dir", "path" : "/Sounds"} + ], + "VIDEO/": + [ + {"type" : "dir", "path" : "/Video"} ] } } diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 45d7ec9dd..3b7d295ea 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 33 - versionCode 1321 - versionName "1.3.2" + versionCode 1400 + versionName "1.4.0" setProperty("archivesBaseName", "vcmi") } diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java index 4f3cc0582..2bbd73ae3 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java @@ -50,7 +50,7 @@ import eu.vcmi.vcmi.util.ServerResponse; public class ActivityMods extends ActivityWithToolbar { private static final boolean ENABLE_REPO_DOWNLOADING = true; - private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.3.json"; + private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.4.json"; private VCMIModsRepo mRepo; private RecyclerView mRecycler; diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java index cb02e2d83..ca62d73a9 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Const.java @@ -1,24 +1,24 @@ -package eu.vcmi.vcmi; - -import android.content.Context; -import android.os.Build; -import android.os.Environment; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStreamWriter; - -/** - * @author F - */ -public class Const -{ - public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls - // used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it) - public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT; - - public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data"; -} +package eu.vcmi.vcmi; + +import android.content.Context; +import android.os.Build; +import android.os.Environment; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; + +/** + * @author F + */ +public class Const +{ + public static final String JNI_METHOD_SUPPRESS = "unused"; // jni methods are marked as unused, because IDE doesn't understand jni calls + // used to disable lint errors about try-with-resources being unsupported on api <19 (it is supported, because retrolambda backports it) + public static final int SUPPRESS_TRY_WITH_RESOURCES_WARNING = Build.VERSION_CODES.KITKAT; + + public static final String VCMI_DATA_ROOT_FOLDER_NAME = "vcmi-data"; +} diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 55ca15691..756fd510f 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -17,6 +17,9 @@ import org.libsdl.app.SDLActivity; import java.io.File; import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.Locale; +import java.text.SimpleDateFormat; import eu.vcmi.vcmi.util.Log; @@ -153,6 +156,14 @@ public class NativeMethods } } + @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) + public static String getFormattedDateTime() + { + String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); + + return currentDate; + } + private static void internalProgressDisplay(final boolean show) { final Context ctx = SDL.getContext(); diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java index 9986f890e..b3fc1b9db 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java @@ -22,7 +22,9 @@ public class Storage public static boolean testH3DataFolder(final File baseDir) { final File testH3Data = new File(baseDir, "Data"); - return testH3Data.exists(); + final File testH3data = new File(baseDir, "data"); + final File testH3DATA = new File(baseDir, "DATA"); + return testH3Data.exists() || testH3data.exists() || testH3DATA.exists(); } public static String getH3DataFolder(Context context){ diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java index 84e555fca..790920528 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java @@ -40,11 +40,11 @@ public class DialogAuthors extends DialogFragment try { // to be checked if this should be converted to async load (not really a file operation so it should be okay) - final String authorsContent = FileUtil.read(getResources().openRawResource(R.raw.authors)); + final String authorsContent = "See ingame credits"; vcmiAuthorsView.setText(authorsContent); launcherAuthorsView.setText("Fay"); // TODO hardcoded for now } - catch (final IOException e) + catch (final Exception e) { Log.e(this, "Could not load authors content", e); } diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java index 9acf5e0a2..2d5396d88 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java @@ -127,9 +127,21 @@ public class CopyDataController extends LauncherSettingController for (DocumentFile child : sourceDir.listFiles()) { - if (allowed != null && !allowed.contains(child.getName())) + if (allowed != null) { - continue; + boolean fileAllowed = false; + + for (String str : allowed) + { + if (str.equalsIgnoreCase(child.getName())) + { + fileAllowed = true; + break; + } + } + + if (!fileAllowed) + continue; } File exported = new File(targetDir, child.getName()); diff --git a/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml b/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml index 3ed263fd7..5f44696a5 100644 --- a/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml +++ b/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml b/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml index 579b3ebc5..2deccc9ef 100644 --- a/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml +++ b/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/menu/menu_mods.xml b/android/vcmi-app/src/main/res/menu/menu_mods.xml index aa713155b..b50094a7f 100644 --- a/android/vcmi-app/src/main/res/menu/menu_mods.xml +++ b/android/vcmi-app/src/main/res/menu/menu_mods.xml @@ -1,9 +1,9 @@ - - - + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/values-uk/strings.xml b/android/vcmi-app/src/main/res/values-uk/strings.xml index cc65cace0..fdad3813e 100644 --- a/android/vcmi-app/src/main/res/values-uk/strings.xml +++ b/android/vcmi-app/src/main/res/values-uk/strings.xml @@ -6,7 +6,7 @@ VCMI сервер Поточна версія VCMI: %1$s Моди - Додати нові замки, істот, об’єкти, розширення + Додати нові замки, істоти, об’єкти, розширення Мова Поточна: невідомо Поточна: %1$s @@ -16,13 +16,13 @@ Поточна версія лаунчера: %1$s Не вдалося знайти файли Героїв у \'%1$s\'. Скопіюйте файли Героїв 3 туди вручну або скористайтеся кнопкою копіювання і перезапустіть гру. Відносна швидкість керування курсором - Громкість звуків - Громкость музики + Гучність звуків + Гучність музики Комп’ютерний гравець - Змінити программу комп’ютерного гравця - Не вдалося створити папку VCMI з даними в %1$s. + Змінити програму комп’ютерного гравця + Не вдалося створити теку VCMI з даними в %1$s. Не вдалося розпакувати файли ресурсів. Спробуйте перевстановити програму. - Не вдалося оновити файлы ресурсів. Спробуйте перевстановити програму. + Не вдалося оновити файли ресурсів. Спробуйте перевстановити програму. VCMI необхідні права для запису контенту до зовнішнього сховища. Не вдалося отримати права. Не вдалося зберегти налаштування; причина: %1$s @@ -33,18 +33,18 @@ Завантажити список модів Звичайне Відносне - О VCMI + Про VCMI Встановлені моди - Не вдалося завантажити мод в папку \'%1$s\' + Не вдалося завантажити мод в теку \'%1$s\' Видалення %1$s Ви впевнені, що хочете видалити %1$s - О VCMI + Про VCMI Версія VCMI: %1$s Версія лаунчера: %1$s - Проект + Проєкт Legal Сайт: %1$s - Репозиторій проекту: %1$s + Репозиторій проєкту: %1$s Репозиторій лаунчера: %1$s Автори Не вдалося відкрити сторінку (ймовірно, не вдалося знайти браузер) @@ -54,6 +54,6 @@ Експортувати дані VCMI Зробити копію даних VCMI перед видаленням гри або перенести сейв-файли на версію для інших платформ Завантажити дані VCMI у внутрішнє сховище - Копіювати дані VCMI у внутрішнє сховище. Ви можете завантажити стару папку vcmi-data від версії 0.99 чи файли героїв + Копіювати дані VCMI у внутрішнє сховище. Ви можете завантажити стару теку vcmi-data від версії 0.99 чи файли героїв Копіюємо %1$s diff --git a/android/vcmi-app/src/main/res/values/colors.xml b/android/vcmi-app/src/main/res/values/colors.xml index 91c8286d4..764ef76ea 100644 --- a/android/vcmi-app/src/main/res/values/colors.xml +++ b/android/vcmi-app/src/main/res/values/colors.xml @@ -1,10 +1,10 @@ - - - #FFFFFF - #AAAAAA - #2C332C - #1F221F - #BBBB55 - @color/accent - #00000000 + + + #FFFFFF + #AAAAAA + #2C332C + #1F221F + #BBBB55 + @color/accent + #00000000 \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/values/dimen.xml b/android/vcmi-app/src/main/res/values/dimen.xml index ca9e1b63d..cd9fe2b7c 100644 --- a/android/vcmi-app/src/main/res/values/dimen.xml +++ b/android/vcmi-app/src/main/res/values/dimen.xml @@ -1,11 +1,11 @@ - - - 16dp - - 80dp - 48dp - - 14sp - 18sp - 22sp + + + 16dp + + 80dp + 48dp + + 14sp + 18sp + 22sp \ No newline at end of file diff --git a/client/CFocusableHelper.cpp b/client/CFocusableHelper.cpp index e4e3c6cfe..c668f825b 100644 --- a/client/CFocusableHelper.cpp +++ b/client/CFocusableHelper.cpp @@ -9,6 +9,7 @@ */ #include "CFocusableHelper.h" #include "../Global.h" +#include "StdInc.h" #include "widgets/TextControls.h" void removeFocusFromActiveInput() diff --git a/client/CGameInfo.cpp b/client/CGameInfo.cpp index 59a523e2b..df2e1372d 100644 --- a/client/CGameInfo.cpp +++ b/client/CGameInfo.cpp @@ -1,115 +1,115 @@ -/* - * CGameInfo.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 "CGameInfo.h" - -#include "../lib/VCMI_Lib.h" - -const CGameInfo * CGI; -CClientState * CCS = nullptr; -CServerHandler * CSH; - - -CGameInfo::CGameInfo() -{ - generaltexth = nullptr; - mh = nullptr; - townh = nullptr; - globalServices = nullptr; -} - -void CGameInfo::setFromLib() -{ - globalServices = VLC; - modh = VLC->modh; - generaltexth = VLC->generaltexth; - creh = VLC->creh; - townh = VLC->townh; - heroh = VLC->heroh; - objh = VLC->objh; - spellh = VLC->spellh; - skillh = VLC->skillh; - objtypeh = VLC->objtypeh; - terrainTypeHandler = VLC->terrainTypeHandler; - battleFieldHandler = VLC->battlefieldsHandler; - obstacleHandler = VLC->obstacleHandler; -} - -const ArtifactService * CGameInfo::artifacts() const -{ - return globalServices->artifacts(); -} - -const BattleFieldService * CGameInfo::battlefields() const -{ - return globalServices->battlefields(); -} - -const CreatureService * CGameInfo::creatures() const -{ - return globalServices->creatures(); -} - -const FactionService * CGameInfo::factions() const -{ - return globalServices->factions(); -} - -const HeroClassService * CGameInfo::heroClasses() const -{ - return globalServices->heroClasses(); -} - -const HeroTypeService * CGameInfo::heroTypes() const -{ - return globalServices->heroTypes(); -} - -#if SCRIPTING_ENABLED -const scripting::Service * CGameInfo::scripts() const -{ - return globalServices->scripts(); -} -#endif - -const spells::Service * CGameInfo::spells() const -{ - return globalServices->spells(); -} - -const SkillService * CGameInfo::skills() const -{ - return globalServices->skills(); -} - -const ObstacleService * CGameInfo::obstacles() const -{ - return globalServices->obstacles(); -} - -const IGameSettings * CGameInfo::settings() const -{ - return globalServices->settings(); -} - -void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) -{ - logGlobal->error("CGameInfo::updateEntity call is not expected."); -} - -spells::effects::Registry * CGameInfo::spellEffects() -{ - return nullptr; -} - -const spells::effects::Registry * CGameInfo::spellEffects() const -{ - return globalServices->spellEffects(); -} +/* + * CGameInfo.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 "CGameInfo.h" + +#include "../lib/VCMI_Lib.h" + +const CGameInfo * CGI; +CClientState * CCS = nullptr; +CServerHandler * CSH; + + +CGameInfo::CGameInfo() +{ + generaltexth = nullptr; + mh = nullptr; + townh = nullptr; + globalServices = nullptr; +} + +void CGameInfo::setFromLib() +{ + globalServices = VLC; + modh = VLC->modh; + generaltexth = VLC->generaltexth; + creh = VLC->creh; + townh = VLC->townh; + heroh = VLC->heroh; + objh = VLC->objh; + spellh = VLC->spellh; + skillh = VLC->skillh; + objtypeh = VLC->objtypeh; + terrainTypeHandler = VLC->terrainTypeHandler; + battleFieldHandler = VLC->battlefieldsHandler; + obstacleHandler = VLC->obstacleHandler; +} + +const ArtifactService * CGameInfo::artifacts() const +{ + return globalServices->artifacts(); +} + +const BattleFieldService * CGameInfo::battlefields() const +{ + return globalServices->battlefields(); +} + +const CreatureService * CGameInfo::creatures() const +{ + return globalServices->creatures(); +} + +const FactionService * CGameInfo::factions() const +{ + return globalServices->factions(); +} + +const HeroClassService * CGameInfo::heroClasses() const +{ + return globalServices->heroClasses(); +} + +const HeroTypeService * CGameInfo::heroTypes() const +{ + return globalServices->heroTypes(); +} + +#if SCRIPTING_ENABLED +const scripting::Service * CGameInfo::scripts() const +{ + return globalServices->scripts(); +} +#endif + +const spells::Service * CGameInfo::spells() const +{ + return globalServices->spells(); +} + +const SkillService * CGameInfo::skills() const +{ + return globalServices->skills(); +} + +const ObstacleService * CGameInfo::obstacles() const +{ + return globalServices->obstacles(); +} + +const IGameSettings * CGameInfo::settings() const +{ + return globalServices->settings(); +} + +void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) +{ + logGlobal->error("CGameInfo::updateEntity call is not expected."); +} + +spells::effects::Registry * CGameInfo::spellEffects() +{ + return nullptr; +} + +const spells::effects::Registry * CGameInfo::spellEffects() const +{ + return globalServices->spellEffects(); +} diff --git a/client/CGameInfo.h b/client/CGameInfo.h index 76b58a610..332068395 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -1,101 +1,101 @@ -/* - * CGameInfo.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/ConstTransitivePtr.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CModHandler; -class CHeroHandler; -class CCreatureHandler; -class CSpellHandler; -class CSkillHandler; -class CBuildingHandler; -class CObjectHandler; -class CObjectClassesHandler; -class CTownHandler; -class CGeneralTextHandler; -class CConsoleHandler; -class CGameState; -class BattleFieldHandler; -class ObstacleHandler; -class TerrainTypeHandler; - -class CMap; - -VCMI_LIB_NAMESPACE_END - -class CMapHandler; -class CSoundHandler; -class CMusicHandler; -class CursorHandler; -class IMainVideoPlayer; -class CServerHandler; - -//a class for non-mechanical client GUI classes -class CClientState -{ -public: - CSoundHandler * soundh; - CMusicHandler * musich; - CConsoleHandler * consoleh; - CursorHandler * curh; - IMainVideoPlayer * videoh; -}; -extern CClientState * CCS; - -/// CGameInfo class -/// for allowing different functions for accessing game informations -class CGameInfo : public Services -{ -public: - const ArtifactService * artifacts() const override; - const CreatureService * creatures() const override; - const FactionService * factions() const override; - const HeroClassService * heroClasses() const override; - const HeroTypeService * heroTypes() const override; -#if SCRIPTING_ENABLED - const scripting::Service * scripts() const override; -#endif - const spells::Service * spells() const override; - 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 spells::effects::Registry * spellEffects() const override; - spells::effects::Registry * spellEffects() override; - - ConstTransitivePtr modh; //public? - ConstTransitivePtr battleFieldHandler; - ConstTransitivePtr heroh; - ConstTransitivePtr creh; - ConstTransitivePtr spellh; - ConstTransitivePtr skillh; - ConstTransitivePtr objh; - ConstTransitivePtr terrainTypeHandler; - ConstTransitivePtr objtypeh; - ConstTransitivePtr obstacleHandler; - CGeneralTextHandler * generaltexth; - CMapHandler * mh; - CTownHandler * townh; - - void setFromLib(); - - CGameInfo(); -private: - const Services * globalServices; -}; -extern const CGameInfo* CGI; +/* + * CGameInfo.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/ConstTransitivePtr.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CModHandler; +class CHeroHandler; +class CCreatureHandler; +class CSpellHandler; +class CSkillHandler; +class CBuildingHandler; +class CObjectHandler; +class CObjectClassesHandler; +class CTownHandler; +class CGeneralTextHandler; +class CConsoleHandler; +class CGameState; +class BattleFieldHandler; +class ObstacleHandler; +class TerrainTypeHandler; + +class CMap; + +VCMI_LIB_NAMESPACE_END + +class CMapHandler; +class CSoundHandler; +class CMusicHandler; +class CursorHandler; +class IMainVideoPlayer; +class CServerHandler; + +//a class for non-mechanical client GUI classes +class CClientState +{ +public: + CSoundHandler * soundh; + CMusicHandler * musich; + CConsoleHandler * consoleh; + CursorHandler * curh; + IMainVideoPlayer * videoh; +}; +extern CClientState * CCS; + +/// CGameInfo class +/// for allowing different functions for accessing game informations +class CGameInfo : public Services +{ +public: + const ArtifactService * artifacts() const override; + const CreatureService * creatures() const override; + const FactionService * factions() const override; + const HeroClassService * heroClasses() const override; + const HeroTypeService * heroTypes() const override; +#if SCRIPTING_ENABLED + const scripting::Service * scripts() const override; +#endif + const spells::Service * spells() const override; + 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 spells::effects::Registry * spellEffects() const override; + spells::effects::Registry * spellEffects() override; + + ConstTransitivePtr modh; //public? + ConstTransitivePtr battleFieldHandler; + ConstTransitivePtr heroh; + ConstTransitivePtr creh; + ConstTransitivePtr spellh; + ConstTransitivePtr skillh; + ConstTransitivePtr objh; + ConstTransitivePtr terrainTypeHandler; + ConstTransitivePtr objtypeh; + ConstTransitivePtr obstacleHandler; + CGeneralTextHandler * generaltexth; + CMapHandler * mh; + CTownHandler * townh; + + void setFromLib(); + + CGameInfo(); +private: + const Services * globalServices; +}; +extern const CGameInfo* CGI; diff --git a/client/CMT.cpp b/client/CMT.cpp index 7e4feb25f..b6ca35859 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -1,521 +1,540 @@ -/* - * CMT.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 - * - */ - -// CMT.cpp : Defines the entry point for the console application. -#include "StdInc.h" -#include "CMT.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 "render/IScreenHandler.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/VCMIDirs.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/CConfigHandler.h" - -#include "../lib/logging/CBasicLogConfigurator.h" - -#include -#include - -#include -#include - -#ifdef VCMI_ANDROID -#include "../lib/CAndroidVMHelper.h" -#include -#endif - -#if __MINGW32__ -#undef main -#endif - -namespace po = boost::program_options; -namespace po_style = boost::program_options::command_line_style; -namespace bfs = boost::filesystem; - -extern boost::thread_specific_ptr inGuiThread; - -static po::variables_map vm; - -#ifndef VCMI_IOS -void processCommand(const std::string &message); -#endif -void playIntro(); -static void mainLoop(); - -static CBasicLogConfigurator *logConfig; - -void init() -{ - CStopWatch tmh; - - loadDLLClasses(); - const_cast(CGI)->setFromLib(); - - logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); - - // Debug code to load all maps on start - //ClientCommandManager commandController; - //commandController.processCommand("convert txt", false); -} - -static void prog_version() -{ - printf("%s\n", GameConstants::VCMI_VERSION.c_str()); - std::cout << VCMIDirs::get().genHelpString(); -} - -static void prog_help(const po::options_description &opts) -{ - auto time = std::time(0); - printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); - printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); - printf("This is free software; see the source for copying conditions. There is NO\n"); - printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); - printf("\n"); - std::cout << opts; -} - -#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE) -int wmain(int argc, wchar_t* argv[]) -#elif defined(VCMI_MOBILE) -int SDL_main(int argc, char *argv[]) -#else -int main(int argc, char * argv[]) -#endif -{ -#ifdef VCMI_ANDROID - CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv()); - // boost will crash without this - setenv("LANG", "C", 1); -#endif - -#if !defined(VCMI_MOBILE) - // Correct working dir executable folder (not bundle folder) so we can use executable relative paths - boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); -#endif - std::cout << "Starting... " << std::endl; - po::options_description opts("Allowed options"); - opts.add_options() - ("help,h", "display help and exit") - ("version,v", "display version information and exit") - ("disable-shm", "force disable shared memory usage") - ("enable-shm-uuid", "use UUID for shared memory identifier") - ("testmap", po::value(), "") - ("testsave", po::value(), "") - ("spectate,s", "enable spectator interface for AI-only games") - ("spectate-ignore-hero", "wont follow heroes on adventure map") - ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") - ("spectate-battle-speed", po::value(), "battle animation speed for spectator") - ("spectate-skip-battle", "skip battles in spectator view") - ("spectate-skip-battle-result", "skip battle result window") - ("onlyAI", "allow to run without human player, all players will be default AI") - ("headless", "runs without GUI, implies --onlyAI") - ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") - ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") - ("autoSkip", "automatically skip turns in GUI") - ("disable-video", "disable video player") - ("nointro,i", "skips intro movies") - ("donotstartserver,d","do not attempt to start server and just connect to it instead server") - ("serverport", po::value(), "override port specified in config file") - ("savefrequency", po::value(), "limit auto save creation to each N days") - ("lobby", "parameters address, port, uuid to connect ro remote lobby session") - ("lobby-address", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port to remote lobby") - ("lobby-host", "if this client hosts session") - ("lobby-uuid", po::value(), "uuid to the server") - ("lobby-connections", po::value(), "connections of server") - ("lobby-username", po::value(), "player name") - ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") - ("uuid", po::value(), "uuid for the client"); - - if(argc > 1) - { - try - { - po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); - } - catch(std::exception &e) - { - std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; - } - } - - po::notify(vm); - if(vm.count("help")) - { - prog_help(opts); -#ifdef VCMI_IOS - exit(0); -#else - return 0; -#endif - } - if(vm.count("version")) - { - prog_version(); -#ifdef VCMI_IOS - exit(0); -#else - return 0; -#endif - } - - // Init old logging system and new (temporary) logging system - CStopWatch total, pomtime; - std::cout.flags(std::ios::unitbuf); -#ifndef VCMI_IOS - console = new CConsoleHandler(); - - auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole) - { - ClientCommandManager commandController; - commandController.processCommand(buffer, calledFromIngameConsole); - }; - - *console->cb = callbackFunction; - console->start(); -#endif - - const bfs::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; - logConfig = new CBasicLogConfigurator(logPath, console); - logConfig->configureDefault(); - logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION); - logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); - logGlobal->info("The log file will be saved to %s", logPath); - - // Init filesystem and settings - preinitDLL(::console); - - Settings session = settings.write["session"]; - auto setSettingBool = [](std::string key, std::string arg) { - Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) - s->Bool() = true; - else if(s->isNull()) - s->Bool() = false; - }; - auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { - Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) - s->Integer() = ::vm[arg].as(); - else if(s->isNull()) - s->Integer() = defaultValue; - }; - - setSettingBool("session/onlyai", "onlyAI"); - if(vm.count("headless")) - { - session["headless"].Bool() = true; - session["onlyai"].Bool() = true; - } - else if(vm.count("spectate")) - { - session["spectate"].Bool() = true; - session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); - session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); - session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); - if(vm.count("spectate-hero-speed")) - session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); - if(vm.count("spectate-battle-speed")) - session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); - } - // Server settings - setSettingBool("session/donotstartserver", "donotstartserver"); - - // Shared memory options - setSettingBool("session/disable-shm", "disable-shm"); - setSettingBool("session/enable-shm-uuid", "enable-shm-uuid"); - - // Init special testing settings - setSettingInteger("session/serverport", "serverport", 0); - setSettingInteger("general/saveFrequency", "savefrequency", 1); - - // Initialize logging based on settings - logConfig->configure(); - logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); - - // Some basic data validation to produce better error messages in cases of incorrect install - auto testFile = [](std::string filename, std::string message) - { - if (!CResourceHandler::get()->existsResource(ResourceID(filename))) - handleFatalError(message, false); - }; - - testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!"); - testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); - testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them."); - testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them."); - testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); - - srand ( (unsigned int)time(nullptr) ); - - if(!settings["session"]["headless"].Bool()) - GH.init(); - - CCS = new CClientState(); - CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) - CSH = new CServerHandler(); - - // Initialize video -#ifdef DISABLE_VIDEO - CCS->videoh = new CEmptyVideoPlayer(); -#else - if (!settings["session"]["headless"].Bool() && !vm.count("disable-video")) - CCS->videoh = new CVideoPlayer(); - else - CCS->videoh = new CEmptyVideoPlayer(); -#endif - - logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); - - if(!settings["session"]["headless"].Bool()) - { - //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()); - } - -#ifndef VCMI_NO_THREADED_LOAD - //we can properly play intro only in the main thread, so we have to move loading to the separate thread - boost::thread loading(init); -#else - 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 - { - CAndroidVMHelper vmHelper; - vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress"); - #endif // ANDROID - loading.join(); - #ifdef VCMI_ANDROID - vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress"); - } - #endif // ANDROID -#endif // THREADED - - if(!settings["session"]["headless"].Bool()) - { - pomtime.getDiff(); - graphics = new Graphics(); // should be before curh - - CCS->curh = new CursorHandler(); - logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); - - CMessage::init(); - logGlobal->info("Message handler: %d ms", pomtime.getDiff()); - - CCS->curh->show(); - } - - logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff()); - - session["autoSkip"].Bool() = vm.count("autoSkip"); - session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); - session["aiSolo"].Bool() = false; - std::shared_ptr mmenu; - - if(vm.count("testmap")) - { - session["testmap"].String() = vm["testmap"].as(); - session["onlyai"].Bool() = true; - boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); - } - else if(vm.count("testsave")) - { - session["testsave"].String() = vm["testsave"].as(); - session["onlyai"].Bool() = true; - boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); - } - else - { - mmenu = CMainMenu::create(); - GH.curInt = mmenu.get(); - } - - std::vector names; - session["lobby"].Bool() = false; - if(vm.count("lobby")) - { - session["lobby"].Bool() = true; - session["host"].Bool() = false; - session["address"].String() = vm["lobby-address"].as(); - if(vm.count("lobby-username")) - session["username"].String() = vm["lobby-username"].as(); - else - session["username"].String() = settings["launcher"]["lobbyUsername"].String(); - if(vm.count("lobby-gamemode")) - session["gamemode"].Integer() = vm["lobby-gamemode"].as(); - else - session["gamemode"].Integer() = 0; - CSH->uuid = vm["uuid"].as(); - session["port"].Integer() = vm["lobby-port"].as(); - logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); - if(vm.count("lobby-host")) - { - session["host"].Bool() = true; - session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); - session["hostUuid"].String() = vm["lobby-uuid"].as(); - logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); - } - - //we should not reconnect to previous game in online mode - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - - //start lobby immediately - names.push_back(session["username"].String()); - ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; - mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); - } - - // Restore remote session - start game immediately - if(settings["server"]["reconnect"].Bool()) - { - CSH->restoreLastSession(); - } - - if(!settings["session"]["headless"].Bool()) - { - mainLoop(); - } - else - { - while(true) - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); - } - - return 0; -} - -//plays intro, ends when intro is over or button has been pressed (handles events) -void playIntro() -{ - if(CCS->videoh->openAndPlayVideo("3DOLOGO.SMK", 0, 1, true, true)) - { - if (CCS->videoh->openAndPlayVideo("NWCLOGO.SMK", 0, 1, true, true)) - CCS->videoh->openAndPlayVideo("H3INTRO.SMK", 0, 1, true, true); - } -} - -static void mainLoop() -{ - inGuiThread.reset(new bool(true)); - - while(1) //main SDL events loop - { - GH.input().fetchEvents(); - CSH->applyPacksOnLobbyScreen(); - GH.renderFrame(); - } -} - -static void quitApplication() -{ - if(!settings["session"]["headless"].Bool()) - { - if(CSH->client) - CSH->endGameplay(); - } - - GH.windows().clear(); - - CMM.reset(); - - if(!settings["session"]["headless"].Bool()) - { - // cleanup, mostly to remove false leaks from analyzer - if(CCS) - { - CCS->musich->release(); - CCS->soundh->release(); - - vstd::clear_pointer(CCS); - } - CMessage::dispose(); - - vstd::clear_pointer(graphics); - } - - vstd::clear_pointer(VLC); - - vstd::clear_pointer(console);// should be removed after everything else since used by logging - - boost::this_thread::sleep(boost::posix_time::milliseconds(750));//??? - - if(!settings["session"]["headless"].Bool()) - GH.screenHandler().close(); - - if(logConfig != nullptr) - { - logConfig->deconfigure(); - delete logConfig; - logConfig = nullptr; - } - - std::cout << "Ending...\n"; - exit(0); -} - -void handleQuit(bool ask) -{ - if(CSH->client && LOCPLINT && ask) - { - CCS->curh->set(Cursor::Map::POINTER); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); - } - else - { - quitApplication(); - } -} - -void handleFatalError(const std::string & message, bool terminate) -{ - logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); - logGlobal->error("Reason: %s", message); - - std::string messageToShow = "Fatal error! " + message; - - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr); - - if (terminate) - throw std::runtime_error(message); - else - exit(1); -} +/* + * CMT.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 + * + */ + +// CMT.cpp : Defines the entry point for the console application. +#include "StdInc.h" +#include "CMT.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 "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/filesystem/Filesystem.h" + +#include "../lib/logging/CBasicLogConfigurator.h" + +#include +#include + +#include +#include + +#ifdef VCMI_ANDROID +#include "../lib/CAndroidVMHelper.h" +#include +#endif + +#if __MINGW32__ +#undef main +#endif + +namespace po = boost::program_options; +namespace po_style = boost::program_options::command_line_style; + +static po::variables_map vm; + +#ifndef VCMI_IOS +void processCommand(const std::string &message); +#endif +void playIntro(); +static void mainLoop(); + +static CBasicLogConfigurator *logConfig; + +void init() +{ + CStopWatch tmh; + + loadDLLClasses(); + const_cast(CGI)->setFromLib(); + + logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); + + // Debug code to load all maps on start + //ClientCommandManager commandController; + //commandController.processCommand("convert txt", false); +} + +static void prog_version() +{ + printf("%s\n", GameConstants::VCMI_VERSION.c_str()); + std::cout << VCMIDirs::get().genHelpString(); +} + +static void prog_help(const po::options_description &opts) +{ + auto time = std::time(nullptr); + printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); + printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); + printf("This is free software; see the source for copying conditions. There is NO\n"); + printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); + printf("\n"); + std::cout << opts; +} + +#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE) +int wmain(int argc, wchar_t* argv[]) +#elif defined(VCMI_MOBILE) +int SDL_main(int argc, char *argv[]) +#else +int main(int argc, char * argv[]) +#endif +{ +#ifdef VCMI_ANDROID + CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv()); + // boost will crash without this + setenv("LANG", "C", 1); +#endif + +#if !defined(VCMI_MOBILE) + // Correct working dir executable folder (not bundle folder) so we can use executable relative paths + boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); +#endif + std::cout << "Starting... " << std::endl; + po::options_description opts("Allowed options"); + opts.add_options() + ("help,h", "display help and exit") + ("version,v", "display version information and exit") + ("testmap", po::value(), "") + ("testsave", po::value(), "") + ("spectate,s", "enable spectator interface for AI-only games") + ("spectate-ignore-hero", "wont follow heroes on adventure map") + ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") + ("spectate-battle-speed", po::value(), "battle animation speed for spectator") + ("spectate-skip-battle", "skip battles in spectator view") + ("spectate-skip-battle-result", "skip battle result window") + ("onlyAI", "allow one to run without human player, all players will be default AI") + ("headless", "runs without GUI, implies --onlyAI") + ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") + ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") + ("autoSkip", "automatically skip turns in GUI") + ("disable-video", "disable video player") + ("nointro,i", "skips intro movies") + ("donotstartserver,d","do not attempt to start server and just connect to it instead server") + ("serverport", po::value(), "override port specified in config file") + ("savefrequency", po::value(), "limit auto save creation to each N days") + ("lobby", "parameters address, port, uuid to connect ro remote lobby session") + ("lobby-address", po::value(), "address to remote lobby") + ("lobby-port", po::value(), "port to remote lobby") + ("lobby-host", "if this client hosts session") + ("lobby-uuid", po::value(), "uuid to the server") + ("lobby-connections", po::value(), "connections of server") + ("lobby-username", po::value(), "player name") + ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") + ("uuid", po::value(), "uuid for the client"); + + if(argc > 1) + { + try + { + po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); + } + catch(boost::program_options::error &e) + { + std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; + } + } + + po::notify(vm); + if(vm.count("help")) + { + prog_help(opts); +#ifdef VCMI_IOS + exit(0); +#else + return 0; +#endif + } + if(vm.count("version")) + { + prog_version(); +#ifdef VCMI_IOS + exit(0); +#else + return 0; +#endif + } + + // Init old logging system and new (temporary) logging system + CStopWatch total, pomtime; + std::cout.flags(std::ios::unitbuf); +#ifndef VCMI_IOS + console = new CConsoleHandler(); + + auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole) + { + ClientCommandManager commandController; + commandController.processCommand(buffer, calledFromIngameConsole); + }; + + *console->cb = callbackFunction; + console->start(); +#endif + + const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; + logConfig = new CBasicLogConfigurator(logPath, console); + logConfig->configureDefault(); + logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION); + logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); + logGlobal->info("The log file will be saved to %s", logPath); + + // Init filesystem and settings + preinitDLL(::console); + + Settings session = settings.write["session"]; + auto setSettingBool = [](std::string key, std::string arg) { + Settings s = settings.write(vstd::split(key, "/")); + if(::vm.count(arg)) + s->Bool() = true; + else if(s->isNull()) + s->Bool() = false; + }; + auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { + Settings s = settings.write(vstd::split(key, "/")); + if(::vm.count(arg)) + s->Integer() = ::vm[arg].as(); + else if(s->isNull()) + s->Integer() = defaultValue; + }; + + setSettingBool("session/onlyai", "onlyAI"); + if(vm.count("headless")) + { + session["headless"].Bool() = true; + session["onlyai"].Bool() = true; + } + else if(vm.count("spectate")) + { + session["spectate"].Bool() = true; + session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); + session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); + session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); + if(vm.count("spectate-hero-speed")) + session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); + if(vm.count("spectate-battle-speed")) + session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); + } + // Server settings + setSettingBool("session/donotstartserver", "donotstartserver"); + + // Init special testing settings + setSettingInteger("session/serverport", "serverport", 0); + setSettingInteger("general/saveFrequency", "savefrequency", 1); + + // Initialize logging based on settings + logConfig->configure(); + logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); + + // Some basic data validation to produce better error messages in cases of incorrect install + auto testFile = [](std::string filename, std::string message) + { + if (!CResourceHandler::get()->existsResource(ResourcePath(filename))) + handleFatalError(message, false); + }; + + testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!"); + testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); + testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them."); + testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them."); + testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); + + srand ( (unsigned int)time(nullptr) ); + + if(!settings["session"]["headless"].Bool()) + GH.init(); + + CCS = new CClientState(); + CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) + CSH = new CServerHandler(); + + // Initialize video +#ifdef DISABLE_VIDEO + CCS->videoh = new CEmptyVideoPlayer(); +#else + if (!settings["session"]["headless"].Bool() && !vm.count("disable-video")) + CCS->videoh = new CVideoPlayer(); + else + CCS->videoh = new CEmptyVideoPlayer(); +#endif + + logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); + + if(!settings["session"]["headless"].Bool()) + { + //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()); + } + +#ifndef VCMI_NO_THREADED_LOAD + //we can properly play intro only in the main thread, so we have to move loading to the separate thread + boost::thread loading([]() + { + setThreadName("initialize"); + init(); + }); +#else + 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 + { + CAndroidVMHelper vmHelper; + vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress"); + #endif // ANDROID + loading.join(); + #ifdef VCMI_ANDROID + vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress"); + } + #endif // ANDROID +#endif // THREADED + + if(!settings["session"]["headless"].Bool()) + { + pomtime.getDiff(); + graphics = new Graphics(); // should be before curh + + CCS->curh = new CursorHandler(); + logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); + + CMessage::init(); + logGlobal->info("Message handler: %d ms", pomtime.getDiff()); + + CCS->curh->show(); + } + + logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff()); + + session["autoSkip"].Bool() = vm.count("autoSkip"); + session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); + session["aiSolo"].Bool() = false; + + if(vm.count("testmap")) + { + session["testmap"].String() = vm["testmap"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); + } + else if(vm.count("testsave")) + { + session["testsave"].String() = vm["testsave"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); + } + else + { + auto mmenu = CMainMenu::create(); + GH.curInt = mmenu.get(); + } + + std::vector names; + session["lobby"].Bool() = false; + if(vm.count("lobby")) + { + session["lobby"].Bool() = true; + session["host"].Bool() = false; + session["address"].String() = vm["lobby-address"].as(); + if(vm.count("lobby-username")) + session["username"].String() = vm["lobby-username"].as(); + else + session["username"].String() = settings["launcher"]["lobbyUsername"].String(); + if(vm.count("lobby-gamemode")) + session["gamemode"].Integer() = vm["lobby-gamemode"].as(); + else + session["gamemode"].Integer() = 0; + CSH->uuid = vm["uuid"].as(); + session["port"].Integer() = vm["lobby-port"].as(); + logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); + if(vm.count("lobby-host")) + { + session["host"].Bool() = true; + session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); + session["hostUuid"].String() = vm["lobby-uuid"].as(); + logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); + } + + //we should not reconnect to previous game in online mode + Settings saveSession = settings.write["server"]["reconnect"]; + saveSession->Bool() = false; + + //start lobby immediately + names.push_back(session["username"].String()); + ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; + CMM->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); + } + + // Restore remote session - start game immediately + if(settings["server"]["reconnect"].Bool()) + { + CSH->restoreLastSession(); + } + + if(!settings["session"]["headless"].Bool()) + { + mainLoop(); + } + else + { + while(true) + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + } + + 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, true, true)) + { + audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); + sound = CCS->soundh->playSound(audioData); + if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) + { + audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); + sound = CCS->soundh->playSound(audioData); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); + } + } + CCS->soundh->stopSound(sound); +} + +static void mainLoop() +{ + setThreadName("MainGUI"); + + while(1) //main SDL events loop + { + GH.input().fetchEvents(); + CSH->applyPacksOnLobbyScreen(); + GH.renderFrame(); + } +} + +static void quitApplication() +{ + if(!settings["session"]["headless"].Bool()) + { + if(CSH->client) + CSH->endGameplay(); + } + + GH.windows().clear(); + + CMM.reset(); + + if(!settings["session"]["headless"].Bool()) + { + // 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; + delete CCS->musich; + delete CCS->soundh; + + vstd::clear_pointer(CCS); + } + CMessage::dispose(); + + vstd::clear_pointer(graphics); + } + + vstd::clear_pointer(CSH); + vstd::clear_pointer(VLC); + + vstd::clear_pointer(console);// should be removed after everything else since used by logging + + if(!settings["session"]["headless"].Bool()) + GH.screenHandler().close(); + + if(logConfig != nullptr) + { + logConfig->deconfigure(); + delete logConfig; + logConfig = nullptr; + } + + std::cout << "Ending...\n"; + + // this method is always called from event/network threads, which keep interface mutex locked + // unlock it here to avoid assertion failure on GH destruction in exit() + GH.interfaceMutex.unlock(); + exit(0); +} + +void handleQuit(bool ask) +{ + if(ask) + { + CCS->curh->set(Cursor::Map::POINTER); + + if (LOCPLINT) + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + else + CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1)); + } + else + { + quitApplication(); + } +} + +void handleFatalError(const std::string & message, bool terminate) +{ + logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); + logGlobal->error("Reason: %s", message); + + std::string messageToShow = "Fatal error! " + message; + + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr); + + if (terminate) + throw std::runtime_error(message); + else + exit(1); +} diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 81debb398..13ba15d49 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -12,6 +12,7 @@ set(client_SRCS adventureMap/CMinimap.cpp adventureMap/CResDataBar.cpp adventureMap/MapAudioPlayer.cpp + adventureMap/TurnTimerWidget.cpp battle/BattleActionsController.cpp battle/BattleAnimationClasses.cpp @@ -50,7 +51,9 @@ set(client_SRCS lobby/CSavingScreen.cpp lobby/CScenarioInfoScreen.cpp lobby/CSelectionBase.cpp + lobby/TurnOptionsTab.cpp lobby/OptionsTab.cpp + lobby/OptionsTabBase.cpp lobby/RandomMapTab.cpp lobby/SelectionTab.cpp @@ -58,6 +61,7 @@ set(client_SRCS mainmenu/CMainMenu.cpp mainmenu/CPrologEpilogVideo.cpp mainmenu/CreditsScreen.cpp + mainmenu/CHighScoreScreen.cpp mapView/MapRenderer.cpp mapView/MapRendererContext.cpp @@ -83,6 +87,7 @@ set(client_SRCS renderSDL/CTrueTypeFont.cpp renderSDL/CursorHardware.cpp renderSDL/CursorSoftware.cpp + renderSDL/RenderHandler.cpp renderSDL/SDLImage.cpp renderSDL/SDLImageLoader.cpp renderSDL/SDLRWwrapper.cpp @@ -90,10 +95,14 @@ set(client_SRCS renderSDL/SDL_Extensions.cpp widgets/Buttons.cpp + widgets/CAltar.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp + widgets/CExchangeController.cpp widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp + widgets/ComboBox.cpp + widgets/CTradeBase.cpp widgets/Images.cpp widgets/MiscWidgets.cpp widgets/ObjectLists.cpp @@ -109,15 +118,19 @@ set(client_SRCS widgets/CWindowWithArtifacts.cpp widgets/RadialMenu.cpp + windows/CAltarWindow.cpp windows/CCastleInterface.cpp windows/CCreatureWindow.cpp + windows/CHeroOverview.cpp windows/CHeroWindow.cpp windows/CKingdomInterface.cpp + windows/CMapOverview.cpp windows/CMessage.cpp windows/CPuzzleWindow.cpp windows/CQuestLog.cpp windows/CSpellWindow.cpp windows/CTradeWindow.cpp + windows/CTutorialWindow.cpp windows/CWindowObject.cpp windows/CreaturePurchaseCard.cpp windows/GUIClasses.cpp @@ -139,6 +152,7 @@ set(client_SRCS CVideoHandler.cpp Client.cpp ClientCommandManager.cpp + HeroMovementController.cpp NetPacksClient.cpp NetPacksLobbyClient.cpp ) @@ -157,6 +171,7 @@ set(client_HEADERS adventureMap/CMinimap.h adventureMap/CResDataBar.h adventureMap/MapAudioPlayer.h + adventureMap/TurnTimerWidget.h battle/BattleActionsController.h battle/BattleAnimationClasses.h @@ -199,7 +214,9 @@ set(client_HEADERS lobby/CSavingScreen.h lobby/CScenarioInfoScreen.h lobby/CSelectionBase.h + lobby/TurnOptionsTab.h lobby/OptionsTab.h + lobby/OptionsTabBase.h lobby/RandomMapTab.h lobby/SelectionTab.h @@ -207,6 +224,7 @@ set(client_HEADERS mainmenu/CMainMenu.h mainmenu/CPrologEpilogVideo.h mainmenu/CreditsScreen.h + mainmenu/CHighScoreScreen.h mapView/IMapRendererContext.h mapView/IMapRendererObserver.h @@ -226,11 +244,13 @@ set(client_HEADERS render/Canvas.h render/ColorFilter.h render/Colors.h + render/EFont.h render/Graphics.h render/ICursor.h render/IFont.h render/IImage.h render/IImageLoader.h + render/IRenderHandler.h render/IScreenHandler.h renderSDL/CBitmapFont.h @@ -238,6 +258,7 @@ set(client_HEADERS renderSDL/CTrueTypeFont.h renderSDL/CursorHardware.h renderSDL/CursorSoftware.h + renderSDL/RenderHandler.h renderSDL/SDLImage.h renderSDL/SDLImageLoader.h renderSDL/SDLRWwrapper.h @@ -246,10 +267,14 @@ set(client_HEADERS renderSDL/SDL_PixelAccess.h widgets/Buttons.h + widgets/CAltar.h widgets/CArtifactHolder.h widgets/CComponent.h + widgets/CExchangeController.h widgets/CGarrisonInt.h widgets/CreatureCostBox.h + widgets/ComboBox.h + widgets/CTradeBase.h widgets/Images.h widgets/MiscWidgets.h widgets/ObjectLists.h @@ -265,15 +290,19 @@ set(client_HEADERS widgets/CWindowWithArtifacts.h widgets/RadialMenu.h + windows/CAltarWindow.h windows/CCastleInterface.h windows/CCreatureWindow.h + windows/CHeroOverview.h windows/CHeroWindow.h windows/CKingdomInterface.h windows/CMessage.h + windows/CMapOverview.h windows/CPuzzleWindow.h windows/CQuestLog.h windows/CSpellWindow.h windows/CTradeWindow.h + windows/CTutorialWindow.h windows/CWindowObject.h windows/CreaturePurchaseCard.h windows/GUIClasses.h @@ -296,6 +325,7 @@ set(client_HEADERS Client.h ClientCommandManager.h ClientNetPackVisitors.h + HeroMovementController.h LobbyClientNetPackVisitors.h resource.h ) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 521d47eb1..b6101c7cb 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -1,668 +1,718 @@ -/* - * 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 "gui/CGuiHandler.h" - -#include "../lib/JsonNode.h" -#include "../lib/GameConstants.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/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 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(JsonNode(ResourceID("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(std::string &sound, bool cache) -{ - try - { - if (cache && soundChunks.find(sound) != soundChunks.end()) - return soundChunks[sound].first; - - auto data = CResourceHandler::get()->load(ResourceID(std::string("SOUNDS/") + sound, EResType::SOUND))->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(std::pair(sound, std::make_pair (chunk, std::move (data.first)))); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound %s chunk: %s", sound, 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(std::string soundId) -{ - stopSound(ambientChannels[soundId]); - setChannelVolume(ambientChannels[soundId], volume); -} - -// 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 = sounds[soundID]; - logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound); - - return playSound(sound, repeats, true); -} - -int CSoundHandler::playSound(std::string 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, Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - else - channel = -1; - - 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::unique_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::soundFinishedCallback(int channel) -{ - boost::unique_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::unique_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel] = {}; -} - -void CSoundHandler::initCallback(int channel, const std::function & function) -{ - boost::unique_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 std::string & 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 std::string & 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 ResourceID & 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 ResourceID & file : mp3files) - { - if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) - addEntryToSet("battle", file.getName()); - else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) - addEntryToSet("enemy-turn", file.getName()); - } - -} - -void CMusicHandler::loadTerrainMusicThemes() -{ - for (const auto & terrain : CGI->terrainTypeHandler->objects) - { - addEntryToSet("terrain_" + terrain->getJsonKey(), "Music/" + terrain->musicFilename); - } -} - -void CMusicHandler::addEntryToSet(const std::string & set, const std::string & 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 std::string & 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, "", 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 std::string & 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, std::string 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); - if (music) - Mix_FreeMusic(music); -} - -void MusicEntry::load(std::string musicURI) -{ - if (music) - { - logGlobal->trace("Del-ing music file %s", currentName); - Mix_FreeMusic(music); - music = nullptr; - } - - currentName = musicURI; - music = nullptr; - - logGlobal->trace("Loading music file %s", musicURI); - - try - { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::SOUND))); - music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); - } - catch(std::exception &e) - { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, musicURI); - logGlobal->error("Exception: %s", e.what()); - } - - if(!music) - { - logGlobal->warn("Warning: Cannot open %s: %s", currentName, 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); - - 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 = SDL_GetTicks(); - playing = true; - return true; -} - -bool MusicEntry::stop(int fade_ms) -{ - if (Mix_PlayingMusic()) - { - playing = false; - loop = 0; - uint32_t endTime = SDL_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, 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(std::string track) -{ - return setName.empty() && track == currentName; -} +/* + * 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/JsonNode.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 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); +} + +// 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::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 index a0180cd74..f0b653e7c 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -1,168 +1,168 @@ -/* - * 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 - -public: - CAudioBase(): initialized(false), volume(0) {}; - virtual void init() = 0; - virtual void release() = 0; - - virtual void setVolume(ui32 percent); - ui32 getVolume() const { return volume; }; -}; - -class CSoundHandler: public CAudioBase -{ -private: - //soundBase::soundID getSoundID(const std::string &fileName); - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - using CachedChunk = std::pair>; - std::map soundChunks; - - Mix_Chunk *GetSoundChunk(std::string &sound, 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(std::string 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 - int playSound(soundBase::soundID soundID, int repeats=0); - int playSound(std::string sound, int repeats=0, bool cache=false); - int playSoundFromSet(std::vector &sound_vec); - void stopSound(int handler); - - void setCallback(int channel, std::function function); - void soundFinishedCallback(int channel); - - int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); - void ambientStopAllChannels(); - - // Sets - std::vector battleIntroSounds; -}; - -// Helper //now it looks somewhat useless -#define battle_sound(creature,what_sound) creature->sounds.what_sound - -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; - std::string currentName; - - void load(std::string musicURI); - -public: - MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart); - ~MusicEntry(); - - bool isSet(std::string setName); - bool isTrack(std::string trackName); - bool isPlaying(); - - bool play(); - bool stop(int fade_ms=0); -}; - -class CMusicHandler: 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 std::string & 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 std::string & 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 std::string & 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; -}; +/* + * 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 + 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 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 824b97b07..722048dd5 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1,2110 +1,1885 @@ -/* - * CPlayerInterface.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 "CPlayerInterface.h" - -#include - -#include "adventureMap/AdventureMapInterface.h" -#include "mapView/mapHandler.h" -#include "adventureMap/CList.h" -#include "battle/BattleInterface.h" -#include "battle/BattleEffectsController.h" -#include "battle/BattleFieldController.h" -#include "battle/BattleInterfaceClasses.h" -#include "battle/BattleWindow.h" -#include "../CCallback.h" -#include "windows/CCastleInterface.h" -#include "eventsSDL/InputHandler.h" -#include "mainmenu/CMainMenu.h" -#include "gui/CursorHandler.h" -#include "windows/CKingdomInterface.h" -#include "CGameInfo.h" -#include "PlayerLocalState.h" -#include "CMT.h" -#include "windows/CHeroWindow.h" -#include "windows/CCreatureWindow.h" -#include "windows/CQuestLog.h" -#include "windows/CPuzzleWindow.h" -#include "widgets/CComponent.h" -#include "widgets/CGarrisonInt.h" -#include "widgets/Buttons.h" -#include "windows/CTradeWindow.h" -#include "windows/CSpellWindow.h" -#include "../lib/CConfigHandler.h" -#include "windows/GUIClasses.h" -#include "render/CAnimation.h" -#include "render/IImage.h" -#include "../lib/CArtHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/bonuses/CBonusSystemNode.h" -#include "../lib/bonuses/Limiters.h" -#include "../lib/bonuses/Updaters.h" -#include "../lib/bonuses/Propagators.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/serializer/BinarySerializer.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/mapObjects/MiscObjects.h" -#include "../lib/mapObjects/ObjectTemplate.h" -#include "../lib/mapping/CMapHeader.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "../lib/CStack.h" -#include "../lib/JsonNode.h" -#include "CMusicHandler.h" -#include "../lib/CondSh.h" -#include "../lib/NetPacksBase.h" -#include "../lib/NetPacks.h"//todo: remove -#include "../lib/VCMIDirs.h" -#include "../lib/CStopWatch.h" -#include "../lib/StartInfo.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameConstants.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "windows/InfoWindows.h" -#include "../lib/UnlockGuard.h" -#include "../lib/RoadHandler.h" -#include "../lib/TerrainHandler.h" -#include "CServerHandler.h" -// FIXME: only needed for CGameState::mutex -#include "../lib/gameState/CGameState.h" -#include "eventsSDL/NotificationHandler.h" -#include "adventureMap/CInGameConsole.h" - -// The macro below is used to mark functions that are called by client when game state changes. -// They all assume that CPlayerInterface::pim mutex is locked. -#define EVENT_HANDLER_CALLED_BY_CLIENT - -// The macro marks functions that are run on a new thread by client. -// They do not own any mutexes intiially. -#define THREAD_CREATED_BY_CLIENT - -#define RETURN_IF_QUICK_COMBAT \ - if (isAutoFightOn && !battleInt) \ - return; - -#define BATTLE_EVENT_POSSIBLE_RETURN\ - if (LOCPLINT != this) \ - return; \ - RETURN_IF_QUICK_COMBAT - -boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex; - -CPlayerInterface * LOCPLINT; - -std::shared_ptr CPlayerInterface::battleInt; - -enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; -CondSh stillMoveHero(STOP_MOVE); //used during hero movement - -struct HeroObjectRetriever -{ - const CGHeroInstance * operator()(const ConstTransitivePtr &h) const - { - return h; - } - const CGHeroInstance * operator()(const ConstTransitivePtr &s) const - { - return nullptr; - } -}; - -CPlayerInterface::CPlayerInterface(PlayerColor Player): - localState(std::make_unique(*this)) -{ - logGlobal->trace("\tHuman player interface for player %s being constructed", Player.getStr()); - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - GH.defActionsDef = 0; - LOCPLINT = this; - curAction = nullptr; - playerID=Player; - human=true; - battleInt = nullptr; - castleInt = nullptr; - makingTurn = false; - showingDialog = new CondSh(false); - cingconsole = new CInGameConsole(); - GH.terminate_cond->set(false); - firstCall = 1; //if loading will be overwritten in serialize - autosaveCount = 0; - isAutoFightOn = false; - - duringMovement = false; - ignoreEvents = false; - numOfMovedArts = 0; -} - -CPlayerInterface::~CPlayerInterface() -{ - logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr()); - delete showingDialog; - delete cingconsole; - if (LOCPLINT == this) - LOCPLINT = nullptr; -} -void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - cb = CB; - env = ENV; - - CCS->musich->loadTerrainMusicThemes(); - - initializeHeroTownList(); - - // always recreate advmap interface to avoid possible memory-corruption bugs - adventureInt.reset(new AdventureMapInterface()); -} - -void CPlayerInterface::playerStartsTurn(PlayerColor player) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(GH.windows().findWindows().empty()) - { - // after map load - remove all active windows and replace them with adventure map - GH.windows().clear(); - GH.windows().pushWindow(adventureInt); - } - - // remove all dialogs that do not expect query answer - while (!GH.windows().topWindow() && !GH.windows().topWindow()) - GH.windows().popWindows(1); - - if (player != playerID && LOCPLINT == this) - { - waitWhileDialog(); - - bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman(); - - adventureInt->onEnemyTurnStarted(player, isHuman); - } -} - -void CPlayerInterface::performAutosave() -{ - int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); - if(frequency > 0 && cb->getDate() % frequency == 0) - { - bool usePrefix = settings["general"]["useSavePrefix"].Bool(); - std::string prefix = std::string(); - - if(usePrefix) - { - prefix = settings["general"]["savePrefix"].String(); - if(prefix.empty()) - { - prefix = cb->getMapHeader()->name.substr(0, 5) + "_"; - } - } - - autosaveCount++; - - int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); - if(autosaveCountLimit > 0) - { - cb->save("Saves/" + prefix + "Autosave_" + std::to_string(autosaveCount)); - autosaveCount %= autosaveCountLimit; - } - else - { - std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH)) - + std::to_string(cb->getDate(Date::WEEK)) - + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - - cb->save("Saves/" + prefix + "Autosave_" + stringifiedDate); - } - } -} - -void CPlayerInterface::yourTurn() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - { - LOCPLINT = this; - GH.curInt = this; - - NotificationHandler::notify("Your turn"); - if(settings["general"]["startTurnAutosave"].Bool()) - { - performAutosave(); - } - - if (CSH->howManyPlayerInterfaces() > 1) //hot seat message - { - adventureInt->onHotseatWaitStarted(playerID); - - makingTurn = true; - std::string msg = CGI->generaltexth->allTexts[13]; - boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); - std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); - showInfoDialog(msg, cmp); - } - else - { - makingTurn = true; - adventureInt->onPlayerTurnStarted(playerID); - } - } - acceptTurn(); -} - -void CPlayerInterface::acceptTurn() -{ - if (settings["session"]["autoSkip"].Bool()) - { - while(auto iw = GH.windows().topWindow()) - iw->close(); - } - - if(CSH->howManyPlayerInterfaces() > 1) - { - waitWhileDialog(); // wait for player to accept turn in hot-seat mode - - adventureInt->onPlayerTurnStarted(playerID); - } - - // warn player if he has no town - if (cb->howManyTowns() == 0) - { - auto playerColor = *cb->getPlayerID(); - - std::vector components; - components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0); - MetaString text; - - const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; - - if(optDaysWithoutCastle) - { - auto daysWithoutCastle = optDaysWithoutCastle.value(); - if (daysWithoutCastle < 6) - { - text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); - text.replaceNumber(7 - daysWithoutCastle); - } - else if (daysWithoutCastle == 6) - { - text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); - } - - showInfoDialogAndWait(components, text); - } - else - logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); - } -} - -void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - if(LOCPLINT != this) - return; - - //FIXME: read once and store - if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) - return; - - const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero - - if (!hero) - return; - - if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) - { - if(hero->getRemovalSound() && hero->tempOwner == playerID) - CCS->soundh->playSound(hero->getRemovalSound().value()); - } - - std::unordered_set changedTiles { - hero->convertToVisitablePos(details.start), - hero->convertToVisitablePos(details.end) - }; - adventureInt->onMapTilesChanged(changedTiles); - adventureInt->onHeroMovementStarted(hero); - - bool directlyAttackingCreature = details.attackedFrom && localState->hasPath(hero) && localState->getPath(hero).endPos() == *details.attackedFrom; - - if(makingTurn && hero->tempOwner == playerID) //we are moving our hero - we may need to update assigned path - { - if(details.result == TryMoveHero::TELEPORTATION) - { - if(localState->hasPath(hero)) - { - assert(localState->getPath(hero).nodes.size() >= 2); - auto nodesIt = localState->getPath(hero).nodes.end() - 1; - - if((nodesIt)->coord == hero->convertToVisitablePos(details.start) - && (nodesIt - 1)->coord == hero->convertToVisitablePos(details.end)) - { - //path was between entrance and exit of teleport -> OK, erase node as usual - localState->removeLastNode(hero); - } - else - { - //teleport was not along current path, it'll now be invalid (hero is somewhere else) - localState->erasePath(hero); - - } - } - } - - if(hero->pos != details.end //hero didn't change tile but visit succeeded - || directlyAttackingCreature) // or creature was attacked from endangering tile. - { - localState->erasePath(hero); - } - else if(localState->hasPath(hero) && hero->pos == details.end) //&& hero is moving - { - if(details.start != details.end) //so we don't touch path when revisiting with spacebar - localState->removeLastNode(hero); - } - } - - if(details.stopMovement()) //hero failed to move - { - stillMoveHero.setn(STOP_MOVE); - adventureInt->onHeroChanged(hero); - return; - } - - CGI->mh->waitForOngoingAnimations(); - - //move finished - adventureInt->onHeroChanged(hero); - - //check if user cancelled movement - { - if (GH.input().ignoreEventsUntilInput()) - stillMoveHero.setn(STOP_MOVE); - } - - if (stillMoveHero.get() == WAITING_MOVE) - stillMoveHero.setn(DURING_MOVE); - - // Hero attacked creature directly, set direction to face it. - if (directlyAttackingCreature) { - // Get direction to attacker. - int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); - static const ui8 dirLookup[3][3] = { - { 1, 2, 3 }, - { 8, 0, 4 }, - { 7, 6, 5 } - }; - // FIXME: Avoid const_cast, make moveDir mutable in some other way? - const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; - } -} - -void CPlayerInterface::heroKilled(const CGHeroInstance* hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID); - - // if hero is not in town garrison - if (vstd::contains(localState->getWanderingHeroes(), hero)) - localState->removeWanderingHero(hero); - - adventureInt->onHeroChanged(hero); - localState->erasePath(hero); -} - -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()); - } -} - -void CPlayerInterface::heroCreated(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->addWanderingHero(hero); - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::openTownWindow(const CGTownInstance * town) -{ - if(castleInt) - castleInt->close(); - castleInt = nullptr; - - auto newCastleInt = std::make_shared(town); - - GH.windows().pushWindow(newCastleInt); -} - -void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (which == 4) - { - for (auto ctw : GH.windows().findWindows()) - ctw->setExpToLevel(); - } - 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(); -} - -void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onHeroChanged(hero); - if (makingTurn && hero->tempOwner == playerID) - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (makingTurn && hero->tempOwner == playerID) - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::receivedResource() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto mw : GH.windows().findWindows()) - mw->resourceChanged(); - - GH.windows().totalRedraw(); -} - -void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector& skills, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->soundh->playSound(soundBase::heroNewLevel); - GH.windows().createAndPushWindow(hero, pskill, skills, [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }); -} - -void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->soundh->playSound(soundBase::heroNewLevel); - GH.windows().createAndPushWindow(commander, skills, [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }); -} - -void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(town->garrisonHero) //wandering hero moved to the garrison - { - // This method also gets called on hero recruitment -> garrisoned hero is already in garrison - if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero)) - localState->removeWanderingHero(town->garrisonHero); - } - - if(town->visitingHero) //hero leaves garrison - { - // This method also gets called on hero recruitment -> wandering heroes already contains new hero - if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero)) - localState->addWanderingHero(town->visitingHero); - } - adventureInt->onHeroChanged(nullptr); - adventureInt->onTownChanged(town); - - if(castleInt) - { - castleInt->garr->selectSlot(nullptr); - castleInt->garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); - castleInt->garr->setArmy(town->visitingHero, EGarrisonType::LOWER); - castleInt->garr->recreateSlots(); - castleInt->heroes->update(); - - // Perform totalRedraw to update hero list on adventure map - GH.windows().totalRedraw(); - } - - for (auto ki : GH.windows().findWindows()) - { - ki->townChanged(town); - ki->updateGarrisons(); - ki->redraw(); - } -} - -void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (hero->tempOwner != playerID ) - return; - - waitWhileDialog(); - openTownWindow(town); -} - -void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) -{ - std::vector instances; - - if(auto obj = cb->getObj(id1)) - instances.push_back(obj); - - - if(id2 != ObjectInstanceID() && id2 != id1) - { - if(auto obj = cb->getObj(id2)) - instances.push_back(obj); - } - - garrisonsChanged(instances); -} - -void CPlayerInterface::garrisonsChanged(std::vector objs) -{ - boost::unique_lock un(*pim); - for (auto object : objs) - { - auto * hero = dynamic_cast(object); - auto * town = dynamic_cast(object); - - if (hero) - { - adventureInt->onHeroChanged(hero); - - if(hero->inTownGarrison) - { - adventureInt->onTownChanged(hero->visitedTown); - } - } - if (town) - adventureInt->onTownChanged(town); - } - - for (auto cgh : GH.windows().findWindows()) - cgh->updateGarrisons(); - - for (auto cmw : GH.windows().findWindows()) - { - if (vstd::contains(objs, cmw->hero)) - cmw->garrisonChanged(); - } - - GH.windows().totalRedraw(); -} - -void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onTownChanged(town); - - if (castleInt) - { - castleInt->townlist->updateElement(town); - - if (castleInt->town == town) - { - switch(what) - { - case 1: - CCS->soundh->playSound(soundBase::newBuilding); - castleInt->addBuilding(buildingID); - break; - case 2: - castleInt->removeBuilding(buildingID); - break; - } - } - - // Perform totalRedraw in order to force redraw of updated town list icon from adventure map - GH.windows().totalRedraw(); - } -} - -void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) -{ - //Don't wait for dialogs when we are non-active hot-seat player - if (LOCPLINT == this) - waitForAllDialogs(); -} - -void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); - bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); - - if ((replayAllowed && useQuickCombat) || forceQuickCombat) - { - autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); - - AutocombatPreferences autocombatPreferences = AutocombatPreferences(); - autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); - - autofightingAI->initBattleInterface(env, cb, autocombatPreferences); - autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false); - isAutoFightOn = true; - cb->registerBattleInterface(autofightingAI); - } - - //Don't wait for dialogs when we are non-active hot-seat player - if (LOCPLINT == this) - waitForAllDialogs(); - - BATTLE_EVENT_POSSIBLE_RETURN; -} - -void CPlayerInterface::battleUnitsChanged(const std::vector & units) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - for(auto & info : units) - { - switch(info.operation) - { - case UnitChanges::EOperation::RESET_STATE: - { - const CStack * stack = cb->battleGetStackByID(info.id ); - - if(!stack) - { - logGlobal->error("Invalid unit ID %d", info.id); - continue; - } - battleInt->stackReset(stack); - } - break; - case UnitChanges::EOperation::REMOVE: - battleInt->stackRemoved(info.id); - break; - case UnitChanges::EOperation::ADD: - { - const CStack * unit = cb->battleGetStackByID(info.id); - if(!unit) - { - logGlobal->error("Invalid unit ID %d", info.id); - continue; - } - battleInt->stackAdded(unit); - } - break; - default: - logGlobal->error("Unknown unit operation %d", (int)info.operation); - break; - } - } -} - -void CPlayerInterface::battleObstaclesChanged(const std::vector & obstacles) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - std::vector> newObstacles; - std::vector removedObstacles; - - for(auto & change : obstacles) - { - if(change.operation == BattleChanges::EOperation::ADD) - { - auto instance = cb->battleGetObstacleByID(change.id); - if(instance) - newObstacles.push_back(instance); - else - logNetwork->error("Invalid obstacle instance %d", change.id); - } - if(change.operation == BattleChanges::EOperation::REMOVE) - removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct - } - - if (!newObstacles.empty()) - battleInt->obstaclePlaced(newObstacles); - - if (!removedObstacles.empty()) - battleInt->obstacleRemoved(removedObstacles); - - battleInt->fieldController->redrawBackgroundWithHexes(); -} - -void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->stackIsCatapulting(ca); -} - -void CPlayerInterface::battleNewRound(int round) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->newRound(round); -} - -void CPlayerInterface::actionStarted(const BattleAction &action) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - curAction = new BattleAction(action); - battleInt->startAction(curAction); -} - -void CPlayerInterface::actionFinished(const BattleAction &action) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->endAction(curAction); - delete curAction; - curAction = nullptr; -} - -void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - logGlobal->trace("Awaiting command for %s", stack->nodeName()); - - assert(!cb->battleIsFinished()); - if (cb->battleIsFinished()) - { - logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); - - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); - return ; - } - - if (autofightingAI) - { - if (isAutoFightOn) - { - //FIXME: we want client rendering to proceed while AI is making actions - // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells - auto unlockPim = vstd::makeUnlockGuard(*pim); - autofightingAI->activeStack(stack); - return; - } - - cb->unregisterBattleInterface(autofightingAI); - autofightingAI.reset(); - } - - assert(battleInt); - if(!battleInt) - { - // probably battle is finished already - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); - } - - { - boost::unique_lock un(*pim); - battleInt->stackActivated(stack); - //Regeneration & mana drain go there - } -} - -void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if(isAutoFightOn || autofightingAI) - { - isAutoFightOn = false; - cb->unregisterBattleInterface(autofightingAI); - autofightingAI.reset(); - - if(!battleInt) - { - bool allowManualReplay = queryID != -1; - - auto wnd = std::make_shared(*br, *this, allowManualReplay); - - if (allowManualReplay) - { - wnd->resultCallback = [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }; - } - GH.windows().pushWindow(wnd); - // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. - // Otherwise NewTurn causes freeze. - waitWhileDialog(); - return; - } - } - - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->battleFinished(*br, queryID); -} - -void CPlayerInterface::battleLogMessage(const std::vector & lines) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->displayBattleLog(lines); -} - -void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->stackMoved(stack, dest, distance, teleport); -} -void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->spellCast(sc); -} -void CPlayerInterface::battleStacksEffectsSet( const SetStackEffect & sse ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->battleStacksEffectsSet(sse); -} -void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - RETURN_IF_QUICK_COMBAT; - battleInt->effectsController->battleTriggerEffect(bte); - - if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN)) - { - const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo)); - battleInt->windowObject->heroManaPointsChanged(manaDrainedHero); - } -} -void CPlayerInterface::battleStacksAttacked(const std::vector & bsa, bool ranged) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - std::vector arg; - for(auto & elem : bsa) - { - const CStack * defender = cb->battleGetStackByID(elem.stackAttacked, false); - const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false); - - assert(defender); - - StackAttackedInfo info; - info.defender = defender; - info.attacker = attacker; - info.damageDealt = elem.damageAmount; - info.amountKilled = elem.killedAmount; - info.spellEffect = SpellID::NONE; - info.indirectAttack = ranged; - info.killed = elem.killed(); - info.rebirth = elem.willRebirth(); - info.cloneKilled = elem.cloneKilled(); - info.fireShield = elem.fireShield(); - - if (elem.isSpell()) - info.spellEffect = elem.spellID; - - arg.push_back(info); - } - battleInt->stacksAreAttacked(arg); -} -void CPlayerInterface::battleAttack(const BattleAttack * ba) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - assert(curAction); - - StackAttackInfo info; - info.attacker = cb->battleGetStackByID(ba->stackAttacking); - info.defender = nullptr; - info.indirectAttack = ba->shot(); - info.lucky = ba->lucky(); - info.unlucky = ba->unlucky(); - info.deathBlow = ba->deathBlow(); - info.lifeDrain = ba->lifeDrain(); - info.tile = ba->tile; - info.spellEffect = SpellID::NONE; - - if (ba->spellLike()) - info.spellEffect = ba->spellID; - - for(auto & elem : ba->bsa) - { - if(!elem.isSecondary()) - { - assert(info.defender == nullptr); - info.defender = cb->battleGetStackByID(elem.stackAttacked); - } - else - { - info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked)); - } - } - assert(info.defender != nullptr); - assert(info.attacker != nullptr); - - battleInt->stackAttacking(info); -} - -void CPlayerInterface::battleGateStateChanged(const EGateState state) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->gateStateChanged(state); -} - -void CPlayerInterface::yourTacticPhase(int distance) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; -} - -void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector & components, int soundID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO; - auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod - - if(autoTryHover || type == EInfoWindowMode::INFO) - { - waitWhileDialog(); //Fix for mantis #98 - adventureInt->showInfoBoxMessage(components, text, timer); - - if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) - CCS->soundh->playSound(static_cast(soundID)); - return; - } - - if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - return; - } - std::vector vect = components; //I do not know currently how to avoid copy here - do - { - std::vector sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))}; - std::vector> intComps; - for (auto & component : sender) - intComps.push_back(std::make_shared(component)); - showInfoDialog(text,intComps,soundID); - vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))); - } - while(!vect.empty()); -} - -void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr component) -{ - std::vector> intComps; - intComps.push_back(component); - - showInfoDialog(text, intComps, soundBase::sound_todo); -} - -void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector> & components, int soundID) -{ - LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); - waitWhileDialog(); - - if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - return; - } - std::shared_ptr temp = CInfoWindow::create(text, playerID, components); - - if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) - { - CCS->soundh->playSound(static_cast(soundID)); - showingDialog->set(true); - stopMovement(); // interrupt movement to show dialog - GH.windows().pushWindow(temp); - } - else - { - dialogs.push_back(temp); - } -} - -void CPlayerInterface::showInfoDialogAndWait(std::vector & components, const MetaString & text) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - std::string str = text.toString(); - - showInfoDialog(EInfoWindowMode::MODAL, str, components, 0); - waitWhileDialog(); -} - -void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) -{ - boost::unique_lock un(*pim); - - stopMovement(); - LOCPLINT->showingDialog->setn(true); - CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); -} - -void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - - stopMovement(); - CCS->soundh->playSound(static_cast(soundID)); - - if (!selection && cancel) //simple yes/no dialog - { - std::vector> intComps; - for (auto & component : components) - intComps.push_back(std::make_shared(component)); //will be deleted by close in window - - showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps); - } - else if (selection) - { - std::vector> intComps; - for (auto & component : components) - intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close - - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); - if (cancel) - { - pom.push_back(std::pair >("ICANCEL.DEF",0)); - } - - int charperline = 35; - if (pom.size() > 1) - charperline = 50; - GH.windows().createAndPushWindow(text, playerID, charperline, intComps, pom, askID); - intComps[0]->clickPressed(GH.getCursorPosition()); - intComps[0]->clickReleased(GH.getCursorPosition()); - } -} - -void CPlayerInterface::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - int choosenExit = -1; - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - if (destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); - - cb->selectionMade(choosenExit, askID); -} - -void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - auto selectCallback = [=](int selection) - { - JsonNode reply(JsonNode::JsonType::DATA_INTEGER); - reply.Integer() = selection; - cb->sendQueryReply(reply, askID); - }; - - auto cancelCallback = [=]() - { - JsonNode reply(JsonNode::JsonType::DATA_NULL); - cb->sendQueryReply(reply, askID); - }; - - const std::string localTitle = title.toString(); - const std::string localDescription = description.toString(); - - std::vector tempList; - tempList.reserve(objects.size()); - - for(auto item : objects) - tempList.push_back(item.getNum()); - - CComponent localIconC(icon); - - std::shared_ptr localIcon = localIconC.image; - localIconC.removeChild(localIcon.get(), false); - - std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); - wnd->onExit = cancelCallback; - GH.windows().pushWindow(wnd); -} - -void CPlayerInterface::tileRevealed(const std::unordered_set &pos) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - //FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas - adventureInt->onMapTilesChanged(pos); -} - -void CPlayerInterface::tileHidden(const std::unordered_set &pos) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onMapTilesChanged(pos); -} - -void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) -{ - boost::unique_lock un(*pim); - GH.windows().createAndPushWindow(hero); -} - -void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (const CGTownInstance * townObj = dynamic_cast(town)) - { - for (auto fortScreen : GH.windows().findWindows()) - fortScreen->creaturesChangedEventHandler(); - - for (auto castleInterface : GH.windows().findWindows()) - castleInterface->creaturesChangedEventHandler(); - - if (townObj) - for (auto ki : GH.windows().findWindows()) - ki->townChanged(townObj); - } - else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1 - || town->ID == Obj::CREATURE_GENERATOR4 || town->ID == Obj::WAR_MACHINE_FACTORY)) - { - for (auto crw : GH.windows().findWindows()) - if (crw->dwelling == town) - crw->availableCreaturesChanged(); - } -} - -void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (bonus.type == BonusType::NONE) - return; - - adventureInt->onHeroChanged(hero); - if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain) - { - //recalculate paths because hero has lost bonus influencing pathfinding - localState->erasePath(hero); - } -} - -void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); -} - -void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); - firstCall = -1; -} - -void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) -{ - LOG_TRACE(logGlobal); - if (!LOCPLINT->makingTurn) - return; - if (!h) - return; //can't find hero - - //It shouldn't be possible to move hero with open dialog (or dialog waiting in bg) - if (showingDialog->get() || !dialogs.empty()) - return; - - setMovementStatus(true); - - if (localState->isHeroSleeping(h)) - localState->setHeroAwaken(h); - - boost::thread moveHeroTask(std::bind(&CPlayerInterface::doMoveHero,this,h,path)); -} - -void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto onEnd = [=](){ cb->selectionMade(0, queryID); }; - - if (stillMoveHero.get() == DURING_MOVE && localState->hasPath(down) && localState->getPath(down).nodes.size() > 1) //to ignore calls on passing through garrisons - { - onEnd(); - return; - } - - waitForAllDialogs(); - - auto cgw = std::make_shared(up, down, removableUnits); - cgw->quit->addCallback(onEnd); - 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(CComponent::artifact, assembledArtifact->getIndex(), 0); - 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 ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (pa->packType == typeList.getTypeID() && stillMoveHero.get() == DURING_MOVE - && destinationTeleport == ObjectInstanceID()) - stillMoveHero.setn(CONTINUE_MOVE); - - if (destinationTeleport != ObjectInstanceID() - && pa->packType == typeList.getTypeID() - && stillMoveHero.get() == DURING_MOVE) - { // After teleportation via CGTeleport object is finished - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - stillMoveHero.setn(CONTINUE_MOVE); - } -} - - -void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) -{ - heroExchangeStarted(hero1, hero2, QueryID(-1)); -} - -void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(hero1, hero2, query); -} - -void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop) -{ - if (sop->what == ObjProperty::OWNER) - { - const CGObjectInstance * obj = cb->getObj(sop->id); - - if(obj->ID == Obj::TOWN) - { - auto town = static_cast(obj); - - if(obj->tempOwner == playerID) - { - localState->removeOwnedTown(town); - adventureInt->onTownChanged(town); - } - } - } -} - -void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if (sop->what == ObjProperty::OWNER) - { - const CGObjectInstance * obj = cb->getObj(sop->id); - - if(obj->ID == Obj::TOWN) - { - auto town = static_cast(obj); - - if(obj->tempOwner == playerID) - { - localState->addOwnedTown(town); - adventureInt->onTownChanged(town); - } - } - - //redraw minimap if owner changed - std::set pos = obj->getBlockedPos(); - std::unordered_set upos(pos.begin(), pos.end()); - adventureInt->onMapTilesChanged(upos); - - assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size()); - } -} - -void CPlayerInterface::initializeHeroTownList() -{ - if(localState->getWanderingHeroes().empty()) - { - for(auto & hero : cb->getHeroesInfo()) - { - if(!hero->inTownGarrison) - localState->addWanderingHero(hero); - } - } - - if(localState->getOwnedTowns().empty()) - { - for(auto & town : cb->getTownsInfo()) - localState->addOwnedTown(town); - } - - if(adventureInt) - adventureInt->onHeroChanged(nullptr); -} - -void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - auto recruitCb = [=](CreatureID id, int count) - { - LOCPLINT->cb->recruitCreatures(dwelling, dst, id, count, -1); - }; - GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb); -} - -void CPlayerInterface::waitWhileDialog(bool unlockPim) -{ - if (GH.amIGuiThread()) - { - logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!"); - return; - } - - auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); - boost::unique_lock un(showingDialog->mx); - while(showingDialog->data) - showingDialog->cond.wait(un); -} - -void CPlayerInterface::showShipyardDialog(const IShipyard *obj) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto state = obj->shipyardStatus(); - TResources cost; - obj->getBoatCost(cost); - GH.windows().createAndPushWindow(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); }); -} - -void CPlayerInterface::newObject( const CGObjectInstance * obj ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - //we might have built a boat in shipyard in opened town screen - if (obj->ID == Obj::BOAT - && LOCPLINT->castleInt - && obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation()) - { - CCS->soundh->playSound(soundBase::newBuilding); - LOCPLINT->castleInt->addBuilding(BuildingID::SHIP); - } -} - -void CPlayerInterface::centerView (int3 pos, int focusTime) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->curh->hide(); - adventureInt->centerOnTile(pos); - if (focusTime) - { - GH.windows().totalRedraw(); - { - auto unlockPim = vstd::makeUnlockGuard(*pim); - IgnoreEvents ignore(*this); - boost::this_thread::sleep(boost::posix_time::milliseconds(focusTime)); - } - } - CCS->curh->show(); -} - -void CPlayerInterface::objectRemoved(const CGObjectInstance * obj) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if(LOCPLINT->cb->getCurrentPlayer() == playerID && obj->getRemovalSound()) - { - waitWhileDialog(); - CCS->soundh->playSound(obj->getRemovalSound().value()); - } - CGI->mh->waitForOngoingAnimations(); - - if(obj->ID == Obj::HERO && obj->tempOwner == playerID) - { - const CGHeroInstance * h = static_cast(obj); - heroKilled(h); - } - GH.fakeMouseMove(); -} - -void CPlayerInterface::objectRemovedAfter() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onMapTilesChanged(boost::none); - - // visiting or garrisoned hero removed - recreate castle window - if (castleInt) - openTownWindow(castleInt->town); - - for (auto ki : GH.windows().findWindows()) - ki->heroRemoved(); -} - -void CPlayerInterface::playerBlocked(int reason, bool start) -{ - if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE) - { - if(CSH->howManyPlayerInterfaces() > 1 && LOCPLINT != this && LOCPLINT->makingTurn == false) - { - //one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode) - LOCPLINT = this; - GH.curInt = this; - adventureInt->onCurrentPlayerChanged(playerID); - std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); - boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); - std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); - makingTurn = true; //workaround for stiff showInfoDialog implementation - showInfoDialog(msg, cmp); - makingTurn = false; - } - } -} - -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); - - // While mutexes were locked away we may be have stopped being the active interface - if (LOCPLINT != this) - return; - - //if there are any waiting dialogs, show them - if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) - { - showingDialog->set(true); - GH.windows().pushWindow(dialogs.front()); - dialogs.pop_front(); - } - - assert(adventureInt); - - // Handles mouse and key input - GH.handleEvents(); - GH.windows().simpleRedraw(); -} - -int CPlayerInterface::getLastIndex( std::string namePrefix) -{ - using namespace boost::filesystem; - using namespace boost::algorithm; - - path gamesDir = VCMIDirs::get().userSavePath(); - std::map dates; //save number => datestamp - - const directory_iterator enddir; - if (!exists(gamesDir)) - create_directory(gamesDir); - else - for (directory_iterator dir(gamesDir); dir != enddir; ++dir) - { - if (is_regular_file(dir->status())) - { - std::string name = dir->path().filename().string(); - if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1")) - { - char nr = name[namePrefix.size()]; - if (std::isdigit(nr)) - dates[last_write_time(dir->path())] = boost::lexical_cast(nr); - } - } - } - - if (!dates.empty()) - return (--dates.end())->second; //return latest file number - return 0; -} - -void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if (player == playerID) - { - if (victoryLossCheckResult.loss()) - showInfoDialog(CGI->generaltexth->allTexts[95]); - - assert(GH.curInt == LOCPLINT); - auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily - - LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday - GH.curInt = this; //waiting for dialogs requires this to get events - - if(!makingTurn) - { - makingTurn = true; //also needed for dialog to show with current implementation - waitForAllDialogs(); - makingTurn = false; - } - else - waitForAllDialogs(); - - GH.curInt = previousInterface; - LOCPLINT = previousInterface; - - if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated - { - if(adventureInt) - { - GH.terminate_cond->setn(true); - GH.windows().popWindows(GH.windows().count()); - adventureInt.reset(); - } - } - - if (victoryLossCheckResult.victory() && LOCPLINT == this) - { - // end game if current human player has won - CSH->sendClientDisconnecting(); - requestReturningToMainMenu(true); - } - else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) - { - //all human players eliminated - CSH->sendClientDisconnecting(); - requestReturningToMainMenu(false); - } - - if (GH.curInt == this) - GH.curInt = nullptr; - } -} - -void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; -} - -void CPlayerInterface::showPuzzleMap() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - - //TODO: interface should not know the real position of Grail... - double ratio = 0; - int3 grailPos = cb->getGrailPos(&ratio); - - GH.windows().createAndPushWindow(grailPos, ratio); -} - -void CPlayerInterface::viewWorldMap() -{ - adventureInt->openWorldView(); -} - -void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(GH.windows().topWindow()) - GH.windows().popWindows(1); - - if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) - localState->erasePath(caster); - - const spells::Spell * spell = CGI->spells()->getByIndex(spellID); - auto castSoundPath = spell->getCastSound(); - if(!castSoundPath.empty()) - CCS->soundh->playSound(castSoundPath); -} - -void CPlayerInterface::tryDigging(const CGHeroInstance * h) -{ - int msgToShow = -1; - - const auto diggingStatus = h->diggingStatus(); - - switch(diggingStatus) - { - case EDiggingStatus::CAN_DIG: - break; - case EDiggingStatus::LACK_OF_MOVEMENT: - msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." - break; - case EDiggingStatus::TILE_OCCUPIED: - msgToShow = 97; //Try searching on clear ground. - break; - case EDiggingStatus::WRONG_TERRAIN: - msgToShow = 60; ////Try looking on land! - break; - case EDiggingStatus::BACKPACK_IS_FULL: - msgToShow = 247; //Searching for the Grail is fruitless... - break; - default: - assert(0); - } - - if(msgToShow < 0) - cb->dig(h); - else - showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); -} - -void CPlayerInterface::battleNewRoundFirst( int round ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->newRoundFirst(round); -} - -void CPlayerInterface::stopMovement() -{ - if (stillMoveHero.get() == DURING_MOVE)//if we are in the middle of hero movement - stillMoveHero.setn(STOP_MOVE); //after showing dialog movement will be stopped -} - -void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) - GH.windows().createAndPushWindow(market, visitor, EMarketMode::ARTIFACT_EXP); - else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) - GH.windows().createAndPushWindow(market, visitor, EMarketMode::CREATURE_EXP); - else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) - GH.windows().createAndPushWindow(market, visitor); - else if(!market->availableModes().empty()) - GH.windows().createAndPushWindow(market, visitor, market->availableModes().front()); -} - -void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(visitor, market); -} - -void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(visitor, object); -} - -void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto cmw : GH.windows().findWindows()) - cmw->artifactsChanged(false); -} - -void CPlayerInterface::showTavernWindow(const CGObjectInstance *townOrTavern) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(townOrTavern); -} - -void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(obj); -} - -void CPlayerInterface::showQuestLog() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(LOCPLINT->cb->getMyQuests()); -} - -void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) -{ - if (obj->shipyardStatus() != IBoatGenerator::GOOD) - { - MetaString txt; - obj->getProblemText(txt); - showInfoDialog(txt.toString()); - } - else - showShipyardDialog(obj); -} - -void CPlayerInterface::requestReturningToMainMenu(bool won) -{ - if(won && cb->getStartInfo()->campState) - CSH->startCampaignScenario(cb->getStartInfo()->campState); - else - { - GH.dispatchMainThread( - []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - } - ); - } -} - -void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) -{ - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - if(hero) - { - 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); - } -} - -void CPlayerInterface::artifactPut(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); -} - -void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactRemoved(al); - - waitWhileDialog(); -} - -void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), dst.artHolder); - adventureInt->onHeroChanged(hero); - - 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(); -} - -void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) -{ - numOfMovedArts = numOfArts; -} - -void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactAssembled(al); -} - -void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactDisassembled(al); -} - -void CPlayerInterface::waitForAllDialogs(bool unlockPim) -{ - while(!dialogs.empty()) - { - auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); - boost::this_thread::sleep(boost::posix_time::milliseconds(5)); - } - waitWhileDialog(unlockPim); -} - -void CPlayerInterface::proposeLoadingGame() -{ - showYesNoDialog( - CGI->generaltexth->allTexts[68], - []() - { - GH.dispatchMainThread( - []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("load"); - } - ); - }, - nullptr - ); -} - -bool CPlayerInterface::capturedAllEvents() -{ - if(duringMovement) - { - //just inform that we are capturing events. they will be processed by heroMoved() in client thread. - return true; - } - - bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); - bool quickCombatOngoing = isAutoFightOn && !battleInt; - - if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing ) - { - GH.input().ignoreEventsUntilInput(); - return true; - } - - return false; -} - -void CPlayerInterface::setMovementStatus(bool value) -{ - duringMovement = value; - if (value) - { - CCS->curh->hide(); - } - else - { - CCS->curh->show(); - } -} - -void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) -{ - int i = 1; - auto getObj = [&](int3 coord, bool ignoreHero) - { - return cb->getTile(h->convertToVisitablePos(coord))->topVisitableObj(ignoreHero); - }; - - auto isTeleportAction = [&](EPathNodeAction action) -> bool - { - if (action != EPathNodeAction::TELEPORT_NORMAL && - action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && - action != EPathNodeAction::TELEPORT_BATTLE) - { - return false; - } - - return true; - }; - - auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * - { - if (CGTeleport::isConnected(currentObject, nextObjectTop)) - return nextObjectTop; - if (nextObjectTop && nextObjectTop->ID == Obj::HERO && - CGTeleport::isConnected(currentObject, nextObject)) - { - return nextObject; - } - - return nullptr; - }; - - boost::unique_lock un(stillMoveHero.mx); - stillMoveHero.data = CONTINUE_MOVE; - auto doMovement = [&](int3 dst, bool transit) - { - stillMoveHero.data = WAITING_MOVE; - cb->moveHero(h, dst, transit); - while(stillMoveHero.data != STOP_MOVE && stillMoveHero.data != CONTINUE_MOVE) - stillMoveHero.cond.wait(un); - }; - - { - for (auto & elem : path.nodes) - elem.coord = h->convertFromVisitablePos(elem.coord); - - int soundChannel = -1; - std::string soundName; - - auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> std::string - { - if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) - return ""; - - if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) - return ""; - - if (moveType == EPathNodeAction::BLOCKING_VISIT) - return ""; - - // flying movement sound - if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) - return "HORSE10.wav"; - - auto prevTile = cb->getTile(h->convertToVisitablePos(posPrev)); - auto nextTile = cb->getTile(h->convertToVisitablePos(posNext)); - - auto prevRoad = prevTile->roadType; - auto nextRoad = nextTile->roadType; - bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; - - if (movingOnRoad) - return nextTile->terType->horseSound; - else - return nextTile->terType->horseSoundPenalty; - }; - - auto canStop = [&](CGPathNode * node) -> bool - { - if (node->layer != EPathfindingLayer::LAND && node->layer != EPathfindingLayer::SAIL) - return false; - - if (node->accessible != EPathAccessibility::ACCESSIBLE) - return false; - - return true; - }; - - for (i=(int)path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) - { - int3 prevCoord = path.nodes[i].coord; - int3 nextCoord = path.nodes[i-1].coord; - - auto prevObject = getObj(prevCoord, prevCoord == h->pos); - auto nextObjectTop = getObj(nextCoord, false); - auto nextObject = getObj(nextCoord, true); - auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject); - if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) - { - CCS->soundh->stopSound(soundChannel); - destinationTeleport = destTeleportObj->id; - destinationTeleportPos = nextCoord; - doMovement(h->pos, false); - if (path.nodes[i-1].action == EPathNodeAction::TELEPORT_BLOCKING_VISIT - || path.nodes[i-1].action == EPathNodeAction::TELEPORT_BATTLE) - { - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - } - if(i != path.nodes.size() - 1) - { - soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } - continue; - } - - if (path.nodes[i-1].turns) - { //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) - stillMoveHero.data = STOP_MOVE; - break; - } - - { - // Start a new sound for the hero movement or let the existing one carry on. - std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - - if(newSoundName != soundName) - { - soundName = newSoundName; - - CCS->soundh->stopSound(soundChannel); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } - } - - assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all - int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); - logGlobal->trace("Requesting hero movement to %s", endpos.toString()); - - bool useTransit = false; - if ((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless - && (CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i-2].coord, false)) - || CGTeleport::isTeleport(nextObjectTop))) - { // Hero should be able to go through object if it's allow transit - useTransit = true; - } - else if (path.nodes[i-1].layer == EPathfindingLayer::AIR) - useTransit = true; - - doMovement(endpos, useTransit); - - logGlobal->trace("Resuming %s", __FUNCTION__); - bool guarded = cb->isInTheMap(cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); - if ((!useTransit && guarded) || showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) - break; - } - - CCS->soundh->stopSound(soundChannel); - } - - //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement - if (!showingDialog->get()) - GH.fakeMouseMove(); - - CGI->mh->waitForOngoingAnimations(); - setMovementStatus(false); -} - -void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions, bool showTerrain) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->openWorldView(objectPositions, showTerrain ); -} +/* + * CPlayerInterface.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 "CPlayerInterface.h" + +#include + +#include "CGameInfo.h" +#include "CMT.h" +#include "CMusicHandler.h" +#include "CServerHandler.h" +#include "HeroMovementController.h" +#include "PlayerLocalState.h" + +#include "adventureMap/AdventureMapInterface.h" +#include "adventureMap/CInGameConsole.h" +#include "adventureMap/CList.h" + +#include "battle/BattleEffectsController.h" +#include "battle/BattleFieldController.h" +#include "battle/BattleInterface.h" +#include "battle/BattleInterfaceClasses.h" +#include "battle/BattleWindow.h" + +#include "eventsSDL/InputHandler.h" +#include "eventsSDL/NotificationHandler.h" + +#include "gui/CGuiHandler.h" +#include "gui/CursorHandler.h" +#include "gui/WindowHandler.h" + +#include "mainmenu/CMainMenu.h" +#include "mainmenu/CHighScoreScreen.h" + +#include "mapView/mapHandler.h" + +#include "render/CAnimation.h" +#include "render/IImage.h" + +#include "widgets/Buttons.h" +#include "widgets/CComponent.h" +#include "widgets/CGarrisonInt.h" + +#include "windows/CAltarWindow.h" +#include "windows/CCastleInterface.h" +#include "windows/CCreatureWindow.h" +#include "windows/CHeroWindow.h" +#include "windows/CKingdomInterface.h" +#include "windows/CPuzzleWindow.h" +#include "windows/CQuestLog.h" +#include "windows/CSpellWindow.h" +#include "windows/CTradeWindow.h" +#include "windows/CTutorialWindow.h" +#include "windows/GUIClasses.h" +#include "windows/InfoWindows.h" + +#include "../CCallback.h" + +#include "../lib/CArtHandler.h" +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/CStack.h" +#include "../lib/CStopWatch.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CTownHandler.h" +#include "../lib/CondSh.h" +#include "../lib/GameConstants.h" +#include "../lib/JsonNode.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" + +#include "../lib/bonuses/CBonusSystemNode.h" +#include "../lib/bonuses/Limiters.h" +#include "../lib/bonuses/Propagators.h" +#include "../lib/bonuses/Updaters.h" + +#include "../lib/gameState/CGameState.h" + +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/MiscObjects.h" +#include "../lib/mapObjects/ObjectTemplate.h" + +#include "../lib/mapping/CMapHeader.h" + +#include "../lib/networkPacks/PacksForClient.h" +#include "../lib/networkPacks/PacksForClientBattle.h" +#include "../lib/networkPacks/PacksForServer.h" + +#include "../lib/pathfinder/CGPathNode.h" + +#include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/CTypeList.h" + +#include "../lib/spells/CSpellHandler.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 + +#define BATTLE_EVENT_POSSIBLE_RETURN \ + if (LOCPLINT != this) \ + return; \ + if (isAutoFightOn && !battleInt) \ + return; + +CPlayerInterface * LOCPLINT; + +std::shared_ptr CPlayerInterface::battleInt; + +struct HeroObjectRetriever +{ + const CGHeroInstance * operator()(const ConstTransitivePtr &h) const + { + return h; + } + const CGHeroInstance * operator()(const ConstTransitivePtr &s) const + { + return nullptr; + } +}; + +CPlayerInterface::CPlayerInterface(PlayerColor Player): + localState(std::make_unique(*this)), + movementController(std::make_unique()) +{ + logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); + GH.defActionsDef = 0; + LOCPLINT = this; + playerID=Player; + human=true; + battleInt = nullptr; + castleInt = nullptr; + makingTurn = false; + showingDialog = new CondSh(false); + cingconsole = new CInGameConsole(); + firstCall = 1; //if loading will be overwritten in serialize + autosaveCount = 0; + isAutoFightOn = false; + ignoreEvents = false; + numOfMovedArts = 0; +} + +CPlayerInterface::~CPlayerInterface() +{ + logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString()); + delete showingDialog; + delete cingconsole; + if (LOCPLINT == this) + LOCPLINT = nullptr; +} +void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + cb = CB; + env = ENV; + + CCS->musich->loadTerrainMusicThemes(); + initializeHeroTownList(); + + adventureInt.reset(new AdventureMapInterface()); +} + +void CPlayerInterface::playerEndsTurn(PlayerColor player) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (player == playerID) + { + makingTurn = false; + + // remove all active dialogs that do not expect query answer + for (;;) + { + auto adventureWindow = GH.windows().topWindow(); + auto infoWindow = GH.windows().topWindow(); + + if(adventureWindow != nullptr) + break; + + if(infoWindow && infoWindow->ID != QueryID::NONE) + break; + + if (infoWindow) + infoWindow->close(); + else + GH.windows().popWindows(1); + } + + // remove all pending dialogs that do not expect query answer + vstd::erase_if(dialogs, [](const std::shared_ptr & window){ + return window->ID == QueryID::NONE; + }); + } +} + +void CPlayerInterface::playerStartsTurn(PlayerColor player) +{ + if(GH.windows().findWindows().empty()) + { + // after map load - remove all active windows and replace them with adventure map + GH.windows().clear(); + GH.windows().pushWindow(adventureInt); + } + + EVENT_HANDLER_CALLED_BY_CLIENT; + if (player != playerID && LOCPLINT == this) + { + waitWhileDialog(); + + bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman(); + + if (makingTurn == false) + adventureInt->onEnemyTurnStarted(player, isHuman); + } +} + +void CPlayerInterface::performAutosave() +{ + int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); + if(frequency > 0 && cb->getDate() % frequency == 0) + { + bool usePrefix = settings["general"]["useSavePrefix"].Bool(); + std::string prefix = std::string(); + + if(usePrefix) + { + prefix = settings["general"]["savePrefix"].String(); + if(prefix.empty()) + { + std::string name = cb->getMapHeader()->name.toString(); + int txtlen = TextOperations::getUnicodeCharactersCount(name); + + TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); + std::string forbiddenChars("\\/:?\"<>| "); + std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' ); + + prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; + } + } + + autosaveCount++; + + int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); + if(autosaveCountLimit > 0) + { + cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount)); + autosaveCount %= autosaveCountLimit; + } + else + { + std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH)) + + std::to_string(cb->getDate(Date::WEEK)) + + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); + + cb->save("Saves/Autosave/" + prefix + stringifiedDate); + } + } +} + +void CPlayerInterface::gamePause(bool pause) +{ + cb->gamePause(pause); +} + +void CPlayerInterface::yourTurn(QueryID queryID) +{ + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); + + EVENT_HANDLER_CALLED_BY_CLIENT; + { + LOCPLINT = this; + GH.curInt = this; + + NotificationHandler::notify("Your turn"); + if(settings["general"]["startTurnAutosave"].Bool()) + { + performAutosave(); + } + + if (CSH->howManyPlayerInterfaces() > 1) //hot seat message + { + adventureInt->onHotseatWaitStarted(playerID); + + makingTurn = true; + std::string msg = CGI->generaltexth->allTexts[13]; + boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); + std::vector> cmp; + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); + showInfoDialog(msg, cmp); + } + else + { + makingTurn = true; + adventureInt->onPlayerTurnStarted(playerID); + } + } + acceptTurn(queryID); +} + +void CPlayerInterface::acceptTurn(QueryID queryID) +{ + if (settings["session"]["autoSkip"].Bool()) + { + while(auto iw = GH.windows().topWindow()) + iw->close(); + } + + if(CSH->howManyPlayerInterfaces() > 1) + { + waitWhileDialog(); // wait for player to accept turn in hot-seat mode + + adventureInt->onPlayerTurnStarted(playerID); + } + + // warn player if he has no town + if (cb->howManyTowns() == 0) + { + auto playerColor = *cb->getPlayerID(); + + std::vector components; + components.emplace_back(ComponentType::FLAG, playerColor); + MetaString text; + + const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; + + if(optDaysWithoutCastle) + { + auto daysWithoutCastle = optDaysWithoutCastle.value(); + if (daysWithoutCastle < 6) + { + text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. + text.replaceName(playerColor); + text.replaceNumber(7 - daysWithoutCastle); + } + else if (daysWithoutCastle == 6) + { + text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. + text.replaceName(playerColor); + } + + showInfoDialogAndWait(components, text); + } + else + logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); + } + + cb->selectionMade(0, queryID); + movementController->onPlayerTurnStarted(); +} + +void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + if(LOCPLINT != this) + return; + + //FIXME: read once and store + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) + return; + + const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero + + if (!hero) + return; + + movementController->onTryMoveHero(hero, details); +} + +void CPlayerInterface::heroKilled(const CGHeroInstance* hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID); + + // if hero is not in town garrison + if (vstd::contains(localState->getWanderingHeroes(), hero)) + localState->removeWanderingHero(hero); + + adventureInt->onHeroChanged(hero); + localState->erasePath(hero); +} + +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()); + } +} + +void CPlayerInterface::heroCreated(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->addWanderingHero(hero); + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::openTownWindow(const CGTownInstance * town) +{ + if(castleInt) + castleInt->close(); + castleInt = nullptr; + + auto newCastleInt = std::make_shared(town); + + GH.windows().pushWindow(newCastleInt); +} + +void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (which == PrimarySkill::EXPERIENCE) + { + for (auto ctw : GH.windows().findWindows()) + ctw->updateExpToLevel(); + } + 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(); +} + +void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onHeroChanged(hero); + if (makingTurn && hero->tempOwner == playerID) + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (makingTurn && hero->tempOwner == playerID) + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::receivedResource() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + for (auto mw : GH.windows().findWindows()) + mw->resourceChanged(); + + GH.windows().totalRedraw(); +} + +void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector& skills, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->soundh->playSound(soundBase::heroNewLevel); + GH.windows().createAndPushWindow(hero, pskill, skills, [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }); +} + +void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->soundh->playSound(soundBase::heroNewLevel); + GH.windows().createAndPushWindow(commander, skills, [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }); +} + +void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(town->garrisonHero) //wandering hero moved to the garrison + { + // This method also gets called on hero recruitment -> garrisoned hero is already in garrison + if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero)) + localState->removeWanderingHero(town->garrisonHero); + } + + if(town->visitingHero) //hero leaves garrison + { + // This method also gets called on hero recruitment -> wandering heroes already contains new hero + if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero)) + localState->addWanderingHero(town->visitingHero); + } + adventureInt->onHeroChanged(nullptr); + adventureInt->onTownChanged(town); + + for (auto cgh : GH.windows().findWindows()) + if (cgh->holdsGarrison(town)) + cgh->updateGarrisons(); + + for (auto ki : GH.windows().findWindows()) + ki->townChanged(town); + + // Perform totalRedraw to update hero list on adventure map, if any dialogs are open + GH.windows().totalRedraw(); +} + +void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (hero->tempOwner != playerID ) + return; + + waitWhileDialog(); + openTownWindow(town); +} + +void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) +{ + std::vector instances; + + if(auto obj = dynamic_cast(cb->getObjInstance(id1))) + instances.push_back(obj); + + + if(id2 != ObjectInstanceID() && id2 != id1) + { + if(auto obj = dynamic_cast(cb->getObjInstance(id2))) + instances.push_back(obj); + } + + garrisonsChanged(instances); +} + +void CPlayerInterface::garrisonsChanged(std::vector objs) +{ + for (auto object : objs) + { + auto * hero = dynamic_cast(object); + auto * town = dynamic_cast(object); + + if (town) + adventureInt->onTownChanged(town); + + if (hero) + { + adventureInt->onHeroChanged(hero); + if(hero->inTownGarrison && hero->visitedTown != town) + adventureInt->onTownChanged(hero->visitedTown); + } + } + + for (auto cgh : GH.windows().findWindows()) + if (cgh->holdsGarrisons(objs)) + cgh->updateGarrisons(); + + GH.windows().totalRedraw(); +} + +void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onTownChanged(town); + + if (castleInt) + { + castleInt->townlist->updateElement(town); + + if (castleInt->town == town) + { + switch(what) + { + case 1: + CCS->soundh->playSound(soundBase::newBuilding); + castleInt->addBuilding(buildingID); + break; + case 2: + castleInt->removeBuilding(buildingID); + break; + } + } + + // Perform totalRedraw in order to force redraw of updated town list icon from adventure map + GH.windows().totalRedraw(); + } + + for (auto cgh : GH.windows().findWindows()) + cgh->buildChanged(); +} + +void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) +{ + movementController->onBattleStarted(); + + //Don't wait for dialogs when we are non-active hot-seat player + if (LOCPLINT == this) + 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) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); + bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); + + if ((replayAllowed && useQuickCombat) || forceQuickCombat) + { + autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); + + AutocombatPreferences autocombatPreferences = AutocombatPreferences(); + autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); + + autofightingAI->initBattleInterface(env, cb, autocombatPreferences); + autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false); + isAutoFightOn = true; + cb->registerBattleInterface(autofightingAI); + } + + //Don't wait for dialogs when we are non-active hot-seat player + if (LOCPLINT == this) + waitForAllDialogs(); + + BATTLE_EVENT_POSSIBLE_RETURN; +} + +void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector & units) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + for(auto & info : units) + { + switch(info.operation) + { + case UnitChanges::EOperation::RESET_STATE: + { + const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id ); + + if(!stack) + { + logGlobal->error("Invalid unit ID %d", info.id); + continue; + } + battleInt->stackReset(stack); + } + break; + case UnitChanges::EOperation::REMOVE: + battleInt->stackRemoved(info.id); + break; + case UnitChanges::EOperation::ADD: + { + const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id); + if(!unit) + { + logGlobal->error("Invalid unit ID %d", info.id); + continue; + } + battleInt->stackAdded(unit); + } + break; + default: + logGlobal->error("Unknown unit operation %d", (int)info.operation); + break; + } + } +} + +void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + std::vector> newObstacles; + std::vector removedObstacles; + + for(auto & change : obstacles) + { + if(change.operation == BattleChanges::EOperation::ADD) + { + auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id); + if(instance) + newObstacles.push_back(instance); + else + logNetwork->error("Invalid obstacle instance %d", change.id); + } + if(change.operation == BattleChanges::EOperation::REMOVE) + removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct + } + + if (!newObstacles.empty()) + battleInt->obstaclePlaced(newObstacles); + + if (!removedObstacles.empty()) + battleInt->obstacleRemoved(removedObstacles); + + battleInt->fieldController->redrawBackgroundWithHexes(); +} + +void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->stackIsCatapulting(ca); +} + +void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->newRound(); +} + +void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->startAction(action); +} + +void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->endAction(action); +} + +void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + logGlobal->trace("Awaiting command for %s", stack->nodeName()); + + assert(!cb->getBattle(battleID)->battleIsFinished()); + if (cb->getBattle(battleID)->battleIsFinished()) + { + logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); + + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return ; + } + + if (autofightingAI) + { + if (isAutoFightOn) + { + //FIXME: we want client rendering to proceed while AI is making actions + // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + autofightingAI->activeStack(battleID, stack); + return; + } + + cb->unregisterBattleInterface(autofightingAI); + autofightingAI.reset(); + } + + assert(battleInt); + if(!battleInt) + { + // probably battle is finished already + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + } + + battleInt->stackActivated(stack); +} + +void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if(isAutoFightOn || autofightingAI) + { + isAutoFightOn = false; + cb->unregisterBattleInterface(autofightingAI); + autofightingAI.reset(); + + if(!battleInt) + { + bool allowManualReplay = queryID != QueryID::NONE; + + auto wnd = std::make_shared(*br, *this, allowManualReplay); + + if (allowManualReplay) + { + wnd->resultCallback = [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }; + } + GH.windows().pushWindow(wnd); + // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. + // Otherwise NewTurn causes freeze. + waitWhileDialog(); + return; + } + } + + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->battleFinished(*br, queryID); +} + +void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector & lines) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->displayBattleLog(lines); +} + +void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->stackMoved(stack, dest, distance, teleport); +} +void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->spellCast(sc); +} +void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->battleStacksEffectsSet(sse); +} +void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->effectsController->battleTriggerEffect(bte); + + if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN)) + { + const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo)); + battleInt->windowObject->heroManaPointsChanged(manaDrainedHero); + } +} +void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + std::vector arg; + for(auto & elem : bsa) + { + const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false); + const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false); + + assert(defender); + + StackAttackedInfo info; + info.defender = defender; + info.attacker = attacker; + info.damageDealt = elem.damageAmount; + info.amountKilled = elem.killedAmount; + info.spellEffect = SpellID::NONE; + info.indirectAttack = ranged; + info.killed = elem.killed(); + info.rebirth = elem.willRebirth(); + info.cloneKilled = elem.cloneKilled(); + info.fireShield = elem.fireShield(); + + if (elem.isSpell()) + info.spellEffect = elem.spellID; + + arg.push_back(info); + } + battleInt->stacksAreAttacked(arg); +} +void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + StackAttackInfo info; + info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking); + info.defender = nullptr; + info.indirectAttack = ba->shot(); + info.lucky = ba->lucky(); + info.unlucky = ba->unlucky(); + info.deathBlow = ba->deathBlow(); + info.lifeDrain = ba->lifeDrain(); + info.tile = ba->tile; + info.spellEffect = SpellID::NONE; + + if (ba->spellLike()) + info.spellEffect = ba->spellID; + + for(auto & elem : ba->bsa) + { + if(!elem.isSecondary()) + { + assert(info.defender == nullptr); + info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked); + } + else + { + info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked)); + } + } + assert(info.defender != nullptr); + assert(info.attacker != nullptr); + + battleInt->stackAttacking(info); +} + +void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->gateStateChanged(state); +} + +void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; +} + +void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector & components, int soundID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO; + auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod + + if(autoTryHover || type == EInfoWindowMode::INFO) + { + waitWhileDialog(); //Fix for mantis #98 + adventureInt->showInfoBoxMessage(components, text, timer); + + // abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on + movementController->requestMovementAbort(); + + if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) + CCS->soundh->playSound(static_cast(soundID)); + return; + } + + if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + return; + } + std::vector vect = components; //I do not know currently how to avoid copy here + do + { + std::vector sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))}; + std::vector> intComps; + for (auto & component : sender) + intComps.push_back(std::make_shared(component)); + showInfoDialog(text,intComps,soundID); + vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))); + } + while(!vect.empty()); +} + +void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr component) +{ + std::vector> intComps; + intComps.push_back(component); + + showInfoDialog(text, intComps, soundBase::sound_todo); +} + +void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector> & components, int soundID) +{ + LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); + waitWhileDialog(); + + if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + return; + } + std::shared_ptr temp = CInfoWindow::create(text, playerID, components); + + if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) + { + CCS->soundh->playSound(static_cast(soundID)); + showingDialog->set(true); + movementController->requestMovementAbort(); // interrupt movement to show dialog + GH.windows().pushWindow(temp); + } + else + { + dialogs.push_back(temp); + } +} + +void CPlayerInterface::showInfoDialogAndWait(std::vector & components, const MetaString & text) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + std::string str = text.toString(); + + showInfoDialog(EInfoWindowMode::MODAL, str, components, 0); + waitWhileDialog(); +} + +void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) +{ + movementController->requestMovementAbort(); + LOCPLINT->showingDialog->setn(true); + CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); +} + +void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + + movementController->requestMovementAbort(); + CCS->soundh->playSound(static_cast(soundID)); + + if (!selection && cancel) //simple yes/no dialog + { + std::vector> intComps; + for (auto & component : components) + intComps.push_back(std::make_shared(component)); //will be deleted by close in window + + showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps); + } + else if (selection) + { + std::vector> intComps; + for (auto & component : components) + intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close + + std::vector > > pom; + pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0}); + if (cancel) + { + pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0}); + } + + int charperline = 35; + if (pom.size() > 1) + charperline = 50; + GH.windows().createAndPushWindow(text, playerID, charperline, intComps, pom, askID); + intComps[0]->clickPressed(GH.getCursorPosition()); + intComps[0]->clickReleased(GH.getCursorPosition()); + } +} + +void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + movementController->showTeleportDialog(hero, channel, exits, impassable, askID); +} + +void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + auto selectCallback = [=](int selection) + { + cb->sendQueryReply(selection, askID); + }; + + auto cancelCallback = [=]() + { + cb->sendQueryReply(std::nullopt, askID); + }; + + const std::string localTitle = title.toString(); + const std::string localDescription = description.toString(); + + std::vector tempList; + tempList.reserve(objects.size()); + + for(auto item : objects) + tempList.push_back(item.getNum()); + + CComponent localIconC(icon); + + std::shared_ptr localIcon = localIconC.image; + localIconC.removeChild(localIcon.get(), false); + + std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); + wnd->onExit = cancelCallback; + GH.windows().pushWindow(wnd); +} + +void CPlayerInterface::tileRevealed(const std::unordered_set &pos) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + //FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas + adventureInt->onMapTilesChanged(pos); +} + +void CPlayerInterface::tileHidden(const std::unordered_set &pos) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onMapTilesChanged(pos); +} + +void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) +{ + GH.windows().createAndPushWindow(hero); +} + +void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (const CGTownInstance * townObj = dynamic_cast(town)) + { + for (auto fortScreen : GH.windows().findWindows()) + fortScreen->creaturesChangedEventHandler(); + + for (auto castleInterface : GH.windows().findWindows()) + if(castleInterface->town == town) + castleInterface->creaturesChangedEventHandler(); + + if (townObj) + for (auto ki : GH.windows().findWindows()) + ki->townChanged(townObj); + } + else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1 + || town->ID == Obj::CREATURE_GENERATOR4 || town->ID == Obj::WAR_MACHINE_FACTORY)) + { + for (auto crw : GH.windows().findWindows()) + if (crw->dwelling == town) + crw->availableCreaturesChanged(); + } +} + +void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (bonus.type == BonusType::NONE) + return; + + adventureInt->onHeroChanged(hero); + if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain) + { + //recalculate paths because hero has lost bonus influencing pathfinding + localState->erasePath(hero); + } +} + +void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->serialize(h, version); +} + +void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->serialize(h, version); + firstCall = -1; +} + +void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) +{ + assert(LOCPLINT->makingTurn); + assert(h); + assert(!showingDialog->get()); + assert(dialogs.empty()); + + LOG_TRACE(logGlobal); + if (!LOCPLINT->makingTurn) + return; + if (!h) + return; //can't find hero + + //It shouldn't be possible to move hero with open dialog (or dialog waiting in bg) + if (showingDialog->get() || !dialogs.empty()) + return; + + if (localState->isHeroSleeping(h)) + localState->setHeroAwaken(h); + + movementController->requestMovementStart(h, path); +} + +void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onEnd = [=](){ cb->selectionMade(0, queryID); }; + + if (movementController->isHeroMovingThroughGarrison(down, up)) + { + onEnd(); + return; + } + + waitForAllDialogs(); + + auto cgw = std::make_shared(up, down, removableUnits); + cgw->quit->addCallback(onEnd); + 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)) + movementController->onMoveHeroApplied(); + + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) + movementController->onQueryReplyApplied(); +} + +void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) +{ + heroExchangeStarted(hero1, hero2, QueryID(-1)); +} + +void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(hero1, hero2, query); +} + +void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop) +{ + if (sop->what == ObjProperty::OWNER) + { + const CGObjectInstance * obj = cb->getObj(sop->id); + + if(obj->ID == Obj::TOWN) + { + auto town = static_cast(obj); + + if(obj->tempOwner == playerID) + { + localState->removeOwnedTown(town); + adventureInt->onTownChanged(town); + } + } + } +} + +void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if (sop->what == ObjProperty::OWNER) + { + const CGObjectInstance * obj = cb->getObj(sop->id); + + if(obj->ID == Obj::TOWN) + { + auto town = static_cast(obj); + + if(obj->tempOwner == playerID) + { + localState->addOwnedTown(town); + adventureInt->onTownChanged(town); + } + } + + //redraw minimap if owner changed + std::set pos = obj->getBlockedPos(); + std::unordered_set upos(pos.begin(), pos.end()); + adventureInt->onMapTilesChanged(upos); + + assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size()); + } +} + +void CPlayerInterface::initializeHeroTownList() +{ + if(localState->getWanderingHeroes().empty()) + { + for(auto & hero : cb->getHeroesInfo()) + { + if(!hero->inTownGarrison) + localState->addWanderingHero(hero); + } + } + + if(localState->getOwnedTowns().empty()) + { + for(auto & town : cb->getTownsInfo()) + localState->addOwnedTown(town); + } + + if(adventureInt) + adventureInt->onHeroChanged(nullptr); +} + +void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + auto recruitCb = [=](CreatureID id, int count) + { + cb->recruitCreatures(dwelling, dst, id, count, -1); + }; + auto closeCb = [=]() + { + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb, closeCb); +} + +void CPlayerInterface::waitWhileDialog() +{ + if (GH.amIGuiThread()) + { + logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!"); + return; + } + + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::unique_lock un(showingDialog->mx); + while(showingDialog->data) + showingDialog->cond.wait(un); +} + +void CPlayerInterface::showShipyardDialog(const IShipyard *obj) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto state = obj->shipyardStatus(); + TResources cost; + obj->getBoatCost(cost); + GH.windows().createAndPushWindow(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); }); +} + +void CPlayerInterface::newObject( const CGObjectInstance * obj ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + //we might have built a boat in shipyard in opened town screen + if (obj->ID == Obj::BOAT + && LOCPLINT->castleInt + && obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation()) + { + CCS->soundh->playSound(soundBase::newBuilding); + LOCPLINT->castleInt->addBuilding(BuildingID::SHIP); + } +} + +void CPlayerInterface::centerView (int3 pos, int focusTime) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->curh->hide(); + adventureInt->centerOnTile(pos); + if (focusTime) + { + GH.windows().totalRedraw(); + { + IgnoreEvents ignore(*this); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); + } + } + CCS->curh->show(); +} + +void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if(playerID == initiator && obj->getRemovalSound()) + { + waitWhileDialog(); + CCS->soundh->playSound(obj->getRemovalSound().value()); + } + CGI->mh->waitForOngoingAnimations(); + + if(obj->ID == Obj::HERO && obj->tempOwner == playerID) + { + const CGHeroInstance * h = static_cast(obj); + heroKilled(h); + } + GH.fakeMouseMove(); +} + +void CPlayerInterface::objectRemovedAfter() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onMapTilesChanged(boost::none); + + // visiting or garrisoned hero removed - update window + if (castleInt) + castleInt->updateGarrisons(); + + for (auto ki : GH.windows().findWindows()) + ki->heroRemoved(); +} + +void CPlayerInterface::playerBlocked(int reason, bool start) +{ + if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE) + { + if(CSH->howManyPlayerInterfaces() > 1 && LOCPLINT != this && LOCPLINT->makingTurn == false) + { + //one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode) + LOCPLINT = this; + GH.curInt = this; + adventureInt->onCurrentPlayerChanged(playerID); + std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); + boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); + std::vector> cmp; + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); + makingTurn = true; //workaround for stiff showInfoDialog implementation + showInfoDialog(msg, cmp); + makingTurn = false; + } + } +} + +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); + + // While mutexes were locked away we may be have stopped being the active interface + if (LOCPLINT != this) + return; + + //if there are any waiting dialogs, show them + if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) + { + showingDialog->set(true); + GH.windows().pushWindow(dialogs.front()); + dialogs.pop_front(); + } + + assert(adventureInt); + + // Handles mouse and key input + GH.handleEvents(); + GH.windows().simpleRedraw(); +} + +int CPlayerInterface::getLastIndex( std::string namePrefix) +{ + using namespace boost::filesystem; + using namespace boost::algorithm; + + path gamesDir = VCMIDirs::get().userSavePath(); + std::map dates; //save number => datestamp + + const directory_iterator enddir; + if (!exists(gamesDir)) + create_directory(gamesDir); + else + for (directory_iterator dir(gamesDir); dir != enddir; ++dir) + { + if (is_regular_file(dir->status())) + { + std::string name = dir->path().filename().string(); + if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1")) + { + char nr = name[namePrefix.size()]; + if (std::isdigit(nr)) + dates[last_write_time(dir->path())] = boost::lexical_cast(nr); + } + } + } + + if (!dates.empty()) + return (--dates.end())->second; //return latest file number + return 0; +} + +void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if (player == playerID) + { + if (victoryLossCheckResult.loss()) + showInfoDialog(CGI->generaltexth->allTexts[95]); + + assert(GH.curInt == LOCPLINT); + auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily + + LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday + GH.curInt = this; //waiting for dialogs requires this to get events + + if(!makingTurn) + { + makingTurn = true; //also needed for dialog to show with current implementation + waitForAllDialogs(); + makingTurn = false; + } + else + waitForAllDialogs(); + + GH.curInt = previousInterface; + LOCPLINT = previousInterface; + + if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated + { + if(adventureInt) + { + GH.windows().popWindows(GH.windows().count()); + adventureInt.reset(); + } + } + + if (victoryLossCheckResult.victory() && LOCPLINT == this) + { + // end game if current human player has won + CSH->sendClientDisconnecting(); + requestReturningToMainMenu(true); + } + else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) + { + //all human players eliminated + CSH->sendClientDisconnecting(); + requestReturningToMainMenu(false); + } + + if (GH.curInt == this) + GH.curInt = nullptr; + } +} + +void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; +} + +void CPlayerInterface::showPuzzleMap() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + + //TODO: interface should not know the real position of Grail... + double ratio = 0; + int3 grailPos = cb->getGrailPos(&ratio); + + GH.windows().createAndPushWindow(grailPos, ratio); +} + +void CPlayerInterface::viewWorldMap() +{ + adventureInt->openWorldView(); +} + +void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(GH.windows().topWindow()) + GH.windows().popWindows(1); + + if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) + localState->erasePath(caster); + + auto castSoundPath = spellID.toSpell()->getCastSound(); + if(!castSoundPath.empty()) + CCS->soundh->playSound(castSoundPath); +} + +void CPlayerInterface::tryDigging(const CGHeroInstance * h) +{ + int msgToShow = -1; + + const auto diggingStatus = h->diggingStatus(); + + switch(diggingStatus) + { + case EDiggingStatus::CAN_DIG: + break; + case EDiggingStatus::LACK_OF_MOVEMENT: + msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." + break; + case EDiggingStatus::TILE_OCCUPIED: + msgToShow = 97; //Try searching on clear ground. + break; + case EDiggingStatus::WRONG_TERRAIN: + msgToShow = 60; ////Try looking on land! + break; + case EDiggingStatus::BACKPACK_IS_FULL: + msgToShow = 247; //Searching for the Grail is fruitless... + break; + default: + assert(0); + } + + if(msgToShow < 0) + cb->dig(h); + else + showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); +} + +void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->newRoundFirst(); +} + +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) && 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()); +} + +void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(visitor, market, onWindowClosed); +} + +void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(visitor, object); +} + +void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + for (auto cmw : GH.windows().findWindows()) + cmw->artifactsChanged(false); +} + +void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(object, onWindowClosed); +} + +void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(obj); +} + +void CPlayerInterface::showQuestLog() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(LOCPLINT->cb->getMyQuests()); +} + +void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) +{ + if (obj->shipyardStatus() != IBoatGenerator::GOOD) + { + MetaString txt; + obj->getProblemText(txt); + showInfoDialog(txt.toString()); + } + else + showShipyardDialog(obj); +} + +void CPlayerInterface::requestReturningToMainMenu(bool won) +{ + HighScoreParameter param; + param.difficulty = cb->getStartInfo()->difficulty; + param.day = cb->getDate(); + param.townAmount = cb->howManyTowns(); + param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated; + param.hasGrail = false; + for(const CGHeroInstance * h : cb->getHeroesInfo()) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->builtBuildings.count(BuildingID::GRAIL)) + param.hasGrail = true; + param.allDefeated = true; + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + auto ps = cb->getPlayerState(player, false); + if(ps && player != *cb->getPlayerID()) + if(!ps->checkVanquished()) + param.allDefeated = false; + } + param.scenarioName = cb->getMapHeader()->name.toString(); + param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; + HighScoreCalculation highScoreCalc; + highScoreCalc.parameters.push_back(param); + highScoreCalc.isCampaign = false; + + if(won && cb->getStartInfo()->campState) + CSH->startCampaignScenario(param, cb->getStartInfo()->campState); + else + { + GH.dispatchMainThread( + [won, highScoreCalc]() + { + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); + GH.windows().createAndPushWindow(won, highScoreCalc); + } + ); + } +} + +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); + } +} + +void CPlayerInterface::artifactPut(const ArtifactLocation &al) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); +} + +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(); +} + +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(); +} + +void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) +{ + numOfMovedArts = numOfArts; +} + +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); +} + +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); +} + +void CPlayerInterface::waitForAllDialogs() +{ + while(!dialogs.empty()) + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); + } + waitWhileDialog(); +} + +void CPlayerInterface::proposeLoadingGame() +{ + showYesNoDialog( + CGI->generaltexth->allTexts[68], + []() + { + GH.dispatchMainThread( + []() + { + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("load"); + } + ); + }, + nullptr + ); +} + +bool CPlayerInterface::capturedAllEvents() +{ + if(movementController->isHeroMoving()) + { + //just inform that we are capturing events. they will be processed by heroMoved() in client thread. + return true; + } + + bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); + bool quickCombatOngoing = isAutoFightOn && !battleInt; + + if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing ) + { + GH.input().ignoreEventsUntilInput(); + return true; + } + + return false; +} + +void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions, bool showTerrain) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->openWorldView(objectPositions, showTerrain ); +} + +std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index d4372ec5f..3d9c17cf2 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -1,240 +1,234 @@ -/* - * CPlayerInterface.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/FunctionList.h" -#include "../lib/CGameInterface.h" -#include "gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Artifact; - -struct TryMoveHero; -class CGHeroInstance; -class CStack; -class CCreature; -struct CGPath; -class CCreatureSet; -class CGObjectInstance; -struct UpgradeInfo; -template struct CondSh; -struct CPathsInfo; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class AdventureMapInterface; -class CCastleInterface; -class BattleInterface; -class CComponent; -class CSelectableComponent; -class CSlider; -class CInGameConsole; -class CInfoWindow; -class IShowActivatable; -class ClickableL; -class ClickableR; -class Hoverable; -class KeyInterested; -class MotionInterested; -class PlayerLocalState; -class TimeInterested; - -namespace boost -{ - class mutex; - class recursive_mutex; -} - -/// Central class for managing user interface logic -class CPlayerInterface : public CGameInterface, public IUpdateable -{ - bool duringMovement; - 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!) - const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) - - ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation - int3 destinationTeleportPos; - -public: // TODO: make private - std::shared_ptr env; - - std::unique_ptr localState; - - //minor interfaces - CondSh *showingDialog; //indicates if dialog box is displayed - - static boost::recursive_mutex *pim; - bool makingTurn; //if player is already making his turn - - CCastleInterface * castleInt; //nullptr if castle window isn't opened - static std::shared_ptr battleInt; //nullptr if no battle - CInGameConsole * cingconsole; - - std::shared_ptr cb; //to communicate with engine - - //During battle is quick combat mode is used - std::shared_ptr autofightingAI; //AI that makes decisions - bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. - -protected: // Call-ins from server, should not be called directly, but only via GameInterface - - void update() override; - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - - void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; - void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished - - 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 artifactAssembled(const ArtifactLocation &al) override; - void askToAssembleArtifact(const ArtifactLocation & dst) override; - void artifactDisassembled(const ArtifactLocation &al) override; - - void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; - void heroCreated(const CGHeroInstance* hero) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; - void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; - void heroInGarrisonChange(const CGTownInstance *town) override; - void heroMoved(const TryMoveHero & details, bool verbose = true) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; - void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; - void heroManaPointsChanged(const CGHeroInstance * hero) override; - void heroMovePointsChanged(const CGHeroInstance * hero) override; - void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; - void receivedResource() override; - void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; - void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override; - void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - void showTeleportDialog(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) override; - void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override; - void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; //called when a hero casts a spell - void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war - void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles - void newObject(const CGObjectInstance * obj) override; - void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) - void yourTurn() override; - void availableCreaturesChanged(const CGDwelling *town) override; - void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it - void playerBonusChanged(const Bonus &bonus, bool gain) override; - void requestRealized(PackageApplied *pa) override; - void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void centerView (int3 pos, int focusTime) override; - void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; - void objectPropertyChanged(const SetObjectProperty * sop) override; - void objectRemoved(const CGObjectInstance *obj) override; - 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 saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading - void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - - //for battles - void actionFinished(const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero - void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero - void activeStack(const CStack * stack) override; //called when it's turn of that stack - void battleAttack(const BattleAttack *ba) override; //stack performs attack - void battleEnd(const BattleResult *br, QueryID queryID) override; //end of battle - void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling - void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleLogMessage(const std::vector & lines) override; - void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - 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; //various one-shot effect - void battleStacksAttacked(const std::vector & bsa, bool ranged) override; - void battleStartBefore(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 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 battleUnitsChanged(const std::vector & units) override; - void battleObstaclesChanged(const std::vector & obstacles) override; - void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack - void battleGateStateChanged(const EGateState state) override; - void yourTacticPhase(int distance) override; - -public: // public interface for use by client via LOCPLINT access - - // part of GameInterface that is also used by client code - void showPuzzleMap() override; - void viewWorldMap() override; - void showQuestLog() override; - void showThievesGuildWindow (const CGObjectInstance * obj) override; - void showTavernWindow(const CGObjectInstance *townOrTavern) override; - 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(bool unlockPim = true); - void waitForAllDialogs(bool unlockPim = true); - void openTownWindow(const CGTownInstance * town); //shows townscreen - void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero - - void showInfoDialog(const std::string &text, std::shared_ptr component); - void showInfoDialog(const std::string &text, const std::vector> & components = std::vector>(), int soundID = 0); - void showInfoDialogAndWait(std::vector & components, const MetaString & text); - void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); - - void stopMovement(); - void moveHero(const CGHeroInstance *h, const CGPath& path); - - void tryDigging(const CGHeroInstance *h); - void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; - void proposeLoadingGame(); - void performAutosave(); - - ///returns true if all events are processed internally - bool capturedAllEvents(); - - CPlayerInterface(PlayerColor Player); - ~CPlayerInterface(); - -private: - struct IgnoreEvents - { - CPlayerInterface & owner; - IgnoreEvents(CPlayerInterface & Owner):owner(Owner) - { - owner.ignoreEvents = true; - }; - ~IgnoreEvents() - { - owner.ignoreEvents = false; - }; - - }; - - void heroKilled(const CGHeroInstance* hero); - void garrisonsChanged(std::vector objs); - void requestReturningToMainMenu(bool won); - void acceptTurn(); //used during hot seat after your turn message is close - void initializeHeroTownList(); - int getLastIndex(std::string namePrefix); - void doMoveHero(const CGHeroInstance *h, CGPath path); - void setMovementStatus(bool value); - -}; - -/// Provides global access to instance of interface of currently active player -extern CPlayerInterface * LOCPLINT; +/* + * CPlayerInterface.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/FunctionList.h" +#include "../lib/CGameInterface.h" +#include "gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Artifact; + +struct TryMoveHero; +class CGHeroInstance; +class CStack; +class CCreature; +struct CGPath; +class CCreatureSet; +class CGObjectInstance; +struct UpgradeInfo; +template struct CondSh; +struct CPathsInfo; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class AdventureMapInterface; +class CCastleInterface; +class BattleInterface; +class CComponent; +class CSelectableComponent; +class CSlider; +class CInGameConsole; +class CInfoWindow; +class IShowActivatable; +class ClickableL; +class ClickableR; +class Hoverable; +class KeyInterested; +class MotionInterested; +class PlayerLocalState; +class TimeInterested; +class HeroMovementController; + +namespace boost +{ + class mutex; + class recursive_mutex; +} + +/// Central class for managing user interface logic +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::shared_ptr env; + + std::unique_ptr localState; + + //minor interfaces + CondSh *showingDialog; //indicates if dialog box is displayed + + bool makingTurn; //if player is already making his turn + + CCastleInterface * castleInt; //nullptr if castle window isn't opened + static std::shared_ptr battleInt; //nullptr if no battle + CInGameConsole * cingconsole; + + std::shared_ptr cb; //to communicate with engine + + //During battle is quick combat mode is used + std::shared_ptr autofightingAI; //AI that makes decisions + bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. + +protected: // Call-ins from server, should not be called directly, but only via GameInterface + + void update() override; + void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + + void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; + void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished + + 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 artifactAssembled(const ArtifactLocation &al) override; + void askToAssembleArtifact(const ArtifactLocation & dst) override; + void artifactDisassembled(const ArtifactLocation &al) override; + + void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; + void heroCreated(const CGHeroInstance* hero) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; + void heroInGarrisonChange(const CGTownInstance *town) override; + void heroMoved(const TryMoveHero & details, bool verbose = true) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; + void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; + void heroManaPointsChanged(const CGHeroInstance * hero) override; + void heroMovePointsChanged(const CGHeroInstance * hero) override; + void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; + void receivedResource() override; + void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; + void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override; + void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + 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 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 + void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war + void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles + void newObject(const CGObjectInstance * obj) override; + void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) + void yourTurn(QueryID queryID) override; + void availableCreaturesChanged(const CGDwelling *town) override; + void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it + void playerBonusChanged(const Bonus &bonus, bool gain) override; + void requestRealized(PackageApplied *pa) override; + void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; + void centerView (int3 pos, int focusTime) override; + void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; + void objectPropertyChanged(const SetObjectProperty * sop) override; + void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override; + 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 playerEndsTurn(PlayerColor player) override; + void saveGame(BinarySerializer & h, const int version) override; //saving + void loadGame(BinaryDeserializer & h, const int version) override; //loading + void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; + + //for battles + void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleLogMessage(const BattleID & battleID, const std::vector & lines) override; + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + 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 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 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 + void battleGateStateChanged(const BattleID & battleID, const EGateState state) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + +public: // public interface for use by client via LOCPLINT access + + // part of GameInterface that is also used by client code + void showPuzzleMap() override; + void viewWorldMap() override; + void showQuestLog() override; + void showThievesGuildWindow (const CGObjectInstance * obj) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; + 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 + void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero + + void showInfoDialog(const std::string &text, std::shared_ptr component); + void showInfoDialog(const std::string &text, const std::vector> & components = std::vector>(), int soundID = 0); + void showInfoDialogAndWait(std::vector & components, const MetaString & text); + void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); + + void moveHero(const CGHeroInstance *h, const CGPath& path); + + void tryDigging(const CGHeroInstance *h); + void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; + void proposeLoadingGame(); + void performAutosave(); + void gamePause(bool pause); + + ///returns true if all events are processed internally + bool capturedAllEvents(); + + CPlayerInterface(PlayerColor Player); + ~CPlayerInterface(); + +private: + struct IgnoreEvents + { + CPlayerInterface & owner; + IgnoreEvents(CPlayerInterface & Owner):owner(Owner) + { + owner.ignoreEvents = true; + }; + ~IgnoreEvents() + { + owner.ignoreEvents = false; + }; + }; + + void heroKilled(const CGHeroInstance* hero); + void garrisonsChanged(std::vector objs); + void requestReturningToMainMenu(bool won); + void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close + void initializeHeroTownList(); + int getLastIndex(std::string namePrefix); +}; + +/// Provides global access to instance of interface of currently active player +extern CPlayerInterface * LOCPLINT; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index b2fc6f70f..2def4c5fa 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -22,14 +22,13 @@ #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" +#include "mainmenu/CHighScoreScreen.h" #ifdef VCMI_ANDROID #include "../lib/CAndroidVMHelper.h" #elif defined(VCMI_IOS) #include "ios/utils.h" #include -#else -#include "../lib/Interprocess.h" #endif #ifdef SINGLE_PROCESS_APP @@ -39,17 +38,19 @@ #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CThreadHelper.h" -#include "../lib/NetPackVisitor.h" #include "../lib/StartInfo.h" +#include "../lib/TurnTimerInfo.h" #include "../lib/VCMIDirs.h" #include "../lib/campaign/CampaignState.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/mapObjects/MiscObjects.h" +#include "../lib/modding/ModIncompatibility.h" #include "../lib/rmg/CMapGenOptions.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" +#include "../lib/UnlockGuard.h" #include #include @@ -89,12 +90,12 @@ template class CApplyOnLobby : public CBaseForLobbyApply public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); - logNetwork->trace("\tImmediately apply on lobby: %s", typeList.getTypeInfo(ptr)->name()); + logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ptr).name()); ptr->visit(visitor); return visitor.getResult(); @@ -105,7 +106,7 @@ public: T * ptr = static_cast(pack); ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby); - logNetwork->trace("\tApply on lobby from queue: %s", typeList.getTypeInfo(ptr)->name()); + logNetwork->trace("\tApply on lobby from queue: %s", typeid(ptr).name()); ptr->visit(visitor); } }; @@ -145,6 +146,7 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std:: { hostClientId = -1; state = EClientState::NONE; + mapToStart = nullptr; th = std::make_unique(); packsForLobbyScreen.clear(); c.reset(); @@ -157,29 +159,6 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std:: myNames = *names; else myNames.push_back(settings["general"]["playerName"].String()); - -#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) - shm.reset(); - - if(!settings["session"]["disable-shm"].Bool()) - { - std::string sharedMemoryName = "vcmi_memory"; - if(settings["session"]["enable-shm-uuid"].Bool()) - { - //used or automated testing when multiple clients start simultaneously - sharedMemoryName += "_" + uuid; - } - try - { - shm = std::make_shared(sharedMemoryName, true); - } - catch(...) - { - shm.reset(); - logNetwork->error("Cannot open interprocess memory. Continue without it..."); - } - } -#endif } void CServerHandler::startLocalServerAndConnect() @@ -197,7 +176,7 @@ void CServerHandler::startLocalServerAndConnect() CInfoWindow::showInfoDialog(errorMsg, {}); return; } - catch(...) + catch(std::runtime_error & error) { //no connection means that port is not busy and we can start local server } @@ -255,24 +234,16 @@ void CServerHandler::startLocalServerAndConnect() while(!androidTestServerReadyFlag.load()) { logNetwork->info("still waiting..."); - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } logNetwork->info("waiting for server finished..."); androidTestServerReadyFlag = false; -#else - if(shm) - shm->sr->waitTillReady(); #endif logNetwork->trace("Waiting for server: %d ms", th->getDiff()); th->update(); //put breakpoint here to attach to server before it does something stupid -#if !defined(VCMI_MOBILE) - const ui16 port = shm ? shm->sr->port : 0; -#else - const ui16 port = 0; -#endif - justConnectToServer(localhostAddress, port); + justConnectToServer(localhostAddress, 0); logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); } @@ -290,10 +261,10 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po port ? port : getHostPort(), NAME, uuid); } - catch(...) + catch(std::runtime_error & error) { - logNetwork->error("\nCannot establish connection! Retrying within 1 second"); - boost::this_thread::sleep(boost::posix_time::seconds(1)); + logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } } @@ -325,11 +296,11 @@ void CServerHandler::applyPacksOnLobbyScreen() boost::unique_lock lock(*mx); while(!packsForLobbyScreen.empty()) { - boost::unique_lock guiLock(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); CPackForLobby * pack = packsForLobbyScreen.front(); packsForLobbyScreen.pop_front(); - CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - apply->applyOnLobbyScreen(static_cast(SEL), this, pack); + CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier + apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); GH.windows().totalRedraw(); delete pack; } @@ -339,7 +310,7 @@ void CServerHandler::stopServerConnection() { if(c->handler) { - while(!c->handler->timed_join(boost::posix_time::milliseconds(50))) + while(!c->handler->timed_join(boost::chrono::milliseconds(50))) applyPacksOnLobbyScreen(); c->handler->join(); } @@ -432,6 +403,7 @@ void CServerHandler::sendClientDisconnecting() return; state = EClientState::DISCONNECTING; + mapToStart = nullptr; LobbyClientDisconnected lcd; lcd.clientId = c->connectionID; logNetwork->info("Connection has been requested to be closed."); @@ -445,6 +417,13 @@ void CServerHandler::sendClientDisconnecting() logNetwork->info("Sent leaving signal to the server"); } sendLobbyPack(lcd); + + { + // Network thread might be applying network pack at this moment + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + c->close(); + c.reset(); + } } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) @@ -490,11 +469,19 @@ void CServerHandler::setPlayer(PlayerColor color) const sendLobbyPack(lsp); } -void CServerHandler::setPlayerOption(ui8 what, si8 dir, PlayerColor player) const +void CServerHandler::setPlayerName(PlayerColor color, const std::string & name) const +{ + LobbySetPlayerName lspn; + lspn.color = color; + lspn.name = name; + sendLobbyPack(lspn); +} + +void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const { LobbyChangePlayerOption lcpo; lcpo.what = what; - lcpo.direction = dir; + lcpo.value = value; lcpo.color = player; sendLobbyPack(lcpo); } @@ -506,11 +493,17 @@ void CServerHandler::setDifficulty(int to) const sendLobbyPack(lsd); } -void CServerHandler::setTurnLength(int npos) const +void CServerHandler::setSimturnsInfo(const SimturnsInfo & info) const +{ + LobbySetSimturns pack; + pack.simturnsInfo = info; + sendLobbyPack(pack); +} + +void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const { - vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1); LobbySetTurnTime lstt; - lstt.turnTime = GameConstants::POSSIBLE_TURNTIME[npos]; + lstt.turnTimerInfo = info; sendLobbyPack(lstt); } @@ -567,6 +560,8 @@ void CServerHandler::sendGuiAction(ui8 action) const void CServerHandler::sendRestartGame() const { + GH.windows().createAndPushWindow(); + LobbyEndGame endGame; endGame.closeConnection = false; endGame.restart = true; @@ -579,13 +574,20 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); } - catch(CModHandler::Incompatibility & e) + catch(ModIncompatibility & e) { logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); - - auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); - + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } showServerError(errorMsg); return false; } @@ -602,7 +604,8 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const void CServerHandler::sendStartGame(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); - + GH.windows().createAndPushWindow(); + LobbyStartGame lsg; if(client) { @@ -616,12 +619,19 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const c->disableStackSendingByID(); } +void CServerHandler::startMapAfterConnection(std::shared_ptr to) +{ + mapToStart = to; +} + void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState) { if(CMM) CMM->disable(); client = new CClient(); + highScoreCalc = nullptr; + switch(si->mode) { case StartInfo::NEW_GAME: @@ -660,9 +670,6 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta void CServerHandler::endGameplay(bool closeConnection, bool restart) { - client->endGame(); - vstd::clear_pointer(client); - if(closeConnection) { // Game is ending @@ -670,11 +677,14 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) CSH->sendClientDisconnecting(); logNetwork->info("Closed connection."); } + + client->endGame(); + vstd::clear_pointer(client); + if(!restart) { if(CMM) { - GH.terminate_cond->setn(false); GH.curInt = CMM.get(); CMM->enable(); } @@ -684,22 +694,34 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) } } - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); + if(c) + { + c->enterLobbyConnectionMode(); + c->disableStackSendingByID(); + } //reset settings Settings saveSession = settings.write["server"]["reconnect"]; saveSession->Bool() = false; } -void CServerHandler::startCampaignScenario(std::shared_ptr cs) +void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) { std::shared_ptr ourCampaign = cs; if (!cs) ourCampaign = si->campState; - GH.dispatchMainThread([ourCampaign]() + if(highScoreCalc == nullptr) + { + highScoreCalc = std::make_shared(); + highScoreCalc->isCampaign = true; + highScoreCalc->parameters.clear(); + } + param.campaignName = cs->getNameTranslated(); + highScoreCalc->parameters.push_back(param); + + GH.dispatchMainThread([ourCampaign, this]() { CSH->campaignServerRestartLock.set(true); CSH->endGameplay(); @@ -707,11 +729,21 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto finisher = [=]() { - if(!ourCampaign->isCampaignFinished()) + if(ourCampaign->campaignSet != "") { - GH.windows().pushWindow(CMM); - GH.windows().pushWindow(CMM->menu); + Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()]; + entry->Bool() = true; + } + + GH.windows().pushWindow(CMM); + GH.windows().pushWindow(CMM->menu); + + if(!ourCampaign->isCampaignFinished()) CMM->openCampaignLobby(ourCampaign); + else + { + CMM->openCampaignScreen(ourCampaign->campaignSet); + GH.windows().createAndPushWindow(true, *highScoreCalc); } }; if(epilogue.hasPrologEpilog) @@ -728,6 +760,9 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) void CServerHandler::showServerError(const std::string & txt) const { + if(auto w = GH.windows().topWindow()) + GH.windows().popWindow(w); + CInfoWindow::showInfoDialog(txt, {}); } @@ -745,7 +780,7 @@ int CServerHandler::howManyPlayerInterfaces() ui8 CServerHandler::getLoadMode() { - if(state == EClientState::GAMEPLAY) + if(loadMode != ELoadMode::TUTORIAL && state == EClientState::GAMEPLAY) { if(si->campState) return ELoadMode::CAMPAIGN; @@ -791,7 +826,7 @@ void CServerHandler::debugStartTest(std::string filename, bool save) if(save) { resetStateForLobby(StartInfo::LOAD_GAME); - mapInfo->saveInit(ResourceID(filename, EResType::SAVEGAME)); + mapInfo->saveInit(ResourcePath(filename, EResType::SAVEGAME)); screenType = ESelectionScreen::loadGame; } else @@ -805,20 +840,20 @@ void CServerHandler::debugStartTest(std::string filename, bool save) else startLocalServerAndConnect(); - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); while(!settings["session"]["headless"].Bool() && !GH.windows().topWindow()) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); while(!mi || mapInfo->fileURI != CSH->mi->fileURI) { setMapInfo(mapInfo); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } // "Click" on color to remove us from it setPlayer(myFirstColor()); while(myFirstColor() != PlayerColor::CANNOT_DETERMINE) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); while(true) { @@ -831,7 +866,7 @@ void CServerHandler::debugStartTest(std::string filename, bool save) { } - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } } @@ -861,16 +896,16 @@ public: void CServerHandler::threadHandleConnection() { - setThreadName("CServerHandler::threadHandleConnection"); + setThreadName("handleConnection"); c->enterLobbyConnectionMode(); try { sendClientConnecting(); - while(c->connected) + while(c && c->connected) { while(state == EClientState::STARTING) - boost::this_thread::sleep(boost::posix_time::milliseconds(10)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); CPack * pack = c->retrievePack(); if(state == EClientState::DISCONNECTING) @@ -924,7 +959,7 @@ void CServerHandler::threadHandleConnection() void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) { - if(applier->getApplier(typeList.getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) + if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) { if(!settings["session"]["headless"].Bool()) { @@ -942,7 +977,7 @@ void CServerHandler::visitForClient(CPackForClient & clientPack) void CServerHandler::threadRunServer() { #if !defined(VCMI_MOBILE) - setThreadName("CServerHandler::threadRunServer"); + setThreadName("runServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() + " --port=" + std::to_string(getHostPort()) @@ -955,15 +990,9 @@ void CServerHandler::threadRunServer() comm += " --lobby-port=" + std::to_string(settings["session"]["port"].Integer()); comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String(); } - - if(shm) - { - comm += " --enable-shm"; - if(settings["session"]["enable-shm-uuid"].Bool()) - comm += " --enable-shm-uuid"; - } + comm += " > \"" + logName + '\"'; - logGlobal->info("Server command line: %s", comm); + logGlobal->info("Server command line: %s", comm); #ifdef VCMI_WINDOWS int result = -1; @@ -998,7 +1027,8 @@ void CServerHandler::threadRunServer() void CServerHandler::onServerFinished() { threadRunLocalServer.reset(); - CSH->campaignServerRestartLock.setn(false); + if (CSH) + CSH->campaignServerRestartLock.setn(false); } void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 1b4f712de..92ade9162 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CConnection; class PlayerColor; struct StartInfo; +struct TurnTimerInfo; class CMapInfo; class CGameState; @@ -34,6 +35,9 @@ VCMI_LIB_NAMESPACE_END class CClient; class CBaseForLobbyApply; +class HighScoreCalculation; +class HighScoreParameter; + // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { @@ -44,7 +48,8 @@ enum class EClientState : ui8 LOBBY_CAMPAIGN, // Client is on scenario bonus selection screen STARTING, // Gameplay interfaces being created, we pause netpacks retrieving GAMEPLAY, // In-game, used by some UI - DISCONNECTING // We disconnecting, drop all netpacks + DISCONNECTING, // We disconnecting, drop all netpacks + CONNECTION_FAILED // We could not connect to server }; class IServerAPI @@ -62,9 +67,11 @@ public: virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; - virtual void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const = 0; + virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0; + virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; - virtual void setTurnLength(int npos) const = 0; + virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; + virtual void setSimturnsInfo(const SimturnsInfo &) const = 0; virtual void sendMessage(const std::string & txt) const = 0; virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? virtual void sendStartGame(bool allowOnlyAI = false) const = 0; @@ -74,13 +81,19 @@ public: /// structure to handle running server and connecting to it class CServerHandler : public IServerAPI, public LobbyInfo { + friend class ApplyOnLobbyHandlerNetPackVisitor; + std::shared_ptr> applier; std::shared_ptr mx; std::list packsForLobbyScreen; //protected by mx + + std::shared_ptr mapToStart; std::vector myNames; + std::shared_ptr highScoreCalc; + void threadHandleConnection(); void threadRunServer(); void onServerFinished(); @@ -115,7 +128,7 @@ public: void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); void startLocalServerAndConnect(); - void justConnectToServer(const std::string &addr = "", const ui16 port = 0); + void justConnectToServer(const std::string & addr, const ui16 port); void applyPacksOnLobbyScreen(); void stopServerConnection(); @@ -140,20 +153,23 @@ public: void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; - void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const override; + void setPlayerName(PlayerColor color, const std::string & name) const override; + void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; - void setTurnLength(int npos) const override; + void setTurnTimerInfo(const TurnTimerInfo &) const override; + void setSimturnsInfo(const SimturnsInfo &) const override; void sendMessage(const std::string & txt) const override; void sendGuiAction(ui8 action) const override; void sendRestartGame() const override; void sendStartGame(bool allowOnlyAI = false) const override; + void startMapAfterConnection(std::shared_ptr to); bool validateGameStart(bool allowOnlyAI = false) const; void debugStartTest(std::string filename, bool save = false); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void endGameplay(bool closeConnection = true, bool restart = false); - void startCampaignScenario(std::shared_ptr cs = {}); + void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); 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 diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 51ea64e0f..afdd19cc7 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -1,484 +1,672 @@ -/* - * 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 - -#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); - - return static_cast(video->data->read(buf, size)); -} - -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); -} - -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(std::string fname, bool scale) -{ - return open(fname, true, false); -} - -// loop = to loop through the video -// useOverlay = directly write to the screen. -bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scale) -{ - close(); - - this->fname = fname; - doLoop = loop; - frameTime = 0; - - ResourceID resource(std::string("Video/") + fname, EResType::VIDEO); - - if (!CResourceHandler::get()->existsResource(resource)) - { - logGlobal->error("Error: video %s was not found", resource.getName()); - return false; - } - - data = CResourceHandler::get()->load(resource); - - 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 (useOverlay) - { - 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, NULL, 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 ) -{ - 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 - { - open(fname); - 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.clear(); - 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; - } -} - -// Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) -{ - // 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; - - while(nextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - return false; - } - - SDL_Rect rect = CSDL_Ext::toSDL(pos); - - 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 - double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); - uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); - - boost::this_thread::sleep(boost::posix_time::millisec(timeToSleepMillisec)); - } - - return true; -} - -bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, bool stopOnKey, bool scale) -{ - open(name, false, true, scale); - bool ret = playVideo(x, y, stopOnKey); - close(); - return ret; -} - -CVideoPlayer::~CVideoPlayer() -{ - close(); -} - -#endif - +/* + * 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 +// useOverlay = directly write to the screen. +bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, 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 (useOverlay) + { + 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; +} + +// Plays a video. Only works for overlays. +bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) +{ + // 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; + + while(nextFrame()) + { + if(stopOnKey) + { + GH.input().fetchEvents(); + if(GH.input().ignoreEventsUntilInput()) + return false; + } + + SDL_Rect rect = CSDL_Ext::toSDL(pos); + + 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 + double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); + uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); + + boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); + } + + return true; +} + +bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale) +{ + open(name, false, true, scale); + bool ret = playVideo(x, y, stopOnKey); + close(); + return ret; +} + +CVideoPlayer::~CVideoPlayer() +{ + close(); +} + +#endif + diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 684270685..211b9db3f 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -1,115 +1,122 @@ -/* - * 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" - -struct SDL_Surface; -struct SDL_Texture; - -class IVideoPlayer -{ -public: - virtual bool open(std::string 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: - std::string fname; //name of current video file (empty if idle) - - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} - virtual bool openAndPlayVideo(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) - { - return false; - } -}; - -class CEmptyVideoPlayer : 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(std::string name, bool scale = false) override {return false;}; -}; - -#ifndef DISABLE_VIDEO - -#include "../lib/filesystem/CInputStream.h" - -struct AVFormatContext; -struct AVCodecContext; -struct AVCodec; -struct AVFrame; -struct AVIOContext; - -class CVideoPlayer : 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; - - // 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 open(std::string fname, bool loop, bool useOverlay = false, bool scale = false); - -public: - CVideoPlayer(); - ~CVideoPlayer(); - - bool init(); - bool open(std::string 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) 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(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) 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; -}; - -#endif +/* + * 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; + +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, bool stopOnKey = false, bool scale = false) + { + return false; + } + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 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 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, bool stopOnKey = false, bool scale = false) override; + + std::pair, si64> getAudio(const VideoPath & videoToOpen) 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 eec33cb18..f41ba25d4 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -1,759 +1,743 @@ -/* - * Client.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 "Global.h" -#include "StdInc.h" -#include "Client.h" - -#include "CGameInfo.h" -#include "CPlayerInterface.h" -#include "CServerHandler.h" -#include "ClientNetPackVisitors.h" -#include "adventureMap/AdventureMapInterface.h" -#include "battle/BattleInterface.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "mapView/mapHandler.h" - -#include "../CCallback.h" -#include "../lib/CConfigHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/VCMIDirs.h" -#include "../lib/UnlockGuard.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" -#include "../lib/serializer/Connection.h" - -#include -#include - -#if SCRIPTING_ENABLED -#include "../lib/ScriptHandler.h" -#endif - -#ifdef VCMI_ANDROID -#include "lib/CAndroidVMHelper.h" - -#ifndef SINGLE_PROCESS_APP -std::atomic_bool androidTestServerReadyFlag; -#endif -#endif - -ThreadSafeVector CClient::waitingRequest; - -template class CApplyOnCL; - -class CBaseForCLApply -{ -public: - virtual void applyOnClAfter(CClient * cl, void * pack) const =0; - virtual void applyOnClBefore(CClient * cl, void * 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, void * pack) const override - { - T * ptr = static_cast(pack); - ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); - ptr->visit(visitor); - } - void applyOnClBefore(CClient * cl, void * 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, void * pack) const override - { - logGlobal->error("Cannot apply on CL plain CPack!"); - assert(0); - } - void applyOnClBefore(CClient * cl, void * 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_), - mainCallback(mainCallback_) -{ - -} - -const Services * CPlayerEnvironment::services() const -{ - return VLC; -} - -vstd::CLoggerBase * CPlayerEnvironment::logger() const -{ - return logGlobal; -} - -events::EventBus * CPlayerEnvironment::eventBus() const -{ - return cl->eventBus();//always get actual value -} - -const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle() const -{ - return mainCallback.get(); -} - -const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const -{ - return mainCallback.get(); -} - - -CClient::CClient() -{ - waitingRequest.clear(); - applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); - IObjectInterface::cb = this; - gs = nullptr; -} - -CClient::~CClient() -{ - IObjectInterface::cb = nullptr; -} - -const Services * CClient::services() const -{ - return VLC; //todo: this should be CGI -} - -const CClient::BattleCb * CClient::battle() const -{ - return this; -} - -const CClient::GameCb * CClient::game() const -{ - return this; -} - -vstd::CLoggerBase * CClient::logger() const -{ - return logGlobal; -} - -events::EventBus * CClient::eventBus() const -{ - return clientEventBus.get(); -} - -void CClient::newGame(CGameState * initializedGameState) -{ - CSH->th->update(); - CMapService mapService; - gs = initializedGameState ? initializedGameState : new CGameState(); - gs->preInit(VLC); - logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); - if(!initializedGameState) - gs->init(&mapService, CSH->si.get(), settings["general"]["saveRandomMaps"].Bool()); - logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); - - initMapHandler(); - reinitScripting(); - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::loadGame(CGameState * initializedGameState) -{ - logNetwork->info("Loading procedure started!"); - - logNetwork->info("Game state was transferred over network, loading."); - gs = initializedGameState; - - gs->preInit(VLC); - gs->updateOnLoad(CSH->si.get()); - logNetwork->info("Game loaded, initialize interfaces."); - - initMapHandler(); - - 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(ResourceID(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.fileVersion); - - 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, const int version) -{ - 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, version); - } - -#if SCRIPTING_ENABLED - if(version >= 800) - { - JsonNode scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - h & scriptsState; - } -#endif -} - -void CClient::serialize(BinaryDeserializer & h, const int version) -{ - 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->c->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, version); - - 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->curB) - { - logNetwork->error("Game cannot be saved during battle!"); - return; - } - - SaveGame save_game(fname); - sendRequest(&save_game, PlayerColor::NEUTRAL); -} - -void CClient::endGame() -{ -#if SCRIPTING_ENABLED - clientScripts.reset(); -#endif - - //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) - for(auto & i : playerint) - i.second->finish(); - - GH.curInt = nullptr; - { - boost::unique_lock un(*CPlayerInterface::pim); - logNetwork->info("Ending current game!"); - removeGUI(); - - vstd::clear_pointer(const_cast(CGI)->mh); - vstd::clear_pointer(gs); - - logNetwork->info("Deleted mapHandler and gameState."); - } - - CPlayerInterface::battleInt.reset(); - playerint.clear(); - battleints.clear(); - battleCallbacks.clear(); - playerEnvironments.clear(); - logNetwork->info("Deleted playerInts."); - logNetwork->info("Client stopped."); -} - -void CClient::initMapHandler() -{ - // TODO: CMapHandler initialization can probably go somewhere else - // It's can't be before initialization of interfaces - // During loading CPlayerInterface from serialized state it's depend on MH - if(!settings["session"]["headless"].Bool()) - { - const_cast(CGI)->mh = new CMapHandler(gs->map); - logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); - } - - pathCache.clear(); -} - -void CClient::initPlayerEnvironments() -{ - playerEnvironments.clear(); - - auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); - bool hasHumanPlayer = false; - for(auto & color : allPlayers) - { - logNetwork->info("Preparing environment for player %s", color.getStr()); - playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); - - if(!hasHumanPlayer && gs->players[color].isHuman()) - hasHumanPlayer = true; - } - - if(!hasHumanPlayer) - { - Settings session = settings.write["session"]; - session["spectate"].Bool() = true; - session["spectate-skip-battle-result"].Bool() = true; - session["spectate-ignore-hero"].Bool() = true; - } - - if(settings["session"]["spectate"].Bool()) - { - playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); - } -} - -void CClient::initPlayerInterfaces() -{ - for(auto & playerInfo : gs->scenarioOps->playerInfos) - { - PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) - continue; - - if(!vstd::contains(playerint, color)) - { - logNetwork->info("Preparing interface for player %s", color.getStr()); - if(playerInfo.second.isControlledByAI()) - { - bool alliedToHuman = false; - for(auto & allyInfo : gs->scenarioOps->playerInfos) - if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) - alliedToHuman = true; - - auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); - logNetwork->info("Player %s will be lead by %s", color.getStr(), AiToGive); - installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); - } - else - { - logNetwork->info("Player %s will be lead by human", color.getStr()); - installNewPlayerInterface(std::make_shared(color), color); - } - } - } - - if(settings["session"]["spectate"].Bool()) - { - installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); - } - - if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) - installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); - - logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); -} - -std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) -{ - if(ps.name.size()) - { - const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); - if(boost::filesystem::exists(aiPath)) - return ps.name; - } - - return aiNameForPlayer(battleAI, alliedToHuman); -} - -std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) -{ - const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; - std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); - std::string goodBattleAI = settings["server"]["neutralAI"].String(); - std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; - std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; - - //TODO what about human players - if(battleints.size() >= sensibleAILimit) - return badAI; - - return goodAI; -} - -void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) -{ - boost::unique_lock un(*CPlayerInterface::pim); - - playerint[color] = gameInterface; - - logGlobal->trace("\tInitializing the interface for player %s", color.getStr()); - auto cb = std::make_shared(gs, color, this); - battleCallbacks[color] = cb; - gameInterface->initGameInterface(playerEnvironments.at(color), cb); - - installNewBattleInterface(gameInterface, color, battlecb); -} - -void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) -{ - boost::unique_lock un(*CPlayerInterface::pim); - - battleints[color] = battleInterface; - - if(needCallback) - { - logGlobal->trace("\tInitializing the battle interface for player %s", color.getStr()); - auto cbc = std::make_shared(color, this); - battleCallbacks[color] = cbc; - battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); - } -} - -void CClient::handlePack(CPack * pack) -{ - CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - if(apply) - { - boost::unique_lock guiLock(*CPlayerInterface::pim); - apply->applyOnClBefore(this, pack); - logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); - gs->apply(pack); - logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); - apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); - } - else - { - logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); - } - delete pack; -} - -int CClient::sendRequest(const CPackForServer * request, PlayerColor player) -{ - static ui32 requestCounter = 0; - - ui32 requestID = requestCounter++; - 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; - CSH->c->sendPack(request); - if(vstd::contains(playerint, player)) - playerint[player]->requestSent(request, requestID); - - return requestID; -} - -void CClient::battleStarted(const BattleInfo * info) -{ - setBattle(info); - - for(auto & battleCb : battleCallbacks) - { - if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || battleCb.first >= PlayerColor::PLAYER_LIMIT) - { - battleCb.second->setBattle(info); - } - } - - std::shared_ptr att, def; - auto & leftSide = info->sides[0], & rightSide = info->sides[1]; - - //If quick combat is not, do not prepare interfaces for battleint - auto callBattleStart = [&](PlayerColor color, ui8 side) - { - if(vstd::contains(battleints, color)) - battleints[color]->battleStart(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); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); - - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); - - //Remove player interfaces for auto battle (quickCombat option) - if(att && att->isAutoFightOn) - { - if (att->cb->battleGetTacticDist()) - { - auto side = att->cb->playerToSide(att->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); - att->cb->battleMakeTacticAction(action); - } - - att.reset(); - def.reset(); - } - - if(!settings["session"]["headless"].Bool()) - { - if(att || def) - { - boost::unique_lock un(*CPlayerInterface::pim); - CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); - } - else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - { - //TODO: This certainly need improvement - auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->setBattle(info); - boost::unique_lock un(*CPlayerInterface::pim); - CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); - } - } - - if(info->tacticDistance) - { - auto tacticianColor = info->sides[info->tacticsSide].color; - - if (vstd::contains(battleints, tacticianColor)) - battleints[tacticianColor]->yourTacticPhase(info->tacticDistance); - } -} - -void CClient::battleFinished() -{ - for(auto & side : gs->curB->sides) - if(battleCallbacks.count(side.color)) - battleCallbacks[side.color]->setBattle(nullptr); - - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); - - setBattle(nullptr); - gs->curB.dellNull(); -} - -void CClient::startPlayerBattleAction(PlayerColor color) -{ - assert(vstd::contains(battleints, color)); - - if(vstd::contains(battleints, color)) - { - // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human); - - assert(vstd::contains(battleints, color)); - battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); - } -} - -void CClient::invalidatePaths() -{ - boost::unique_lock pathLock(pathCacheMutex); - pathCache.clear(); -} - -std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) -{ - assert(h); - boost::unique_lock pathLock(pathCacheMutex); - - auto iter = pathCache.find(h); - - if(iter == std::end(pathCache)) - { - std::shared_ptr paths = std::make_shared(getMapSize(), h); - - gs->calculatePaths(h, *paths.get()); - - pathCache[h] = paths; - return paths; - } - else - { - return iter->second; - } -} - -PlayerColor CClient::getLocalPlayer() const -{ - if(LOCPLINT) - return LOCPLINT->playerID; - return getCurrentPlayer(); -} - -#if SCRIPTING_ENABLED -scripting::Pool * CClient::getGlobalContextPool() const -{ - return clientScripts.get(); -} - -scripting::Pool * CClient::getContextPool() const -{ - return clientScripts.get(); -} -#endif - -void CClient::reinitScripting() -{ - clientEventBus = std::make_unique(); -#if SCRIPTING_ENABLED - clientScripts.reset(new scripting::PoolImpl(this)); -#endif -} - -void CClient::removeGUI() -{ - // CClient::endGame - GH.curInt = nullptr; - GH.windows().clear(); - adventureInt.reset(); - logGlobal->info("Removed GUI."); - - LOCPLINT = nullptr; -} - -#ifdef VCMI_ANDROID -#ifndef SINGLE_PROCESS_APP -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server closed signal"); - if (CSH) { - CSH->campaignServerRestartLock.setn(false); - } -} - -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server ready signal"); - androidTestServerReadyFlag.store(true); -} -#endif - -extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) -{ - logGlobal->info("Received emergency save game request"); - if(!LOCPLINT || !LOCPLINT->cb) - { - return false; - } - - LOCPLINT->cb->save("Saves/_Android_Autosave"); - return true; -} -#endif +/* + * Client.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 "Global.h" +#include "StdInc.h" +#include "Client.h" + +#include "CGameInfo.h" +#include "CPlayerInterface.h" +#include "CServerHandler.h" +#include "ClientNetPackVisitors.h" +#include "adventureMap/AdventureMapInterface.h" +#include "battle/BattleInterface.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" +#include "../lib/CConfigHandler.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/UnlockGuard.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/registerTypes/RegisterTypesClientPacks.h" +#include "../lib/serializer/Connection.h" + +#include +#include + +#if SCRIPTING_ENABLED +#include "../lib/ScriptHandler.h" +#endif + +#ifdef VCMI_ANDROID +#include "lib/CAndroidVMHelper.h" + +#ifndef SINGLE_PROCESS_APP +std::atomic_bool androidTestServerReadyFlag; +#endif +#endif + +ThreadSafeVector CClient::waitingRequest; + +template class CApplyOnCL; + +class CBaseForCLApply +{ +public: + virtual void applyOnClAfter(CClient * cl, void * pack) const =0; + virtual void applyOnClBefore(CClient * cl, void * 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, void * pack) const override + { + T * ptr = static_cast(pack); + ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); + ptr->visit(visitor); + } + void applyOnClBefore(CClient * cl, void * 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, void * pack) const override + { + logGlobal->error("Cannot apply on CL plain CPack!"); + assert(0); + } + void applyOnClBefore(CClient * cl, void * 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_), + mainCallback(mainCallback_) +{ + +} + +const Services * CPlayerEnvironment::services() const +{ + return VLC; +} + +vstd::CLoggerBase * CPlayerEnvironment::logger() const +{ + return logGlobal; +} + +events::EventBus * CPlayerEnvironment::eventBus() const +{ + return cl->eventBus();//always get actual value +} + +const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const +{ + return mainCallback->getBattle(battleID).get(); +} + +const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const +{ + return mainCallback.get(); +} + + +CClient::CClient() +{ + waitingRequest.clear(); + applier = std::make_shared>(); + registerTypesClientPacks(*applier); + IObjectInterface::cb = this; + gs = nullptr; +} + +CClient::~CClient() +{ + IObjectInterface::cb = nullptr; +} + +const Services * CClient::services() const +{ + return VLC; //todo: this should be CGI +} + +const CClient::BattleCb * CClient::battle(const BattleID & battleID) const +{ + return nullptr; //todo? +} + +const CClient::GameCb * CClient::game() const +{ + return this; +} + +vstd::CLoggerBase * CClient::logger() const +{ + return logGlobal; +} + +events::EventBus * CClient::eventBus() const +{ + return clientEventBus.get(); +} + +void CClient::newGame(CGameState * initializedGameState) +{ + CSH->th->update(); + CMapService mapService; + gs = initializedGameState ? initializedGameState : new CGameState(); + gs->preInit(VLC); + logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); + if(!initializedGameState) + { + Load::ProgressAccumulator progressTracking; + gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); + } + logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); + + initMapHandler(); + reinitScripting(); + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::loadGame(CGameState * initializedGameState) +{ + logNetwork->info("Loading procedure started!"); + + logNetwork->info("Game state was transferred over network, loading."); + gs = initializedGameState; + + gs->preInit(VLC); + gs->updateOnLoad(CSH->si.get()); + logNetwork->info("Game loaded, initialize interfaces."); + + initMapHandler(); + + 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.fileVersion); + + 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, const int version) +{ + 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, version); + } + +#if SCRIPTING_ENABLED + if(version >= 800) + { + JsonNode scriptsState; + clientScripts->serializeState(h.saving, scriptsState); + h & scriptsState; + } +#endif +} + +void CClient::serialize(BinaryDeserializer & h, const int version) +{ + 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->c->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, version); + + 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()) + { + logNetwork->error("Game cannot be saved during battle!"); + return; + } + + SaveGame save_game(fname); + sendRequest(&save_game, PlayerColor::NEUTRAL); +} + +void CClient::endGame() +{ +#if SCRIPTING_ENABLED + clientScripts.reset(); +#endif + + //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) + for(auto & i : playerint) + i.second->finish(); + + GH.curInt = nullptr; + { + logNetwork->info("Ending current game!"); + removeGUI(); + + vstd::clear_pointer(const_cast(CGI)->mh); + vstd::clear_pointer(gs); + + logNetwork->info("Deleted mapHandler and gameState."); + } + + CPlayerInterface::battleInt.reset(); + playerint.clear(); + battleints.clear(); + battleCallbacks.clear(); + playerEnvironments.clear(); + logNetwork->info("Deleted playerInts."); + logNetwork->info("Client stopped."); +} + +void CClient::initMapHandler() +{ + // TODO: CMapHandler initialization can probably go somewhere else + // It's can't be before initialization of interfaces + // During loading CPlayerInterface from serialized state it's depend on MH + if(!settings["session"]["headless"].Bool()) + { + const_cast(CGI)->mh = new CMapHandler(gs->map); + logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); + } + + pathCache.clear(); +} + +void CClient::initPlayerEnvironments() +{ + playerEnvironments.clear(); + + auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); + bool hasHumanPlayer = false; + for(auto & color : allPlayers) + { + logNetwork->info("Preparing environment for player %s", color.toString()); + playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); + + if(!hasHumanPlayer && gs->players[color].isHuman()) + hasHumanPlayer = true; + } + + if(!hasHumanPlayer) + { + Settings session = settings.write["session"]; + session["spectate"].Bool() = true; + session["spectate-skip-battle-result"].Bool() = true; + session["spectate-ignore-hero"].Bool() = true; + } + + if(settings["session"]["spectate"].Bool()) + { + playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); + } +} + +void CClient::initPlayerInterfaces() +{ + for(auto & playerInfo : gs->scenarioOps->playerInfos) + { + PlayerColor color = playerInfo.first; + if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) + continue; + + if(!vstd::contains(playerint, color)) + { + logNetwork->info("Preparing interface for player %s", color.toString()); + if(playerInfo.second.isControlledByAI()) + { + bool alliedToHuman = false; + for(auto & allyInfo : gs->scenarioOps->playerInfos) + if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) + alliedToHuman = true; + + auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); + logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); + installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); + } + else + { + logNetwork->info("Player %s will be lead by human", color.toString()); + installNewPlayerInterface(std::make_shared(color), color); + } + } + } + + if(settings["session"]["spectate"].Bool()) + { + installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); + } + + if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) + installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); + + logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); +} + +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) +{ + if(ps.name.size()) + { + const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); + if(boost::filesystem::exists(aiPath)) + return ps.name; + } + + return aiNameForPlayer(battleAI, alliedToHuman); +} + +std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) +{ + const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; + std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); + std::string goodBattleAI = settings["server"]["neutralAI"].String(); + std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; + std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; + + //TODO what about human players + if(battleints.size() >= sensibleAILimit) + return badAI; + + return goodAI; +} + +void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) +{ + playerint[color] = gameInterface; + + logGlobal->trace("\tInitializing the interface for player %s", color.toString()); + auto cb = std::make_shared(gs, color, this); + battleCallbacks[color] = cb; + gameInterface->initGameInterface(playerEnvironments.at(color), cb); + + installNewBattleInterface(gameInterface, color, battlecb); +} + +void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) +{ + battleints[color] = battleInterface; + + if(needCallback) + { + logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); + auto cbc = std::make_shared(color, this); + battleCallbacks[color] = cbc; + battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); + } +} + +void CClient::handlePack(CPack * pack) +{ + CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier + if(apply) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + apply->applyOnClBefore(this, pack); + logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); + 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()); + } + else + { + logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(pack).name()); + } + delete pack; +} + +int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +{ + static ui32 requestCounter = 0; + + ui32 requestID = requestCounter++; + 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; + CSH->c->sendPack(request); + if(vstd::contains(playerint, player)) + playerint[player]->requestSent(request, requestID); + + return requestID; +} + +void CClient::battleStarted(const BattleInfo * info) +{ + for(auto & battleCb : battleCallbacks) + { + if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) + || !battleCb.first.isValidPlayer()) + { + battleCb.second->onBattleStarted(info); + } + } + + std::shared_ptr att, def; + auto & leftSide = info->sides[0], & rightSide = info->sides[1]; + + //If quick combat is not, do not prepare interfaces for battleint + auto callBattleStart = [&](PlayerColor color, ui8 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); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, 1); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + + //Remove player interfaces for auto battle (quickCombat option) + if(att && att->isAutoFightOn) + { + if (att->cb->getBattle(info->battleID)->battleGetTacticDist()) + { + auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID); + auto action = BattleAction::makeEndOFTacticPhase(*side); + att->cb->battleMakeTacticAction(info->battleID, action); + } + + att.reset(); + def.reset(); + } + + if(!settings["session"]["headless"].Bool()) + { + if(att || def) + { + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); + } + else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + { + //TODO: This certainly need improvement + auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->onBattleStarted(info); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); + } + } + + if(info->tacticDistance) + { + auto tacticianColor = info->sides[info->tacticsSide].color; + + if (vstd::contains(battleints, tacticianColor)) + battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); + } +} + +void CClient::battleFinished(const BattleID & battleID) +{ + for(auto & side : gs->getBattle(battleID)->sides) + if(battleCallbacks.count(side.color)) + battleCallbacks[side.color]->onBattleEnded(battleID); + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); +} + +void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) +{ + if (battleints.count(color) == 0) + return; // not our combat in MP + + auto battleint = battleints.at(color); + + if (!battleint->human) + { + // we want to avoid locking gamestate and causing UI to freeze while AI is making turn + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } + else + { + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } +} + +void CClient::invalidatePaths() +{ + boost::unique_lock pathLock(pathCacheMutex); + pathCache.clear(); +} + +std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) +{ + assert(h); + boost::unique_lock pathLock(pathCacheMutex); + + auto iter = pathCache.find(h); + + if(iter == std::end(pathCache)) + { + std::shared_ptr paths = std::make_shared(getMapSize(), h); + + gs->calculatePaths(h, *paths.get()); + + pathCache[h] = paths; + return paths; + } + else + { + return iter->second; + } +} + +#if SCRIPTING_ENABLED +scripting::Pool * CClient::getGlobalContextPool() const +{ + return clientScripts.get(); +} +#endif + +void CClient::reinitScripting() +{ + clientEventBus = std::make_unique(); +#if SCRIPTING_ENABLED + clientScripts.reset(new scripting::PoolImpl(this)); +#endif +} + +void CClient::removeGUI() const +{ + // CClient::endGame + GH.curInt = nullptr; + GH.windows().clear(); + adventureInt.reset(); + logGlobal->info("Removed GUI."); + + LOCPLINT = nullptr; +} + +#ifdef VCMI_ANDROID +#ifndef SINGLE_PROCESS_APP +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls) +{ + logNetwork->info("Received server closed signal"); + if (CSH) { + CSH->campaignServerRestartLock.setn(false); + } +} + +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls) +{ + logNetwork->info("Received server ready signal"); + androidTestServerReadyFlag.store(true); +} +#endif + +extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) +{ + logGlobal->info("Received emergency save game request"); + if(!LOCPLINT || !LOCPLINT->cb) + { + return false; + } + + LOCPLINT->cb->save("Saves/_Android_Autosave"); + return true; +} +#endif diff --git a/client/Client.h b/client/Client.h index 64a3f5f04..32cbe1db2 100644 --- a/client/Client.h +++ b/client/Client.h @@ -1,242 +1,240 @@ -/* - * Client.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 "../lib/IGameCallback.h" -#include "../lib/battle/BattleAction.h" -#include "../lib/battle/CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct CPack; -struct CPackForServer; -class IBattleEventsReceiver; -class CBattleGameInterface; -class CGameInterface; -class BinaryDeserializer; -class BinarySerializer; - -template class CApplier; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class PoolImpl; -} -#endif - -namespace events -{ - class EventBus; -} - -VCMI_LIB_NAMESPACE_END - -class CBattleCallback; -class CCallback; -class CClient; -class CBaseForCLApply; - -namespace boost { class thread; } - -template -class ThreadSafeVector -{ - using TLock = boost::unique_lock; - std::vector items; - boost::mutex mx; - boost::condition_variable cond; - -public: - void clear() - { - TLock lock(mx); - items.clear(); - cond.notify_all(); - } - - void pushBack(const T & item) - { - TLock lock(mx); - items.push_back(item); - cond.notify_all(); - } - - void waitWhileContains(const T & item) - { - TLock lock(mx); - while(vstd::contains(items, item)) - cond.wait(lock); - } - - bool tryRemovingElement(const T & item) //returns false if element was not present - { - TLock lock(mx); - auto itr = vstd::find(items, item); - if(itr == items.end()) //not in container - { - return false; - } - - items.erase(itr); - cond.notify_all(); - return true; - } -}; - -class CPlayerEnvironment : public Environment -{ -public: - PlayerColor player; - CClient * cl; - std::shared_ptr mainCallback; - - CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_); - const Services * services() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - const BattleCb * battle() const override; - const GameCb * game() const override; -}; - -/// Class which handles client - server logic -class CClient : public IGameCallback, public CBattleInfoCallback, public Environment -{ -public: - std::map> playerint; - std::map> battleints; - - std::map>> additionalBattleInts; - - std::optional curbaction; - - CClient(); - ~CClient(); - - const Services * services() const override; - const BattleCb * battle() const override; - const GameCb * game() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - - void newGame(CGameState * gameState); - void loadGame(CGameState * gameState); - void serialize(BinarySerializer & h, const int version); - void serialize(BinaryDeserializer & h, const int version); - - void save(const std::string & fname); - void endGame(); - - 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); - 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 battleStarted(const BattleInfo * info); - void battleFinished(); - void startPlayerBattleAction(PlayerColor color); - - void invalidatePaths(); - std::shared_ptr getPathsInfo(const CGHeroInstance * h); - virtual PlayerColor getLocalPlayer() const override; - - friend class CCallback; //handling players actions - friend class CBattleCallback; //handling players actions - - void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; - bool removeObject(const CGObjectInstance * obj) override {return false;}; - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override {}; - void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::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 showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; - void showTeleportDialog(TeleportDialog * iw) override {}; - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {}; - void giveResource(PlayerColor player, GameResID which, int val) override {}; - virtual void giveResources(PlayerColor player, TResources resources) override {}; - - void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; - void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; - bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; - bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; - bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; - bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; - bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} - bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} - void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} - bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} - - 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 giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; - void removeArtifact(const ArtifactLocation & al) override {}; - bool moveArtifact(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 - bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; - void giveHeroBonus(GiveBonus * bonus) override {}; - void setMovePoints(SetMovePoints * smp) override {}; - void setManaPoints(ObjectInstanceID hid, int val) override {}; - void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; - void changeObjPos(ObjectInstanceID objid, int3 newPos) 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, bool hide) override {} - void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} - - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} - - void showInfoDialog(InfoWindow * iw) override {}; - void showInfoDialog(const std::string & msg, PlayerColor player) override {}; - void removeGUI(); - -#if SCRIPTING_ENABLED - scripting::Pool * getGlobalContextPool() const override; - scripting::Pool * getContextPool() const override; -#endif - -private: - std::map> battleCallbacks; //callbacks given to player interfaces - std::map> playerEnvironments; - -#if SCRIPTING_ENABLED - std::shared_ptr clientScripts; -#endif - std::unique_ptr clientEventBus; - - std::shared_ptr> applier; - - mutable boost::mutex pathCacheMutex; - std::map> pathCache; - - void reinitScripting(); -}; +/* + * Client.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 "../lib/IGameCallback.h" + +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; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class PoolImpl; +} +#endif + +namespace events +{ + class EventBus; +} + +VCMI_LIB_NAMESPACE_END + +class CBattleCallback; +class CCallback; +class CClient; +class CBaseForCLApply; + +namespace boost { class thread; } + +template +class ThreadSafeVector +{ + using TLock = boost::unique_lock; + std::vector items; + boost::mutex mx; + boost::condition_variable cond; + +public: + void clear() + { + TLock lock(mx); + items.clear(); + cond.notify_all(); + } + + void pushBack(const T & item) + { + TLock lock(mx); + items.push_back(item); + cond.notify_all(); + } + + void waitWhileContains(const T & item) + { + TLock lock(mx); + while(vstd::contains(items, item)) + cond.wait(lock); + } + + bool tryRemovingElement(const T & item) //returns false if element was not present + { + TLock lock(mx); + auto itr = vstd::find(items, item); + if(itr == items.end()) //not in container + { + return false; + } + + items.erase(itr); + cond.notify_all(); + return true; + } +}; + +class CPlayerEnvironment : public Environment +{ +public: + PlayerColor player; + CClient * cl; + std::shared_ptr mainCallback; + + CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_); + const Services * services() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + const BattleCb * battle(const BattleID & battle) const override; + const GameCb * game() const override; +}; + +/// Class which handles client - server logic +class CClient : public IGameCallback, public Environment +{ +public: + std::map> playerint; + std::map> battleints; + + std::map>> additionalBattleInts; + + std::unique_ptr currentBattleAction; + + CClient(); + ~CClient(); + + const Services * services() const override; + const BattleCb * battle(const BattleID & battle) const override; + const GameCb * game() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + + void newGame(CGameState * gameState); + void loadGame(CGameState * gameState); + void serialize(BinarySerializer & h, const int version); + void serialize(BinaryDeserializer & h, const int version); + + void save(const std::string & fname); + void endGame(); + + 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); + 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 battleStarted(const BattleInfo * info); + void battleFinished(const BattleID & battleID); + void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); + + void invalidatePaths(); + 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 {}; + 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 setOwner(const CGObjectInstance * obj, PlayerColor owner) 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 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 {}; + void giveResource(PlayerColor player, GameResID which, int val) override {}; + virtual void giveResources(PlayerColor player, TResources resources) override {}; + + void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; + void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; + bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; + bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; + bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; + bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; + bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} + bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} + void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} + bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} + + 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;}; + void removeArtifact(const ArtifactLocation & al) override {}; + bool moveArtifact(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 + bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; + void giveHeroBonus(GiveBonus * bonus) override {}; + void setMovePoints(SetMovePoints * smp) override {}; + 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 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 setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}; + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}; + + void showInfoDialog(InfoWindow * iw) override {}; + void showInfoDialog(const std::string & msg, PlayerColor player) override {}; + void removeGUI() const; + +#if SCRIPTING_ENABLED + scripting::Pool * getGlobalContextPool() const override; +#endif + +private: + std::map> battleCallbacks; //callbacks given to player interfaces + std::map> playerEnvironments; + +#if SCRIPTING_ENABLED + std::shared_ptr clientScripts; +#endif + std::unique_ptr clientEventBus; + + std::shared_ptr> applier; + + mutable boost::mutex pathCacheMutex; + std::map> pathCache; + + void reinitScripting(); +}; diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index c2c2fa461..412131142 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -17,22 +17,25 @@ #include "CServerHandler.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" -#include "../lib/NetPacks.h" +#include "render/IRenderHandler.h" #include "ClientNetPackVisitors.h" #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" #include "../lib/CPlayerState.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/campaign/CampaignHandler.h" #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "windows/CCastleInterface.h" +#include "../lib/mapObjects/CGHeroInstance.h" #include "render/CAnimation.h" #include "../CCallback.h" #include "../lib/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/CModHandler.h" #include "../lib/VCMIDirs.h" #include "CMT.h" @@ -71,7 +74,8 @@ void ClientCommandManager::handleGoSoloCommand() { Settings session = settings.write["session"]; - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(!CSH->client) { printCommandMessage("Game is not in playing state"); @@ -95,7 +99,7 @@ void ClientCommandManager::handleGoSoloCommand() if(elem.second.human) { auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false, false); - printCommandMessage("Player " + elem.first.getStr() + " will be lead by " + AiToGive, ELogLevel::INFO); + printCommandMessage("Player " + elem.first.toString() + " will be lead by " + AiToGive, ELogLevel::INFO); CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); } } @@ -117,7 +121,8 @@ void ClientCommandManager::handleControlaiCommand(std::istringstream& singleWord singleWordBuffer >> colorName; boost::to_lower(colorName); - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(!CSH->client) { printCommandMessage("Game is not in playing state"); @@ -180,12 +185,12 @@ void ClientCommandManager::handleNotDialogCommand() void ClientCommandManager::handleConvertTextCommand() { logGlobal->info("Searching for available maps"); - std::unordered_set mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == EResType::MAP; }); - std::unordered_set campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == EResType::CAMPAIGN; }); @@ -229,7 +234,8 @@ void ClientCommandManager::handleGetConfigCommand() for(auto contentName : contentNames) { - auto& content = (*VLC->modh->content)[contentName]; + auto const & handler = *VLC->modh->content; + auto const & content = handler[contentName]; auto contentOutPath = outPath / contentName; boost::filesystem::create_directories(contentOutPath); @@ -242,12 +248,12 @@ void ClientCommandManager::handleGetConfigCommand() { const JsonNode& object = nameAndObject.second; - std::string name = CModHandler::makeFullIdentifier(object.meta, contentName, nameAndObject.first); + std::string name = ModUtility::makeFullIdentifier(object.meta, contentName, nameAndObject.first); boost::algorithm::replace_all(name, ":", "_"); const boost::filesystem::path filePath = contentOutPath / (name + ".json"); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); file << object.toJson(); } } @@ -273,7 +279,7 @@ void ClientCommandManager::handleGetScriptsCommand() const scripting::ScriptImpl * script = kv.second.get(); boost::filesystem::path filePath = outPath / (name + ".lua"); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); file << script->getSource(); } printCommandMessage("\rExtracting done :)\n"); @@ -289,7 +295,7 @@ void ClientCommandManager::handleGetTextCommand() VCMIDirs::get().userExtractedPath(); auto list = - CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident) + CResourceHandler::get()->getFilteredFiles([](const ResourcePath & ident) { return ident.getType() == EResType::TEXT && boost::algorithm::starts_with(ident.getName(), "DATA/"); }); @@ -300,7 +306,7 @@ void ClientCommandManager::handleGetTextCommand() boost::filesystem::create_directories(filePath.parent_path()); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); auto text = CResourceHandler::get()->load(filename)->readAll(); file.write((char*)text.first.get(), text.second); @@ -314,7 +320,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu { std::string URI; singleWordBuffer >> URI; - std::unique_ptr anim = std::make_unique(URI); + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI)); anim->preload(); anim->exportBitmaps(VCMIDirs::get().userExtractedPath()); } @@ -324,14 +330,14 @@ void ClientCommandManager::handleExtractCommand(std::istringstream& singleWordBu std::string URI; singleWordBuffer >> URI; - if(CResourceHandler::get()->existsResource(ResourceID(URI))) + if(CResourceHandler::get()->existsResource(ResourcePath(URI))) { const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / URI; - auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll(); + auto data = CResourceHandler::get()->load(ResourcePath(URI))->readAll(); boost::filesystem::create_directories(outPath.parent_path()); - boost::filesystem::ofstream outFile(outPath, boost::filesystem::ofstream::binary); + std::ofstream outFile(outPath.c_str(), std::ofstream::binary); outFile.write((char*)data.first.get(), data.second); } else @@ -412,14 +418,6 @@ void ClientCommandManager::handleSetCommand(std::istringstream& singleWordBuffer } } -void ClientCommandManager::handleUnlockCommand(std::istringstream& singleWordBuffer) -{ - std::string mxname; - singleWordBuffer >> mxname; - if(mxname == "pim" && LOCPLINT) - LOCPLINT->pim->unlock(); -} - void ClientCommandManager::handleCrashCommand() { int* ptr = nullptr; @@ -456,7 +454,7 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage if(currentCallFromIngameConsole) { - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if(LOCPLINT && LOCPLINT->cingconsole) { LOCPLINT->cingconsole->print(commandMessage); @@ -466,9 +464,9 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier) { - YourTurn yt; + PlayerStartsTurn yt; yt.player = colorIdentifier; - yt.daysWithoutCastle = CSH->client->getPlayerState(colorIdentifier)->daysWithoutCastle; + yt.queryID = QueryID::NONE; ApplyClientNetPackVisitor visitor(*CSH->client, *CSH->client->gameState()); yt.visit(visitor); @@ -543,9 +541,6 @@ void ClientCommandManager::processCommand(const std::string & message, bool call else if (commandName == "set") handleSetCommand(singleWordBuffer); - else if(commandName == "unlock") - handleUnlockCommand(singleWordBuffer); - else if(commandName == "crash") handleCrashCommand(); diff --git a/client/ClientCommandManager.h b/client/ClientCommandManager.h index 409ccc6ff..ecb555212 100644 --- a/client/ClientCommandManager.h +++ b/client/ClientCommandManager.h @@ -78,9 +78,6 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a // set - sets special temporary settings that reset on game quit. void handleSetCommand(std::istringstream& singleWordBuffer); - // Unlocks specific mutex known in VCMI code as "pim" - void handleUnlockCommand(std::istringstream& singleWordBuffer); - // Crashes the game forcing an exception void handleCrashCommand(); diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index c997f0896..08c0908e2 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class CClient; @@ -56,6 +56,7 @@ public: void visitNewTurn(NewTurn & pack) override; void visitGiveBonus(GiveBonus & pack) override; void visitChangeObjPos(ChangeObjPos & pack) override; + void visitPlayerEndsTurn(PlayerEndsTurn & pack) override; void visitPlayerEndsGame(PlayerEndsGame & pack) override; void visitPlayerReinitInterface(PlayerReinitInterface & pack) override; void visitRemoveBonus(RemoveBonus & pack) override; @@ -93,7 +94,8 @@ public: void visitPackageApplied(PackageApplied & pack) override; void visitSystemMessage(SystemMessage & pack) override; void visitPlayerBlocked(PlayerBlocked & pack) override; - void visitYourTurn(YourTurn & pack) override; + void visitPlayerStartsTurn(PlayerStartsTurn & pack) override; + void visitTurnTimeUpdate(TurnTimeUpdate & pack) override; void visitPlayerMessageClient(PlayerMessageClient & pack) override; void visitAdvmapSpellCast(AdvmapSpellCast & pack) override; void visitShowWorldViewEx(ShowWorldViewEx & pack) override; diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp new file mode 100644 index 000000000..6b1152f60 --- /dev/null +++ b/client/HeroMovementController.cpp @@ -0,0 +1,378 @@ +/* + * HeroMovementController.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 "HeroMovementController.h" + +#include "CGameInfo.h" +#include "CMusicHandler.h" +#include "CPlayerInterface.h" +#include "PlayerLocalState.h" +#include "adventureMap/AdventureMapInterface.h" +#include "eventsSDL/InputHandler.h" +#include "gui/CGuiHandler.h" +#include "gui/CursorHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" + +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/networkPacks/PacksForClient.h" +#include "../lib/RoadHandler.h" +#include "../lib/TerrainHandler.h" + +bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const +{ + if(!duringMovement) + return false; + + if(!LOCPLINT->localState->hasPath(hero)) + return false; + + if(garrison->visitableAt(LOCPLINT->localState->getPath(hero).lastNode().coord)) + return false; // hero want to enter garrison, not pass through it + + return true; +} + +bool HeroMovementController::isHeroMoving() const +{ + return duringMovement; +} + +void HeroMovementController::onPlayerTurnStarted() +{ + assert(duringMovement == false); + assert(stoppingMovement == false); + duringMovement = false; + currentlyMovingHero = nullptr; +} + +void HeroMovementController::onBattleStarted() +{ + // when battle starts, game will send battleStart pack *before* movement confirmation + // and since network thread wait for battle intro to play, movement confirmation will only happen after intro + // leading to several bugs, such as blocked input during intro + requestMovementAbort(); +} + +void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + if (impassable || exits.empty()) //FIXME: why we even have this dialog in such case? + { + LOCPLINT->cb->selectionMade(-1, askID); + return; + } + + // Player entered teleporter + // Check whether hero that has entered teleporter has paths that goes through teleporter and select appropriate exit + // othervice, ask server to select one randomly by sending invalid (-1) value as answer + assert(waitingForQueryApplyReply == false); + waitingForQueryApplyReply = true; + + if(!LOCPLINT->localState->hasPath(hero)) + { + // Hero enters teleporter without specifying exit - select it randomly + LOCPLINT->cb->selectionMade(-1, askID); + return; + } + + const auto & heroPath = LOCPLINT->localState->getPath(hero); + const auto & nextNode = heroPath.nextNode(); + + for(size_t i = 0; i < exits.size(); ++i) + { + const auto * teleporter = LOCPLINT->cb->getObj(exits[i].first); + + if(teleporter && teleporter->visitableAt(nextNode.coord)) + { + // Remove this node from path - it will be covered by teleportation + //LOCPLINT->localState->removeLastNode(hero); + LOCPLINT->cb->selectionMade(i, askID); + return; + } + } + + // may happen when hero has path but does not moves alongside it + // for example, while standing on teleporter set path that does not leads throught teleporter and press space + LOCPLINT->cb->selectionMade(-1, askID); + return; +} + +void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMoveHero & details) +{ + // Once hero moved (or attempted to move) we need to update path + // to make sure that it is still valid or remove it completely if destination has been reached + + if(hero->tempOwner != LOCPLINT->playerID) + return; + + if(!LOCPLINT->localState->hasPath(hero)) + return; // may happen when hero teleports + + assert(LOCPLINT->makingTurn); + + bool directlyAttackingCreature = details.attackedFrom.has_value() && LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom; + + int3 desiredTarget = LOCPLINT->localState->getPath(hero).nextNode().coord; + int3 actualTarget = hero->convertToVisitablePos(details.end); + + //don't erase path when revisiting with spacebar + bool heroChangedTile = details.start != details.end; + + if(heroChangedTile) + { + if(desiredTarget != actualTarget) + { + //invalidate path - movement was not along current path + //possible reasons: teleport, visit of object with "blocking visit" property + LOCPLINT->localState->erasePath(hero); + } + else + { + //movement along desired path - remove one node and keep rest of path + LOCPLINT->localState->removeLastNode(hero); + } + + if(directlyAttackingCreature) + LOCPLINT->localState->erasePath(hero); + } +} + +void HeroMovementController::onTryMoveHero(const CGHeroInstance * hero, const TryMoveHero & details) +{ + // Server initiated movement -> start movement animation + // Note that this movement is not necessarily of owned heroes - other players movement will also pass through this method + + if(details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) + { + if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) + CCS->soundh->playSound(hero->getRemovalSound().value()); + } + + bool directlyAttackingCreature = + details.attackedFrom.has_value() && + LOCPLINT->localState->hasPath(hero) && + LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom; + + std::unordered_set changedTiles { + hero->convertToVisitablePos(details.start), + hero->convertToVisitablePos(details.end) + }; + adventureInt->onMapTilesChanged(changedTiles); + adventureInt->onHeroMovementStarted(hero); + + updatePath(hero, details); + + if(details.stopMovement()) + { + if(duringMovement) + endMove(hero); + return; + } + + // We are in network thread + // Block netpack processing until movement animation is over + CGI->mh->waitForOngoingAnimations(); + + //move finished + adventureInt->onHeroChanged(hero); + + // Hero attacked creature, set direction to face it. + if(directlyAttackingCreature) + { + // Get direction to attacker. + int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); + static const ui8 dirLookup[3][3] = + { + { 1, 2, 3 }, + { 8, 0, 4 }, + { 7, 6, 5 } + }; + + //FIXME: better handling of this case without const_cast + const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; + } +} + +void HeroMovementController::onQueryReplyApplied() +{ + waitingForQueryApplyReply = false; + + // Server accepted our TeleportDialog query reply and moved hero + // Continue moving alongside our path, if any + if(duringMovement) + onMoveHeroApplied(); +} + +void HeroMovementController::onMoveHeroApplied() +{ + // at this point, server have finished processing of hero movement request + // as well as all side effectes from movement, such as object visit or combat start + + // this was request to move alongside path from player, but either another player or teleport action + if(!duringMovement) + return; + + // hero has moved onto teleporter and activated it + // in this case next movement should be done only after query reply has been acknowledged + // and hero has been moved to teleport destination + if(waitingForQueryApplyReply) + return; + + if(GH.input().ignoreEventsUntilInput()) + stoppingMovement = true; + + assert(currentlyMovingHero); + const auto * hero = currentlyMovingHero; + + bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0; + bool wantStop = stoppingMovement; + bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); + + if(!canMove || (wantStop && canStop)) + { + endMove(hero); + } + else + { + moveOnce(hero, LOCPLINT->localState->getPath(hero)); + } +} + +void HeroMovementController::requestMovementAbort() +{ + if(duringMovement) + endMove(currentlyMovingHero); +} + +void HeroMovementController::endMove(const CGHeroInstance * hero) +{ + assert(duringMovement == true); + assert(currentlyMovingHero != nullptr); + duringMovement = false; + stoppingMovement = false; + currentlyMovingHero = nullptr; + stopMovementSound(); + adventureInt->onHeroChanged(hero); + CCS->curh->show(); +} + +AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) +{ + if(moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) + return {}; + + if(moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) + return {}; + + if(moveType == EPathNodeAction::BLOCKING_VISIT) + return {}; + + // flying movement sound + if(hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) + return AudioPath::builtin("HORSE10.wav"); + + 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; + + if(movingOnRoad) + return nextTile->terType->horseSound; + else + return nextTile->terType->horseSoundPenalty; +}; + +void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3 posPrev, int3 nextCoord, EPathNodeAction moveType) +{ + // Start a new sound for the hero movement or let the existing one carry on. + AudioPath newSoundName = getMovementSoundFor(h, posPrev, nextCoord, moveType); + + if(newSoundName != currentMovementSoundName) + { + currentMovementSoundName = newSoundName; + + if(currentMovementSoundChannel != -1) + CCS->soundh->stopSound(currentMovementSoundChannel); + + if(!currentMovementSoundName.empty()) + currentMovementSoundChannel = CCS->soundh->playSound(currentMovementSoundName, -1, true); + else + currentMovementSoundChannel = -1; + } +} + +void HeroMovementController::stopMovementSound() +{ + if(currentMovementSoundChannel != -1) + CCS->soundh->stopSound(currentMovementSoundChannel); + currentMovementSoundChannel = -1; + currentMovementSoundName = AudioPath(); +} + +bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const +{ + if(node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL) + return false; + + if(node.accessible != EPathAccessibility::ACCESSIBLE) + return false; + + return true; +} + +void HeroMovementController::requestMovementStart(const CGHeroInstance * h, const CGPath & path) +{ + assert(duringMovement == false); + duringMovement = true; + currentlyMovingHero = h; + + CCS->curh->hide(); + moveOnce(h, path); +} + +void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & path) +{ + // Moves hero once, sends request to server and immediately returns + // movement alongside paths will be done on receiving response from server + + assert(duringMovement == true); + + const auto & currNode = path.currNode(); + const auto & nextNode = path.nextNode(); + + assert(nextNode.turns == 0); + assert(currNode.coord == h->visitablePos()); + + int3 nextCoord = h->convertFromVisitablePos(nextNode.coord); + + if(nextNode.isTeleportAction()) + { + stopMovementSound(); + logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString()); + LOCPLINT->cb->moveHero(h, h->pos, false); + return; + } + else + { + 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 + + logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString()); + + bool useTransit = nextNode.layer == EPathfindingLayer::AIR || nextNode.layer == EPathfindingLayer::WATER; + LOCPLINT->cb->moveHero(h, nextCoord, useTransit); + return; + } +} diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h new file mode 100644 index 000000000..53314afd8 --- /dev/null +++ b/client/HeroMovementController.h @@ -0,0 +1,74 @@ +/* + * HeroMovementController.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/int3.h" +#include "../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN +using TTeleportExitsList = std::vector>; + +class CGHeroInstance; +class CArmedInstance; +struct CGPathNode; + +struct CGPath; +struct TryMoveHero; +enum class EPathNodeAction : ui8; +VCMI_LIB_NAMESPACE_END + +class HeroMovementController +{ + /// there is an ongoing movement loop, in one or another stage + bool duringMovement = false; + /// movement was requested to be terminated, e.g. by player or due to inability to move + bool stoppingMovement = false; + + bool waitingForQueryApplyReply = false; + + const CGHeroInstance * currentlyMovingHero = nullptr; + AudioPath currentMovementSoundName; + int currentMovementSoundChannel = -1; + + bool canHeroStopAtNode(const CGPathNode & node) const; + + void updatePath(const CGHeroInstance * hero, const TryMoveHero & details); + + /// Moves hero 1 tile / path node + void moveOnce(const CGHeroInstance * h, const CGPath & path); + + void endMove(const CGHeroInstance * h); + + AudioPath getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); + void updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); + void stopMovementSound(); + +public: + // const queries + + /// Returns true if hero should move through garrison without displaying garrison dialog + bool isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const; + + /// Returns true if there is an ongoing hero movement process + bool isHeroMoving() const; + + // netpack handlers + void onMoveHeroApplied(); + void onQueryReplyApplied(); + void onPlayerTurnStarted(); + void onBattleStarted(); + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); + void onTryMoveHero(const CGHeroInstance * hero, const TryMoveHero & details); + + // UI handlers + void requestMovementStart(const CGHeroInstance * h, const CGPath & path); + void requestMovementAbort(); +}; diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 7ea105b02..7e19e614b 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -9,9 +9,11 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" +#include "../lib/networkPacks/PacksForLobby.h" class CClient; +class CLobbyScreen; VCMI_LIB_NAMESPACE_BEGIN class CGameState; VCMI_LIB_NAMESPACE_END @@ -53,6 +55,7 @@ public: virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; virtual void visitLobbyStartGame(LobbyStartGame & pack) override; + virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override; }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 422aa3481..a6d0bff9a 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -1,1003 +1,1046 @@ -/* - * NetPacksClient.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 "ClientNetPackVisitors.h" - -#include "Client.h" -#include "CPlayerInterface.h" -#include "CGameInfo.h" -#include "windows/GUIClasses.h" -#include "mapView/mapHandler.h" -#include "adventureMap/CInGameConsole.h" -#include "battle/BattleInterface.h" -#include "battle/BattleWindow.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "widgets/MiscWidgets.h" -#include "CMT.h" -#include "CServerHandler.h" - -#include "../CCallback.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileInfo.h" -#include "../lib/serializer/Connection.h" -#include "../lib/serializer/BinarySerializer.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/mapping/CMap.h" -#include "../lib/VCMIDirs.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CSoundBase.h" -#include "../lib/StartInfo.h" -#include "../lib/CConfigHandler.h" -#include "../lib/mapObjects/CGMarket.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CStack.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/GameConstants.h" -#include "../lib/CPlayerState.h" - -// TODO: as Tow suggested these template should all be part of CClient -// This will require rework spectator interface properly though - -template -bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - if(vstd::contains(cl.playerint, player)) - { - ((*cl.playerint[player]).*ptr)(std::forward(args)...); - return true; - } - return false; -} - -template -bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - bool called = callOnlyThatInterface(cl, player, ptr, std::forward(args)...); - return called; -} - -template -void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - if(vstd::contains(cl.battleints,player)) - ((*cl.battleints[player]).*ptr)(std::forward(args)...); - - if(cl.additionalBattleInts.count(player)) - { - for(auto bInt : cl.additionalBattleInts[player]) - ((*bInt).*ptr)(std::forward(args)...); - } -} - -template -void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - callOnlyThatInterface(cl, player, ptr, std::forward(args)...); -} - -//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy -template -void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) -{ - for(auto pInt : cl.playerint) - ((*pInt.second).*ptr)(std::forward(args)...); -} - -//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy -template -void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) -{ - assert(cl.gameState()->curB); - - if (!cl.gameState()->curB) - { - logGlobal->error("Attempt to call battle interface without ongoing battle!"); - return; - } - - callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward(args)...); - callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].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)...); - } -} - -void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack) -{ - //todo: inform on actual resource set transfered - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource); -} - -void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack) -{ - const CGHeroInstance * h = cl.getHero(pack.id); - if(!h) - { - logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); - return; - } - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val); -} - -void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.id); - if(!h) - { - logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); - return; - } - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val); -} - -void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - - if(pack.start()) - { - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid)); - } -} - -void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); - - for (auto window : GH.windows().findWindows()) - window->heroManaPointsChanged(h); -} - -void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - cl.invalidatePaths(); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); -} - -void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) -{ - for(auto &i : cl.playerint) - { - if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get()) - { - LOCPLINT->waitWhileDialog(); - } - if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) - { - if(pack.mode) - i.second->tileRevealed(pack.tiles); - else - i.second->tileHidden(pack.tiles); - } - } - cl.invalidatePaths(); -} - -static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2) -{ - auto obj1 = cl.getObj(army1); - if(!obj1) - { - logNetwork->error("Cannot find army with pack.id %d", army1.getNum()); - return; - } - - callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); - - if(army2 != ObjectInstanceID() && army2 != army1) - { - auto obj2 = cl.getObj(army2); - if(!obj2) - { - logNetwork->error("Cannot find army with pack.id %d", army2.getNum()); - return; - } - - if(obj1->tempOwner != obj2->tempOwner) - callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); - } -} - -void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) -{ - dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); -} - -void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) -{ - dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); -} - -void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) -{ - if(!pack.moves.empty()) - { - auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy - ? ObjectInstanceID() - : pack.moves[0].dstArmy; - dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); - } -} - -void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) -{ - if(!pack.moves.empty()) - { - assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy); - dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID()); - } - else if(!pack.changes.empty()) - { - dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID()); - } -} - -void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al); - if(pack.askAssemble) - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al); -} - -void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &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.src.owningPlayer()); - if(pack.src.owningPlayer() != pack.dst.owningPlayer()) - moveArtifact(pack.dst.owningPlayer()); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) -{ - auto applyMove = [this, &pack](std::vector & artsPack) -> void - { - for(auto & slotToMove : artsPack) - { - auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); - auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); - MoveArtifact ma(&srcLoc, &dstLoc, false); - visitMoveArtifact(ma); - } - }; - - // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. - callInterfaceIfPresent(cl, cl.getCurrentPlayer(), &IGameEventsReceiver::bulkArtMovementStart, - pack.artsPack0.size() + pack.artsPack1.size()); - applyMove(pack.artsPack0); - if(pack.swap) - applyMove(pack.artsPack1); -} - -void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) -{ - auto hero = cl.getHero(pack.heroId); - auto obj = cl.getObj(pack.objId, false); - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting); -} - -void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) -{ - cl.invalidatePaths(); -} - -void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) -{ - cl.invalidatePaths(); - switch(pack.who) - { - case GiveBonus::ETarget::HERO: - { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); - } - break; - case GiveBonus::ETarget::PLAYER: - { - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); - } - break; - } -} - -void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) -{ - CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeOut(obj); - - CGI->mh->waitForOngoingAnimations(); -} -void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) -{ - CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeIn(obj); - - CGI->mh->waitForOngoingAnimations(); - cl.invalidatePaths(); -} - -void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) -{ - callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult); - - // In auto testing pack.mode we always close client if red pack.player won or lose - if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0)) - handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not -} - -void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack) -{ - auto initInterfaces = [this]() - { - cl.initPlayerInterfaces(); - auto currentPlayer = cl.gameState()->currentPlayer; - callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, currentPlayer); - callOnlyThatInterface(cl, currentPlayer, &CGameInterface::yourTurn); - }; - - for(auto player : pack.players) - { - auto & plSettings = CSH->si->getIthPlayersSettings(player); - if(pack.playerConnectionId == PlayerSettings::PLAYER_AI) - { - plSettings.connectedPlayerIDs.clear(); - cl.initPlayerEnvironments(); - initInterfaces(); - } - else if(pack.playerConnectionId == CSH->c->connectionID) - { - plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); - cl.playerint.clear(); - initInterfaces(); - } - } -} - -void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) -{ - cl.invalidatePaths(); - switch(pack.who) - { - case GiveBonus::ETarget::HERO: - { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); - } - break; - case GiveBonus::ETarget::PLAYER: - { - //const PlayerState *p = gs.getPlayerState(pack.id); - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); - } - break; - } -} - -void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) -{ - const CGObjectInstance *o = cl.getObj(pack.id); - - if(CGI->mh) - CGI->mh->onObjectFadeOut(o); - - //notify interfaces about removal - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW - //TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this - if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) - i->second->objectRemoved(o); - } - - CGI->mh->waitForOngoingAnimations(); -} - -void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) -{ - cl.invalidatePaths(); - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - i->second->objectRemovedAfter(); -} - -void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) -{ - CGHeroInstance *h = gs.getHero(pack.id); - - if(CGI->mh) - { - switch (pack.result) - { - case TryMoveHero::EMBARK: - CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end); - break; - case TryMoveHero::TELEPORTATION: - CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end); - break; - case TryMoveHero::DISEMBARK: - CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end); - break; - } - CGI->mh->waitForOngoingAnimations(); - } -} - -void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.id); - cl.invalidatePaths(); - - if(CGI->mh) - { - switch(pack.result) - { - case TryMoveHero::SUCCESS: - CGI->mh->onHeroMoved(h, pack.start, pack.end); - break; - case TryMoveHero::EMBARK: - CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end); - break; - case TryMoveHero::TELEPORTATION: - CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end); - break; - case TryMoveHero::DISEMBARK: - CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end); - break; - } - } - - PlayerColor player = h->tempOwner; - - for(auto &i : cl.playerint) - if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) - i.second->tileRevealed(pack.fowRevealed); - - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface - continue; - - if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first) - || gs.isVisible(h->convertToVisitablePos(pack.end), i->first)) - { - // pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false - const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES; - i->second->heroMoved(pack, verbose); - } - } -} - -void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) -{ - CGTownInstance *town = gs.getTown(pack.tid); - for(const auto & id : pack.bid) - { - callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 1); - } - - // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town); - CGI->mh->onObjectInstantAdd(town); - -} -void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) -{ - CGTownInstance * town = gs.getTown(pack.tid); - for(const auto & id : pack.bid) - { - callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 2); - } - - // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town); - CGI->mh->onObjectInstantAdd(town); -} - -void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) -{ - const CGDwelling * dw = static_cast(cl.getObj(pack.tid)); - - PlayerColor p; - if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor - p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner; - else - p = dw->tempOwner; - - callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw); -} - -void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) -{ - CGTownInstance * t = gs.getTown(pack.tid); - CGHeroInstance * hGarr = gs.getHero(pack.garrison); - CGHeroInstance * hVisit = gs.getHero(pack.visiting); - - //inform all players that see this object - for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i) - { - if(i->first >= PlayerColor::PLAYER_LIMIT) - continue; - - if(gs.isVisible(t, i->first) || - (hGarr && gs.isVisible(hGarr, i->first)) || - (hVisit && gs.isVisible(hVisit, i->first))) - { - cl.playerint[i->first]->heroInGarrisonChange(t); - } - } -} - -void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) -{ - CGHeroInstance *h = gs.map->heroesOnMap.back(); - if(h->subID != pack.hid) - { - logNetwork->error("Something wrong with hero recruited!"); - } - - if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) - { - if(const CGTownInstance *t = gs.getTown(pack.tid)) - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroInGarrisonChange, t); - } - if(CGI->mh) - CGI->mh->onObjectInstantAdd(h); -} - -void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack) -{ - CGHeroInstance *h = gs.getHero(pack.id); - if(CGI->mh) - CGI->mh->onObjectInstantAdd(h); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); -} - -void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack) -{ -} - -void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack) -{ - std::string str = pack.text.toString(); - - if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID)) - logNetwork->warn("We received InfoWindow for not our player..."); -} - -void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) -{ - //inform all players that see this object - for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) - { - if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) - callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack); - } - - // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) - CGI->mh->onObjectInstantRemove(gs.getObjInstance(pack.id)); -} - -void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) -{ - //inform all players that see this object - for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) - { - if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) - callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack); - } - - // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) - CGI->mh->onObjectInstantAdd(gs.getObjInstance(pack.id)); -} - -void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack) -{ - const CGHeroInstance * hero = cl.getHero(pack.heroId); - assert(hero); - callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack) -{ - const CGHeroInstance * hero = cl.getHero(pack.heroId); - assert(hero); - const CCommanderInstance * commander = hero->commander; - assert(commander); - assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance? - callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack) -{ - std::string str = pack.text.toString(); - - if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel())) - logNetwork->warn("We received YesNoDialog for not our player..."); -} - -void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - const CArmedInstance *obj = static_cast(cl.getObj(pack.objid)); - - callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack) -{ - callOnlyThatInterface(cl, pack.player, &CGameInterface::showTeleportDialog, pack.channel, pack.exits, pack.impassable, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack) -{ - callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects); -} - -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.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.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.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); -} - -void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) -{ - cl.battleStarted(pack.info); -} - -void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRoundFirst, pack.round); -} - -void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRound, pack.round); -} - -void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) -{ - if(!pack.askPlayerInterface) - return; - - const CStack *activated = gs.curB->battleGetStackByID(pack.stack); - PlayerColor playerToCall; //pack.player that will move activated stack - if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) - { - playerToCall = (gs.curB->sides[0].color == activated->unitOwner() - ? gs.curB->sides[1].color - : gs.curB->sides[0].color); - } - else - { - playerToCall = activated->unitOwner(); - } - - cl.startPlayerBattleAction(playerToCall); -} - -void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleLogMessage, pack.lines); -} - -void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleTriggerEffect, pack); -} - -void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleGateStateChanged, pack.state); -} - -void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, pack.queryID); - cl.battleFinished(); -} - -void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack) -{ - const CStack * movedStack = gs.curB->battleGetStackByID(pack.stack); - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); -} - -void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, &pack); - - // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit - // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot()); -} - -void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) -{ -} - -void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) -{ - cl.curbaction = std::make_optional(pack.ba); - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba); -} - -void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleSpellCast, &pack); -} - -void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack) -{ - //informing about effects - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksEffectsSet, pack); -} - -void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.stacks, false); -} - -void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) -{ - callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied); - callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied); - callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); -} - -void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, pack.changedStacks); -} - -void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack) -{ - //inform interfaces about removed obstacles - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleObstaclesChanged, pack.changes); -} - -void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) -{ - //inform interfaces about catapult attack - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleCatapultAttacked, pack); -} - -void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.curbaction); - cl.curbaction.reset(); -} - -void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack); - if(!CClient::waitingRequest.tryRemovingElement(pack.requestID)) - logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); -} - -void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack) -{ - std::ostringstream str; - str << "System message: " << pack.text; - - logNetwork->error(str.str()); // usually used to receive error messages from server - if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool()) - LOCPLINT->cingconsole->print(str.str()); -} - -void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED); -} - -void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) -{ - logNetwork->debug("Server gives turn to %s", pack.player.getStr()); - - callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); - callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn); -} - -void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) -{ - logNetwork->debug("pack.player %s sends a message: %s", pack.player.getStr(), pack.text); - - std::ostringstream str; - if(pack.player.isSpectator()) - str << "Spectator: " << pack.text; - else - str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text; - if(LOCPLINT) - LOCPLINT->cingconsole->print(str.str()); -} - -void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack) -{ - cl.invalidatePaths(); - auto caster = cl.getHero(pack.casterID); - if(caster) - //consider notifying other interfaces that see hero? - callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID); - else - logNetwork->error("Invalid hero instance"); -} - -void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack) -{ - callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain); -} - -void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) -{ - switch(pack.window) - { - case EOpenWindowMode::RECRUITMENT_FIRST: - case EOpenWindowMode::RECRUITMENT_ALL: - { - const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id1))); - const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id2))); - callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1); - } - break; - case EOpenWindowMode::SHIPYARD_WINDOW: - { - const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.id1))); - callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); - } - break; - case EOpenWindowMode::THIEVES_GUILD: - { - //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showThievesGuildWindow, obj); - } - break; - case EOpenWindowMode::UNIVERSITY_WINDOW: - { - //displays University window (when hero enters University on adventure map) - const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.id1))); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero); - } - break; - case EOpenWindowMode::MARKET_WINDOW: - { - //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); - const IMarket *market = IMarket::castFrom(obj); - callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero); - } - break; - case EOpenWindowMode::HILL_FORT_WINDOW: - { - //displays Hill fort window - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); - } - break; - case EOpenWindowMode::PUZZLE_MAP: - { - callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showPuzzleMap); - } - break; - case EOpenWindowMode::TAVERN_WINDOW: - const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.id1)), - *obj2 = cl.getObj(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::showTavernWindow, obj2); - break; - } -} - -void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime); -} - -void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) -{ - cl.invalidatePaths(); - - const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); - if(CGI->mh) - CGI->mh->onObjectFadeIn(obj); - - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - if(gs.isVisible(obj, i->first)) - i->second->newObject(obj); - } - CGI->mh->waitForOngoingAnimations(); -} - -void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack) -{ - if(pack.id < 0) //artifact merchants globally - { - callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr); - } - else - { - const CGBlackMarket *bm = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id))); - assert(bm); - callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm); - } -} - - -void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack) -{ - cl.invalidatePaths(); -} +/* + * NetPacksClient.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 "ClientNetPackVisitors.h" + +#include "Client.h" +#include "CPlayerInterface.h" +#include "CGameInfo.h" +#include "windows/GUIClasses.h" +#include "mapView/mapHandler.h" +#include "adventureMap/CInGameConsole.h" +#include "battle/BattleInterface.h" +#include "battle/BattleWindow.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "widgets/MiscWidgets.h" +#include "CMT.h" +#include "CServerHandler.h" + +#include "../CCallback.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/FileInfo.h" +#include "../lib/serializer/Connection.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/mapping/CMap.h" +#include "../lib/VCMIDirs.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CSoundBase.h" +#include "../lib/StartInfo.h" +#include "../lib/CConfigHandler.h" +#include "../lib/mapObjects/CGMarket.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CStack.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/GameConstants.h" +#include "../lib/CPlayerState.h" + +// TODO: as Tow suggested these template should all be part of CClient +// This will require rework spectator interface properly though + +template +bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + if(vstd::contains(cl.playerint, player)) + { + ((*cl.playerint[player]).*ptr)(std::forward(args)...); + return true; + } + return false; +} + +template +bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + bool called = callOnlyThatInterface(cl, player, ptr, std::forward(args)...); + return called; +} + +template +void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + if(vstd::contains(cl.battleints,player)) + ((*cl.battleints[player]).*ptr)(std::forward(args)...); + + if(cl.additionalBattleInts.count(player)) + { + for(auto bInt : cl.additionalBattleInts[player]) + ((*bInt).*ptr)(std::forward(args)...); + } +} + +template +void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + callOnlyThatInterface(cl, player, ptr, std::forward(args)...); +} + +//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy +template +void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) +{ + for(auto pInt : cl.playerint) + ((*pInt.second).*ptr)(std::forward(args)...); +} + +//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy +template +void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args) +{ + assert(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)...); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) + { + callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); + } +} + +void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack) +{ + //todo: inform on actual resource set transfered + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource); +} + +void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack) +{ + const CGHeroInstance * h = cl.getHero(pack.id); + if(!h) + { + logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); + return; + } + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val); +} + +void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.id); + if(!h) + { + logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); + return; + } + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val); +} + +void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + + if(pack.start()) + { + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid)); + } +} + +void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); + + for (auto window : GH.windows().findWindows()) + window->heroManaPointsChanged(h); +} + +void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + cl.invalidatePaths(); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); +} + +void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) +{ + for(auto &i : cl.playerint) + { + if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get()) + { + LOCPLINT->waitWhileDialog(); + } + if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) + { + if(pack.mode == ETileVisibility::REVEALED) + i.second->tileRevealed(pack.tiles); + else + i.second->tileHidden(pack.tiles); + } + } + cl.invalidatePaths(); +} + +static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2) +{ + auto obj1 = cl.getObj(army1); + if(!obj1) + { + logNetwork->error("Cannot find army with pack.id %d", army1.getNum()); + return; + } + + callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); + + if(army2 != ObjectInstanceID() && army2 != army1) + { + auto obj2 = cl.getObj(army2); + if(!obj2) + { + logNetwork->error("Cannot find army with pack.id %d", army2.getNum()); + return; + } + + if(obj1->tempOwner != obj2->tempOwner) + callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); + } +} + +void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) +{ + dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); +} + +void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) +{ + dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); +} + +void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) +{ + if(!pack.moves.empty()) + { + auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy + ? ObjectInstanceID() + : pack.moves[0].dstArmy; + dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); + } +} + +void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) +{ + if(!pack.moves.empty()) + { + assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy); + dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID()); + } + else if(!pack.changes.empty()) + { + dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID()); + } +} + +void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) +{ + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al); + if(pack.askAssemble) + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al); +} + +void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & 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(cl.getOwner(pack.src.artHolder)); + if(cl.getOwner(pack.src.artHolder) != cl.getOwner(pack.dst.artHolder)) + moveArtifact(cl.getOwner(pack.dst.artHolder)); + + cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings +} + +void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) +{ + auto applyMove = [this, &pack](std::vector & artsPack) -> void + { + for(auto & slotToMove : artsPack) + { + auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); + auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); + MoveArtifact ma(&srcLoc, &dstLoc, pack.askAssemble); + visitMoveArtifact(ma); + } + }; + + auto srcOwner = cl.getOwner(pack.srcArtHolder); + auto dstOwner = cl.getOwner(pack.dstArtHolder); + + // 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()); + + applyMove(pack.artsPack0); + if(pack.swap) + applyMove(pack.artsPack1); +} + +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 +} + +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 +} + +void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) +{ + auto hero = cl.getHero(pack.heroId); + auto obj = cl.getObj(pack.objId, false); + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting); +} + +void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) +{ + cl.invalidatePaths(); +} + +void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) +{ + cl.invalidatePaths(); + switch(pack.who) + { + case GiveBonus::ETarget::OBJECT: + { + const CGHeroInstance *h = gs.getHero(pack.id.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); + } + break; + case GiveBonus::ETarget::PLAYER: + { + callInterfaceIfPresent(cl, pack.id.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); + } + break; + } +} + +void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) +{ + CGObjectInstance *obj = gs.getObjInstance(pack.objid); + if(CGI && CGI->mh) + { + CGI->mh->onObjectFadeOut(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); + } +} + +void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) +{ + CGObjectInstance *obj = gs.getObjInstance(pack.objid); + if(CGI && CGI->mh) + { + CGI->mh->onObjectFadeIn(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); + } + cl.invalidatePaths(); +} + +void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) +{ + callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult); + + // In auto testing pack.mode we always close client if red pack.player won or lose + if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0)) + handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not +} + +void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack) +{ + auto initInterfaces = [this]() + { + cl.initPlayerInterfaces(); + + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (cl.gameState()->isPlayerMakingTurn(player)) + { + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); + callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); + } + } + }; + + for(auto player : pack.players) + { + auto & plSettings = CSH->si->getIthPlayersSettings(player); + if(pack.playerConnectionId == PlayerSettings::PLAYER_AI) + { + plSettings.connectedPlayerIDs.clear(); + cl.initPlayerEnvironments(); + initInterfaces(); + } + else if(pack.playerConnectionId == CSH->c->connectionID) + { + plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); + cl.playerint.clear(); + initInterfaces(); + } + } +} + +void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) +{ + cl.invalidatePaths(); + switch(pack.who) + { + case GiveBonus::ETarget::OBJECT: + { + const CGHeroInstance *h = gs.getHero(pack.whoID.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); + } + break; + case GiveBonus::ETarget::PLAYER: + { + //const PlayerState *p = gs.getPlayerState(pack.id); + callInterfaceIfPresent(cl, pack.whoID.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); + } + break; + } +} + +void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) +{ + const CGObjectInstance *o = cl.getObj(pack.objectID); + + if(CGI->mh) + CGI->mh->onObjectFadeOut(o, pack.initiator); + + //notify interfaces about removal + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW + //TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this + if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) + i->second->objectRemoved(o, pack.initiator); + } + + CGI->mh->waitForOngoingAnimations(); +} + +void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) +{ + cl.invalidatePaths(); + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + i->second->objectRemovedAfter(); +} + +void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) +{ + CGHeroInstance *h = gs.getHero(pack.id); + + if(CGI->mh) + { + switch (pack.result) + { + case TryMoveHero::EMBARK: + CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end); + break; + case TryMoveHero::TELEPORTATION: + CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end); + break; + case TryMoveHero::DISEMBARK: + CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end); + break; + } + CGI->mh->waitForOngoingAnimations(); + } +} + +void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.id); + cl.invalidatePaths(); + + if(CGI->mh) + { + switch(pack.result) + { + case TryMoveHero::SUCCESS: + CGI->mh->onHeroMoved(h, pack.start, pack.end); + break; + case TryMoveHero::EMBARK: + CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end); + break; + case TryMoveHero::TELEPORTATION: + CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end); + break; + case TryMoveHero::DISEMBARK: + CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end); + break; + } + } + + PlayerColor player = h->tempOwner; + + for(auto &i : cl.playerint) + if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) + i.second->tileRevealed(pack.fowRevealed); + + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface + continue; + + if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first) + || gs.isVisible(h->convertToVisitablePos(pack.end), i->first)) + { + // pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false + const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES; + i->second->heroMoved(pack, verbose); + } + } +} + +void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) +{ + CGTownInstance *town = gs.getTown(pack.tid); + for(const auto & id : pack.bid) + { + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1); + } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); + +} +void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) +{ + CGTownInstance * town = gs.getTown(pack.tid); + for(const auto & id : pack.bid) + { + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2); + } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); +} + +void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) +{ + const CGDwelling * dw = static_cast(cl.getObj(pack.tid)); + + PlayerColor p; + if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor + p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner; + else + p = dw->tempOwner; + + callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw); +} + +void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) +{ + CGTownInstance * t = gs.getTown(pack.tid); + CGHeroInstance * hGarr = gs.getHero(pack.garrison); + CGHeroInstance * hVisit = gs.getHero(pack.visiting); + + //inform all players that see this object + for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i) + { + if(!i->first.isValidPlayer()) + continue; + + if(gs.isVisible(t, i->first) || + (hGarr && gs.isVisible(hGarr, i->first)) || + (hVisit && gs.isVisible(hVisit, i->first))) + { + cl.playerint[i->first]->heroInGarrisonChange(t); + } + } +} + +void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) +{ + CGHeroInstance *h = gs.map->heroesOnMap.back(); + if(h->getHeroType() != pack.hid) + { + logNetwork->error("Something wrong with hero recruited!"); + } + + if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) + { + if(const CGTownInstance *t = gs.getTown(pack.tid)) + callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t); + } + if(CGI->mh) + CGI->mh->onObjectInstantAdd(h, h->getOwner()); +} + +void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack) +{ + CGHeroInstance *h = gs.getHero(pack.id); + if(CGI->mh) + CGI->mh->onObjectInstantAdd(h, h->getOwner()); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); +} + +void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack) +{ +} + +void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack) +{ + std::string str = pack.text.toString(); + + if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID)) + logNetwork->warn("We received InfoWindow for not our player..."); +} + +void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) +{ + //inform all players that see this object + for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) + { + if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) + callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack); + } + + // invalidate section of map view with our object and force an update with new flag color + if (pack.what == ObjProperty::OWNER) + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantRemove(object, object->getOwner()); + } +} + +void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) +{ + //inform all players that see this object + for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) + { + if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) + callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack); + } + + // invalidate section of map view with our object and force an update with new flag color + if (pack.what == ObjProperty::OWNER) + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantAdd(object, object->getOwner()); + } +} + +void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack) +{ + const CGHeroInstance * hero = cl.getHero(pack.heroId); + assert(hero); + callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack) +{ + const CGHeroInstance * hero = cl.getHero(pack.heroId); + assert(hero); + const CCommanderInstance * commander = hero->commander; + assert(commander); + assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance? + callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack) +{ + std::string str = pack.text.toString(); + + if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel())) + logNetwork->warn("We received YesNoDialog for not our player..."); +} + +void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + const CArmedInstance *obj = static_cast(cl.getObj(pack.objid)); + + callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hero); + callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack) +{ + callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects); +} + +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); +} + +void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) +{ + cl.battleStarted(pack.info); +} + +void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID); +} + +void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID); +} + +void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) +{ + if(!pack.askPlayerInterface) + return; + + const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); + PlayerColor playerToCall; //pack.player that will move activated stack + 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); + } + else + { + playerToCall = activated->unitOwner(); + } + + cl.startPlayerBattleAction(pack.battleID, playerToCall); +} + +void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines); +} + +void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack); +} + +void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state); +} + +void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID); + cl.battleFinished(pack.battleID); +} + +void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack) +{ + const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); +} + +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 + // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot()); +} + +void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) +{ +} + +void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) +{ + cl.currentBattleAction = std::make_unique(pack.ba); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba); +} + +void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack); +} + +void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack) +{ + //informing about effects + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack); +} + +void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false); +} + +void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) +{ + callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied); + callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied); + callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); +} + +void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks); +} + +void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack) +{ + //inform interfaces about removed obstacles + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes); +} + +void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) +{ + //inform interfaces about catapult attack + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack); +} + +void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction); + cl.currentBattleAction.reset(); +} + +void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack); + if(!CClient::waitingRequest.tryRemovingElement(pack.requestID)) + logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); +} + +void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack) +{ + std::ostringstream str; + str << "System message: " << pack.text; + + logNetwork->error(str.str()); // usually used to receive error messages from server + if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool()) + LOCPLINT->cingconsole->print(str.str()); +} + +void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED); +} + +void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack) +{ + logNetwork->debug("Server gives turn to %s", pack.player.toString()); + + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); + callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) +{ + logNetwork->debug("Server ends turn of %s", pack.player.toString()); + + callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player); +} + +void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) +{ + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.unitTimer, pack.player.toString()); +} + +void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) +{ + logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text); + + std::ostringstream str; + if(pack.player.isSpectator()) + str << "Spectator: " << pack.text; + else + str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text; + if(LOCPLINT) + LOCPLINT->cingconsole->print(str.str()); +} + +void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack) +{ + cl.invalidatePaths(); + auto caster = cl.getHero(pack.casterID); + if(caster) + //consider notifying other interfaces that see hero? + callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID); + else + logNetwork->error("Invalid hero instance"); +} + +void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack) +{ + callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain); +} + +void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) +{ + switch(pack.window) + { + case EOpenWindowMode::RECRUITMENT_FIRST: + case EOpenWindowMode::RECRUITMENT_ALL: + { + const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); + const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.visitor))); + callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID); + } + break; + case EOpenWindowMode::SHIPYARD_WINDOW: + { + assert(pack.queryID == QueryID::NONE); + const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); + } + break; + case EOpenWindowMode::THIEVES_GUILD: + { + assert(pack.queryID == QueryID::NONE); + //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)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj); + } + break; + case EOpenWindowMode::UNIVERSITY_WINDOW: + { + //displays University window (when hero enters University on adventure map) + const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); + } + break; + case EOpenWindowMode::MARKET_WINDOW: + { + //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 IMarket *market = IMarket::castFrom(obj); + callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); + } + break; + case EOpenWindowMode::HILL_FORT_WINDOW: + { + assert(pack.queryID == QueryID::NONE); + //displays Hill fort window + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); + } + break; + case EOpenWindowMode::PUZZLE_MAP: + { + assert(pack.queryID == QueryID::NONE); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap); + } + break; + case EOpenWindowMode::TAVERN_WINDOW: + { + const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID); + } + break; + } +} + +void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime); +} + +void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) +{ + cl.invalidatePaths(); + + const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); + if(CGI->mh) + CGI->mh->onObjectFadeIn(obj, pack.initiator); + + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + if(gs.isVisible(obj, i->first)) + i->second->newObject(obj); + } + CGI->mh->waitForOngoingAnimations(); +} + +void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack) +{ + if(pack.id < 0) //artifact merchants globally + { + callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr); + } + else + { + const CGBlackMarket *bm = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id))); + assert(bm); + callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm); + } +} + + +void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack) +{ + cl.invalidatePaths(); +} diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 1f037e776..dc3632757 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -27,7 +27,6 @@ #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" -#include "../lib/NetPacksLobby.h" #include "../lib/serializer/Connection.h" void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) @@ -38,7 +37,9 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon if(pack.uuid == handler.c->uuid) { handler.c->connectionID = pack.clientId; - if(!settings["session"]["headless"].Bool()) + if(handler.mapToStart) + handler.setMapInfo(handler.mapToStart); + else if(!settings["session"]["headless"].Bool()) GH.windows().createAndPushWindow(static_cast(handler.screenType)); handler.state = EClientState::LOBBY; } @@ -57,6 +58,9 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClient void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { + if(auto w = GH.windows().topWindow()) + GH.windows().popWindow(w); + if(GH.windows().count() > 0) GH.windows().popWindows(1); } @@ -68,7 +72,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & lobby->card->chat->addNewMessage(pack.playerName + ": " + pack.message); lobby->card->setChat(true); if(lobby->buttonChat) - lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); } } @@ -123,22 +127,38 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pac handler.si = pack.initializedStartInfo; handler.si->mode = modeBackup; } - if(settings["session"]["headless"].Bool()) - handler.startGameplay(pack.initializedGameState); + handler.startGameplay(pack.initializedGameState); } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) { - if(pack.clientId != -1 && pack.clientId != handler.c->connectionID) - return; - - GH.windows().createAndPushWindow(std::bind(&CServerHandler::startGameplay, &handler, pack.initializedGameState)); + if(auto w = GH.windows().topWindow()) + { + w->finish(); + w->tick(0); + w->redraw(); + } +} + +void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress & pack) +{ + if(auto w = GH.windows().topWindow()) + { + w->set(pack.progress); + w->tick(0); + w->redraw(); + } } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) { pack.hostChanged = pack.state.hostClientId != handler.hostClientId; static_cast(handler) = pack.state; + if(handler.mapToStart && handler.mi) + { + handler.startMapAfterConnection(nullptr); + handler.sendStartGame(); + } } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 6ace13258..5fb7ed65f 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -1,271 +1,285 @@ -/* - * PlayerLocalState.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 "PlayerLocalState.h" - -#include "../CCallback.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "CPlayerInterface.h" -#include "adventureMap/AdventureMapInterface.h" - -PlayerLocalState::PlayerLocalState(CPlayerInterface & owner) - : owner(owner) - , currentSelection(nullptr) -{ -} - -void PlayerLocalState::saveHeroPaths(std::map & pathsMap) -{ - 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()); - } -} - -void PlayerLocalState::loadHeroPaths(std::map & pathsMap) -{ - 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()); - } - } -} - -void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path) -{ - paths[h] = path; -} - -const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const -{ - assert(hasPath(h)); - return paths.at(h); -} - -bool PlayerLocalState::hasPath(const CGHeroInstance * h) const -{ - return paths.count(h) > 0; -} - -bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination) -{ - CGPath path; - 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) - return false; - } - - setPath(h, path); - return true; -} - -void PlayerLocalState::removeLastNode(const CGHeroInstance * h) -{ - assert(hasPath(h)); - if(!hasPath(h)) - return; - - auto & path = paths[h]; - path.nodes.pop_back(); - if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path - erasePath(h); -} - -void PlayerLocalState::erasePath(const CGHeroInstance * h) -{ - paths.erase(h); - adventureInt->onHeroChanged(h); -} - -void PlayerLocalState::verifyPath(const CGHeroInstance * h) -{ - if(!hasPath(h)) - return; - setPath(h, getPath(h).endPos()); -} - -const CGHeroInstance * PlayerLocalState::getCurrentHero() const -{ - if(currentSelection && currentSelection->ID == Obj::HERO) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero) -{ - bool currentHeroFound = false; - const CGHeroInstance * firstSuitable = nullptr; - const CGHeroInstance * nextSuitable = nullptr; - - for(const auto * hero : getWanderingHeroes()) - { - if (hero == currentHero) - { - currentHeroFound = true; - continue; - } - - if (isHeroSleeping(hero)) - continue; - - if (hero->movementPointsRemaining() == 0) - continue; - - if (!firstSuitable) - firstSuitable = hero; - - if (!nextSuitable && currentHeroFound) - nextSuitable = hero; - } - - // if we found suitable hero after currently selected hero -> return this hero - if (nextSuitable) - return nextSuitable; - - // othervice -> loop over and return first suitable hero in the list (or null if none) - return firstSuitable; -} - -const CGTownInstance * PlayerLocalState::getCurrentTown() const -{ - if(currentSelection && currentSelection->ID == Obj::TOWN) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -const CArmedInstance * PlayerLocalState::getCurrentArmy() const -{ - if(currentSelection) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -void PlayerLocalState::setSelection(const CArmedInstance * selection) -{ - if (currentSelection == selection) - return; - - currentSelection = selection; - - if (selection) - adventureInt->onSelectionChanged(selection); -} - -bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const -{ - return vstd::contains(sleepingHeroes, hero); -} - -void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - assert(!vstd::contains(sleepingHeroes, hero)); - - sleepingHeroes.push_back(hero); -} - -void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - assert(vstd::contains(sleepingHeroes, hero)); - - vstd::erase(sleepingHeroes, hero); -} - -const std::vector & PlayerLocalState::getWanderingHeroes() -{ - return wanderingHeroes; -} - -const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index) -{ - if(index < wanderingHeroes.size()) - return wanderingHeroes[index]; - return nullptr; -} - -void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) -{ - assert(hero); - assert(!vstd::contains(wanderingHeroes, hero)); - wanderingHeroes.push_back(hero); -} - -void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - - if (hero == currentSelection) - { - auto const * nextHero = getNextWanderingHero(hero); - setSelection(nextHero); - } - - vstd::erase(wanderingHeroes, hero); - vstd::erase(sleepingHeroes, hero); - - if (currentSelection == nullptr && !wanderingHeroes.empty()) - setSelection(wanderingHeroes.front()); - - if (currentSelection == nullptr && !ownedTowns.empty()) - setSelection(ownedTowns.front()); -} - -const std::vector & PlayerLocalState::getOwnedTowns() -{ - return ownedTowns; -} - -const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index) -{ - if(index < ownedTowns.size()) - return ownedTowns[index]; - return nullptr; -} - -void PlayerLocalState::addOwnedTown(const CGTownInstance * town) -{ - assert(town); - assert(!vstd::contains(ownedTowns, town)); - ownedTowns.push_back(town); -} - -void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) -{ - assert(town); - assert(vstd::contains(ownedTowns, town)); - vstd::erase(ownedTowns, town); - - if (town == currentSelection) - setSelection(nullptr); - - if (currentSelection == nullptr && !wanderingHeroes.empty()) - setSelection(wanderingHeroes.front()); - - if (currentSelection == nullptr && !ownedTowns.empty()) - setSelection(ownedTowns.front()); -} +/* + * PlayerLocalState.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 "PlayerLocalState.h" + +#include "../CCallback.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "CPlayerInterface.h" +#include "adventureMap/AdventureMapInterface.h" + +PlayerLocalState::PlayerLocalState(CPlayerInterface & owner) + : owner(owner) + , currentSelection(nullptr) +{ +} + +void PlayerLocalState::saveHeroPaths(std::map & pathsMap) +{ + 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()); + } +} + +void PlayerLocalState::loadHeroPaths(std::map & pathsMap) +{ + 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()); + } + } +} + +void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path) +{ + paths[h] = path; +} + +const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const +{ + assert(hasPath(h)); + return paths.at(h); +} + +bool PlayerLocalState::hasPath(const CGHeroInstance * h) const +{ + return paths.count(h) > 0; +} + +bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination) +{ + CGPath path; + 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) + return false; + } + + setPath(h, path); + return true; +} + +void PlayerLocalState::removeLastNode(const CGHeroInstance * h) +{ + assert(hasPath(h)); + if(!hasPath(h)) + return; + + auto & path = paths[h]; + path.nodes.pop_back(); + if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path + erasePath(h); +} + +void PlayerLocalState::erasePath(const CGHeroInstance * h) +{ + paths.erase(h); + adventureInt->onHeroChanged(h); +} + +void PlayerLocalState::verifyPath(const CGHeroInstance * h) +{ + if(!hasPath(h)) + return; + setPath(h, getPath(h).endPos()); +} + +const CGHeroInstance * PlayerLocalState::getCurrentHero() const +{ + if(currentSelection && currentSelection->ID == Obj::HERO) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero) +{ + bool currentHeroFound = false; + const CGHeroInstance * firstSuitable = nullptr; + const CGHeroInstance * nextSuitable = nullptr; + + for(const auto * hero : getWanderingHeroes()) + { + if (hero == currentHero) + { + currentHeroFound = true; + continue; + } + + if (isHeroSleeping(hero)) + continue; + + if (hero->movementPointsRemaining() == 0) + continue; + + if (!firstSuitable) + firstSuitable = hero; + + if (!nextSuitable && currentHeroFound) + nextSuitable = hero; + } + + // if we found suitable hero after currently selected hero -> return this hero + if (nextSuitable) + return nextSuitable; + + // othervice -> loop over and return first suitable hero in the list (or null if none) + return firstSuitable; +} + +const CGTownInstance * PlayerLocalState::getCurrentTown() const +{ + if(currentSelection && currentSelection->ID == Obj::TOWN) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +const CArmedInstance * PlayerLocalState::getCurrentArmy() const +{ + if(currentSelection) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +void PlayerLocalState::setSelection(const CArmedInstance * selection) +{ + if (currentSelection == selection) + return; + + currentSelection = selection; + + if (selection) + adventureInt->onSelectionChanged(selection); +} + +bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const +{ + return vstd::contains(sleepingHeroes, hero); +} + +void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + assert(!vstd::contains(sleepingHeroes, hero)); + + sleepingHeroes.push_back(hero); +} + +void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + assert(vstd::contains(sleepingHeroes, hero)); + + vstd::erase(sleepingHeroes, hero); +} + +const std::vector & PlayerLocalState::getWanderingHeroes() +{ + return wanderingHeroes; +} + +const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index) +{ + if(index < wanderingHeroes.size()) + return wanderingHeroes[index]; + return nullptr; +} + +void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) +{ + assert(hero); + assert(!vstd::contains(wanderingHeroes, hero)); + wanderingHeroes.push_back(hero); +} + +void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + + if (hero == currentSelection) + { + auto const * nextHero = getNextWanderingHero(hero); + setSelection(nextHero); + } + + vstd::erase(wanderingHeroes, hero); + vstd::erase(sleepingHeroes, hero); + + if (currentSelection == nullptr && !wanderingHeroes.empty()) + setSelection(wanderingHeroes.front()); + + if (currentSelection == nullptr && !ownedTowns.empty()) + setSelection(ownedTowns.front()); +} + +void PlayerLocalState::swapWanderingHero(int pos1, int pos2) +{ + assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]); + std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]); +} + +const std::vector & PlayerLocalState::getOwnedTowns() +{ + return ownedTowns; +} + +const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index) +{ + if(index < ownedTowns.size()) + return ownedTowns[index]; + return nullptr; +} + +void PlayerLocalState::addOwnedTown(const CGTownInstance * town) +{ + assert(town); + assert(!vstd::contains(ownedTowns, town)); + ownedTowns.push_back(town); +} + +void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) +{ + assert(town); + assert(vstd::contains(ownedTowns, town)); + vstd::erase(ownedTowns, town); + + if (town == currentSelection) + setSelection(nullptr); + + if (currentSelection == nullptr && !wanderingHeroes.empty()) + setSelection(wanderingHeroes.front()); + + if (currentSelection == nullptr && !ownedTowns.empty()) + setSelection(ownedTowns.front()); +} + +void PlayerLocalState::swapOwnedTowns(int pos1, int pos2) +{ + assert(ownedTowns[pos1] && ownedTowns[pos2]); + std::swap(ownedTowns[pos1], ownedTowns[pos2]); + + adventureInt->onTownOrderChanged(); +} diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index e88ee6ca3..85a367f21 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -1,111 +1,113 @@ -/* - * PlayerLocalState.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 CGHeroInstance; -class CGTownInstance; -class CArmedInstance; -struct CGPath; -class int3; - -VCMI_LIB_NAMESPACE_END - -class CPlayerInterface; - -/// Class that contains potentially serializeable state of a local player -class PlayerLocalState -{ - CPlayerInterface & owner; - - /// Currently selected object, can be town, hero or null - const CArmedInstance * currentSelection; - - std::map paths; //maps hero => selected path in adventure map - std::vector sleepingHeroes; //if hero is in here, he's sleeping - 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); - -public: - struct SpellbookLastSetting - { - //on which page we left spellbook - int spellbookLastPageBattle = 0; - int spellbokLastPageAdvmap = 0; - int spellbookLastTabBattle = 4; - int spellbookLastTabAdvmap = 4; - - template - void serialize(Handler & h, const int version) - { - h & spellbookLastPageBattle; - h & spellbokLastPageAdvmap; - h & spellbookLastTabBattle; - h & spellbookLastTabAdvmap; - } - } spellbookSettings; - - explicit PlayerLocalState(CPlayerInterface & owner); - - bool isHeroSleeping(const CGHeroInstance * hero) const; - void setHeroAsleep(const CGHeroInstance * hero); - void setHeroAwaken(const CGHeroInstance * hero); - - const std::vector & getOwnedTowns(); - const CGTownInstance * getOwnedTown(size_t index); - void addOwnedTown(const CGTownInstance * hero); - void removeOwnedTown(const CGTownInstance * hero); - - const std::vector & getWanderingHeroes(); - const CGHeroInstance * getWanderingHero(size_t index); - const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); - void addWanderingHero(const CGHeroInstance * hero); - void removeWanderingHero(const CGHeroInstance * hero); - - void setPath(const CGHeroInstance * h, const CGPath & path); - bool setPath(const CGHeroInstance * h, const int3 & destination); - - const CGPath & getPath(const CGHeroInstance * h) const; - bool hasPath(const CGHeroInstance * h) const; - - void removeLastNode(const CGHeroInstance * h); - void erasePath(const CGHeroInstance * h); - void verifyPath(const CGHeroInstance * h); - - /// Returns currently selected object - const CGHeroInstance * getCurrentHero() const; - const CGTownInstance * getCurrentTown() const; - const CArmedInstance * getCurrentArmy() const; - - /// Changes currently selected object - void setSelection(const CArmedInstance *sel); - - template - void serialize(Handler & h, int version) - { - //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; - } -}; +/* + * PlayerLocalState.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 CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +struct CGPath; +class int3; + +VCMI_LIB_NAMESPACE_END + +class CPlayerInterface; + +/// Class that contains potentially serializeable state of a local player +class PlayerLocalState +{ + CPlayerInterface & owner; + + /// Currently selected object, can be town, hero or null + const CArmedInstance * currentSelection; + + std::map paths; //maps hero => selected path in adventure map + std::vector sleepingHeroes; //if hero is in here, he's sleeping + 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); + +public: + struct SpellbookLastSetting + { + //on which page we left spellbook + int spellbookLastPageBattle = 0; + int spellbokLastPageAdvmap = 0; + int spellbookLastTabBattle = 4; + int spellbookLastTabAdvmap = 4; + + template + void serialize(Handler & h, const int version) + { + h & spellbookLastPageBattle; + h & spellbokLastPageAdvmap; + h & spellbookLastTabBattle; + h & spellbookLastTabAdvmap; + } + } spellbookSettings; + + explicit PlayerLocalState(CPlayerInterface & owner); + + bool isHeroSleeping(const CGHeroInstance * hero) const; + void setHeroAsleep(const CGHeroInstance * hero); + void setHeroAwaken(const CGHeroInstance * hero); + + const std::vector & getOwnedTowns(); + const CGTownInstance * getOwnedTown(size_t index); + void addOwnedTown(const CGTownInstance * hero); + void removeOwnedTown(const CGTownInstance * hero); + void swapOwnedTowns(int pos1, int pos2); + + const std::vector & getWanderingHeroes(); + const CGHeroInstance * getWanderingHero(size_t index); + const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); + void addWanderingHero(const CGHeroInstance * hero); + void removeWanderingHero(const CGHeroInstance * hero); + void swapWanderingHero(int pos1, int pos2); + + void setPath(const CGHeroInstance * h, const CGPath & path); + bool setPath(const CGHeroInstance * h, const int3 & destination); + + const CGPath & getPath(const CGHeroInstance * h) const; + bool hasPath(const CGHeroInstance * h) const; + + void removeLastNode(const CGHeroInstance * h); + void erasePath(const CGHeroInstance * h); + void verifyPath(const CGHeroInstance * h); + + /// Returns currently selected object + const CGHeroInstance * getCurrentHero() const; + const CGTownInstance * getCurrentTown() const; + const CArmedInstance * getCurrentArmy() const; + + /// Changes currently selected object + void setSelection(const CArmedInstance *sel); + + template + void serialize(Handler & h, int version) + { + //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/StdInc.cpp b/client/StdInc.cpp index c8f4ddf05..c17377322 100644 --- a/client/StdInc.cpp +++ b/client/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" \ No newline at end of file diff --git a/client/StdInc.h b/client/StdInc.h index 1f4e8bafc..90fdbd301 100644 --- a/client/StdInc.h +++ b/client/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp index 845082b67..081be52ae 100644 --- a/client/VCMI_client.cbp +++ b/client/VCMI_client.cbp @@ -198,6 +198,8 @@ + + @@ -225,6 +227,10 @@ + + + + diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj index 73de261cb..997bdba6a 100644 --- a/client/VCMI_client.vcxproj +++ b/client/VCMI_client.vcxproj @@ -217,6 +217,7 @@ + @@ -236,6 +237,8 @@ + + @@ -285,6 +288,7 @@ + @@ -301,6 +305,8 @@ + + diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters index a93e38825..9b717e834 100644 --- a/client/VCMI_client.vcxproj.filters +++ b/client/VCMI_client.vcxproj.filters @@ -22,6 +22,12 @@ windows + + windows + + + windows + windows @@ -129,6 +135,7 @@ + @@ -172,6 +179,12 @@ windows + + windows + + + windows + windows @@ -284,5 +297,6 @@ + \ No newline at end of file diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 7a8d4b3ea..8aa0de501 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -1,853 +1,898 @@ -/* - * AdventureMapInterface.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 "AdventureMapInterface.h" - -#include "AdventureOptions.h" -#include "AdventureState.h" -#include "CInGameConsole.h" -#include "CMinimap.h" -#include "CList.h" -#include "CInfoBar.h" -#include "MapAudioPlayer.h" -#include "AdventureMapWidget.h" -#include "AdventureMapShortcuts.h" - -#include "../mapView/mapHandler.h" -#include "../mapView/MapView.h" -#include "../windows/InfoWindows.h" -#include "../CGameInfo.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../render/Canvas.h" -#include "../CMT.h" -#include "../PlayerLocalState.h" -#include "../CPlayerInterface.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapping/CMapDefines.h" -#include "../../lib/pathfinder/CGPathNode.h" - -std::shared_ptr adventureInt; - -AdventureMapInterface::AdventureMapInterface(): - mapAudio(new MapAudioPlayer()), - spellBeingCasted(nullptr), - scrollingWasActive(false), - scrollingWasBlocked(false) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - shortcuts = std::make_shared(*this); - - widget = std::make_shared(shortcuts); - shortcuts->setState(EAdventureState::MAKING_TURN); - widget->getMapView()->onViewMapActivated(); - - addUsedEvents(KEYBOARD | TIME); -} - -void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel) -{ - shortcuts->onMapViewMoved(visibleArea, mapLevel); - widget->getMinimap()->onMapViewMoved(visibleArea, mapLevel); - widget->onMapViewMoved(visibleArea, mapLevel); -} - -void AdventureMapInterface::onAudioResumed() -{ - mapAudio->onAudioResumed(); -} - -void AdventureMapInterface::onAudioPaused() -{ - mapAudio->onAudioPaused(); -} - -void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero) -{ - if (shortcuts->optionMapViewActive()) - { - widget->getInfoBar()->popAll(); - widget->getInfoBar()->showSelection(); - } -} - -void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) -{ - widget->getHeroList()->updateElement(h); - - if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents()) - widget->getInfoBar()->showSelection(); - - widget->updateActiveState(); -} - -void AdventureMapInterface::onTownChanged(const CGTownInstance * town) -{ - widget->getTownList()->updateElement(town); - - if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents()) - widget->getInfoBar()->showSelection(); -} - -void AdventureMapInterface::showInfoBoxMessage(const std::vector & components, std::string message, int timer) -{ - widget->getInfoBar()->pushComponents(components, message, timer); -} - -void AdventureMapInterface::activate() -{ - CIntObject::activate(); - - adjustActiveness(); - - screenBuf = screen; - - if(LOCPLINT) - { - LOCPLINT->cingconsole->activate(); - LOCPLINT->cingconsole->pos = this->pos; - } - - GH.fakeMouseMove(); //to restore the cursor - - // workaround for an edge case: - // if player unequips Angel Wings / Boots of Levitation of currently active hero - // game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero - if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero()) - LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero()); -} - -void AdventureMapInterface::deactivate() -{ - CIntObject::deactivate(); - CCS->curh->set(Cursor::Map::POINTER); - - if(LOCPLINT) - LOCPLINT->cingconsole->deactivate(); -} - -void AdventureMapInterface::showAll(Canvas & to) -{ - CIntObject::showAll(to); - LOCPLINT->cingconsole->show(to); -} - -void AdventureMapInterface::show(Canvas & to) -{ - CIntObject::show(to); - LOCPLINT->cingconsole->show(to); -} - -void AdventureMapInterface::tick(uint32_t msPassed) -{ - handleMapScrollingUpdate(msPassed); - - // we want animations to be active during enemy turn but map itself to be non-interactive - // so call timer update directly on inactive element - widget->getMapView()->tick(msPassed); -} - -void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) -{ - /// Width of window border, in pixels, that triggers map scrolling - static constexpr int32_t borderScrollWidth = 15; - - int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float(); - int32_t scrollDistance = scrollSpeedPixels * timePassed / 1000; - - Point cursorPosition = GH.getCursorPosition(); - Point scrollDirection; - - if (cursorPosition.x < borderScrollWidth) - scrollDirection.x = -1; - - if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth) - scrollDirection.x = +1; - - if (cursorPosition.y < borderScrollWidth) - scrollDirection.y = -1; - - if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth) - scrollDirection.y = +1; - - Point scrollDelta = scrollDirection * scrollDistance; - - bool cursorInScrollArea = scrollDelta != Point(0,0); - bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; - bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); - - if (!scrollingWasActive && scrollingBlocked) - { - scrollingWasBlocked = true; - return; - } - - if (!cursorInScrollArea && scrollingWasBlocked) - { - scrollingWasBlocked = false; - return; - } - - if (scrollingActive) - widget->getMapView()->onMapScrolled(scrollDelta); - - if (!scrollingActive && !scrollingWasActive) - return; - - if(scrollDelta.x > 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::SCROLL_EAST); - } - if(scrollDelta.x < 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::SCROLL_WEST); - } - - if (scrollDelta.x == 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTH); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTH); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::POINTER); - } - - scrollingWasActive = scrollingActive; -} - -void AdventureMapInterface::centerOnTile(int3 on) -{ - widget->getMapView()->onCenteredTile(on); -} - -void AdventureMapInterface::centerOnObject(const CGObjectInstance * obj) -{ - widget->getMapView()->onCenteredObject(obj); -} - -void AdventureMapInterface::keyPressed(EShortcut key) -{ - if (key == EShortcut::GLOBAL_CANCEL && spellBeingCasted) - hotkeyAbortCastingMode(); - - //fake mouse use to trigger onTileHovered() - GH.fakeMouseMove(); -} - -void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) -{ - assert(sel); - - widget->getInfoBar()->popAll(); - mapAudio->onSelectionChanged(sel); - bool centerView = !settings["session"]["autoSkip"].Bool(); - - if (centerView) - centerOnObject(sel); - - if(sel->ID==Obj::TOWN) - { - auto town = dynamic_cast(sel); - - widget->getInfoBar()->showTownSelection(town); - widget->getTownList()->select(town); - widget->getHeroList()->select(nullptr); - onHeroChanged(nullptr); - } - else //hero selected - { - auto hero = dynamic_cast(sel); - - widget->getInfoBar()->showHeroSelection(hero); - widget->getHeroList()->select(hero); - widget->getTownList()->select(nullptr); - - LOCPLINT->localState->verifyPath(hero); - onHeroChanged(hero); - } - - widget->updateActiveState(); - widget->getHeroList()->redraw(); - widget->getTownList()->redraw(); -} - -void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) -{ - if (positions) - widget->getMinimap()->updateTiles(*positions); - else - widget->getMinimap()->update(); -} - -void AdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID) -{ - onCurrentPlayerChanged(playerID); - setState(EAdventureState::HOTSEAT_WAIT); -} - -void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman) -{ - if(settings["session"]["spectate"].Bool()) - return; - - mapAudio->onEnemyTurnStarted(); - widget->getMinimap()->setAIRadar(!isHuman); - widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer()); - setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); -} - -void AdventureMapInterface::setState(EAdventureState state) -{ - shortcuts->setState(state); - adjustActiveness(); - widget->updateActiveState(); -} - -void AdventureMapInterface::adjustActiveness() -{ - bool widgetMustBeActive = isActive() && shortcuts->optionSidePanelActive(); - bool mapViewMustBeActive = isActive() && (shortcuts->optionMapViewActive()); - - widget->setInputEnabled(widgetMustBeActive); - widget->getMapView()->setInputEnabled(mapViewMustBeActive); -} - -void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID) -{ - LOCPLINT->localState->setSelection(nullptr); - - if (playerID == currentPlayerID) - return; - - currentPlayerID = playerID; - widget->setPlayer(playerID); -} - -void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) -{ - onCurrentPlayerChanged(playerID); - - setState(EAdventureState::MAKING_TURN); - if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID - || settings["session"]["spectate"].Bool()) - { - widget->getMinimap()->setAIRadar(false); - widget->getInfoBar()->showSelection(); - } - - widget->getHeroList()->updateWidget(); - widget->getTownList()->updateWidget(); - - const CGHeroInstance * heroToSelect = nullptr; - - // find first non-sleeping hero - for (auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if (!LOCPLINT->localState->isHeroSleeping(hero)) - { - heroToSelect = hero; - break; - } - } - - //select first hero if available. - if (heroToSelect != nullptr) - { - LOCPLINT->localState->setSelection(heroToSelect); - } - else if (LOCPLINT->localState->getOwnedTowns().size()) - { - LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0)); - } - else - { - LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0)); - } - - //show new day animation and sound on infobar, except for 1st day of the game - if (LOCPLINT->cb->getDate(Date::DAY) != 1) - widget->getInfoBar()->showDate(); - - onHeroChanged(nullptr); - Canvas canvas = Canvas::createFromSurface(screen); - showAll(canvas); - mapAudio->onPlayerTurnStarted(); - - if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - if(auto iw = GH.windows().topWindow()) - iw->close(); - - hotkeyEndingTurn(); - } -} - -void AdventureMapInterface::hotkeyEndingTurn() -{ - if(settings["session"]["spectate"].Bool()) - return; - - if(!settings["general"]["startTurnAutosave"].Bool()) - { - LOCPLINT->performAutosave(); - } - - LOCPLINT->makingTurn = false; - LOCPLINT->cb->endTurn(); - - mapAudio->onPlayerTurnEnded(); -} - -const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) -{ - std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos); //blocking objects at tile - - if (bobjs.empty()) - return nullptr; - - return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder); -} - -void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - //FIXME: this line breaks H3 behavior for Dimension Door - if(!LOCPLINT->cb->isVisible(mapPos)) - return; - if(!LOCPLINT->makingTurn) - return; - - const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos); - - const CGObjectInstance *topBlocking = getActiveObject(mapPos); - - int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - if(spellBeingCasted) - { - assert(shortcuts->optionSpellcasting()); - - if (!isInScreenRange(selPos, mapPos)) - return; - - const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos); - - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: //Scuttle Boat - if(topBlocking && topBlocking->ID == Obj::BOAT) - performSpellcasting(mapPos); - break; - case SpellID::DIMENSION_DOOR: - if(!tile || tile->isClear(heroTile)) - performSpellcasting(mapPos); - break; - } - return; - } - //check if we can select this object - bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; - canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner); - - bool isHero = false; - if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town) - { - if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked - LOCPLINT->openTownWindow(static_cast(topBlocking)); - else if(canSelect) - LOCPLINT->localState->setSelection(static_cast(topBlocking)); - } - else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected - { - isHero = true; - - const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos); - if(currentHero == topBlocking) //clicked selected hero - { - LOCPLINT->openHeroWindow(currentHero); - return; - } - else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile - { - LOCPLINT->localState->setSelection(static_cast(topBlocking)); - return; - } - 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() == mapPos)//we'll be moving - { - if(!CGI->mh->hasOngoingAnimations()) - LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); - return; - } - else //remove old path and find a new one if we clicked on accessible tile - { - LOCPLINT->localState->setPath(currentHero, mapPos); - onHeroChanged(currentHero); - } - } - } //end of hero is selected "case" - else - { - throw std::runtime_error("Nothing is selected..."); - } - - const auto shipyard = ourInaccessibleShipyard(topBlocking); - if(isHero && shipyard != nullptr) - { - LOCPLINT->showShipyardDialogOrProblemPopup(shipyard); - } -} - -void AdventureMapInterface::onTileHovered(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - //may occur just at the start of game (fake move before full intiialization) - if(!LOCPLINT->localState->getCurrentArmy()) - return; - - if(!LOCPLINT->cb->isVisible(mapPos)) - { - CCS->curh->set(Cursor::Map::POINTER); - GH.statusbar()->clear(); - return; - } - auto objRelations = PlayerRelations::ALLIES; - const CGObjectInstance *objAtTile = getActiveObject(mapPos); - if(objAtTile) - { - objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner); - std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID); - boost::replace_all(text,"\n"," "); - GH.statusbar()->write(text); - } - else - { - std::string hlp = CGI->mh->getTerrainDescr(mapPos, false); - GH.statusbar()->write(hlp); - } - - if(spellBeingCasted) - { - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: - { - int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - - if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos)) - CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); - else - CCS->curh->set(Cursor::Map::POINTER); - return; - } - case SpellID::DIMENSION_DOOR: - { - const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false); - int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos)) - CCS->curh->set(Cursor::Map::TELEPORT); - else - CCS->curh->set(Cursor::Map::POINTER); - return; - } - } - } - - if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN) - { - if(objAtTile) - { - if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES) - CCS->curh->set(Cursor::Map::TOWN); - else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(Cursor::Map::POINTER); - } - else - CCS->curh->set(Cursor::Map::POINTER); - } - else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero()) - { - std::array cursorMove = { Cursor::Map::T1_MOVE, Cursor::Map::T2_MOVE, Cursor::Map::T3_MOVE, Cursor::Map::T4_MOVE, }; - std::array cursorAttack = { Cursor::Map::T1_ATTACK, Cursor::Map::T2_ATTACK, Cursor::Map::T3_ATTACK, Cursor::Map::T4_ATTACK, }; - std::array cursorSail = { Cursor::Map::T1_SAIL, Cursor::Map::T2_SAIL, Cursor::Map::T3_SAIL, Cursor::Map::T4_SAIL, }; - std::array cursorDisembark = { Cursor::Map::T1_DISEMBARK, Cursor::Map::T2_DISEMBARK, Cursor::Map::T3_DISEMBARK, Cursor::Map::T4_DISEMBARK, }; - std::array cursorExchange = { Cursor::Map::T1_EXCHANGE, Cursor::Map::T2_EXCHANGE, Cursor::Map::T3_EXCHANGE, Cursor::Map::T4_EXCHANGE, }; - std::array cursorVisit = { Cursor::Map::T1_VISIT, Cursor::Map::T2_VISIT, Cursor::Map::T3_VISIT, Cursor::Map::T4_VISIT, }; - std::array cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, }; - - const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos); - assert(pathNode); - - if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info - { - showMoveDetailsInStatusbar(*hero, *pathNode); - } - - int turns = pathNode->turns; - vstd::amin(turns, 3); - switch(pathNode->action) - { - case EPathNodeAction::NORMAL: - case EPathNodeAction::TELEPORT_NORMAL: - if(pathNode->layer == EPathfindingLayer::LAND) - CCS->curh->set(cursorMove[turns]); - else - CCS->curh->set(cursorSailVisit[turns]); - break; - - case EPathNodeAction::VISIT: - case EPathNodeAction::BLOCKING_VISIT: - case EPathNodeAction::TELEPORT_BLOCKING_VISIT: - if(objAtTile && objAtTile->ID == Obj::HERO) - { - if(LOCPLINT->localState->getCurrentArmy() == objAtTile) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(cursorExchange[turns]); - } - else if(pathNode->layer == EPathfindingLayer::LAND) - CCS->curh->set(cursorVisit[turns]); - else - CCS->curh->set(cursorSailVisit[turns]); - break; - - case EPathNodeAction::BATTLE: - case EPathNodeAction::TELEPORT_BATTLE: - CCS->curh->set(cursorAttack[turns]); - break; - - case EPathNodeAction::EMBARK: - CCS->curh->set(cursorSail[turns]); - break; - - case EPathNodeAction::DISEMBARK: - CCS->curh->set(cursorDisembark[turns]); - break; - - default: - if(objAtTile && objRelations != PlayerRelations::ENEMIES) - { - if(objAtTile->ID == Obj::TOWN) - CCS->curh->set(Cursor::Map::TOWN); - else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(Cursor::Map::POINTER); - } - else - CCS->curh->set(Cursor::Map::POINTER); - break; - } - } - - if(ourInaccessibleShipyard(objAtTile)) - { - CCS->curh->set(Cursor::Map::T1_SAIL); - } -} - -void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode) -{ - const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining(); - const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains; - const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0; - - std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns"); - - boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns)); - boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost)); - boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove)); - - GH.statusbar()->write(result); -} - -void AdventureMapInterface::onTileRightClicked(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - if(spellBeingCasted) - { - hotkeyAbortCastingMode(); - return; - } - - if(!LOCPLINT->cb->isVisible(mapPos)) - { - CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory - return; - } - - const CGObjectInstance * obj = getActiveObject(mapPos); - if(!obj) - { - // Bare or undiscovered terrain - const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos); - if(tile) - { - std::string hlp = CGI->mh->getTerrainDescr(mapPos, true); - CRClickPopup::createAndPush(hlp); - } - return; - } - - CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER); -} - -void AdventureMapInterface::enterCastingMode(const CSpell * sp) -{ - assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR); - spellBeingCasted = sp; - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = true; - - setState(EAdventureState::CASTING_SPELL); -} - -void AdventureMapInterface::exitCastingMode() -{ - assert(spellBeingCasted); - spellBeingCasted = nullptr; - setState(EAdventureState::MAKING_TURN); - - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = false; -} - -void AdventureMapInterface::hotkeyAbortCastingMode() -{ - exitCastingMode(); - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled -} - -void AdventureMapInterface::performSpellcasting(const int3 & dest) -{ - SpellID id = spellBeingCasted->id; - exitCastingMode(); - LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest); -} - -Rect AdventureMapInterface::terrainAreaPixels() const -{ - return widget->getMapView()->pos; -} - -const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const -{ - const IShipyard *ret = IShipyard::castFrom(obj); - - if(!ret || - obj->tempOwner != currentPlayerID || - (CCS->curh->get() != Cursor::Map::T1_SAIL && CCS->curh->get() != Cursor::Map::POINTER)) - return nullptr; - - return ret; -} - -void AdventureMapInterface::hotkeyExitWorldView() -{ - setState(EAdventureState::MAKING_TURN); - widget->getMapView()->onViewMapActivated(); -} - -void AdventureMapInterface::openWorldView(int tileSize) -{ - setState(EAdventureState::WORLD_VIEW); - widget->getMapView()->onViewWorldActivated(tileSize); -} - -void AdventureMapInterface::openWorldView() -{ - openWorldView(11); -} - -void AdventureMapInterface::openWorldView(const std::vector& objectPositions, bool showTerrain) -{ - openWorldView(11); - widget->getMapView()->onViewSpellActivated(11, objectPositions, showTerrain); -} - -void AdventureMapInterface::hotkeyNextTown() -{ - widget->getTownList()->selectNext(); -} - -void AdventureMapInterface::hotkeySwitchMapLevel() -{ - widget->getMapView()->onMapLevelSwitched(); -} - -void AdventureMapInterface::hotkeyZoom(int delta) -{ - widget->getMapView()->onMapZoomLevelChanged(delta); -} - -void AdventureMapInterface::onScreenResize() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - // remember our activation state and reactive after reconstruction - // since othervice activate() calls for created elements will bypass virtual dispatch - // and will call directly CIntObject::activate() instead of dispatching virtual function call - bool widgetActive = isActive(); - - if (widgetActive) - deactivate(); - - widget.reset(); - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - widget = std::make_shared(shortcuts); - widget->getMapView()->onViewMapActivated(); - widget->setPlayer(currentPlayerID); - widget->updateActiveState(); - widget->getMinimap()->update(); - widget->getInfoBar()->showSelection(); - - if (LOCPLINT && LOCPLINT->localState->getCurrentArmy()) - widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); - - adjustActiveness(); - - if (widgetActive) - activate(); -} +/* + * AdventureMapInterface.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 "AdventureMapInterface.h" + +#include "AdventureOptions.h" +#include "AdventureState.h" +#include "CInGameConsole.h" +#include "CMinimap.h" +#include "CList.h" +#include "CInfoBar.h" +#include "MapAudioPlayer.h" +#include "TurnTimerWidget.h" +#include "AdventureMapWidget.h" +#include "AdventureMapShortcuts.h" + +#include "../mapView/mapHandler.h" +#include "../mapView/MapView.h" +#include "../windows/InfoWindows.h" +#include "../widgets/RadialMenu.h" +#include "../CGameInfo.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../render/Canvas.h" +#include "../CMT.h" +#include "../PlayerLocalState.h" +#include "../CPlayerInterface.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/StartInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/pathfinder/CGPathNode.h" + +std::shared_ptr adventureInt; + +AdventureMapInterface::AdventureMapInterface(): + mapAudio(new MapAudioPlayer()), + spellBeingCasted(nullptr), + scrollingWasActive(false), + scrollingWasBlocked(false), + backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + shortcuts = std::make_shared(*this); + + widget = std::make_shared(shortcuts); + shortcuts->setState(EAdventureState::MAKING_TURN); + widget->getMapView()->onViewMapActivated(); + + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled()) + watches = std::make_shared(); + + addUsedEvents(KEYBOARD | TIME); +} + +void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel) +{ + shortcuts->onMapViewMoved(visibleArea, mapLevel); + widget->getMinimap()->onMapViewMoved(visibleArea, mapLevel); + widget->onMapViewMoved(visibleArea, mapLevel); +} + +void AdventureMapInterface::onAudioResumed() +{ + mapAudio->onAudioResumed(); +} + +void AdventureMapInterface::onAudioPaused() +{ + mapAudio->onAudioPaused(); +} + +void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero) +{ + if (shortcuts->optionMapViewActive()) + { + widget->getInfoBar()->popAll(); + widget->getInfoBar()->showSelection(); + } +} + +void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) +{ + widget->getHeroList()->updateElement(h); + + if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents()) + widget->getInfoBar()->showSelection(); + + widget->updateActiveState(); +} + +void AdventureMapInterface::onTownChanged(const CGTownInstance * town) +{ + widget->getTownList()->updateElement(town); + + if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents()) + widget->getInfoBar()->showSelection(); +} + +void AdventureMapInterface::showInfoBoxMessage(const std::vector & components, std::string message, int timer) +{ + widget->getInfoBar()->pushComponents(components, message, timer); +} + +void AdventureMapInterface::activate() +{ + CIntObject::activate(); + + adjustActiveness(); + + screenBuf = screen; + + if(LOCPLINT) + { + LOCPLINT->cingconsole->activate(); + LOCPLINT->cingconsole->pos = this->pos; + } + + GH.fakeMouseMove(); //to restore the cursor + + // workaround for an edge case: + // if player unequips Angel Wings / Boots of Levitation of currently active hero + // game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero + if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero()) + LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero()); +} + +void AdventureMapInterface::deactivate() +{ + CIntObject::deactivate(); + CCS->curh->set(Cursor::Map::POINTER); + + if(LOCPLINT) + LOCPLINT->cingconsole->deactivate(); +} + +void AdventureMapInterface::showAll(Canvas & to) +{ + CIntObject::showAll(to); + dim(to); + LOCPLINT->cingconsole->show(to); +} + +void AdventureMapInterface::show(Canvas & to) +{ + CIntObject::show(to); + dim(to); + LOCPLINT->cingconsole->show(to); +} + +void AdventureMapInterface::dim(Canvas & to) +{ + for (auto window : GH.windows().findWindows()) + { + if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) + { + Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); + ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); + if(backgroundDimLevel > 0) + to.drawColorBlended(targetRect, colorToFill); + return; + } + } +} + +void AdventureMapInterface::tick(uint32_t msPassed) +{ + handleMapScrollingUpdate(msPassed); + + // we want animations to be active during enemy turn but map itself to be non-interactive + // so call timer update directly on inactive element + widget->getMapView()->tick(msPassed); +} + +void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) +{ + /// Width of window border, in pixels, that triggers map scrolling + static constexpr int32_t borderScrollWidth = 15; + + int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float(); + int32_t scrollDistance = scrollSpeedPixels * timePassed / 1000; + + Point cursorPosition = GH.getCursorPosition(); + Point scrollDirection; + + if (cursorPosition.x < borderScrollWidth) + scrollDirection.x = -1; + + if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth) + scrollDirection.x = +1; + + if (cursorPosition.y < borderScrollWidth) + scrollDirection.y = -1; + + if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth) + scrollDirection.y = +1; + + Point scrollDelta = scrollDirection * scrollDistance; + + bool cursorInScrollArea = scrollDelta != Point(0,0); + bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; + bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); + + if (!scrollingWasActive && scrollingBlocked) + { + scrollingWasBlocked = true; + return; + } + + if (!cursorInScrollArea && scrollingWasBlocked) + { + scrollingWasBlocked = false; + return; + } + + if (scrollingActive) + widget->getMapView()->onMapScrolled(scrollDelta); + + if (!scrollingActive && !scrollingWasActive) + return; + + if(scrollDelta.x > 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::SCROLL_EAST); + } + if(scrollDelta.x < 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::SCROLL_WEST); + } + + if (scrollDelta.x == 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTH); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTH); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::POINTER); + } + + scrollingWasActive = scrollingActive; +} + +void AdventureMapInterface::centerOnTile(int3 on) +{ + widget->getMapView()->onCenteredTile(on); +} + +void AdventureMapInterface::centerOnObject(const CGObjectInstance * obj) +{ + widget->getMapView()->onCenteredObject(obj); +} + +void AdventureMapInterface::keyPressed(EShortcut key) +{ + if (key == EShortcut::GLOBAL_CANCEL && spellBeingCasted) + hotkeyAbortCastingMode(); + + //fake mouse use to trigger onTileHovered() + GH.fakeMouseMove(); +} + +void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) +{ + assert(sel); + + widget->getInfoBar()->popAll(); + mapAudio->onSelectionChanged(sel); + bool centerView = !settings["session"]["autoSkip"].Bool(); + + if (centerView) + centerOnObject(sel); + + if(sel->ID==Obj::TOWN) + { + auto town = dynamic_cast(sel); + + widget->getInfoBar()->showTownSelection(town); + widget->getTownList()->updateWidget();; + widget->getTownList()->select(town); + widget->getHeroList()->select(nullptr); + onHeroChanged(nullptr); + } + else //hero selected + { + auto hero = dynamic_cast(sel); + + widget->getInfoBar()->showHeroSelection(hero); + widget->getHeroList()->select(hero); + widget->getTownList()->select(nullptr); + + LOCPLINT->localState->verifyPath(hero); + onHeroChanged(hero); + } + + widget->updateActiveState(); + widget->getHeroList()->redraw(); + widget->getTownList()->redraw(); +} + +void AdventureMapInterface::onTownOrderChanged() +{ + widget->getTownList()->updateWidget(); +} + +void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) +{ + if (positions) + widget->getMinimap()->updateTiles(*positions); + else + widget->getMinimap()->update(); +} + +void AdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID) +{ + backgroundDimLevel = 255; + + onCurrentPlayerChanged(playerID); + setState(EAdventureState::HOTSEAT_WAIT); +} + +void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman) +{ + if(settings["session"]["spectate"].Bool()) + return; + + mapAudio->onEnemyTurnStarted(); + widget->getMinimap()->setAIRadar(!isHuman); + widget->getInfoBar()->startEnemyTurn(playerID); + setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); +} + +void AdventureMapInterface::setState(EAdventureState state) +{ + shortcuts->setState(state); + adjustActiveness(); + widget->updateActiveState(); +} + +void AdventureMapInterface::adjustActiveness() +{ + bool widgetMustBeActive = isActive() && shortcuts->optionSidePanelActive(); + bool mapViewMustBeActive = isActive() && (shortcuts->optionMapViewActive()); + + widget->setInputEnabled(widgetMustBeActive); + widget->getMapView()->setInputEnabled(mapViewMustBeActive); +} + +void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID) +{ + LOCPLINT->localState->setSelection(nullptr); + + if (playerID == currentPlayerID) + return; + + currentPlayerID = playerID; + widget->setPlayer(playerID); +} + +void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) +{ + backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); + + onCurrentPlayerChanged(playerID); + + setState(EAdventureState::MAKING_TURN); + if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool()) + { + widget->getMinimap()->setAIRadar(false); + widget->getInfoBar()->showSelection(); + } + + widget->getHeroList()->updateWidget(); + widget->getTownList()->updateWidget(); + + const CGHeroInstance * heroToSelect = nullptr; + + // find first non-sleeping hero + for (auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if (!LOCPLINT->localState->isHeroSleeping(hero)) + { + heroToSelect = hero; + break; + } + } + + //select first hero if available. + if (heroToSelect != nullptr) + { + LOCPLINT->localState->setSelection(heroToSelect); + } + else if (LOCPLINT->localState->getOwnedTowns().size()) + { + LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0)); + } + else + { + LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0)); + } + + //show new day animation and sound on infobar, except for 1st day of the game + if (LOCPLINT->cb->getDate(Date::DAY) != 1) + widget->getInfoBar()->showDate(); + + onHeroChanged(nullptr); + Canvas canvas = Canvas::createFromSurface(screen); + showAll(canvas); + mapAudio->onPlayerTurnStarted(); + + if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + if(auto iw = GH.windows().topWindow()) + iw->close(); + + GH.dispatchMainThread([this]() + { + hotkeyEndingTurn(); + }); + } +} + +void AdventureMapInterface::hotkeyEndingTurn() +{ + if(settings["session"]["spectate"].Bool()) + return; + + if(!settings["general"]["startTurnAutosave"].Bool()) + { + LOCPLINT->performAutosave(); + } + + LOCPLINT->makingTurn = false; + LOCPLINT->cb->endTurn(); + + mapAudio->onPlayerTurnEnded(); +} + +const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) +{ + std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos); //blocking objects at tile + + if (bobjs.empty()) + return nullptr; + + return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder); +} + +void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + //FIXME: this line breaks H3 behavior for Dimension Door + if(!LOCPLINT->cb->isVisible(mapPos)) + return; + if(!LOCPLINT->makingTurn) + return; + + const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos); + + const CGObjectInstance *topBlocking = getActiveObject(mapPos); + + int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + if(spellBeingCasted) + { + assert(shortcuts->optionSpellcasting()); + + if (!isInScreenRange(selPos, mapPos)) + return; + + const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos); + + switch(spellBeingCasted->id) + { + case SpellID::SCUTTLE_BOAT: //Scuttle Boat + if(topBlocking && topBlocking->ID == Obj::BOAT) + performSpellcasting(mapPos); + break; + case SpellID::DIMENSION_DOOR: + if(!tile || tile->isClear(heroTile)) + performSpellcasting(mapPos); + break; + } + return; + } + //check if we can select this object + bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; + canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES; + + bool isHero = false; + if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town) + { + if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked + LOCPLINT->openTownWindow(static_cast(topBlocking)); + else if(canSelect) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + } + else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected + { + isHero = true; + + const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos); + if(currentHero == topBlocking) //clicked selected hero + { + LOCPLINT->openHeroWindow(currentHero); + return; + } + else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile + { + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + return; + } + 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() == mapPos)//we'll be moving + { + assert(!CGI->mh->hasOngoingAnimations()); + if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0) + LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); + return; + } + else + { + if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected) + { + if(canSelect) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + } + else //remove old path and find a new one if we clicked on accessible tile + { + LOCPLINT->localState->setPath(currentHero, mapPos); + onHeroChanged(currentHero); + } + } + } + } //end of hero is selected "case" + else + { + throw std::runtime_error("Nothing is selected..."); + } + + const auto shipyard = ourInaccessibleShipyard(topBlocking); + if(isHero && shipyard != nullptr) + { + LOCPLINT->showShipyardDialogOrProblemPopup(shipyard); + } +} + +void AdventureMapInterface::onTileHovered(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + //may occur just at the start of game (fake move before full intiialization) + if(!LOCPLINT->localState->getCurrentArmy()) + return; + + if(!LOCPLINT->cb->isVisible(mapPos)) + { + CCS->curh->set(Cursor::Map::POINTER); + GH.statusbar()->clear(); + return; + } + auto objRelations = PlayerRelations::ALLIES; + const CGObjectInstance *objAtTile = getActiveObject(mapPos); + if(objAtTile) + { + objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner); + std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID); + boost::replace_all(text,"\n"," "); + GH.statusbar()->write(text); + } + else + { + std::string hlp = CGI->mh->getTerrainDescr(mapPos, false); + GH.statusbar()->write(hlp); + } + + if(spellBeingCasted) + { + switch(spellBeingCasted->id) + { + case SpellID::SCUTTLE_BOAT: + { + int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + + if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos)) + CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); + else + CCS->curh->set(Cursor::Map::POINTER); + return; + } + case SpellID::DIMENSION_DOOR: + { + const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false); + int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos)) + CCS->curh->set(Cursor::Map::TELEPORT); + else + CCS->curh->set(Cursor::Map::POINTER); + return; + } + } + } + + if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown()) + { + if(objAtTile) + { + if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES) + CCS->curh->set(Cursor::Map::TOWN); + else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(Cursor::Map::POINTER); + } + else + CCS->curh->set(Cursor::Map::POINTER); + } + else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero()) + { + std::array cursorMove = { Cursor::Map::T1_MOVE, Cursor::Map::T2_MOVE, Cursor::Map::T3_MOVE, Cursor::Map::T4_MOVE, }; + std::array cursorAttack = { Cursor::Map::T1_ATTACK, Cursor::Map::T2_ATTACK, Cursor::Map::T3_ATTACK, Cursor::Map::T4_ATTACK, }; + std::array cursorSail = { Cursor::Map::T1_SAIL, Cursor::Map::T2_SAIL, Cursor::Map::T3_SAIL, Cursor::Map::T4_SAIL, }; + std::array cursorDisembark = { Cursor::Map::T1_DISEMBARK, Cursor::Map::T2_DISEMBARK, Cursor::Map::T3_DISEMBARK, Cursor::Map::T4_DISEMBARK, }; + std::array cursorExchange = { Cursor::Map::T1_EXCHANGE, Cursor::Map::T2_EXCHANGE, Cursor::Map::T3_EXCHANGE, Cursor::Map::T4_EXCHANGE, }; + std::array cursorVisit = { Cursor::Map::T1_VISIT, Cursor::Map::T2_VISIT, Cursor::Map::T3_VISIT, Cursor::Map::T4_VISIT, }; + std::array cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, }; + + const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos); + assert(pathNode); + + if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info + { + showMoveDetailsInStatusbar(*hero, *pathNode); + } + + int turns = pathNode->turns; + vstd::amin(turns, 3); + switch(pathNode->action) + { + case EPathNodeAction::NORMAL: + case EPathNodeAction::TELEPORT_NORMAL: + if(pathNode->layer == EPathfindingLayer::LAND) + CCS->curh->set(cursorMove[turns]); + else + CCS->curh->set(cursorSailVisit[turns]); + break; + + case EPathNodeAction::VISIT: + case EPathNodeAction::BLOCKING_VISIT: + case EPathNodeAction::TELEPORT_BLOCKING_VISIT: + if(objAtTile && objAtTile->ID == Obj::HERO) + { + if(LOCPLINT->localState->getCurrentArmy() == objAtTile) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(cursorExchange[turns]); + } + else if(pathNode->layer == EPathfindingLayer::LAND) + CCS->curh->set(cursorVisit[turns]); + else + CCS->curh->set(cursorSailVisit[turns]); + break; + + case EPathNodeAction::BATTLE: + case EPathNodeAction::TELEPORT_BATTLE: + CCS->curh->set(cursorAttack[turns]); + break; + + case EPathNodeAction::EMBARK: + CCS->curh->set(cursorSail[turns]); + break; + + case EPathNodeAction::DISEMBARK: + CCS->curh->set(cursorDisembark[turns]); + break; + + default: + if(objAtTile && objRelations != PlayerRelations::ENEMIES) + { + if(objAtTile->ID == Obj::TOWN) + CCS->curh->set(Cursor::Map::TOWN); + else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(Cursor::Map::POINTER); + } + else + CCS->curh->set(Cursor::Map::POINTER); + break; + } + } + + if(ourInaccessibleShipyard(objAtTile)) + { + CCS->curh->set(Cursor::Map::T1_SAIL); + } +} + +void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode) +{ + const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining(); + const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains; + const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0; + + std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns"); + + boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns)); + boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost)); + boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove)); + + GH.statusbar()->write(result); +} + +void AdventureMapInterface::onTileRightClicked(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + if(spellBeingCasted) + { + hotkeyAbortCastingMode(); + return; + } + + if(!LOCPLINT->cb->isVisible(mapPos)) + { + CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory + return; + } + + const CGObjectInstance * obj = getActiveObject(mapPos); + if(!obj) + { + // Bare or undiscovered terrain + const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos); + if(tile) + { + std::string hlp = CGI->mh->getTerrainDescr(mapPos, true); + CRClickPopup::createAndPush(hlp); + } + return; + } + + CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER); +} + +void AdventureMapInterface::enterCastingMode(const CSpell * sp) +{ + assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR); + spellBeingCasted = sp; + Settings config = settings.write["session"]["showSpellRange"]; + config->Bool() = true; + + setState(EAdventureState::CASTING_SPELL); +} + +void AdventureMapInterface::exitCastingMode() +{ + assert(spellBeingCasted); + spellBeingCasted = nullptr; + setState(EAdventureState::MAKING_TURN); + + Settings config = settings.write["session"]["showSpellRange"]; + config->Bool() = false; +} + +void AdventureMapInterface::hotkeyAbortCastingMode() +{ + exitCastingMode(); + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled +} + +void AdventureMapInterface::performSpellcasting(const int3 & dest) +{ + SpellID id = spellBeingCasted->id; + exitCastingMode(); + LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest); +} + +Rect AdventureMapInterface::terrainAreaPixels() const +{ + return widget->getMapView()->pos; +} + +const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const +{ + const IShipyard *ret = IShipyard::castFrom(obj); + + if(!ret || + obj->tempOwner != currentPlayerID || + (CCS->curh->get() != Cursor::Map::T1_SAIL && CCS->curh->get() != Cursor::Map::POINTER)) + return nullptr; + + return ret; +} + +void AdventureMapInterface::hotkeyExitWorldView() +{ + setState(EAdventureState::MAKING_TURN); + widget->getMapView()->onViewMapActivated(); +} + +void AdventureMapInterface::openWorldView(int tileSize) +{ + setState(EAdventureState::WORLD_VIEW); + widget->getMapView()->onViewWorldActivated(tileSize); +} + +void AdventureMapInterface::openWorldView() +{ + openWorldView(11); +} + +void AdventureMapInterface::openWorldView(const std::vector& objectPositions, bool showTerrain) +{ + openWorldView(11); + widget->getMapView()->onViewSpellActivated(11, objectPositions, showTerrain); +} + +void AdventureMapInterface::hotkeyNextTown() +{ + widget->getTownList()->selectNext(); +} + +void AdventureMapInterface::hotkeySwitchMapLevel() +{ + widget->getMapView()->onMapLevelSwitched(); +} + +void AdventureMapInterface::hotkeyZoom(int delta) +{ + widget->getMapView()->onMapZoomLevelChanged(delta); +} + +void AdventureMapInterface::onScreenResize() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // remember our activation state and reactive after reconstruction + // since othervice activate() calls for created elements will bypass virtual dispatch + // and will call directly CIntObject::activate() instead of dispatching virtual function call + bool widgetActive = isActive(); + + if (widgetActive) + deactivate(); + + widget.reset(); + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + widget = std::make_shared(shortcuts); + widget->getMapView()->onViewMapActivated(); + widget->setPlayer(currentPlayerID); + widget->updateActiveState(); + widget->getMinimap()->update(); + widget->getInfoBar()->showSelection(); + + if (LOCPLINT && LOCPLINT->localState->getCurrentArmy()) + widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); + + adjustActiveness(); + + if (widgetActive) + activate(); +} diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 6f69b2cdd..3a8c96add 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -1,183 +1,194 @@ -/* - * AdventureMapInterface.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" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CGTownInstance; -class CArmedInstance; -class IShipyard; -struct CGPathNode; -struct ObjectPosInfo; -struct Component; -class int3; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class IImage; -class CAnimImage; -class CGStatusBar; -class AdventureMapWidget; -class AdventureMapShortcuts; -class CAnimation; -class MapView; -class CResDataBar; -class CHeroList; -class CTownList; -class CInfoBar; -class CMinimap; -class MapAudioPlayer; -enum class EAdventureState; - -struct MapDrawingInfo; - -/// That's a huge class which handles general adventure map actions and -/// shows the right menu(questlog, spellbook, end turn,..) from where you -/// can get to the towns and heroes. -class AdventureMapInterface : public CIntObject -{ -private: - /// currently acting player - PlayerColor currentPlayerID; - - /// if true, cursor was changed to scrolling and must be reset back once scroll is over - bool scrollingWasActive; - - /// if true, then scrolling was blocked via ctrl and should not restart until player move cursor outside scrolling area - bool scrollingWasBlocked; - - /// spell for which player is selecting target, or nullptr if none - const CSpell *spellBeingCasted; - - std::shared_ptr mapAudio; - std::shared_ptr widget; - std::shared_ptr shortcuts; - -private: - void setState(EAdventureState state); - - /// updates active state of game window whenever game state changes - void adjustActiveness(); - - /// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else - const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; - - /// check and if necessary reacts on scrolling by moving cursor to screen edge - void handleMapScrollingUpdate(uint32_t msPassed); - - void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode); - - const CGObjectInstance *getActiveObject(const int3 &tile); - - /// exits currently opened world view mode and returns to normal map - void exitCastingMode(); - - /// casts current spell at specified location - void performSpellcasting(const int3 & castTarget); - -protected: - /// CIntObject interface implementation - - void activate() override; - void deactivate() override; - - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - void keyPressed(EShortcut key) override; - - void onScreenResize() override; - -public: - AdventureMapInterface(); - - void hotkeyAbortCastingMode(); - void hotkeyExitWorldView(); - void hotkeyEndingTurn(); - void hotkeyNextTown(); - void hotkeySwitchMapLevel(); - void hotkeyZoom(int delta); - - /// Called by PlayerInterface when specified player is ready to start his turn - void onHotseatWaitStarted(PlayerColor playerID); - - /// Called by PlayerInterface when AI or remote human player starts his turn - void onEnemyTurnStarted(PlayerColor playerID, bool isHuman); - - /// Called by PlayerInterface when local human player starts his turn - void onPlayerTurnStarted(PlayerColor playerID); - - /// Called by PlayerInterface when interface should be switched to specified player without starting turn - void onCurrentPlayerChanged(PlayerColor playerID); - - /// Called by PlayerInterface when specific map tile changed and must be updated on minimap - void onMapTilesChanged(boost::optional> positions); - - /// Called by PlayerInterface when hero starts movement - void onHeroMovementStarted(const CGHeroInstance * hero); - - /// Called by PlayerInterface when hero state changed and hero list must be updated - void onHeroChanged(const CGHeroInstance * hero); - - /// Called by PlayerInterface when town state changed and town list must be updated - void onTownChanged(const CGTownInstance * town); - - /// Called when currently selected object changes - void onSelectionChanged(const CArmedInstance *sel); - - /// Called when map audio should be paused, e.g. on combat or town screen access - void onAudioPaused(); - - /// Called when map audio should be resume, opposite to onPaused - void onAudioResumed(); - - /// Requests to display provided information inside infobox - void showInfoBoxMessage(const std::vector & components, std::string message, int timer); - - /// Changes position on map to center selected location - void centerOnTile(int3 on); - void centerOnObject(const CGObjectInstance *obj); - - /// called by MapView whenever currently visible area changes - /// visibleArea describes now visible map section measured in tiles - void onMapViewMoved(const Rect & visibleArea, int mapLevel); - - /// called by MapView whenever tile is clicked - void onTileLeftClicked(const int3 & mapPos); - - /// called by MapView whenever tile is hovered - void onTileHovered(const int3 & mapPos); - - /// called by MapView whenever tile is clicked - void onTileRightClicked(const int3 & mapPos); - - /// called by spell window when spell to cast has been selected - void enterCastingMode(const CSpell * sp); - - /// returns area of screen covered by terrain (main game area) - Rect terrainAreaPixels() const; - - /// opens world view at default scale - void openWorldView(); - - /// opens world view at specific scale - void openWorldView(int tileSize); - - /// opens world view with specific info, e.g. after View Earth/Air is shown - void openWorldView(const std::vector& objectPositions, bool showTerrain); -}; - -extern std::shared_ptr adventureInt; +/* + * AdventureMapInterface.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" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +class IShipyard; +struct CGPathNode; +struct ObjectPosInfo; +struct Component; +class int3; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class IImage; +class CAnimImage; +class CGStatusBar; +class AdventureMapWidget; +class AdventureMapShortcuts; +class CAnimation; +class MapView; +class CResDataBar; +class CHeroList; +class CTownList; +class CInfoBar; +class CMinimap; +class MapAudioPlayer; +class TurnTimerWidget; +enum class EAdventureState; + +struct MapDrawingInfo; + +/// That's a huge class which handles general adventure map actions and +/// shows the right menu(questlog, spellbook, end turn,..) from where you +/// can get to the towns and heroes. +class AdventureMapInterface : public CIntObject +{ +private: + /// currently acting player + PlayerColor currentPlayerID; + + /// if true, cursor was changed to scrolling and must be reset back once scroll is over + bool scrollingWasActive; + + /// if true, then scrolling was blocked via ctrl and should not restart until player move cursor outside scrolling area + bool scrollingWasBlocked; + + /// how much should the background dimmed, when windows are on the top + int backgroundDimLevel; + + /// spell for which player is selecting target, or nullptr if none + const CSpell *spellBeingCasted; + + std::shared_ptr mapAudio; + std::shared_ptr widget; + std::shared_ptr shortcuts; + std::shared_ptr watches; + +private: + void setState(EAdventureState state); + + /// updates active state of game window whenever game state changes + void adjustActiveness(); + + /// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else + const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; + + /// check and if necessary reacts on scrolling by moving cursor to screen edge + void handleMapScrollingUpdate(uint32_t msPassed); + + void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode); + + const CGObjectInstance *getActiveObject(const int3 &tile); + + /// exits currently opened world view mode and returns to normal map + void exitCastingMode(); + + /// casts current spell at specified location + void performSpellcasting(const int3 & castTarget); + + /// dim interface if some windows opened + void dim(Canvas & to); + +protected: + /// CIntObject interface implementation + + void activate() override; + void deactivate() override; + + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + void keyPressed(EShortcut key) override; + + void onScreenResize() override; + +public: + AdventureMapInterface(); + + void hotkeyAbortCastingMode(); + void hotkeyExitWorldView(); + void hotkeyEndingTurn(); + void hotkeyNextTown(); + void hotkeySwitchMapLevel(); + void hotkeyZoom(int delta); + + /// Called by PlayerInterface when specified player is ready to start his turn + void onHotseatWaitStarted(PlayerColor playerID); + + /// Called by PlayerInterface when AI or remote human player starts his turn + void onEnemyTurnStarted(PlayerColor playerID, bool isHuman); + + /// Called by PlayerInterface when local human player starts his turn + void onPlayerTurnStarted(PlayerColor playerID); + + /// Called by PlayerInterface when interface should be switched to specified player without starting turn + void onCurrentPlayerChanged(PlayerColor playerID); + + /// Called by PlayerInterface when specific map tile changed and must be updated on minimap + void onMapTilesChanged(boost::optional> positions); + + /// Called by PlayerInterface when hero starts movement + void onHeroMovementStarted(const CGHeroInstance * hero); + + /// Called by PlayerInterface when hero state changed and hero list must be updated + void onHeroChanged(const CGHeroInstance * hero); + + /// Called by PlayerInterface when town state changed and town list must be updated + void onTownChanged(const CGTownInstance * town); + + /// Called when currently selected object changes + void onSelectionChanged(const CArmedInstance *sel); + + /// Called when town order changes + void onTownOrderChanged(); + + /// Called when map audio should be paused, e.g. on combat or town screen access + void onAudioPaused(); + + /// Called when map audio should be resume, opposite to onPaused + void onAudioResumed(); + + /// Requests to display provided information inside infobox + void showInfoBoxMessage(const std::vector & components, std::string message, int timer); + + /// Changes position on map to center selected location + void centerOnTile(int3 on); + void centerOnObject(const CGObjectInstance *obj); + + /// called by MapView whenever currently visible area changes + /// visibleArea describes now visible map section measured in tiles + void onMapViewMoved(const Rect & visibleArea, int mapLevel); + + /// called by MapView whenever tile is clicked + void onTileLeftClicked(const int3 & mapPos); + + /// called by MapView whenever tile is hovered + void onTileHovered(const int3 & mapPos); + + /// called by MapView whenever tile is clicked + void onTileRightClicked(const int3 & mapPos); + + /// called by spell window when spell to cast has been selected + void enterCastingMode(const CSpell * sp); + + /// returns area of screen covered by terrain (main game area) + Rect terrainAreaPixels() const; + + /// opens world view at default scale + void openWorldView(); + + /// opens world view at specific scale + void openWorldView(int tileSize); + + /// opens world view with specific info, e.g. after View Earth/Air is shown + void openWorldView(const std::vector& objectPositions, bool showTerrain); +}; + +extern std::shared_ptr adventureInt; diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 38193b405..abdb52cd1 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -71,7 +71,7 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_GAME_OPTIONS, optionInMapView(), [this]() { this->adventureOptions(); } }, { EShortcut::GLOBAL_OPTIONS, optionInMapView(), [this]() { this->systemOptions(); } }, { EShortcut::ADVENTURE_NEXT_HERO, optionHasNextHero(), [this]() { this->nextHero(); } }, - { EShortcut::GAME_END_TURN, optionInMapView(), [this]() { this->endTurn(); } }, + { EShortcut::GAME_END_TURN, optionCanEndTurn(), [this]() { this->endTurn(); } }, { EShortcut::ADVENTURE_THIEVES_GUILD, optionInMapView(), [this]() { this->showThievesGuild(); } }, { EShortcut::ADVENTURE_VIEW_SCENARIO, optionInMapView(), [this]() { this->showScenarioInfo(); } }, { EShortcut::GAME_SAVE_GAME, optionInMapView(), [this]() { this->saveGame(); } }, @@ -79,7 +79,7 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_DIG_GRAIL, optionHeroSelected(), [this]() { this->digGrail(); } }, { EShortcut::ADVENTURE_VIEW_PUZZLE, optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } }, { EShortcut::GAME_RESTART_GAME, optionInMapView(), [this]() { this->restartGame(); } }, - { EShortcut::ADVENTURE_VISIT_OBJECT, optionHeroSelected(), [this]() { this->visitObject(); } }, + { EShortcut::ADVENTURE_VISIT_OBJECT, optionCanVisitObject(), [this]() { this->visitObject(); } }, { EShortcut::ADVENTURE_VIEW_SELECTED, optionInMapView(), [this]() { this->openObject(); } }, { EShortcut::GAME_OPEN_MARKETPLACE, optionInMapView(), [this]() { this->showMarketplace(); } }, { EShortcut::ADVENTURE_ZOOM_IN, optionSidePanelActive(),[this]() { this->zoom(+1); } }, @@ -314,7 +314,7 @@ void AdventureMapShortcuts::visitObject() const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); if(h) - LOCPLINT->cb->moveHero(h,h->pos); + LOCPLINT->cb->moveHero(h, h->pos); } void AdventureMapShortcuts::openObject() @@ -342,7 +342,7 @@ void AdventureMapShortcuts::showMarketplace() } if(townWithMarket) //if any town has marketplace, open window - GH.windows().createAndPushWindow(townWithMarket); + GH.windows().createAndPushWindow(townWithMarket, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); else //if not - complain LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); } @@ -422,6 +422,18 @@ bool AdventureMapShortcuts::optionHeroAwake() return optionInMapView() && hero && !LOCPLINT->localState->isHeroSleeping(hero); } +bool AdventureMapShortcuts::optionCanVisitObject() +{ + if (!optionHeroSelected()) + return false; + + 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 +} + bool AdventureMapShortcuts::optionHeroSelected() { return optionInMapView() && LOCPLINT->localState->getCurrentHero() != nullptr; @@ -441,6 +453,11 @@ bool AdventureMapShortcuts::optionHasNextHero() return optionInMapView() && nextSuitableHero != nullptr; } +bool AdventureMapShortcuts::optionCanEndTurn() +{ + return optionInMapView() && LOCPLINT->makingTurn; +} + bool AdventureMapShortcuts::optionSpellcasting() { return state == EAdventureState::CASTING_SPELL; diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index 8e86779a7..435c321cc 100644 --- a/client/adventureMap/AdventureMapShortcuts.h +++ b/client/adventureMap/AdventureMapShortcuts.h @@ -77,6 +77,8 @@ public: bool optionHeroSelected(); bool optionHeroCanMove(); bool optionHasNextHero(); + bool optionCanVisitObject(); + bool optionCanEndTurn(); bool optionSpellcasting(); bool optionInMapView(); bool optionInWorldView(); diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 65e1230be..7333f1d37 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -1,459 +1,455 @@ -/* - * CAdventureMapWidget.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 "AdventureMapWidget.h" - -#include "AdventureMapShortcuts.h" -#include "CInfoBar.h" -#include "CList.h" -#include "CMinimap.h" -#include "CResDataBar.h" -#include "AdventureState.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../mapView/MapView.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" - -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" - -#include "../../lib/StringConstants.h" -#include "../../lib/filesystem/ResourceID.h" - -AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) - : shortcuts(shortcuts) - , mapLevel(0) -{ - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - REGISTER_BUILDER("adventureInfobar", &AdventureMapWidget::buildInfobox ); - REGISTER_BUILDER("adventureMapImage", &AdventureMapWidget::buildMapImage ); - REGISTER_BUILDER("adventureMapButton", &AdventureMapWidget::buildMapButton ); - REGISTER_BUILDER("adventureMapContainer", &AdventureMapWidget::buildMapContainer ); - REGISTER_BUILDER("adventureMapGameArea", &AdventureMapWidget::buildMapGameArea ); - REGISTER_BUILDER("adventureMapHeroList", &AdventureMapWidget::buildMapHeroList ); - REGISTER_BUILDER("adventureMapIcon", &AdventureMapWidget::buildMapIcon ); - REGISTER_BUILDER("adventureMapTownList", &AdventureMapWidget::buildMapTownList ); - REGISTER_BUILDER("adventureMinimap", &AdventureMapWidget::buildMinimap ); - REGISTER_BUILDER("adventureResourceDateBar", &AdventureMapWidget::buildResourceDateBar ); - REGISTER_BUILDER("adventureStatusBar", &AdventureMapWidget::buildStatusBar ); - REGISTER_BUILDER("adventurePlayerTexture", &AdventureMapWidget::buildTexturePlayerColored); - - for (const auto & entry : shortcuts->getShortcuts()) - addShortcut(entry.shortcut, entry.callback); - - const JsonNode config(ResourceID("config/widgets/adventureMap.json")); - - for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) - { - ResourceID resourceName(entry.String(), EResType::IMAGE); - playerColorerImages.push_back(resourceName.getName()); - } - - build(config); - addUsedEvents(KEYBOARD); -} - -void AdventureMapWidget::onMapViewMoved(const Rect & visibleArea, int newMapLevel) -{ - if(mapLevel == newMapLevel) - return; - - mapLevel = newMapLevel; - updateActiveState(); -} - -Rect AdventureMapWidget::readSourceArea(const JsonNode & source, const JsonNode & sourceCommon) -{ - const auto & input = source.isNull() ? sourceCommon : source; - - return readArea(input, Rect(Point(0, 0), Point(800, 600))); -} - -Rect AdventureMapWidget::readTargetArea(const JsonNode & source) -{ - if(subwidgetSizes.empty()) - return readArea(source, pos); - return readArea(source, subwidgetSizes.back()); -} - -Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & boundingBox) -{ - const auto & object = source.Struct(); - - if(object.count("left") + object.count("width") + object.count("right") != 2) - logGlobal->error("Invalid area definition in widget! Unable to load width!"); - - if(object.count("top") + object.count("height") + object.count("bottom") != 2) - logGlobal->error("Invalid area definition in widget! Unable to load height!"); - - int left = source["left"].Integer(); - int width = source["width"].Integer(); - int right = source["right"].Integer(); - - int top = source["top"].Integer(); - int height = source["height"].Integer(); - int bottom = source["bottom"].Integer(); - - Point topLeft(left, top); - Point dimensions(width, height); - - if(source["left"].isNull()) - topLeft.x = boundingBox.w - right - width; - - if(source["width"].isNull()) - dimensions.x = boundingBox.w - right - left; - - if(source["top"].isNull()) - topLeft.y = boundingBox.h - bottom - height; - - if(source["height"].isNull()) - dimensions.y = boundingBox.h - bottom - top; - - return Rect(topLeft + boundingBox.topLeft(), dimensions); -} - -std::shared_ptr AdventureMapWidget::loadImage(const std::string & name) -{ - ResourceID resource(name, EResType::IMAGE); - - if(images.count(resource.getName()) == 0) - images[resource.getName()] = IImage::createFromFile(resource.getName()); - - return images[resource.getName()]; -} - -std::shared_ptr AdventureMapWidget::loadAnimation(const std::string & name) -{ - ResourceID resource(name, EResType::ANIMATION); - - if(animations.count(resource.getName()) == 0) - animations[resource.getName()] = std::make_shared(resource.getName()); - - return animations[resource.getName()]; -} - -std::shared_ptr AdventureMapWidget::buildInfobox(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - infoBar = std::make_shared(area); - return infoBar; -} - -std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & input) -{ - Rect targetArea = readTargetArea(input["area"]); - Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); - std::string image = input["image"].String(); - - return std::make_shared(loadImage(image), targetArea, sourceArea); -} - -std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) -{ - auto position = readTargetArea(input["area"]); - auto image = input["image"].String(); - auto help = readHintText(input["help"]); - bool playerColored = input["playerColored"].Bool(); - - auto button = std::make_shared(position.topLeft(), image, help, 0, EShortcut::NONE, playerColored); - - loadButtonBorderColor(button, input["borderColor"]); - loadButtonHotkey(button, input["hotkey"]); - - return button; -} - -std::shared_ptr AdventureMapWidget::buildMapContainer(const JsonNode & input) -{ - auto position = readTargetArea(input["area"]); - std::shared_ptr result; - - if (!input["exists"].isNull()) - { - if (!input["exists"]["heightMin"].isNull() && input["exists"]["heightMin"].Integer() >= pos.h) - return nullptr; - if (!input["exists"]["heightMax"].isNull() && input["exists"]["heightMax"].Integer() < pos.h) - return nullptr; - if (!input["exists"]["widthMin"].isNull() && input["exists"]["widthMin"].Integer() >= pos.w) - return nullptr; - if (!input["exists"]["widthMax"].isNull() && input["exists"]["widthMax"].Integer() < pos.w) - return nullptr; - } - - if (input["overlay"].Bool()) - result = std::make_shared(); - else - result = std::make_shared(); - - result->disableCondition = input["hideWhen"].String(); - - result->moveBy(position.topLeft()); - subwidgetSizes.push_back(position); - for(const auto & entry : input["items"].Vector()) - { - auto widget = buildWidget(entry); - - addWidget(entry["name"].String(), widget); - result->ownedChildren.push_back(widget); - - // FIXME: remove cast and replace it with better check - if (std::dynamic_pointer_cast(widget) || std::dynamic_pointer_cast(widget)) - result->addChild(widget.get(), true); - else - result->addChild(widget.get(), false); - } - subwidgetSizes.pop_back(); - - return result; -} - -std::shared_ptr AdventureMapWidget::buildMapGameArea(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - mapView = std::make_shared(area.topLeft(), area.dimensions()); - return mapView; -} - -std::shared_ptr AdventureMapWidget::buildMapHeroList(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - subwidgetSizes.push_back(area); - - Rect item = readTargetArea(input["item"]); - - Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); - int itemsCount = input["itemsCount"].Integer(); - - auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getWanderingHeroes().size()); - - - if(!input["scrollUp"].isNull()) - result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); - - if(!input["scrollDown"].isNull()) - result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); - - subwidgetSizes.pop_back(); - - heroList = result; - return result; -} - -std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - size_t index = input["index"].Integer(); - size_t perPlayer = input["perPlayer"].Integer(); - std::string image = input["image"].String(); - - return std::make_shared(area.topLeft(), loadAnimation(image), index, perPlayer); -} - -std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - subwidgetSizes.push_back(area); - - Rect item = readTargetArea(input["item"]); - Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); - int itemsCount = input["itemsCount"].Integer(); - - auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getOwnedTowns().size()); - - - if(!input["scrollUp"].isNull()) - result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); - - if(!input["scrollDown"].isNull()) - result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); - - subwidgetSizes.pop_back(); - - townList = result; - return result; -} - -std::shared_ptr AdventureMapWidget::buildMinimap(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - minimap = std::make_shared(area); - return minimap; -} - -std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - std::string image = input["image"].String(); - - auto result = std::make_shared(image, area.topLeft()); - - for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - { - const auto & node = input[GameConstants::RESOURCE_NAMES[i]]; - - if(node.isNull()) - continue; - - result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer())); - } - - result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer())); - - return result; -} - -std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - std::string image = input["image"].String(); - - auto background = std::make_shared(image, area); - - return CGStatusBar::create(background); -} - -std::shared_ptr AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input) -{ - logGlobal->debug("Building widget CFilledTexture"); - auto image = input["image"].String(); - Rect area = readTargetArea(input["area"]); - return std::make_shared(image, area); -} - -std::shared_ptr AdventureMapWidget::getHeroList() -{ - return heroList; -} - -std::shared_ptr AdventureMapWidget::getTownList() -{ - return townList; -} - -std::shared_ptr AdventureMapWidget::getMinimap() -{ - return minimap; -} - -std::shared_ptr AdventureMapWidget::getMapView() -{ - return mapView; -} - -std::shared_ptr AdventureMapWidget::getInfoBar() -{ - return infoBar; -} - -void AdventureMapWidget::setPlayer(const PlayerColor & player) -{ - setPlayerChildren(this, player); -} - -void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColor & player) -{ - for(auto & entry : widget->children) - { - auto container = dynamic_cast(entry); - auto icon = dynamic_cast(entry); - auto button = dynamic_cast(entry); - auto resDataBar = dynamic_cast(entry); - auto texture = dynamic_cast(entry); - - if(button) - button->setPlayerColor(player); - - if(resDataBar) - resDataBar->colorize(player); - - if(icon) - icon->setPlayer(player); - - if(container) - setPlayerChildren(container, player); - - if (texture) - texture->playerColored(player); - } - - for(const auto & entry : playerColorerImages) - { - if(images.count(entry)) - images[entry]->playerColored(player); - } - - redraw(); -} - -CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr animation, size_t index, size_t iconsPerPlayer) - : index(index) - , iconsPerPlayer(iconsPerPlayer) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += position; - image = std::make_shared(animation, index); -} - -void CAdventureMapIcon::setPlayer(const PlayerColor & player) -{ - image->setFrame(index + player.getNum() * iconsPerPlayer); -} - -void CAdventureMapOverlayWidget::show(Canvas & to) -{ - CIntObject::showAll(to); -} - -void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget) -{ - for(auto & entry : widget->children) - { - auto container = dynamic_cast(entry); - - if (container) - { - if (container->disableCondition == "heroAwake") - container->setEnabled(!shortcuts->optionHeroSleeping()); - - if (container->disableCondition == "heroSleeping") - container->setEnabled(shortcuts->optionHeroSleeping()); - - if (container->disableCondition == "mapLayerSurface") - container->setEnabled(shortcuts->optionMapLevelSurface()); - - if (container->disableCondition == "mapLayerUnderground") - container->setEnabled(!shortcuts->optionMapLevelSurface()); - - if (container->disableCondition == "mapViewMode") - container->setEnabled(shortcuts->optionInWorldView()); - - if (container->disableCondition == "worldViewMode") - container->setEnabled(!shortcuts->optionInWorldView()); - - updateActiveStateChildden(container); - } - } -} - -void AdventureMapWidget::updateActiveState() -{ - updateActiveStateChildden(this); - - for (auto entry: shortcuts->getShortcuts()) - setShortcutBlocked(entry.shortcut, !entry.isEnabled); -} +/* + * CAdventureMapWidget.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 "AdventureMapWidget.h" + +#include "AdventureMapShortcuts.h" +#include "CInfoBar.h" +#include "CList.h" +#include "CMinimap.h" +#include "CResDataBar.h" +#include "AdventureState.h" + +#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" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" + +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" + +#include "../../lib/constants/StringConstants.h" +#include "../../lib/filesystem/ResourcePath.h" + +AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) + : shortcuts(shortcuts) + , mapLevel(0) +{ + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + REGISTER_BUILDER("adventureInfobar", &AdventureMapWidget::buildInfobox ); + REGISTER_BUILDER("adventureMapImage", &AdventureMapWidget::buildMapImage ); + REGISTER_BUILDER("adventureMapButton", &AdventureMapWidget::buildMapButton ); + REGISTER_BUILDER("adventureMapContainer", &AdventureMapWidget::buildMapContainer ); + REGISTER_BUILDER("adventureMapGameArea", &AdventureMapWidget::buildMapGameArea ); + REGISTER_BUILDER("adventureMapHeroList", &AdventureMapWidget::buildMapHeroList ); + REGISTER_BUILDER("adventureMapIcon", &AdventureMapWidget::buildMapIcon ); + REGISTER_BUILDER("adventureMapTownList", &AdventureMapWidget::buildMapTownList ); + REGISTER_BUILDER("adventureMinimap", &AdventureMapWidget::buildMinimap ); + REGISTER_BUILDER("adventureResourceDateBar", &AdventureMapWidget::buildResourceDateBar ); + REGISTER_BUILDER("adventureStatusBar", &AdventureMapWidget::buildStatusBar ); + REGISTER_BUILDER("adventurePlayerTexture", &AdventureMapWidget::buildTexturePlayerColored); + + for (const auto & entry : shortcuts->getShortcuts()) + addShortcut(entry.shortcut, entry.callback); + + const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); + + for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) + playerColorerImages.push_back(ImagePath::fromJson(entry)); + + build(config); + addUsedEvents(KEYBOARD); +} + +void AdventureMapWidget::onMapViewMoved(const Rect & visibleArea, int newMapLevel) +{ + if(mapLevel == newMapLevel) + return; + + mapLevel = newMapLevel; + updateActiveState(); +} + +Rect AdventureMapWidget::readSourceArea(const JsonNode & source, const JsonNode & sourceCommon) +{ + const auto & input = source.isNull() ? sourceCommon : source; + + return readArea(input, Rect(Point(0, 0), Point(800, 600))); +} + +Rect AdventureMapWidget::readTargetArea(const JsonNode & source) +{ + if(subwidgetSizes.empty()) + return readArea(source, pos); + return readArea(source, subwidgetSizes.back()); +} + +Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & boundingBox) +{ + const auto & object = source.Struct(); + + if(object.count("left") + object.count("width") + object.count("right") != 2) + logGlobal->error("Invalid area definition in widget! Unable to load width!"); + + if(object.count("top") + object.count("height") + object.count("bottom") != 2) + logGlobal->error("Invalid area definition in widget! Unable to load height!"); + + int left = source["left"].Integer(); + int width = source["width"].Integer(); + int right = source["right"].Integer(); + + int top = source["top"].Integer(); + int height = source["height"].Integer(); + int bottom = source["bottom"].Integer(); + + Point topLeft(left, top); + Point dimensions(width, height); + + if(source["left"].isNull()) + topLeft.x = boundingBox.w - right - width; + + if(source["width"].isNull()) + dimensions.x = boundingBox.w - right - left; + + if(source["top"].isNull()) + topLeft.y = boundingBox.h - bottom - height; + + if(source["height"].isNull()) + dimensions.y = boundingBox.h - bottom - top; + + 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"]); + infoBar = std::make_shared(area); + return infoBar; +} + +std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & input) +{ + Rect targetArea = readTargetArea(input["area"]); + Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); + + return std::make_shared(loadImage(input["image"]), targetArea, sourceArea); +} + +std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) +{ + auto position = readTargetArea(input["area"]); + auto image = AnimationPath::fromJson(input["image"]); + auto help = readHintText(input["help"]); + bool playerColored = input["playerColored"].Bool(); + + auto button = std::make_shared(position.topLeft(), image, help, 0, EShortcut::NONE, playerColored); + + loadButtonBorderColor(button, input["borderColor"]); + loadButtonHotkey(button, input["hotkey"]); + + return button; +} + +std::shared_ptr AdventureMapWidget::buildMapContainer(const JsonNode & input) +{ + auto position = readTargetArea(input["area"]); + std::shared_ptr result; + + if (!input["exists"].isNull()) + { + if (!input["exists"]["heightMin"].isNull() && input["exists"]["heightMin"].Integer() >= pos.h) + return nullptr; + if (!input["exists"]["heightMax"].isNull() && input["exists"]["heightMax"].Integer() < pos.h) + return nullptr; + if (!input["exists"]["widthMin"].isNull() && input["exists"]["widthMin"].Integer() >= pos.w) + return nullptr; + if (!input["exists"]["widthMax"].isNull() && input["exists"]["widthMax"].Integer() < pos.w) + return nullptr; + } + + if (input["overlay"].Bool()) + result = std::make_shared(); + else + result = std::make_shared(); + + result->disableCondition = input["hideWhen"].String(); + + result->moveBy(position.topLeft()); + subwidgetSizes.push_back(position); + for(const auto & entry : input["items"].Vector()) + { + auto widget = buildWidget(entry); + + addWidget(entry["name"].String(), widget); + result->ownedChildren.push_back(widget); + + // FIXME: remove cast and replace it with better check + if (std::dynamic_pointer_cast(widget) || std::dynamic_pointer_cast(widget)) + result->addChild(widget.get(), true); + else + result->addChild(widget.get(), false); + } + subwidgetSizes.pop_back(); + + return result; +} + +std::shared_ptr AdventureMapWidget::buildMapGameArea(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + mapView = std::make_shared(area.topLeft(), area.dimensions()); + return mapView; +} + +std::shared_ptr AdventureMapWidget::buildMapHeroList(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + subwidgetSizes.push_back(area); + + Rect item = readTargetArea(input["item"]); + + Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); + int itemsCount = input["itemsCount"].Integer(); + + auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getWanderingHeroes().size()); + + + if(!input["scrollUp"].isNull()) + result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); + + if(!input["scrollDown"].isNull()) + result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); + + subwidgetSizes.pop_back(); + + heroList = result; + return result; +} + +std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + size_t index = input["index"].Integer(); + size_t perPlayer = input["perPlayer"].Integer(); + + return std::make_shared(area.topLeft(), loadAnimation(input["image"]), index, perPlayer); +} + +std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + subwidgetSizes.push_back(area); + + Rect item = readTargetArea(input["item"]); + Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); + int itemsCount = input["itemsCount"].Integer(); + + auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getOwnedTowns().size()); + + + if(!input["scrollUp"].isNull()) + result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); + + if(!input["scrollDown"].isNull()) + result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); + + subwidgetSizes.pop_back(); + + townList = result; + return result; +} + +std::shared_ptr AdventureMapWidget::buildMinimap(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + minimap = std::make_shared(area); + return minimap; +} + +std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + auto image = ImagePath::fromJson(input["image"]); + + auto result = std::make_shared(image, area.topLeft()); + + for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + { + const auto & node = input[GameConstants::RESOURCE_NAMES[i]]; + + if(node.isNull()) + continue; + + result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer())); + } + + result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer())); + + return result; +} + +std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + auto image = ImagePath::fromJson(input["image"]); + + auto background = std::make_shared(image, area); + + return CGStatusBar::create(background); +} + +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); +} + +std::shared_ptr AdventureMapWidget::getHeroList() +{ + return heroList; +} + +std::shared_ptr AdventureMapWidget::getTownList() +{ + return townList; +} + +std::shared_ptr AdventureMapWidget::getMinimap() +{ + return minimap; +} + +std::shared_ptr AdventureMapWidget::getMapView() +{ + return mapView; +} + +std::shared_ptr AdventureMapWidget::getInfoBar() +{ + return infoBar; +} + +void AdventureMapWidget::setPlayer(const PlayerColor & player) +{ + setPlayerChildren(this, player); +} + +void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColor & player) +{ + for(auto & entry : widget->children) + { + auto container = dynamic_cast(entry); + auto icon = dynamic_cast(entry); + auto button = dynamic_cast(entry); + auto resDataBar = dynamic_cast(entry); + auto texture = dynamic_cast(entry); + + if(button) + button->setPlayerColor(player); + + if(resDataBar) + resDataBar->colorize(player); + + if(icon) + icon->setPlayer(player); + + if(container) + setPlayerChildren(container, player); + + if (texture) + texture->playerColored(player); + } + + for(const auto & entry : playerColorerImages) + { + if(images.count(entry)) + images[entry]->playerColored(player); + } + + redraw(); +} + +CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr animation, size_t index, size_t iconsPerPlayer) + : index(index) + , iconsPerPlayer(iconsPerPlayer) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += position; + image = std::make_shared(animation, index); +} + +void CAdventureMapIcon::setPlayer(const PlayerColor & player) +{ + image->setFrame(index + player.getNum() * iconsPerPlayer); +} + +void CAdventureMapOverlayWidget::show(Canvas & to) +{ + CIntObject::showAll(to); +} + +void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget) +{ + for(auto & entry : widget->children) + { + auto container = dynamic_cast(entry); + + if (container) + { + if (container->disableCondition == "heroAwake") + container->setEnabled(!shortcuts->optionHeroSleeping()); + + if (container->disableCondition == "heroSleeping") + container->setEnabled(shortcuts->optionHeroSleeping()); + + if (container->disableCondition == "mapLayerSurface") + container->setEnabled(shortcuts->optionMapLevelSurface()); + + if (container->disableCondition == "mapLayerUnderground") + container->setEnabled(!shortcuts->optionMapLevelSurface()); + + if (container->disableCondition == "mapViewMode") + container->setEnabled(shortcuts->optionInWorldView()); + + if (container->disableCondition == "worldViewMode") + container->setEnabled(!shortcuts->optionInWorldView()); + + updateActiveStateChildden(container); + } + } +} + +void AdventureMapWidget::updateActiveState() +{ + updateActiveStateChildden(this); + + for (auto entry: shortcuts->getShortcuts()) + setShortcutBlocked(entry.shortcut, !entry.isEnabled); +} diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index b955208aa..93a19eeb6 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -1,109 +1,110 @@ -/* - * CAdventureMapWidget.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/InterfaceObjectConfigurable.h" - -class CHeroList; -class CTownList; -class CMinimap; -class MapView; -class CInfoBar; -class IImage; -class AdventureMapShortcuts; -enum class EAdventureState; - -/// Internal class of AdventureMapInterface that contains actual UI elements -class AdventureMapWidget : public InterfaceObjectConfigurable -{ - int mapLevel; - /// temporary stack of sizes of currently building widgets - 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; - - /// Widgets that require access from adventure map - std::shared_ptr heroList; - std::shared_ptr townList; - std::shared_ptr minimap; - std::shared_ptr mapView; - std::shared_ptr infoBar; - - std::shared_ptr shortcuts; - - Rect readTargetArea(const JsonNode & source); - Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon); - Rect readArea(const JsonNode & source, const Rect & boundingBox); - - std::shared_ptr loadImage(const std::string & name); - std::shared_ptr loadAnimation(const std::string & name); - - std::shared_ptr buildInfobox(const JsonNode & input); - std::shared_ptr buildMapImage(const JsonNode & input); - std::shared_ptr buildMapButton(const JsonNode & input); - std::shared_ptr buildMapContainer(const JsonNode & input); - std::shared_ptr buildMapGameArea(const JsonNode & input); - std::shared_ptr buildMapHeroList(const JsonNode & input); - std::shared_ptr buildMapIcon(const JsonNode & input); - std::shared_ptr buildMapTownList(const JsonNode & input); - std::shared_ptr buildMinimap(const JsonNode & input); - std::shared_ptr buildResourceDateBar(const JsonNode & input); - 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: - explicit AdventureMapWidget( std::shared_ptr shortcuts ); - - std::shared_ptr getHeroList(); - std::shared_ptr getTownList(); - std::shared_ptr getMinimap(); - std::shared_ptr getMapView(); - std::shared_ptr getInfoBar(); - - void setPlayer(const PlayerColor & player); - - void onMapViewMoved(const Rect & visibleArea, int mapLevel); - void updateActiveState(); -}; - -/// Small helper class that provides ownership for shared_ptr's of child elements -class CAdventureMapContainerWidget : public CIntObject -{ - friend class AdventureMapWidget; - std::vector> ownedChildren; - std::string disableCondition; -}; - -class CAdventureMapOverlayWidget : public CAdventureMapContainerWidget -{ -public: - void show(Canvas & to) override; -}; - -/// Small helper class that provides player-colorable icon using animation file -class CAdventureMapIcon : public CIntObject -{ - std::shared_ptr image; - - size_t index; - size_t iconsPerPlayer; -public: - CAdventureMapIcon(const Point & position, std::shared_ptr image, size_t index, size_t iconsPerPlayer); - - void setPlayer(const PlayerColor & player); -}; +/* + * CAdventureMapWidget.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/InterfaceObjectConfigurable.h" + +class CAnimation; +class CHeroList; +class CTownList; +class CMinimap; +class MapView; +class CInfoBar; +class IImage; +class AdventureMapShortcuts; +enum class EAdventureState; + +/// Internal class of AdventureMapInterface that contains actual UI elements +class AdventureMapWidget : public InterfaceObjectConfigurable +{ + int mapLevel; + /// temporary stack of sizes of currently building widgets + 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; + + /// Widgets that require access from adventure map + std::shared_ptr heroList; + std::shared_ptr townList; + std::shared_ptr minimap; + std::shared_ptr mapView; + std::shared_ptr infoBar; + + std::shared_ptr shortcuts; + + Rect readTargetArea(const JsonNode & source); + 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); + std::shared_ptr buildMapContainer(const JsonNode & input); + std::shared_ptr buildMapGameArea(const JsonNode & input); + std::shared_ptr buildMapHeroList(const JsonNode & input); + std::shared_ptr buildMapIcon(const JsonNode & input); + std::shared_ptr buildMapTownList(const JsonNode & input); + std::shared_ptr buildMinimap(const JsonNode & input); + std::shared_ptr buildResourceDateBar(const JsonNode & input); + 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: + explicit AdventureMapWidget( std::shared_ptr shortcuts ); + + std::shared_ptr getHeroList(); + std::shared_ptr getTownList(); + std::shared_ptr getMinimap(); + std::shared_ptr getMapView(); + std::shared_ptr getInfoBar(); + + void setPlayer(const PlayerColor & player); + + void onMapViewMoved(const Rect & visibleArea, int mapLevel); + void updateActiveState(); +}; + +/// Small helper class that provides ownership for shared_ptr's of child elements +class CAdventureMapContainerWidget : public CIntObject +{ + friend class AdventureMapWidget; + std::vector> ownedChildren; + std::string disableCondition; +}; + +class CAdventureMapOverlayWidget : public CAdventureMapContainerWidget +{ +public: + void show(Canvas & to) override; +}; + +/// Small helper class that provides player-colorable icon using animation file +class CAdventureMapIcon : public CIntObject +{ + std::shared_ptr image; + + size_t index; + size_t iconsPerPlayer; +public: + CAdventureMapIcon(const Point & position, std::shared_ptr image, size_t index, size_t iconsPerPlayer); + + void setPlayer(const PlayerColor & player); +}; diff --git a/client/adventureMap/AdventureOptions.cpp b/client/adventureMap/AdventureOptions.cpp index 5f9dc7b67..265f8e5a0 100644 --- a/client/adventureMap/AdventureOptions.cpp +++ b/client/adventureMap/AdventureOptions.cpp @@ -1,61 +1,61 @@ -/* - * CAdventureOptions.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 "AdventureOptions.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" -#include "../lobby/CCampaignInfoScreen.h" -#include "../lobby/CScenarioInfoScreen.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" -#include "../gui/Shortcut.h" -#include "../widgets/Buttons.h" - -#include "../../CCallback.h" -#include "../../lib/StartInfo.h" - -AdventureOptions::AdventureOptions() - : CWindowObject(PLAYER_COLORED, "ADVOPTS") -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - viewWorld = std::make_shared(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); - viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); - - exit = std::make_shared(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); - - scenInfo = std::make_shared(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); - scenInfo->addCallback(AdventureOptions::showScenarioInfo); - - puzzle = std::make_shared(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); - puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); - - dig = std::make_shared(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); - if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero()) - dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); - else - dig->block(true); -} - -void AdventureOptions::showScenarioInfo() -{ - if(LOCPLINT->cb->getStartInfo()->campState) - { - GH.windows().createAndPushWindow(); - } - else - { - GH.windows().createAndPushWindow(); - } -} - +/* + * CAdventureOptions.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 "AdventureOptions.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" +#include "../lobby/CCampaignInfoScreen.h" +#include "../lobby/CScenarioInfoScreen.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" + +#include "../../CCallback.h" +#include "../../lib/StartInfo.h" + +AdventureOptions::AdventureOptions() + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); + viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); + + exit = std::make_shared(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); + + scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); + scenInfo->addCallback(AdventureOptions::showScenarioInfo); + + puzzle = std::make_shared(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); + puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); + + dig = std::make_shared(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); + if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero()) + dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); + else + dig->block(true); +} + +void AdventureOptions::showScenarioInfo() +{ + if(LOCPLINT->cb->getStartInfo()->campState) + { + GH.windows().createAndPushWindow(); + } + else + { + GH.windows().createAndPushWindow(); + } +} + diff --git a/client/adventureMap/AdventureOptions.h b/client/adventureMap/AdventureOptions.h index 2c1c8ddd3..41b47c56e 100644 --- a/client/adventureMap/AdventureOptions.h +++ b/client/adventureMap/AdventureOptions.h @@ -1,31 +1,31 @@ -/* - * CAdventureOptions.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" - -class CButton; - -/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,... -class AdventureOptions : public CWindowObject -{ - std::shared_ptr exit; - std::shared_ptr viewWorld; - std::shared_ptr puzzle; - std::shared_ptr dig; - std::shared_ptr scenInfo; - /*std::shared_ptr replay*/ - -public: - AdventureOptions(); - - static void showScenarioInfo(); -}; - +/* + * CAdventureOptions.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" + +class CButton; + +/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,... +class AdventureOptions : public CWindowObject +{ + std::shared_ptr exit; + std::shared_ptr viewWorld; + std::shared_ptr puzzle; + std::shared_ptr dig; + std::shared_ptr scenInfo; + /*std::shared_ptr replay*/ + +public: + AdventureOptions(); + + static void showScenarioInfo(); +}; + diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 3236a5bc2..555923631 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -22,11 +22,13 @@ #include "../gui/TextAlignment.h" #include "../render/Colors.h" #include "../render/Canvas.h" +#include "../render/IScreenHandler.h" #include "../adventureMap/AdventureMapInterface.h" #include "../windows/CMessage.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/CThreadHelper.h" #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CArmedInstance.h" @@ -104,7 +106,13 @@ void CInGameConsole::print(const std::string & txt) } GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set - CCS->soundh->playSound("CHAT"); + + int volume = CCS->soundh->getVolume(); + if(volume == 0) + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + int handle = CCS->soundh->playSound(AudioPath::builtin("CHAT")); + if(volume == 0) + CCS->soundh->setCallback(handle, [&]() { if(!GH.screenHandler().hasFocus()) CCS->soundh->setVolume(0); }); } bool CInGameConsole::captureThisKey(EShortcut key) @@ -255,6 +263,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText) //some commands like gosolo don't work when executed from GUI thread auto threadFunction = [=]() { + setThreadName("processCommand"); ClientCommandManager commandController; commandController.processCommand(txt.substr(1), true); }; diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 7f5226d46..c583f461f 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -25,6 +25,7 @@ #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../render/IScreenHandler.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" @@ -51,7 +52,7 @@ CInfoBar::EmptyVisibleInfo::EmptyVisibleInfo() CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATHR"); + background = std::make_shared(ImagePath::builtin("ADSTATHR")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) heroTooltip = std::make_shared(Point(0,0), hero); @@ -62,7 +63,7 @@ CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATCS"); + background = std::make_shared(ImagePath::builtin("ADSTATCS")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) townTooltip = std::make_shared(Point(0,0), town); @@ -88,36 +89,36 @@ CInfoBar::VisibleDateInfo::VisibleDateInfo() forceRefresh.push_back(label); } -std::string CInfoBar::VisibleDateInfo::getNewDayName() +AnimationPath CInfoBar::VisibleDateInfo::getNewDayName() { if(LOCPLINT->cb->getDate(Date::DAY) == 1) - return "NEWDAY"; + return AnimationPath::builtin("NEWDAY"); if(LOCPLINT->cb->getDate(Date::DAY_OF_WEEK) != 1) - return "NEWDAY"; + return AnimationPath::builtin("NEWDAY"); switch(LOCPLINT->cb->getDate(Date::WEEK)) { case 1: - return "NEWWEEK1"; + return AnimationPath::builtin("NEWWEEK1"); case 2: - return "NEWWEEK2"; + return AnimationPath::builtin("NEWWEEK2"); case 3: - return "NEWWEEK3"; + return AnimationPath::builtin("NEWWEEK3"); case 4: - return "NEWWEEK4"; + return AnimationPath::builtin("NEWWEEK4"); default: - return ""; + return AnimationPath(); } } CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATNX"); - banner = std::make_shared("CREST58", player.getNum(), 0, 20, 51); - sand = std::make_shared(99, 51, "HOURSAND", 0, 100); // H3 uses around 100 ms per frame - glass = std::make_shared(99, 51, "HOURGLAS", CShowableAnim::PLAY_ONCE, 1000); // H3 scales this nicely for AI turn duration, don't have anything like that in vcmi + 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 + glass = std::make_shared(99, 51, AnimationPath::builtin("HOURGLAS"), CShowableAnim::PLAY_ONCE, 1000); // H3 scales this nicely for AI turn duration, don't have anything like that in vcmi } CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() @@ -148,14 +149,14 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() } //generate widgets - background = std::make_shared("ADSTATIN"); + background = std::make_shared(ImagePath::builtin("ADSTATIN")); allyLabel = std::make_shared(10, 106, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); enemyLabel = std::make_shared(10, 136, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); int posx = allyLabel->pos.w + allyLabel->pos.x - pos.x + 4; for(PlayerColor & player : allies) { - auto image = std::make_shared("ITGFLAGS", player.getNum(), 0, posx, 102); + auto image = std::make_shared(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, posx, 102); posx += image->pos.w; flags.push_back(image); } @@ -163,14 +164,14 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() posx = enemyLabel->pos.w + enemyLabel->pos.x - pos.x + 4; for(PlayerColor & player : enemies) { - auto image = std::make_shared("ITGFLAGS", player.getNum(), 0, posx, 132); + auto image = std::make_shared(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, posx, 132); posx += image->pos.w; flags.push_back(image); } for(size_t i=0; i("itmtl", i, 0, 6 + 42 * (int)i , 11)); + hallIcons.push_back(std::make_shared(AnimationPath::builtin("itmtl"), i, 0, 6 + 42 * (int)i , 11)); if(halls[i]) hallLabels.push_back(std::make_shared( 26 + 42 * (int)i, 64, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(halls[i]))); } @@ -180,7 +181,7 @@ CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector("ADSTATOT", 1, 0); + 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); auto textRect = fullRect; auto imageRect = fullRect; @@ -228,14 +229,22 @@ CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vectorsoundh->getVolume(); + int handle = -1; + if(volume == 0) + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + if(LOCPLINT->cb->getDate(Date::DAY_OF_WEEK) != 1) // not first day of the week - CCS->soundh->playSound(soundBase::newDay); + handle = CCS->soundh->playSound(soundBase::newDay); else if(LOCPLINT->cb->getDate(Date::WEEK) != 1) // not first week in month - CCS->soundh->playSound(soundBase::newWeek); + handle = CCS->soundh->playSound(soundBase::newWeek); else if(LOCPLINT->cb->getDate(Date::MONTH) != 1) // not first month - CCS->soundh->playSound(soundBase::newMonth); + handle = CCS->soundh->playSound(soundBase::newMonth); else - CCS->soundh->playSound(soundBase::newDay); + handle = CCS->soundh->playSound(soundBase::newDay); + + if(volume == 0) + CCS->soundh->setCallback(handle, [&]() { if(!GH.screenHandler().hasFocus()) CCS->soundh->setVolume(0); }); } void CInfoBar::reset() @@ -280,17 +289,15 @@ void CInfoBar::tick(uint32_t msPassed) } } -void CInfoBar::clickReleased(const Point & cursorPosition) +void CInfoBar::clickReleased(const Point & cursorPosition, bool lastActivated) { timerCounter = 0; removeUsedEvents(TIME); //expiration trigger from just clicked element is not valid anymore if(state == HERO || state == TOWN) { - if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) - return; - - showGameStatus(); + if(lastActivated) + showGameStatus(); } else if(state == GAME) showDate(); @@ -369,47 +376,51 @@ void CInfoBar::pushComponents(const std::vector & components, std::st std::array, int>, 10> reward_map; for(const auto & c : components) { - switch(c.id) + switch(c.type) { - case Component::EComponentType::PRIM_SKILL: - case Component::EComponentType::EXPERIENCE: + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + case ComponentType::MANA: reward_map.at(0).first.push_back(c); reward_map.at(0).second = 8; //At most 8, cannot be more break; - case Component::EComponentType::SEC_SKILL: + case ComponentType::SEC_SKILL: reward_map.at(1).first.push_back(c); reward_map.at(1).second = 4; //At most 4 break; - case Component::EComponentType::SPELL: + case ComponentType::SPELL: reward_map.at(2).first.push_back(c); reward_map.at(2).second = 4; //At most 4 break; - case Component::EComponentType::ARTIFACT: + case ComponentType::ARTIFACT: + case ComponentType::SPELL_SCROLL: reward_map.at(3).first.push_back(c); reward_map.at(3).second = 4; //At most 4, too long names break; - case Component::EComponentType::CREATURE: + case ComponentType::CREATURE: reward_map.at(4).first.push_back(c); reward_map.at(4).second = 4; //At most 4, too long names break; - case Component::EComponentType::RESOURCE: + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: reward_map.at(5).first.push_back(c); reward_map.at(5).second = 7; //At most 7 break; - case Component::EComponentType::MORALE: - case Component::EComponentType::LUCK: + case ComponentType::MORALE: + case ComponentType::LUCK: reward_map.at(6).first.push_back(c); reward_map.at(6).second = 2; //At most 2 - 1 for morale + 1 for luck break; - case Component::EComponentType::BUILDING: + case ComponentType::BUILDING: reward_map.at(7).first.push_back(c); reward_map.at(7).second = 1; //At most 1 - only large icons available AFAIK break; - case Component::EComponentType::HERO_PORTRAIT: + case ComponentType::HERO_PORTRAIT: reward_map.at(8).first.push_back(c); reward_map.at(8).second = 1; //I do not think than we even can get more than 1 hero break; - case Component::EComponentType::FLAG: + case ComponentType::FLAG: reward_map.at(9).first.push_back(c); reward_map.at(9).second = 1; //I do not think than we even can get more than 1 player in notification break; diff --git a/client/adventureMap/CInfoBar.h b/client/adventureMap/CInfoBar.h index c69fed1f0..496016354 100644 --- a/client/adventureMap/CInfoBar.h +++ b/client/adventureMap/CInfoBar.h @@ -11,6 +11,8 @@ #include "../gui/CIntObject.h" #include "CConfigHandler.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -86,7 +88,7 @@ private: std::shared_ptr animation; std::shared_ptr label; - std::string getNewDayName(); + AnimationPath getNewDayName(); public: VisibleDateInfo(); }; @@ -158,7 +160,7 @@ private: void tick(uint32_t msPassed) override; - void clickReleased(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition, bool lastActivated) override; void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index e53968d9f..49f3a383f 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -16,16 +16,18 @@ #include "../widgets/Images.h" #include "../widgets/Buttons.h" #include "../widgets/ObjectLists.h" +#include "../widgets/RadialMenu.h" #include "../windows/InfoWindows.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../render/Colors.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -206,9 +208,9 @@ void CList::selectPrev() CHeroList::CEmptyHeroItem::CEmptyHeroItem() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - movement = std::make_shared("IMOBIL", 0, 0, 0, 1); - portrait = std::make_shared("HPSXXX", movement->pos.w + 1, 0); - mana = std::make_shared("IMANA", 0, 0, movement->pos.w + portrait->pos.w + 2, 1 ); + 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 ); pos.w = mana->pos.w + mana->pos.x - pos.x; pos.h = std::max(std::max(movement->pos.h + 1, mana->pos.h + 1), portrait->pos.h); @@ -216,17 +218,20 @@ CHeroList::CEmptyHeroItem::CEmptyHeroItem() CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) : CListItem(parent), - hero(Hero) + hero(Hero), + parentList(parent) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - movement = std::make_shared("IMOBIL", 0, 0, 0, 1); - portrait = std::make_shared("PortraitsSmall", hero->portrait, 0, movement->pos.w + 1); - mana = std::make_shared("IMANA", 0, 0, movement->pos.w + portrait->pos.w + 2, 1); + 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); pos.w = mana->pos.w + mana->pos.x - pos.x; pos.h = std::max(std::max(movement->pos.h + 1, mana->pos.h + 1), portrait->pos.h); update(); + + addUsedEvents(GESTURE); } void CHeroList::CHeroItem::update() @@ -238,7 +243,7 @@ void CHeroList::CHeroItem::update() std::shared_ptr CHeroList::CHeroItem::genSelection() { - return std::make_shared("HPSYYY", movement->pos.w + 1, 0); + return std::make_shared(ImagePath::builtin("HPSYYY"), movement->pos.w + 1, 0); } void CHeroList::CHeroItem::select(bool on) @@ -262,6 +267,43 @@ std::string CHeroList::CHeroItem::getHoverText() return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated()); } +void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(!hero) + return; + + auto & heroes = LOCPLINT->localState->getWanderingHeroes(); + + if(heroes.size() < 2) + return; + + int heroPos = vstd::find_pos(heroes, hero); + const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes[heroPos - 1]; + const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1]; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [this, heroPos]() + { + for (int i = heroPos; i > 0; i--) + LOCPLINT->localState->swapWanderingHero(i, i - 1); + parentList->updateWidget(); + } }, + { RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [this, heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [this, heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, heroPos, heroes]() + { + for (int i = heroPos; i < heroes.size() - 1; i++) + LOCPLINT->localState->swapWanderingHero(i, i + 1); + parentList->updateWidget(); + } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements, true); +} + std::shared_ptr CHeroList::createItem(size_t index) { if (LOCPLINT->localState->getWanderingHeroes().size() > index) @@ -293,7 +335,7 @@ void CHeroList::updateWidget() for (size_t i = 0; i < heroes.size(); ++i) { - auto item = std::dynamic_pointer_cast(listBox->getItem(i)); + auto item = std::dynamic_pointer_cast(listBox->getItem(i)); if (!item) continue; @@ -319,26 +361,32 @@ std::shared_ptr CTownList::createItem(size_t index) { if (LOCPLINT->localState->getOwnedTowns().size() > index) return std::make_shared(this, LOCPLINT->localState->getOwnedTown(index)); - return std::make_shared("ITPA", 0); + return std::make_shared(AnimationPath::builtin("ITPA"), 0); } CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): CListItem(parent), - town(Town) + parentList(parent) { + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town)); + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - picture = std::make_shared("ITPA", 0); + picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; update(); + + addUsedEvents(GESTURE); } std::shared_ptr CTownList::CTownItem::genSelection() { - return std::make_shared("ITPA", 1); + return std::make_shared(AnimationPath::builtin("ITPA"), 1); } void CTownList::CTownItem::update() { + const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townIndex]; size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture->setFrame(iconIndex + 2); @@ -348,22 +396,58 @@ void CTownList::CTownItem::update() void CTownList::CTownItem::select(bool on) { if(on) - LOCPLINT->localState->setSelection(town); + LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townIndex]); } void CTownList::CTownItem::open() { - LOCPLINT->openTownWindow(town); + LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townIndex]); } void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(town, GH.getCursorPosition()); + CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townIndex], GH.getCursorPosition()); +} + +void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + + if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex]) + return; + + if(towns.size() < 2) + return; + + int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1; + int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this]() + { + for (int i = townIndex; i > 0; i--) + LOCPLINT->localState->swapOwnedTowns(i, i - 1); + parentList->updateWidget(); + } }, + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, towns]() + { + for (int i = townIndex; i < towns.size() - 1; i++) + LOCPLINT->localState->swapOwnedTowns(i, i + 1); + parentList->updateWidget(); + } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements, true); } std::string CTownList::CTownItem::getHoverText() { - return town->getObjectName(); + return LOCPLINT->localState->getOwnedTowns()[townIndex]->getObjectName(); } CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount) @@ -390,20 +474,12 @@ void CTownList::updateWidget() for (size_t i = 0; i < towns.size(); ++i) { - auto item = std::dynamic_pointer_cast(listBox->getItem(i)); + auto item = std::dynamic_pointer_cast(listBox->getItem(i)); if (!item) continue; - if (item->town == towns[i]) - { - item->update(); - } - else - { - listBox->reset(); - break; - } + listBox->reset(); } if (LOCPLINT->localState->getCurrentTown()) diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 0cefd814e..01312e725 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -117,6 +117,7 @@ class CHeroList : public CList std::shared_ptr movement; std::shared_ptr mana; std::shared_ptr portrait; + CHeroList *parentList; public: const CGHeroInstance * const hero; @@ -127,6 +128,7 @@ class CHeroList : public CList void select(bool on) override; void open() override; void showTooltip() override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; std::string getHoverText() override; }; @@ -150,8 +152,9 @@ class CTownList : public CList class CTownItem : public CListItem { std::shared_ptr picture; + CTownList *parentList; public: - const CGTownInstance * const town; + int townIndex; CTownItem(CTownList *parent, const CGTownInstance * town); @@ -160,6 +163,7 @@ class CTownList : public CList void select(bool on) override; void open() override; void showTooltip() override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; std::string getHoverText() override; }; diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 9c9b9134f..bdc142e01 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -20,8 +20,9 @@ #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" #include "../render/Colors.h" -#include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../renderSDL/SDL_Extensions.h" #include "../windows/InfoWindows.h" #include "../../CCallback.h" @@ -30,25 +31,23 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapping/CMapDefines.h" -#include - ColorRGBA CMinimapInstance::getTileColor(const int3 & pos) const { const TerrainTile * tile = LOCPLINT->cb->getTile(pos, false); // if tile is not visible it will be black on minimap if(!tile) - return CSDL_Ext::fromSDL(Colors::BLACK); + return Colors::BLACK; // if object at tile is owned - it will be colored as its owner for (const CGObjectInstance *obj : tile->blockingObjects) { PlayerColor player = obj->getOwner(); if(player == PlayerColor::NEUTRAL) - return CSDL_Ext::fromSDL(*graphics->neutralColor); + return graphics->neutralColor; - if (player < PlayerColor::PLAYER_LIMIT) - return CSDL_Ext::fromSDL(graphics->playerColors[player.getNum()]); + if (player.isValidPlayer()) + return graphics->playerColors[player.getNum()]; } if (tile->blocked && (!tile->visitable)) @@ -92,10 +91,19 @@ CMinimap::CMinimap(const Rect & position) level(0) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.w = position.w; - pos.h = position.h; - aiShield = std::make_shared("AIShield"); + double maxSideLenghtSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y); + double maxSideLenghtDst = std::max(position.w, position.h); + double resize = maxSideLenghtSrc / maxSideLenghtDst; + 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); + + pos.x += offset.x; + pos.y += offset.y; + pos.w = newMinimapSize.x; + pos.h = newMinimapSize.y; + + aiShield = std::make_shared(ImagePath::builtin("AIShield"), -offset); aiShield->disable(); } @@ -168,7 +176,7 @@ void CMinimap::mouseDragged(const Point & cursorPosition, const Point & lastUpda void CMinimap::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), aiShield->pos); CIntObject::showAll(to); if(minimap) @@ -185,7 +193,7 @@ void CMinimap::showAll(Canvas & to) }; Canvas clippedTarget(to, pos); - clippedTarget.drawBorderDashed(radar, CSDL_Ext::fromSDL(Colors::PURPLE)); + clippedTarget.drawBorderDashed(radar, Colors::PURPLE); } } @@ -239,4 +247,3 @@ void CMinimap::updateTiles(const std::unordered_set & positions) } redraw(); } - diff --git a/client/adventureMap/CResDataBar.cpp b/client/adventureMap/CResDataBar.cpp index 6f8a340ff..7096b47bc 100644 --- a/client/adventureMap/CResDataBar.cpp +++ b/client/adventureMap/CResDataBar.cpp @@ -1,89 +1,90 @@ -/* - * CResDataBar.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 "CResDataBar.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/Colors.h" -#include "../gui/CGuiHandler.h" -#include "../gui/TextAlignment.h" -#include "../widgets/Images.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/ResourceSet.h" - -CResDataBar::CResDataBar(const std::string & imageName, const Point & position) -{ - pos.x += position.x; - pos.y += position.y; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared(imageName, 0, 0); - background->colorize(LOCPLINT->playerID); - - pos.w = background->pos.w; - pos.h = background->pos.h; -} - -CResDataBar::CResDataBar(const std::string & defname, int x, int y, int offx, int offy, int resdist, int datedist): - CResDataBar(defname, Point(x,y)) -{ - for (int i = 0; i < 7 ; i++) - resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy ); - - datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0); -} - -void CResDataBar::setDatePosition(const Point & position) -{ - datePosition = position; -} - -void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position) -{ - resourcePositions[resource] = position; -} - -std::string CResDataBar::buildDateString() -{ - std::string pattern = "%s: %d, %s: %d, %s: %d"; - - auto formatted = boost::format(pattern) - % CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH) - % CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK) - % CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK); - - return boost::str(formatted); -} - -void CResDataBar::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - //TODO: all this should be labels, but they require proper text update on change - for (auto & entry : resourcePositions) - { - std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first)); - - to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text); - } - - if (datePosition) - to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString()); -} - -void CResDataBar::colorize(PlayerColor player) -{ - background->colorize(player); -} +/* + * CResDataBar.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 "CResDataBar.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/EFont.h" +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../widgets/Images.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/ResourceSet.h" + +CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) +{ + pos.x += position.x; + pos.y += position.y; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + background = std::make_shared(imageName, 0, 0); + background->colorize(LOCPLINT->playerID); + + pos.w = background->pos.w; + pos.h = background->pos.h; +} + +CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist): + CResDataBar(defname, Point(x,y)) +{ + for (int i = 0; i < 7 ; i++) + resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy ); + + datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0); +} + +void CResDataBar::setDatePosition(const Point & position) +{ + datePosition = position; +} + +void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position) +{ + resourcePositions[resource] = position; +} + +std::string CResDataBar::buildDateString() +{ + std::string pattern = "%s: %d, %s: %d, %s: %d"; + + auto formatted = boost::format(pattern) + % CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH) + % CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK) + % CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK); + + return boost::str(formatted); +} + +void CResDataBar::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + //TODO: all this should be labels, but they require proper text update on change + for (auto & entry : resourcePositions) + { + std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first)); + + to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text); + } + + if (datePosition) + to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString()); +} + +void CResDataBar::colorize(PlayerColor player) +{ + background->colorize(player); +} diff --git a/client/adventureMap/CResDataBar.h b/client/adventureMap/CResDataBar.h index d4696cd26..3bf294ad4 100644 --- a/client/adventureMap/CResDataBar.h +++ b/client/adventureMap/CResDataBar.h @@ -1,44 +1,40 @@ -/* - * CResDataBar.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" - -VCMI_LIB_NAMESPACE_BEGIN -enum class EGameResID : int8_t; -using GameResID = Identifier; -VCMI_LIB_NAMESPACE_END - -/// Resources bar which shows information about how many gold, crystals,... you have -/// Current date is displayed too -class CResDataBar : public CIntObject -{ - std::string buildDateString(); - - std::shared_ptr background; - - std::map resourcePositions; - std::optional datePosition; - -public: - - /// For dynamically-sized UI windows, e.g. adventure map interface - CResDataBar(const std::string & imageName, const Point & position); - - /// For fixed-size UI windows, e.g. CastleInterface - CResDataBar(const std::string &defname, int x, int y, int offx, int offy, int resdist, int datedist); - - void setDatePosition(const Point & position); - void setResourcePosition(const GameResID & resource, const Point & position); - - void colorize(PlayerColor player); - void showAll(Canvas & to) override; -}; - +/* + * CResDataBar.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" + +/// Resources bar which shows information about how many gold, crystals,... you have +/// Current date is displayed too +class CResDataBar : public CIntObject +{ + std::string buildDateString(); + + std::shared_ptr background; + + std::map resourcePositions; + std::optional datePosition; + +public: + + /// For dynamically-sized UI windows, e.g. adventure map interface + CResDataBar(const ImagePath & imageName, const Point & position); + + /// For fixed-size UI windows, e.g. CastleInterface + CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist); + + void setDatePosition(const Point & position); + void setResourcePosition(const GameResID & resource, const Point & position); + + void colorize(PlayerColor player); + void showAll(Canvas & to) override; +}; + diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 17e8aeecb..64218498a 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -1,252 +1,252 @@ -/* - * MapAudioPlayer.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 "MapAudioPlayer.h" - -#include "../CCallback.h" -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../mapView/mapHandler.h" - -#include "../../lib/TerrainHandler.h" -#include "../../lib/mapObjects/CArmedInstance.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapping/CMap.h" - -bool MapAudioPlayer::hasOngoingAnimations() -{ - return false; -} - -void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj) -{ - addObject(obj); -} - -void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj) -{ - removeObject(obj); -} - -void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj) -{ - addObject(obj); -} - -void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj) -{ - removeObject(obj); -} - -void MapAudioPlayer::addObject(const CGObjectInstance * obj) -{ - if(obj->isTile2Terrain()) - { - // terrain overlay - all covering tiles act as sound source - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - } - } - return; - } - - if(obj->isVisitable()) - { - // visitable object - visitable tile acts as sound source - int3 currTile = obj->visitablePos(); - - if(LOCPLINT->cb->isInTheMap(currTile)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - - return; - } - - if(!obj->isVisitable()) - { - // static object - blocking tiles act as sound source - auto tiles = obj->getBlockedOffsets(); - - for(const auto & tile : tiles) - { - int3 currTile = obj->pos + tile; - - if(LOCPLINT->cb->isInTheMap(currTile)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - } - return; - } -} - -void MapAudioPlayer::removeObject(const CGObjectInstance * obj) -{ - for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) - for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) - for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) - vstd::erase(objects[z][x][y], obj->id); -} - -std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) -{ - std::vector result; - - for(auto & objectID : objects[tile.z][tile.x][tile.y]) - { - const auto & object = CGI->mh->getMap()->objects[objectID.getNum()]; - - assert(object); - 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(CGI->mh->getMap()->isCoastalTile(tile)) - result.emplace_back("LOOPOCEA"); - - return result; -} - -void MapAudioPlayer::updateAmbientSounds() -{ - std::map currentSounds; - auto updateSounds = [&](const std::string& soundId, int distance) -> void - { - if(vstd::contains(currentSounds, soundId)) - currentSounds[soundId] = std::min(currentSounds[soundId], distance); - else - currentSounds.insert(std::make_pair(soundId, distance)); - }; - - int3 pos = currentSelection->getSightCenter(); - std::unordered_set tiles; - LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV); - for(int3 tile : tiles) - { - int dist = pos.dist(tile, int3::DIST_CHEBYSHEV); - - for(auto & soundName : getAmbientSounds(tile)) - updateSounds(soundName, dist); - } - CCS->soundh->ambientUpdateChannels(currentSounds); -} - -void MapAudioPlayer::updateMusic() -{ - if(audioPlaying && playerMakingTurn && currentSelection) - { - const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; - CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); - } - - if(audioPlaying && enemyMakingTurn) - { - CCS->musich->playMusicFromSet("enemy-turn", true, false); - } -} - -void MapAudioPlayer::update() -{ - updateMusic(); - - if(audioPlaying && playerMakingTurn && currentSelection) - updateAmbientSounds(); -} - -MapAudioPlayer::MapAudioPlayer() -{ - auto mapSize = LOCPLINT->cb->getMapSize(); - - objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); - - for(const auto & obj : CGI->mh->getMap()->objects) - { - if (obj) - addObject(obj); - } -} - -MapAudioPlayer::~MapAudioPlayer() -{ - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} - -void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection) -{ - currentSelection = newSelection; - update(); -} - -void MapAudioPlayer::onAudioPaused() -{ - audioPlaying = false; - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} - -void MapAudioPlayer::onAudioResumed() -{ - audioPlaying = true; - update(); -} - -void MapAudioPlayer::onPlayerTurnStarted() -{ - enemyMakingTurn = false; - playerMakingTurn = true; - update(); -} - -void MapAudioPlayer::onEnemyTurnStarted() -{ - playerMakingTurn = false; - enemyMakingTurn = true; - update(); -} - -void MapAudioPlayer::onPlayerTurnEnded() -{ - playerMakingTurn = false; - enemyMakingTurn = false; - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} +/* + * MapAudioPlayer.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 "MapAudioPlayer.h" + +#include "../CCallback.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../mapView/mapHandler.h" + +#include "../../lib/TerrainHandler.h" +#include "../../lib/mapObjects/CArmedInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapping/CMap.h" + +bool MapAudioPlayer::hasOngoingAnimations() +{ + return false; +} + +void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +} + +void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +} + +void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +} + +void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +} + +void MapAudioPlayer::addObject(const CGObjectInstance * obj) +{ + if(obj->isTile2Terrain()) + { + // terrain overlay - all covering tiles act as sound source + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + } + } + return; + } + + if(obj->isVisitable()) + { + // visitable object - visitable tile acts as sound source + int3 currTile = obj->visitablePos(); + + if(LOCPLINT->cb->isInTheMap(currTile)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + + return; + } + + if(!obj->isVisitable()) + { + // static object - blocking tiles act as sound source + auto tiles = obj->getBlockedOffsets(); + + for(const auto & tile : tiles) + { + int3 currTile = obj->pos + tile; + + if(LOCPLINT->cb->isInTheMap(currTile)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + } + return; + } +} + +void MapAudioPlayer::removeObject(const CGObjectInstance * obj) +{ + for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) + for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) + for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) + vstd::erase(objects[z][x][y], obj->id); +} + +std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) +{ + std::vector result; + + for(auto & objectID : objects[tile.z][tile.x][tile.y]) + { + const auto & object = CGI->mh->getMap()->objects[objectID.getNum()]; + + assert(object); + 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(CGI->mh->getMap()->isCoastalTile(tile)) + result.emplace_back(AudioPath::builtin("LOOPOCEA")); + + return result; +} + +void MapAudioPlayer::updateAmbientSounds() +{ + std::map currentSounds; + auto updateSounds = [&](const AudioPath& soundId, int distance) -> void + { + if(vstd::contains(currentSounds, soundId)) + currentSounds[soundId] = std::min(currentSounds[soundId], distance); + else + currentSounds.insert(std::make_pair(soundId, distance)); + }; + + int3 pos = currentSelection->getSightCenter(); + std::unordered_set tiles; + LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV); + for(int3 tile : tiles) + { + int dist = pos.dist(tile, int3::DIST_CHEBYSHEV); + + for(auto & soundName : getAmbientSounds(tile)) + updateSounds(soundName, dist); + } + CCS->soundh->ambientUpdateChannels(currentSounds); +} + +void MapAudioPlayer::updateMusic() +{ + if(audioPlaying && playerMakingTurn && currentSelection) + { + const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; + CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); + } + + if(audioPlaying && enemyMakingTurn) + { + CCS->musich->playMusicFromSet("enemy-turn", true, false); + } +} + +void MapAudioPlayer::update() +{ + updateMusic(); + + if(audioPlaying && playerMakingTurn && currentSelection) + updateAmbientSounds(); +} + +MapAudioPlayer::MapAudioPlayer() +{ + auto mapSize = LOCPLINT->cb->getMapSize(); + + objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + for(const auto & obj : CGI->mh->getMap()->objects) + { + if (obj) + addObject(obj); + } +} + +MapAudioPlayer::~MapAudioPlayer() +{ + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} + +void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection) +{ + currentSelection = newSelection; + update(); +} + +void MapAudioPlayer::onAudioPaused() +{ + audioPlaying = false; + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} + +void MapAudioPlayer::onAudioResumed() +{ + audioPlaying = true; + update(); +} + +void MapAudioPlayer::onPlayerTurnStarted() +{ + enemyMakingTurn = false; + playerMakingTurn = true; + update(); +} + +void MapAudioPlayer::onEnemyTurnStarted() +{ + playerMakingTurn = false; + enemyMakingTurn = true; + update(); +} + +void MapAudioPlayer::onPlayerTurnEnded() +{ + playerMakingTurn = false; + enemyMakingTurn = false; + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} diff --git a/client/adventureMap/MapAudioPlayer.h b/client/adventureMap/MapAudioPlayer.h index 31a00e6d3..938bf1abe 100644 --- a/client/adventureMap/MapAudioPlayer.h +++ b/client/adventureMap/MapAudioPlayer.h @@ -1,75 +1,77 @@ -/* - * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h" - -VCMI_LIB_NAMESPACE_BEGIN -class ObjectInstanceID; -class CArmedInstance; -VCMI_LIB_NAMESPACE_END - -class MapAudioPlayer : public IMapObjectObserver -{ - using MapObjectsList = std::vector; - - boost::multi_array objects; - const CArmedInstance * currentSelection = nullptr; - bool playerMakingTurn = false; - bool enemyMakingTurn = false; - bool audioPlaying = true; - - void addObject(const CGObjectInstance * obj); - void removeObject(const CGObjectInstance * obj); - - std::vector getAmbientSounds(const int3 & tile); - void updateAmbientSounds(); - void updateMusic(); - void update(); - -protected: - // IMapObjectObserver impl - bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj) override; - void onObjectFadeOut(const CGObjectInstance * obj) override; - void onObjectInstantAdd(const CGObjectInstance * obj) override; - void onObjectInstantRemove(const CGObjectInstance * obj) override; - - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - -public: - MapAudioPlayer(); - ~MapAudioPlayer() override; - - /// Called whenever current adventure map selection changes - void onSelectionChanged(const CArmedInstance * newSelection); - - /// Called when local player starts his turn - void onPlayerTurnStarted(); - - /// Called when AI or non-local player start his turn - void onEnemyTurnStarted(); - - /// Called when local player ends his turn - void onPlayerTurnEnded(); - - /// Called when map audio should be paused, e.g. on combat or town scren access - void onAudioPaused(); - - /// Called when map audio should be resume, opposite to onPaused - void onAudioResumed(); -}; +/* + * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN +class ObjectInstanceID; +class CArmedInstance; +class PlayerColor; +VCMI_LIB_NAMESPACE_END + +class MapAudioPlayer : public IMapObjectObserver +{ + using MapObjectsList = std::vector; + + boost::multi_array objects; + const CArmedInstance * currentSelection = nullptr; + bool playerMakingTurn = false; + bool enemyMakingTurn = false; + bool audioPlaying = true; + + void addObject(const CGObjectInstance * obj); + void removeObject(const CGObjectInstance * obj); + + std::vector getAmbientSounds(const int3 & tile); + void updateAmbientSounds(); + void updateMusic(); + void update(); + +protected: + // IMapObjectObserver impl + bool hasOngoingAnimations() override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; + + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + +public: + MapAudioPlayer(); + ~MapAudioPlayer() override; + + /// Called whenever current adventure map selection changes + void onSelectionChanged(const CArmedInstance * newSelection); + + /// Called when local player starts his turn + void onPlayerTurnStarted(); + + /// Called when AI or non-local player start his turn + void onEnemyTurnStarted(); + + /// Called when local player ends his turn + void onPlayerTurnEnded(); + + /// Called when map audio should be paused, e.g. on combat or town scren access + void onAudioPaused(); + + /// Called when map audio should be resume, opposite to onPaused + void onAudioResumed(); +}; diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp new file mode 100644 index 000000000..41b77255a --- /dev/null +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -0,0 +1,167 @@ +/* + * TurnTimerWidget.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 "TurnTimerWidget.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../battle/BattleInterface.h" +#include "../battle/BattleStacksController.h" + +#include "../render/EFont.h" +#include "../render/Graphics.h" +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/filesystem/ResourcePath.h" + +TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c): + CIntObject(), rect(r), color(c) +{ +} + +void TurnTimerWidget::DrawRect::showAll(Canvas & to) +{ + to.drawColor(rect, color); + + CIntObject::showAll(to); +} + +TurnTimerWidget::TurnTimerWidget(): + InterfaceObjectConfigurable(TIME), + turnTime(0), lastTurnTime(0), cachedTurnTime(0), lastPlayer(PlayerColor::CANNOT_DETERMINE) +{ + REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect); + + recActions &= ~DEACTIVATE; + + const JsonNode config(JsonPath::builtin("config/widgets/turnTimer.json")); + + build(config); + + std::transform(variables["notificationTime"].Vector().begin(), + variables["notificationTime"].Vector().end(), + std::inserter(notifications, notifications.begin()), + [](const JsonNode & node){ return node.Integer(); }); +} + +std::shared_ptr TurnTimerWidget::buildDrawRect(const JsonNode & config) const +{ + logGlobal->debug("Building widget TurnTimerWidget::DrawRect"); + auto rect = readRect(config["rect"]); + auto color = readColor(config["color"]); + return std::make_shared(rect, color); +} + +void TurnTimerWidget::show(Canvas & to) +{ + showAll(to); +} + +void TurnTimerWidget::setTime(PlayerColor player, int time) +{ + int newTime = time / 1000; + if(player == LOCPLINT->playerID + && newTime != turnTime + && notifications.count(newTime)) + { + CCS->soundh->playSound(AudioPath::fromJson(variables["notificationSound"])); + } + + turnTime = newTime; + + if(auto w = widget("timer")) + { + std::ostringstream oss; + oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; + w->setText(oss.str()); + + if(graphics && LOCPLINT && LOCPLINT->cb + && variables["textColorFromPlayerColor"].Bool() + && player.isValidPlayer()) + { + w->setColor(graphics->playerColors[player]); + } + } +} + +void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed) +{ + const auto & time = LOCPLINT->cb->getPlayerTurnTime(player); + if(time.isActive) + cachedTurnTime -= msPassed; + + if(cachedTurnTime < 0) + cachedTurnTime = 0; //do not go below zero + + if(lastPlayer != player) + { + lastPlayer = player; + lastTurnTime = 0; + } + + auto timeCheckAndUpdate = [&](int time) + { + if(time / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time; + cachedTurnTime = time; + } + else + setTime(player, cachedTurnTime); + }; + + auto * playerInfo = LOCPLINT->cb->getPlayer(player); + if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) + { + if(time.isBattle) + timeCheckAndUpdate(time.baseTimer + time.turnTimer + time.battleTimer + time.unitTimer); + else + timeCheckAndUpdate(time.baseTimer + time.turnTimer); + } + else + timeCheckAndUpdate(0); +} + +void TurnTimerWidget::tick(uint32_t msPassed) +{ + if(!LOCPLINT || !LOCPLINT->cb) + return; + + if(LOCPLINT->battleInt) + { + if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) + updateTimer(stack->getOwner(), msPassed); + else + updateTimer(PlayerColor::NEUTRAL, msPassed); + } + else + { + if(LOCPLINT->makingTurn) + updateTimer(LOCPLINT->playerID, msPassed); + else + { + for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + { + if(LOCPLINT->cb->isPlayerMakingTurn(p)) + { + updateTimer(p, msPassed); + break; + } + } + } + } +} diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h new file mode 100644 index 000000000..f8b4b97fc --- /dev/null +++ b/client/adventureMap/TurnTimerWidget.h @@ -0,0 +1,60 @@ +/* + * TurnTimerWidget.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 "../gui/InterfaceObjectConfigurable.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" + +class CAnimImage; +class CLabel; + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; + +VCMI_LIB_NAMESPACE_END + +class TurnTimerWidget : public InterfaceObjectConfigurable +{ +private: + + class DrawRect : public CIntObject + { + const Rect rect; + const ColorRGBA color; + + public: + DrawRect(const Rect &, const ColorRGBA &); + void showAll(Canvas & to) override; + }; + + int turnTime; + int lastTurnTime; + int cachedTurnTime; + PlayerColor lastPlayer; + + std::set notifications; + + std::shared_ptr buildDrawRect(const JsonNode & config) const; + + void updateTimer(PlayerColor player, uint32_t msPassed); + +public: + + void show(Canvas & to) override; + void tick(uint32_t msPassed) override; + + void setTime(PlayerColor player, int time); + + TurnTimerWidget(); +}; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 8c525c072..25fd62093 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -1,1054 +1,1054 @@ -/* - * BattleActionsController.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 "BattleActionsController.h" - -#include "BattleWindow.h" -#include "BattleStacksController.h" -#include "BattleInterface.h" -#include "BattleFieldController.h" -#include "BattleSiegeController.h" -#include "BattleInterfaceClasses.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CIntObject.h" -#include "../gui/WindowHandler.h" -#include "../windows/CCreatureWindow.h" -#include "../windows/InfoWindows.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CStack.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/Problem.h" - -struct TextReplacement -{ - std::string placeholder; - std::string replacement; -}; - -using TextReplacementList = std::vector; - -static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) -{ - for(const auto & entry : format) - boost::replace_all(input, entry.placeholder, entry.replacement); - - return input; -} - -static std::string translatePlural(int amount, const std::string& baseTextID) -{ - if(amount == 1) - return CGI->generaltexth->translate(baseTextID + ".1"); - return CGI->generaltexth->translate(baseTextID); -} - -static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) -{ - std::string baseString = translatePlural(amount, baseTextID); - TextReplacementList replacements { - { "%d", amountString } - }; - - return replacePlaceholders(baseString, replacements); -} - -static std::string formatPlural(int amount, const std::string & baseTextID) -{ - return formatPluralImpl(amount, std::to_string(amount), baseTextID); -} - -static std::string formatPlural(DamageRange range, const std::string & baseTextID) -{ - if (range.min == range.max) - return formatPlural(range.min, baseTextID); - - std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); - - return formatPluralImpl(range.max, rangeString, baseTextID); -} - -static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) -{ - TextReplacementList replacements = { - { "%CREATURE", creatureName }, - { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, - { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, - { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, - }; - - return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); -} - -static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) -{ - std::string baseTextID = estimation.kills.max == 0 ? - "vcmi.battleWindow.damageEstimation.melee" : - "vcmi.battleWindow.damageEstimation.meleeKills"; - - return formatAttack(estimation, creatureName, baseTextID, 0); -} - -static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) -{ - std::string baseTextID = estimation.kills.max == 0 ? - "vcmi.battleWindow.damageEstimation.ranged" : - "vcmi.battleWindow.damageEstimation.rangedKills"; - - return formatAttack(estimation, creatureName, baseTextID, shotsLeft); -} - -BattleActionsController::BattleActionsController(BattleInterface & owner): - owner(owner), - selectedStack(nullptr), - heroSpellToCast(nullptr) -{ -} - -void BattleActionsController::endCastingSpell() -{ - if(heroSpellToCast) - { - heroSpellToCast.reset(); - owner.windowObject->blockUI(false); - } - - if(owner.stacksController->getActiveStack()) - possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared - - selectedStack = nullptr; - GH.fakeMouseMove(); -} - -bool BattleActionsController::isActiveStackSpellcaster() const -{ - const CStack * casterStack = owner.stacksController->getActiveStack(); - if (!casterStack) - return false; - - bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); - return (spellcaster && casterStack->canCast()); -} - -void BattleActionsController::enterCreatureCastingMode() -{ - //silently check for possible errors - if (owner.tacticsMode) - return; - - //hero is casting a spell - if (heroSpellToCast) - return; - - if (!owner.stacksController->getActiveStack()) - return; - - if (!isActiveStackSpellcaster()) - return; - - for(const auto & action : possibleActions) - { - if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) - continue; - - const spells::Caster * caster = owner.stacksController->getActiveStack(); - const CSpell * spell = action.spell().toSpell(); - - spells::Target target; - target.emplace_back(); - - spells::BattleCast cast(owner.curInt->cb.get(), caster, spells::Mode::CREATURE_ACTIVE, spell); - - auto m = spell->battleMechanics(&cast); - spells::detail::ProblemImpl ignored; - - const bool isCastingPossible = m->canBeCastAt(target, ignored); - - if (isCastingPossible) - { - owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId()); - selectedStack = nullptr; - - CCS->curh->set(Cursor::Combat::POINTER); - } - return; - } - - possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); - - auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) - { - return !x.spellcast(); - }; - - vstd::erase_if(possibleActions, actionFilterPredicate); - GH.fakeMouseMove(); -} - -std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const -{ - BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass - - for(const auto & spell : creatureSpells) - data.creatureSpellsToCast.push_back(spell->id); - - data.tacticsMode = owner.tacticsMode; - auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data); - - allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); - allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); - - return std::vector(allActions); -} - -void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack) -{ - if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack - - auto assignPriority = [&](const PossiblePlayerBattleAction & item - ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it - { - switch(item.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::NO_LOCATION: - case PossiblePlayerBattleAction::FREE_LOCATION: - case PossiblePlayerBattleAction::OBSTACLE: - if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) - { - PlayerColor stackOwner = owner.curInt->cb->battleGetOwner(targetStack); - bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; - bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; - - if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast) - return 1; - } - return 100; //bottom priority - - break; - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - return 2; - break; - case PossiblePlayerBattleAction::SHOOT: - return 4; - break; - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - return 5; - break; - case PossiblePlayerBattleAction::ATTACK: - return 6; - break; - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - return 7; - break; - case PossiblePlayerBattleAction::MOVE_STACK: - return 8; - break; - case PossiblePlayerBattleAction::CATAPULT: - return 9; - break; - case PossiblePlayerBattleAction::HEAL: - return 10; - break; - case PossiblePlayerBattleAction::CREATURE_INFO: - return 11; - break; - case PossiblePlayerBattleAction::HERO_INFO: - return 12; - break; - case PossiblePlayerBattleAction::TELEPORT: - return 13; - break; - default: - assert(0); - return 200; - break; - } - }; - - auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs) - { - return assignPriority(lhs) < assignPriority(rhs); - }; - - std::sort(possibleActions.begin(), possibleActions.end(), comparer); -} - -void BattleActionsController::castThisSpell(SpellID spellID) -{ - heroSpellToCast = std::make_shared(); - heroSpellToCast->actionType = EActionType::HERO_SPELL; - heroSpellToCast->actionSubtype = spellID; //spell number - heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; - heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - - //choosing possible targets - const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; - assert(castingHero); // code below assumes non-null hero - PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); - - if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location - { - heroSpellToCast->aimToHex(BattleHex::INVALID); - owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast); - endCastingSpell(); - } - else - { - possibleActions.clear(); - possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment - GH.fakeMouseMove();//update cursor - } - - owner.windowObject->blockUI(true); -} - -const CSpell * BattleActionsController::getHeroSpellToCast( ) const -{ - if (heroSpellToCast) - return SpellID(heroSpellToCast->actionSubtype).toSpell(); - return nullptr; -} - -const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) -{ - if (heroSpellToCast) - return nullptr; - - if (!owner.stacksController->getActiveStack()) - return nullptr; - - if (!hoveredHex.isValid()) - return nullptr; - - auto action = selectAction(hoveredHex); - - if (action.spell() == SpellID::NONE) - return nullptr; - - return action.spell().toSpell(); -} - -const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) -{ - if (getHeroSpellToCast()) - return getHeroSpellToCast(); - return getStackSpellToCast(hoveredHex); -} - -const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) -{ - const CStack * shere = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - if(shere) - return shere; - return owner.curInt->cb->battleGetStackByPos(hoveredHex, false); -} - -void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - CCS->curh->set(Cursor::Combat::POINTER); - return; - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) - CCS->curh->set(Cursor::Combat::FLY); - else - CCS->curh->set(Cursor::Combat::MOVE); - return; - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - { - static const std::map sectorCursor = { - {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST}, - {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST}, - {BattleHex::RIGHT, Cursor::Combat::HIT_WEST }, - {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST}, - {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST}, - {BattleHex::LEFT, Cursor::Combat::HIT_EAST }, - {BattleHex::TOP, Cursor::Combat::HIT_SOUTH }, - {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH } - }; - - auto direction = owner.fieldController->selectAttackDirection(targetHex); - - assert(sectorCursor.count(direction) > 0); - if (sectorCursor.count(direction)) - CCS->curh->set(sectorCursor.at(direction)); - - return; - } - - case PossiblePlayerBattleAction::SHOOT: - if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) - CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); - else - CCS->curh->set(Cursor::Combat::SHOOT); - return; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - case PossiblePlayerBattleAction::FREE_LOCATION: - case PossiblePlayerBattleAction::OBSTACLE: - CCS->curh->set(Cursor::Spellcast::SPELL); - return; - - case PossiblePlayerBattleAction::TELEPORT: - CCS->curh->set(Cursor::Combat::TELEPORT); - return; - - case PossiblePlayerBattleAction::SACRIFICE: - CCS->curh->set(Cursor::Combat::SACRIFICE); - return; - - case PossiblePlayerBattleAction::HEAL: - CCS->curh->set(Cursor::Combat::HEAL); - return; - - case PossiblePlayerBattleAction::CATAPULT: - CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT); - return; - - case PossiblePlayerBattleAction::CREATURE_INFO: - CCS->curh->set(Cursor::Combat::QUERY); - return; - case PossiblePlayerBattleAction::HERO_INFO: - CCS->curh->set(Cursor::Combat::HERO); - return; - } - assert(0); -} - -void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - case PossiblePlayerBattleAction::TELEPORT: - case PossiblePlayerBattleAction::SACRIFICE: - case PossiblePlayerBattleAction::FREE_LOCATION: - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - default: - if (targetHex == -1) - CCS->curh->set(Cursor::Combat::POINTER); - else - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - } - assert(0); -} - -std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - - switch (action.get()) //display console message, realize selected action - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) - return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here - else - return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return - { - BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); - estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); - estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); - - return formatMeleeAttack(estimation, targetStack->getName()); - } - - case PossiblePlayerBattleAction::SHOOT: - { - const auto * shooter = owner.stacksController->getActiveStack(); - - DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); - estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); - estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); - - return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); - } - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s - - case PossiblePlayerBattleAction::ANY_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s - - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell - return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % - - case PossiblePlayerBattleAction::TELEPORT: - return CGI->generaltexth->allTexts[25]; //Teleport Here - - case PossiblePlayerBattleAction::OBSTACLE: - return CGI->generaltexth->allTexts[550]; - - case PossiblePlayerBattleAction::SACRIFICE: - return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s - - case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s - - case PossiblePlayerBattleAction::HEAL: - return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s - - case PossiblePlayerBattleAction::CATAPULT: - return ""; // TODO - - case PossiblePlayerBattleAction::CREATURE_INFO: - return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str(); - - case PossiblePlayerBattleAction::HERO_INFO: - return CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats" - } - assert(0); - return ""; -} - -std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - return CGI->generaltexth->allTexts[23]; - break; - case PossiblePlayerBattleAction::TELEPORT: - return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination - break; - case PossiblePlayerBattleAction::SACRIFICE: - return CGI->generaltexth->allTexts[543]; //choose army to sacrifice - break; - case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here - break; - default: - return ""; - } -} - -bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID; - - switch (action.get()) - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (targetStack && targetStackOwned && targetStack->speed() > 0); - - case PossiblePlayerBattleAction::CREATURE_INFO: - return (targetStack && targetStackOwned && targetStack->alive()); - - case PossiblePlayerBattleAction::HERO_INFO: - if (targetHex == BattleHex::HERO_ATTACKER) - return owner.attackingHero != nullptr; - - if (targetHex == BattleHex::HERO_DEFENDER) - return owner.defendingHero != nullptr; - - return false; - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (!(targetStack && targetStack->alive())) //we can walk on dead stacks - { - if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex)) - return true; - } - return false; - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - if(owner.curInt->cb->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) - { - if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? - return true; - } - return false; - - case PossiblePlayerBattleAction::SHOOT: - return owner.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); - - case PossiblePlayerBattleAction::NO_LOCATION: - return false; - - case PossiblePlayerBattleAction::ANY_LOCATION: - return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures - { - int spellID = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); - return spellID > -1; - } - return false; - - case PossiblePlayerBattleAction::TELEPORT: - return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex); - - case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice - return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive(); - - case PossiblePlayerBattleAction::OBSTACLE: - case PossiblePlayerBattleAction::FREE_LOCATION: - return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::CATAPULT: - return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); - - case PossiblePlayerBattleAction::HEAL: - return targetStack && targetStackOwned && targetStack->canBeHealed(); - } - - assert(0); - return false; -} - -void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - - switch (action.get()) //display console message, realize selected action - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - { - owner.stackActivated(targetStack); - return; - } - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - { - if(owner.stacksController->getActiveStack()->doubleWide()) - { - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); - BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); - if(vstd::contains(acc, targetHex)) - owner.giveCommand(EActionType::WALK, targetHex); - else if(vstd::contains(acc, shiftedDest)) - owner.giveCommand(EActionType::WALK, shiftedDest); - } - else - { - owner.giveCommand(EActionType::WALK, targetHex); - } - return; - } - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return - { - bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; - BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) - { - BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack); - owner.sendCommand(command, owner.stacksController->getActiveStack()); - } - return; - } - - case PossiblePlayerBattleAction::SHOOT: - { - owner.giveCommand(EActionType::SHOOT, targetHex); - return; - } - - case PossiblePlayerBattleAction::HEAL: - { - owner.giveCommand(EActionType::STACK_HEAL, targetHex); - return; - }; - - case PossiblePlayerBattleAction::CATAPULT: - { - owner.giveCommand(EActionType::CATAPULT, targetHex); - return; - } - - case PossiblePlayerBattleAction::CREATURE_INFO: - { - GH.windows().createAndPushWindow(targetStack, false); - return; - } - - case PossiblePlayerBattleAction::HERO_INFO: - { - if (targetHex == BattleHex::HERO_ATTACKER) - owner.attackingHero->heroLeftClicked(); - - if (targetHex == BattleHex::HERO_DEFENDER) - owner.defendingHero->heroLeftClicked(); - - return; - } - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell - case PossiblePlayerBattleAction::TELEPORT: - case PossiblePlayerBattleAction::OBSTACLE: - case PossiblePlayerBattleAction::SACRIFICE: - case PossiblePlayerBattleAction::FREE_LOCATION: - { - if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) - { - if (action.spell() == SpellID::SACRIFICE) - { - heroSpellToCast->aimToHex(targetHex); - possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()}); - selectedStack = targetStack; - return; - } - if (action.spell() == SpellID::TELEPORT) - { - heroSpellToCast->aimToUnit(targetStack); - possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()}); - selectedStack = targetStack; - return; - } - } - - if (!spellcastingModeActive()) - { - if (action.spell().toSpell()) - { - owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); - } - else //unknown random spell - { - owner.giveCommand(EActionType::MONSTER_SPELL, targetHex); - } - } - else - { - assert(getHeroSpellToCast()); - switch (getHeroSpellToCast()->id.toEnum()) - { - case SpellID::SACRIFICE: - heroSpellToCast->aimToUnit(targetStack);//victim - break; - default: - heroSpellToCast->aimToHex(targetHex); - break; - } - owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast); - endCastingSpell(); - } - selectedStack = nullptr; - return; - } - } - assert(0); - return; -} - -PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex) -{ - assert(owner.stacksController->getActiveStack() != nullptr); - assert(!possibleActions.empty()); - assert(targetHex.isValid()); - - if (owner.stacksController->getActiveStack() == nullptr) - return PossiblePlayerBattleAction::INVALID; - - if (possibleActions.empty()) - return PossiblePlayerBattleAction::INVALID; - - const CStack * targetStack = getStackForHex(targetHex); - - reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack); - - for (PossiblePlayerBattleAction action : possibleActions) - { - if (actionIsLegal(action, targetHex)) - return action; - } - return possibleActions.front(); -} - -void BattleActionsController::onHexHovered(BattleHex hoveredHex) -{ - if (owner.openingPlaying()) - { - currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro"); - GH.statusbar()->write(currentConsoleMsg); - return; - } - - if (owner.stacksController->getActiveStack() == nullptr) - return; - - if (hoveredHex == BattleHex::INVALID) - { - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - currentConsoleMsg.clear(); - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - } - - auto action = selectAction(hoveredHex); - - std::string newConsoleMsg; - - if (actionIsLegal(action, hoveredHex)) - { - actionSetCursor(action, hoveredHex); - newConsoleMsg = actionGetStatusMessage(action, hoveredHex); - } - else - { - actionSetCursorBlocked(action, hoveredHex); - newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex); - } - - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - if (!newConsoleMsg.empty()) - GH.statusbar()->write(newConsoleMsg); - - currentConsoleMsg = newConsoleMsg; -} - -void BattleActionsController::onHoverEnded() -{ - CCS->curh->set(Cursor::Combat::POINTER); - - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - currentConsoleMsg.clear(); -} - -void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) -{ - if (owner.stacksController->getActiveStack() == nullptr) - return; - - auto action = selectAction(clickedHex); - - std::string newConsoleMsg; - - if (!actionIsLegal(action, clickedHex)) - return; - - actionRealize(action, clickedHex); - GH.statusbar()->clear(); -} - -void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) -{ - creatureSpells.clear(); - - bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); - if(casterStack->canCast() && spellcaster) - { - // faerie dragon can cast only one, randomly selected spell until their next move - //TODO: faerie dragon type spell should be selected by server - const auto * spellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); - - if (spellToCast) - creatureSpells.push_back(spellToCast); - } - - TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); - - for(const auto & bonus : *bl) - { - if (bonus->additionalInfo[0] <= 0) - creatureSpells.push_back(SpellID(bonus->subtype).toSpell()); - } -} - -const spells::Caster * BattleActionsController::getCurrentSpellcaster() const -{ - if (heroSpellToCast) - return owner.getActiveHero(); - else - return owner.stacksController->getActiveStack(); -} - -spells::Mode BattleActionsController::getCurrentCastMode() const -{ - if (heroSpellToCast) - return spells::Mode::HERO; - else - return spells::Mode::CREATURE_ACTIVE; - -} - -bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex) -{ - assert(currentSpell); - if (!currentSpell) - return false; - - auto caster = getCurrentSpellcaster(); - - const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE; - - spells::Target target; - if(targetStack) - target.emplace_back(targetStack); - target.emplace_back(targetHex); - - spells::BattleCast cast(owner.curInt->cb.get(), caster, mode, currentSpell); - - auto m = currentSpell->battleMechanics(&cast); - spells::detail::ProblemImpl problem; //todo: display problem in status bar - - return m->canBeCastAt(target, problem); -} - -bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const -{ - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(stackToMove, false); - BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); - - if (vstd::contains(acc, myNumber)) - return true; - else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest)) - return true; - else - return false; -} - -void BattleActionsController::activateStack() -{ - const CStack * s = owner.stacksController->getActiveStack(); - if(s) - { - tryActivateStackSpellcasting(s); - - possibleActions = getPossibleActionsForStack(s); - std::list actionsToSelect; - if(!possibleActions.empty()) - { - switch(possibleActions.front().get()) - { - case PossiblePlayerBattleAction::SHOOT: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); - break; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - case PossiblePlayerBattleAction::ANY_LOCATION: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - } - } - owner.windowObject->setAlternativeActions(actionsToSelect); - } -} - -void BattleActionsController::onHexRightClicked(BattleHex clickedHex) -{ - auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) - { - return action.spellcast(); - }; - - bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); - - if (spellcastingModeActive() || isCurrentStackInSpellcastMode) - { - endCastingSpell(); - CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled - return; - } - - auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true); - - if (selectedStack != nullptr) - GH.windows().createAndPushWindow(selectedStack, true); - - if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero) - owner.attackingHero->heroRightClicked(); - - if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero) - owner.defendingHero->heroRightClicked(); -} - -bool BattleActionsController::spellcastingModeActive() const -{ - return heroSpellToCast != nullptr; -} - -bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) -{ - if (heroSpellToCast) - return true; - - if (!owner.stacksController->getActiveStack()) - return false; - - auto action = selectAction(hoveredHex); - - return action.spellcast(); -} - -const std::vector & BattleActionsController::getPossibleActions() const -{ - return possibleActions; -} - -void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action) -{ - vstd::erase(possibleActions, action); -} - -void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action) -{ - possibleActions.insert(possibleActions.begin(), action); -} +/* + * BattleActionsController.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 "BattleActionsController.h" + +#include "BattleWindow.h" +#include "BattleStacksController.h" +#include "BattleInterface.h" +#include "BattleFieldController.h" +#include "BattleSiegeController.h" +#include "BattleInterfaceClasses.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CIntObject.h" +#include "../gui/WindowHandler.h" +#include "../windows/CCreatureWindow.h" +#include "../windows/InfoWindows.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/Problem.h" + +struct TextReplacement +{ + std::string placeholder; + std::string replacement; +}; + +using TextReplacementList = std::vector; + +static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) +{ + for(const auto & entry : format) + boost::replace_all(input, entry.placeholder, entry.replacement); + + return input; +} + +static std::string translatePlural(int amount, const std::string& baseTextID) +{ + if(amount == 1) + return CGI->generaltexth->translate(baseTextID + ".1"); + return CGI->generaltexth->translate(baseTextID); +} + +static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) +{ + std::string baseString = translatePlural(amount, baseTextID); + TextReplacementList replacements { + { "%d", amountString } + }; + + return replacePlaceholders(baseString, replacements); +} + +static std::string formatPlural(int amount, const std::string & baseTextID) +{ + return formatPluralImpl(amount, std::to_string(amount), baseTextID); +} + +static std::string formatPlural(DamageRange range, const std::string & baseTextID) +{ + if (range.min == range.max) + return formatPlural(range.min, baseTextID); + + std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); + + return formatPluralImpl(range.max, rangeString, baseTextID); +} + +static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) +{ + TextReplacementList replacements = { + { "%CREATURE", creatureName }, + { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, + { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, + { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, + }; + + return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); +} + +static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.melee" : + "vcmi.battleWindow.damageEstimation.meleeKills"; + + return formatAttack(estimation, creatureName, baseTextID, 0); +} + +static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.ranged" : + "vcmi.battleWindow.damageEstimation.rangedKills"; + + return formatAttack(estimation, creatureName, baseTextID, shotsLeft); +} + +BattleActionsController::BattleActionsController(BattleInterface & owner): + owner(owner), + selectedStack(nullptr), + heroSpellToCast(nullptr) +{ +} + +void BattleActionsController::endCastingSpell() +{ + if(heroSpellToCast) + { + heroSpellToCast.reset(); + owner.windowObject->blockUI(false); + } + + if(owner.stacksController->getActiveStack()) + possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared + + selectedStack = nullptr; + GH.fakeMouseMove(); +} + +bool BattleActionsController::isActiveStackSpellcaster() const +{ + const CStack * casterStack = owner.stacksController->getActiveStack(); + if (!casterStack) + return false; + + bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); + return (spellcaster && casterStack->canCast()); +} + +void BattleActionsController::enterCreatureCastingMode() +{ + //silently check for possible errors + if (owner.tacticsMode) + return; + + //hero is casting a spell + if (heroSpellToCast) + return; + + if (!owner.stacksController->getActiveStack()) + return; + + if (!isActiveStackSpellcaster()) + return; + + for(const auto & action : possibleActions) + { + if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) + continue; + + const spells::Caster * caster = owner.stacksController->getActiveStack(); + const CSpell * spell = action.spell().toSpell(); + + spells::Target target; + target.emplace_back(); + + spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell); + + auto m = spell->battleMechanics(&cast); + spells::detail::ProblemImpl ignored; + + const bool isCastingPossible = m->canBeCastAt(target, ignored); + + if (isCastingPossible) + { + owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId()); + selectedStack = nullptr; + + CCS->curh->set(Cursor::Combat::POINTER); + } + return; + } + + possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); + + auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) + { + return !x.spellcast(); + }; + + vstd::erase_if(possibleActions, actionFilterPredicate); + GH.fakeMouseMove(); +} + +std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const +{ + BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass + + for(const auto & spell : creatureSpells) + data.creatureSpellsToCast.push_back(spell->id); + + data.tacticsMode = owner.tacticsMode; + auto allActions = owner.getBattle()->getClientActionsForStack(stack, data); + + allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); + allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); + + return std::vector(allActions); +} + +void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack) +{ + if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack + + auto assignPriority = [&](const PossiblePlayerBattleAction & item + ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it + { + switch(item.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::NO_LOCATION: + case PossiblePlayerBattleAction::FREE_LOCATION: + case PossiblePlayerBattleAction::OBSTACLE: + if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) + { + PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack); + bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; + bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; + + if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast) + return 1; + } + return 100; //bottom priority + + break; + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + return 2; + break; + case PossiblePlayerBattleAction::SHOOT: + return 4; + break; + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + return 5; + break; + case PossiblePlayerBattleAction::ATTACK: + return 6; + break; + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + return 7; + break; + case PossiblePlayerBattleAction::MOVE_STACK: + return 8; + break; + case PossiblePlayerBattleAction::CATAPULT: + return 9; + break; + case PossiblePlayerBattleAction::HEAL: + return 10; + break; + case PossiblePlayerBattleAction::CREATURE_INFO: + return 11; + break; + case PossiblePlayerBattleAction::HERO_INFO: + return 12; + break; + case PossiblePlayerBattleAction::TELEPORT: + return 13; + break; + default: + assert(0); + return 200; + break; + } + }; + + auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs) + { + return assignPriority(lhs) < assignPriority(rhs); + }; + + std::sort(possibleActions.begin(), possibleActions.end(), comparer); +} + +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; + + //choosing possible targets + const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; + assert(castingHero); // code below assumes non-null hero + PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); + + if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location + { + heroSpellToCast->aimToHex(BattleHex::INVALID); + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); + endCastingSpell(); + } + else + { + possibleActions.clear(); + possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment + GH.fakeMouseMove();//update cursor + } + + owner.windowObject->blockUI(true); +} + +const CSpell * BattleActionsController::getHeroSpellToCast( ) const +{ + if (heroSpellToCast) + return heroSpellToCast->spell.toSpell(); + return nullptr; +} + +const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) +{ + if (heroSpellToCast) + return nullptr; + + if (!owner.stacksController->getActiveStack()) + return nullptr; + + if (!hoveredHex.isValid()) + return nullptr; + + auto action = selectAction(hoveredHex); + + if (action.spell() == SpellID::NONE) + return nullptr; + + return action.spell().toSpell(); +} + +const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) +{ + if (getHeroSpellToCast()) + return getHeroSpellToCast(); + return getStackSpellToCast(hoveredHex); +} + +const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) +{ + const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(shere) + return shere; + return owner.getBattle()->battleGetStackByPos(hoveredHex, false); +} + +void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + CCS->curh->set(Cursor::Combat::POINTER); + return; + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) + CCS->curh->set(Cursor::Combat::FLY); + else + CCS->curh->set(Cursor::Combat::MOVE); + return; + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + { + static const std::map sectorCursor = { + {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST}, + {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST}, + {BattleHex::RIGHT, Cursor::Combat::HIT_WEST }, + {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST}, + {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST}, + {BattleHex::LEFT, Cursor::Combat::HIT_EAST }, + {BattleHex::TOP, Cursor::Combat::HIT_SOUTH }, + {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH } + }; + + auto direction = owner.fieldController->selectAttackDirection(targetHex); + + assert(sectorCursor.count(direction) > 0); + if (sectorCursor.count(direction)) + CCS->curh->set(sectorCursor.at(direction)); + + return; + } + + case PossiblePlayerBattleAction::SHOOT: + if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) + CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); + else + CCS->curh->set(Cursor::Combat::SHOOT); + return; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + case PossiblePlayerBattleAction::FREE_LOCATION: + case PossiblePlayerBattleAction::OBSTACLE: + CCS->curh->set(Cursor::Spellcast::SPELL); + return; + + case PossiblePlayerBattleAction::TELEPORT: + CCS->curh->set(Cursor::Combat::TELEPORT); + return; + + case PossiblePlayerBattleAction::SACRIFICE: + CCS->curh->set(Cursor::Combat::SACRIFICE); + return; + + case PossiblePlayerBattleAction::HEAL: + CCS->curh->set(Cursor::Combat::HEAL); + return; + + case PossiblePlayerBattleAction::CATAPULT: + CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT); + return; + + case PossiblePlayerBattleAction::CREATURE_INFO: + CCS->curh->set(Cursor::Combat::QUERY); + return; + case PossiblePlayerBattleAction::HERO_INFO: + CCS->curh->set(Cursor::Combat::HERO); + return; + } + assert(0); +} + +void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + case PossiblePlayerBattleAction::TELEPORT: + case PossiblePlayerBattleAction::SACRIFICE: + case PossiblePlayerBattleAction::FREE_LOCATION: + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + default: + if (targetHex == -1) + CCS->curh->set(Cursor::Combat::POINTER); + else + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + } + assert(0); +} + +std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + + switch (action.get()) //display console message, realize selected action + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) + return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here + else + return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return + { + BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatMeleeAttack(estimation, targetStack->getName()); + } + + case PossiblePlayerBattleAction::SHOOT: + { + const auto * shooter = owner.stacksController->getActiveStack(); + + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); + } + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s + + case PossiblePlayerBattleAction::ANY_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s + + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell + return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % + + case PossiblePlayerBattleAction::TELEPORT: + return CGI->generaltexth->allTexts[25]; //Teleport Here + + case PossiblePlayerBattleAction::OBSTACLE: + return CGI->generaltexth->allTexts[550]; + + case PossiblePlayerBattleAction::SACRIFICE: + return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s + + case PossiblePlayerBattleAction::FREE_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s + + case PossiblePlayerBattleAction::HEAL: + return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s + + case PossiblePlayerBattleAction::CATAPULT: + return ""; // TODO + + case PossiblePlayerBattleAction::CREATURE_INFO: + return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str(); + + case PossiblePlayerBattleAction::HERO_INFO: + return CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats" + } + assert(0); + return ""; +} + +std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + return CGI->generaltexth->allTexts[23]; + break; + case PossiblePlayerBattleAction::TELEPORT: + return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination + break; + case PossiblePlayerBattleAction::SACRIFICE: + return CGI->generaltexth->allTexts[543]; //choose army to sacrifice + break; + case PossiblePlayerBattleAction::FREE_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here + break; + default: + return ""; + } +} + +bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID; + + switch (action.get()) + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + return (targetStack && targetStackOwned && targetStack->speed() > 0); + + case PossiblePlayerBattleAction::CREATURE_INFO: + return (targetStack && targetStackOwned && targetStack->alive()); + + case PossiblePlayerBattleAction::HERO_INFO: + if (targetHex == BattleHex::HERO_ATTACKER) + return owner.attackingHero != nullptr; + + if (targetHex == BattleHex::HERO_DEFENDER) + return owner.defendingHero != nullptr; + + return false; + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (!(targetStack && targetStack->alive())) //we can walk on dead stacks + { + if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex)) + return true; + } + return false; + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) + { + if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? + return true; + } + return false; + + case PossiblePlayerBattleAction::SHOOT: + return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); + + case PossiblePlayerBattleAction::NO_LOCATION: + return false; + + case PossiblePlayerBattleAction::ANY_LOCATION: + return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures + { + int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); + return spellID > -1; + } + return false; + + case PossiblePlayerBattleAction::TELEPORT: + return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex); + + case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice + return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive(); + + case PossiblePlayerBattleAction::OBSTACLE: + case PossiblePlayerBattleAction::FREE_LOCATION: + return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::CATAPULT: + return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); + + case PossiblePlayerBattleAction::HEAL: + return targetStack && targetStackOwned && targetStack->canBeHealed(); + } + + assert(0); + return false; +} + +void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + + switch (action.get()) //display console message, realize selected action + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + { + owner.stackActivated(targetStack); + return; + } + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + { + if(owner.stacksController->getActiveStack()->doubleWide()) + { + std::vector acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); + BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); + if(vstd::contains(acc, targetHex)) + owner.giveCommand(EActionType::WALK, targetHex); + else if(vstd::contains(acc, shiftedDest)) + owner.giveCommand(EActionType::WALK, shiftedDest); + } + else + { + owner.giveCommand(EActionType::WALK, targetHex); + } + return; + } + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return + { + bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; + BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) + { + BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack); + owner.sendCommand(command, owner.stacksController->getActiveStack()); + } + return; + } + + case PossiblePlayerBattleAction::SHOOT: + { + owner.giveCommand(EActionType::SHOOT, targetHex); + return; + } + + case PossiblePlayerBattleAction::HEAL: + { + owner.giveCommand(EActionType::STACK_HEAL, targetHex); + return; + }; + + case PossiblePlayerBattleAction::CATAPULT: + { + owner.giveCommand(EActionType::CATAPULT, targetHex); + return; + } + + case PossiblePlayerBattleAction::CREATURE_INFO: + { + GH.windows().createAndPushWindow(targetStack, false); + return; + } + + case PossiblePlayerBattleAction::HERO_INFO: + { + if (targetHex == BattleHex::HERO_ATTACKER) + owner.attackingHero->heroLeftClicked(); + + if (targetHex == BattleHex::HERO_DEFENDER) + owner.defendingHero->heroLeftClicked(); + + return; + } + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell + case PossiblePlayerBattleAction::TELEPORT: + case PossiblePlayerBattleAction::OBSTACLE: + case PossiblePlayerBattleAction::SACRIFICE: + case PossiblePlayerBattleAction::FREE_LOCATION: + { + if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) + { + if (action.spell() == SpellID::SACRIFICE) + { + heroSpellToCast->aimToHex(targetHex); + possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()}); + selectedStack = targetStack; + return; + } + if (action.spell() == SpellID::TELEPORT) + { + heroSpellToCast->aimToUnit(targetStack); + possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()}); + selectedStack = targetStack; + return; + } + } + + if (!spellcastingModeActive()) + { + if (action.spell().toSpell()) + { + owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); + } + else //unknown random spell + { + owner.giveCommand(EActionType::MONSTER_SPELL, targetHex); + } + } + else + { + assert(getHeroSpellToCast()); + switch (getHeroSpellToCast()->id.toEnum()) + { + case SpellID::SACRIFICE: + heroSpellToCast->aimToUnit(targetStack);//victim + break; + default: + heroSpellToCast->aimToHex(targetHex); + break; + } + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); + endCastingSpell(); + } + selectedStack = nullptr; + return; + } + } + assert(0); + return; +} + +PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex) +{ + assert(owner.stacksController->getActiveStack() != nullptr); + assert(!possibleActions.empty()); + assert(targetHex.isValid()); + + if (owner.stacksController->getActiveStack() == nullptr) + return PossiblePlayerBattleAction::INVALID; + + if (possibleActions.empty()) + return PossiblePlayerBattleAction::INVALID; + + const CStack * targetStack = getStackForHex(targetHex); + + reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack); + + for (PossiblePlayerBattleAction action : possibleActions) + { + if (actionIsLegal(action, targetHex)) + return action; + } + return possibleActions.front(); +} + +void BattleActionsController::onHexHovered(BattleHex hoveredHex) +{ + if (owner.openingPlaying()) + { + currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro"); + GH.statusbar()->write(currentConsoleMsg); + return; + } + + if (owner.stacksController->getActiveStack() == nullptr) + return; + + if (hoveredHex == BattleHex::INVALID) + { + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + currentConsoleMsg.clear(); + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + } + + auto action = selectAction(hoveredHex); + + std::string newConsoleMsg; + + if (actionIsLegal(action, hoveredHex)) + { + actionSetCursor(action, hoveredHex); + newConsoleMsg = actionGetStatusMessage(action, hoveredHex); + } + else + { + actionSetCursorBlocked(action, hoveredHex); + newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex); + } + + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + if (!newConsoleMsg.empty()) + GH.statusbar()->write(newConsoleMsg); + + currentConsoleMsg = newConsoleMsg; +} + +void BattleActionsController::onHoverEnded() +{ + CCS->curh->set(Cursor::Combat::POINTER); + + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + currentConsoleMsg.clear(); +} + +void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) +{ + if (owner.stacksController->getActiveStack() == nullptr) + return; + + auto action = selectAction(clickedHex); + + std::string newConsoleMsg; + + if (!actionIsLegal(action, clickedHex)) + return; + + actionRealize(action, clickedHex); + GH.statusbar()->clear(); +} + +void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) +{ + creatureSpells.clear(); + + bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); + if(casterStack->canCast() && spellcaster) + { + // faerie dragon can cast only one, randomly selected spell until their next move + //TODO: faerie dragon type spell should be selected by server + const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + + if (spellToCast) + creatureSpells.push_back(spellToCast); + } + + TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); + + for(const auto & bonus : *bl) + { + if (bonus->additionalInfo[0] <= 0) + creatureSpells.push_back(bonus->subtype.as().toSpell()); + } +} + +const spells::Caster * BattleActionsController::getCurrentSpellcaster() const +{ + if (heroSpellToCast) + return owner.currentHero(); + else + return owner.stacksController->getActiveStack(); +} + +spells::Mode BattleActionsController::getCurrentCastMode() const +{ + if (heroSpellToCast) + return spells::Mode::HERO; + else + return spells::Mode::CREATURE_ACTIVE; + +} + +bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex) +{ + assert(currentSpell); + if (!currentSpell) + return false; + + auto caster = getCurrentSpellcaster(); + + const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE; + + spells::Target target; + if(targetStack) + target.emplace_back(targetStack); + target.emplace_back(targetHex); + + spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell); + + auto m = currentSpell->battleMechanics(&cast); + spells::detail::ProblemImpl problem; //todo: display problem in status bar + + return m->canBeCastAt(target, problem); +} + +bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const +{ + std::vector acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false); + BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); + + if (vstd::contains(acc, myNumber)) + return true; + else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest)) + return true; + else + return false; +} + +void BattleActionsController::activateStack() +{ + const CStack * s = owner.stacksController->getActiveStack(); + if(s) + { + tryActivateStackSpellcasting(s); + + possibleActions = getPossibleActionsForStack(s); + std::list actionsToSelect; + if(!possibleActions.empty()) + { + switch(possibleActions.front().get()) + { + case PossiblePlayerBattleAction::SHOOT: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); + break; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + case PossiblePlayerBattleAction::ANY_LOCATION: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + } + } + owner.windowObject->setAlternativeActions(actionsToSelect); + } +} + +void BattleActionsController::onHexRightClicked(BattleHex clickedHex) +{ + auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) + { + return action.spellcast(); + }; + + bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); + + if (spellcastingModeActive() || isCurrentStackInSpellcastMode) + { + endCastingSpell(); + CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled + return; + } + + auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true); + + if (selectedStack != nullptr) + GH.windows().createAndPushWindow(selectedStack, true); + + if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero) + owner.attackingHero->heroRightClicked(); + + if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero) + owner.defendingHero->heroRightClicked(); +} + +bool BattleActionsController::spellcastingModeActive() const +{ + return heroSpellToCast != nullptr; +} + +bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) +{ + if (heroSpellToCast) + return true; + + if (!owner.stacksController->getActiveStack()) + return false; + + auto action = selectAction(hoveredHex); + + return action.spellcast(); +} + +const std::vector & BattleActionsController::getPossibleActions() const +{ + return possibleActions; +} + +void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action) +{ + vstd::erase(possibleActions, action); +} + +void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action) +{ + possibleActions.insert(possibleActions.begin(), action); +} diff --git a/client/battle/BattleActionsController.h b/client/battle/BattleActionsController.h index 86acc8e44..3c9b35660 100644 --- a/client/battle/BattleActionsController.h +++ b/client/battle/BattleActionsController.h @@ -1,125 +1,125 @@ -/* - * BattleActionsController.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/battle/CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BattleAction; -namespace spells { -class Caster; -enum class Mode; -} - -VCMI_LIB_NAMESPACE_END - -class BattleInterface; - -/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc -/// As well as all relevant feedback for these actions in user interface -class BattleActionsController -{ - BattleInterface & owner; - - /// all actions possible to call at the moment by player - std::vector possibleActions; - - /// spell for which player's hero is choosing destination - std::shared_ptr heroSpellToCast; - - /// cached message that was set by this class in status bar - std::string currentConsoleMsg; - - /// if true, active stack could possibly cast some target spell - std::vector creatureSpells; - - /// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice) - const CStack * selectedStack; - - bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber); - bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback - std::vector getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn - void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack); - - bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex); - void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex); - std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - PossiblePlayerBattleAction selectAction(BattleHex myNumber); - - const CStack * getStackForHex(BattleHex myNumber) ; - - /// attempts to initialize spellcasting action for stack - /// will silently return if stack is not a spellcaster - void tryActivateStackSpellcasting(const CStack *casterStack); - - /// returns spell that is currently being cast by hero or nullptr if none - const CSpell * getHeroSpellToCast() const; - - /// if current stack is spellcaster, returns spell being cast, or null othervice - const CSpell * getStackSpellToCast(BattleHex hoveredHex); - - /// returns true if current stack is a spellcaster - bool isActiveStackSpellcaster() const; - -public: - BattleActionsController(BattleInterface & owner); - - /// 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 one of the following is true: - /// - we are casting spell by hero - /// - we are casting spell by creature in targeted mode (F hotkey) - /// - current creature is spellcaster and preferred action for current hex is spellcast - bool currentActionSpellcasting(BattleHex hoveredHex); - - /// enter targeted spellcasting mode for creature, e.g. via "F" hotkey - void enterCreatureCastingMode(); - - /// initialize hero spellcasting mode, e.g. on selecting spell in spellbook - void castThisSpell(SpellID spellID); - - /// ends casting spell (eg. when spell has been cast or canceled) - void endCastingSpell(); - - /// update cursor and status bar according to new active hex - void onHexHovered(BattleHex hoveredHex); - - /// called when cursor is no longer over battlefield and cursor/battle log should be reset - void onHoverEnded(); - - /// performs action according to selected hex - void onHexLeftClicked(BattleHex clickedHex); - - /// performs action according to selected hex - void onHexRightClicked(BattleHex clickedHex); - - const spells::Caster * getCurrentSpellcaster() const; - const CSpell * getCurrentSpell(BattleHex hoveredHex); - spells::Mode getCurrentCastMode() const; - - /// methods to work with array of possible actions, needed to control special creatures abilities - const std::vector & getPossibleActions() const; - void removePossibleAction(PossiblePlayerBattleAction); - - /// inserts possible action in the beggining in order to prioritize it - void pushFrontPossibleAction(PossiblePlayerBattleAction); -}; +/* + * BattleActionsController.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/battle/CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleAction; +namespace spells { +class Caster; +enum class Mode; +} + +VCMI_LIB_NAMESPACE_END + +class BattleInterface; + +/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc +/// As well as all relevant feedback for these actions in user interface +class BattleActionsController +{ + BattleInterface & owner; + + /// all actions possible to call at the moment by player + std::vector possibleActions; + + /// spell for which player's hero is choosing destination + std::shared_ptr heroSpellToCast; + + /// cached message that was set by this class in status bar + std::string currentConsoleMsg; + + /// if true, active stack could possibly cast some target spell + std::vector creatureSpells; + + /// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice) + const CStack * selectedStack; + + bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber); + bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback + std::vector getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn + void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack); + + bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex); + void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex); + std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + PossiblePlayerBattleAction selectAction(BattleHex myNumber); + + const CStack * getStackForHex(BattleHex myNumber) ; + + /// attempts to initialize spellcasting action for stack + /// will silently return if stack is not a spellcaster + void tryActivateStackSpellcasting(const CStack *casterStack); + + /// returns spell that is currently being cast by hero or nullptr if none + const CSpell * getHeroSpellToCast() const; + + /// if current stack is spellcaster, returns spell being cast, or null othervice + const CSpell * getStackSpellToCast(BattleHex hoveredHex); + + /// returns true if current stack is a spellcaster + bool isActiveStackSpellcaster() const; + +public: + BattleActionsController(BattleInterface & owner); + + /// 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 one of the following is true: + /// - we are casting spell by hero + /// - we are casting spell by creature in targeted mode (F hotkey) + /// - current creature is spellcaster and preferred action for current hex is spellcast + bool currentActionSpellcasting(BattleHex hoveredHex); + + /// enter targeted spellcasting mode for creature, e.g. via "F" hotkey + void enterCreatureCastingMode(); + + /// initialize hero spellcasting mode, e.g. on selecting spell in spellbook + void castThisSpell(SpellID spellID); + + /// ends casting spell (eg. when spell has been cast or canceled) + void endCastingSpell(); + + /// update cursor and status bar according to new active hex + void onHexHovered(BattleHex hoveredHex); + + /// called when cursor is no longer over battlefield and cursor/battle log should be reset + void onHoverEnded(); + + /// performs action according to selected hex + void onHexLeftClicked(BattleHex clickedHex); + + /// performs action according to selected hex + void onHexRightClicked(BattleHex clickedHex); + + const spells::Caster * getCurrentSpellcaster() const; + const CSpell * getCurrentSpell(BattleHex hoveredHex); + spells::Mode getCurrentCastMode() const; + + /// methods to work with array of possible actions, needed to control special creatures abilities + const std::vector & getPossibleActions() const; + void removePossibleAction(PossiblePlayerBattleAction); + + /// inserts possible action in the beggining in order to prioritize it + void pushFrontPossibleAction(PossiblePlayerBattleAction); +}; diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index f0db62fd3..f56298930 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -1,1136 +1,1137 @@ -/* - * BattleAnimationClasses.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 "BattleAnimationClasses.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleProjectileController.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleEffectsController.h" -#include "BattleStacksController.h" -#include "CreatureAnimation.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" - -BattleAnimation::BattleAnimation(BattleInterface & owner) - : owner(owner), - ID(owner.stacksController->animIDhelper++), - initialized(false) -{ - logAnim->trace("Animation #%d created", ID); -} - -bool BattleAnimation::tryInitialize() -{ - assert(!initialized); - - if ( init() ) - { - initialized = true; - return true; - } - return false; -} - -bool BattleAnimation::isInitialized() -{ - return initialized; -} - -BattleAnimation::~BattleAnimation() -{ - logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); - for(auto & elem : pendingAnimations()) - { - if(elem == this) - elem = nullptr; - } - logAnim->trace("Animation #%d deleted", ID); -} - -std::vector & BattleAnimation::pendingAnimations() -{ - return owner.stacksController->currentAnimations; -} - -std::shared_ptr BattleAnimation::stackAnimation(const CStack * stack) const -{ - return owner.stacksController->stackAnimation[stack->unitId()]; -} - -bool BattleAnimation::stackFacingRight(const CStack * stack) -{ - return owner.stacksController->stackFacingRight[stack->unitId()]; -} - -void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) -{ - owner.stacksController->stackFacingRight[stack->unitId()] = facingRight; -} - -BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack) - : BattleAnimation(owner), - myAnim(stackAnimation(stack)), - stack(stack) -{ - assert(myAnim); -} - -StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack) - : BattleStackAnimation(owner, stack) - , nextGroup(ECreatureAnimType::HOLDING) - , currGroup(ECreatureAnimType::HOLDING) -{ -} - -ECreatureAnimType StackActionAnimation::getGroup() const -{ - return currGroup; -} - -void StackActionAnimation::setNextGroup( ECreatureAnimType group ) -{ - nextGroup = group; -} - -void StackActionAnimation::setGroup( ECreatureAnimType group ) -{ - currGroup = group; -} - -void StackActionAnimation::setSound( std::string sound ) -{ - this->sound = sound; -} - -bool StackActionAnimation::init() -{ - if (!sound.empty()) - CCS->soundh->playSound(sound); - - if (myAnim->framesInGroup(currGroup) > 0) - { - myAnim->playOnce(currGroup); - myAnim->onAnimationReset += [&](){ delete this; }; - } - else - delete this; - - return true; -} - -StackActionAnimation::~StackActionAnimation() -{ - if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED) - myAnim->setType(ECreatureAnimType::HOLDING); - else - myAnim->setType(nextGroup); - -} - -ECreatureAnimType AttackAnimation::findValidGroup( const std::vector candidates ) const -{ - for ( auto group : candidates) - { - if(myAnim->framesInGroup(group) > 0) - return group; - } - - assert(0); - return ECreatureAnimType::HOLDING; -} - -const CCreature * AttackAnimation::getCreature() const -{ - if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS) - return owner.siegeController->getTurretCreature(); - else - return attackingStack->unitType(); -} - - -AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) - : StackActionAnimation(owner, attacker), - dest(_dest), - defendingStack(defender), - attackingStack(attacker) -{ - assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); - attackingStackPosBeforeReturn = attackingStack->getPosition(); -} - -HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) - : StackActionAnimation(owner, stack) -{ - setGroup(ECreatureAnimType::HITTED); - setSound(battle_sound(stack->unitType(), wince)); - logAnim->debug("Created HittedAnimation for %s", stack->getName()); -} - -DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack) - : StackActionAnimation(owner, stack) -{ - setGroup(ECreatureAnimType::DEFENCE); - setSound(battle_sound(stack->unitType(), defend)); - logAnim->debug("Created DefenceAnimation for %s", stack->getName()); -} - -DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): - StackActionAnimation(owner, stack) -{ - setSound(battle_sound(stack->unitType(), killed)); - - if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) - setGroup(ECreatureAnimType::DEATH_RANGED); - else - setGroup(ECreatureAnimType::DEATH); - - if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0) - setNextGroup(ECreatureAnimType::DEAD_RANGED); - else - setNextGroup(ECreatureAnimType::DEAD); - - logAnim->debug("Created DeathAnimation for %s", stack->getName()); -} - -DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames) - : BattleAnimation(owner), - counter(0), - howMany(howManyFrames) -{ - logAnim->debug("Created dummy animation for %d frames", howManyFrames); -} - -bool DummyAnimation::init() -{ - return true; -} - -void DummyAnimation::tick(uint32_t msPassed) -{ - counter++; - if(counter > howMany) - delete this; -} - -ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_UP; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_UP, - ECreatureAnimType::SPECIAL_UP, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::ATTACK_UP - }); -} - -ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_FRONT; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_FRONT, - ECreatureAnimType::SPECIAL_FRONT, - ECreatureAnimType::ATTACK_FRONT - }); -} - -ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_DOWN; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_DOWN, - ECreatureAnimType::SPECIAL_DOWN, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::ATTACK_DOWN - }); -} - -ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack) -{ - const ECreatureAnimType mutPosToGroup[] = - { - getUpwardsGroup (multiAttack), - getUpwardsGroup (multiAttack), - getForwardGroup (multiAttack), - getDownwardsGroup(multiAttack), - getDownwardsGroup(multiAttack), - getForwardGroup (multiAttack) - }; - - int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1); - - int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); - if(mutPos == -1 && attackingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition()); - } - if (mutPos == -1 && defendingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex()); - } - if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex()); - } - - assert(mutPos >= 0 && mutPos <=5); - - return mutPosToGroup[mutPos]; -} - -void MeleeAttackAnimation::tick(uint32_t msPassed) -{ - size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame(); - size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - - if ( currentFrame * 2 >= totalFrames ) - owner.executeAnimationStage(EAnimationEvents::HIT); - - AttackAnimation::tick(msPassed); -} - -MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack) - : AttackAnimation(owner, attacker, _dest, _attacked) -{ - logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); - setSound(battle_sound(getCreature(), attack)); - setGroup(selectGroup(multiAttack)); -} - -StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex): - BattleStackAnimation(owner, _stack), - prevHex(prevHex), - nextHex(nextHex) -{ -} - -bool MovementAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0); - - if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0) - { - //no movement, end immediately - delete this; - return false; - } - - logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex); - - //reverse unit if necessary - if(owner.stacksController->shouldRotate(stack, prevHex, nextHex)) - { - // it seems that H3 does NOT plays full rotation animation during movement - // Logical since it takes quite a lot of time - rotateStack(prevHex); - } - - if(myAnim->getType() != ECreatureAnimType::MOVING) - { - myAnim->setType(ECreatureAnimType::MOVING); - } - - if (moveSoundHander == -1) - { - moveSoundHander = CCS->soundh->playSound(battle_sound(stack->unitType(), move), -1); - } - - Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); - Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); - - progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); - - begX = begPosition.x; - begY = begPosition.y; - //progress = 0; - distanceX = endPosition.x - begPosition.x; - distanceY = endPosition.y - begPosition.y; - - if (stack->hasBonus(Selector::type()(BonusType::FLYING))) - { - float distance = static_cast(sqrt(distanceX * distanceX + distanceY * distanceY)); - progressPerSecond = AnimationControls::getFlightDistance(stack->unitType()) / distance; - } - - return true; -} - -void MovementAnimation::tick(uint32_t msPassed) -{ - progress += float(msPassed) / 1000 * progressPerSecond; - - //moving instructions - myAnim->pos.x = static_cast(begX + distanceX * progress ); - myAnim->pos.y = static_cast(begY + distanceY * progress ); - - BattleAnimation::tick(msPassed); - - if(progress >= 1.0) - { - progress -= 1.0; - // Sets the position of the creature animation sprites - Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack); - myAnim->pos.moveTo(coords); - - // true if creature haven't reached the final destination hex - if ((curentMoveIndex + 1) < destTiles.size()) - { - // update the next hex field which has to be reached by the stack - curentMoveIndex++; - prevHex = nextHex; - nextHex = destTiles[curentMoveIndex]; - - // request re-initialization - initialized = false; - } - else - delete this; - } -} - -MovementAnimation::~MovementAnimation() -{ - assert(stack); - - if(moveSoundHander != -1) - CCS->soundh->stopSound(moveSoundHander); -} - -MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) - : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), - destTiles(_destTiles), - curentMoveIndex(0), - begX(0), begY(0), - distanceX(0), distanceY(0), - progressPerSecond(0.0), - moveSoundHander(-1), - progress(0.0) -{ - logAnim->debug("Created MovementAnimation for %s", stack->getName()); -} - -MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) -: StackMoveAnimation(owner, _stack, destTile, destTile) -{ - logAnim->debug("Created MovementEndAnimation for %s", stack->getName()); -} - -bool MovementEndAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - - if(!stack || myAnim->isDeadOrDying()) - { - delete this; - return false; - } - - logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); - myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); - - CCS->soundh->playSound(battle_sound(stack->unitType(), endMoving)); - - if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) - { - delete this; - return false; - } - - - myAnim->setType(ECreatureAnimType::MOVE_END); - myAnim->onAnimationReset += [&](){ delete this; }; - - return true; -} - -MovementEndAnimation::~MovementEndAnimation() -{ - if(myAnim->getType() != ECreatureAnimType::DEAD) - myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default - - CCS->curh->show(); -} - -MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack) - : StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition()) -{ - logAnim->debug("Created MovementStartAnimation for %s", stack->getName()); -} - -bool MovementStartAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - - if(!stack || myAnim->isDeadOrDying()) - { - delete this; - return false; - } - - logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); - CCS->soundh->playSound(battle_sound(stack->unitType(), startMoving)); - - if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) - { - delete this; - return false; - } - - myAnim->setType(ECreatureAnimType::MOVE_START); - myAnim->onAnimationReset += [&](){ delete this; }; - return true; -} - -ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest) - : StackMoveAnimation(owner, stack, dest, dest) -{ - logAnim->debug("Created ReverseAnimation for %s", stack->getName()); -} - -bool ReverseAnimation::init() -{ - assert(myAnim); - assert(!myAnim->isDeadOrDying()); - - if(myAnim == nullptr || myAnim->isDeadOrDying()) - { - delete this; - return false; //there is no such creature - } - - logAnim->debug("CReverseAnimation::init: stack %s", stack->getName()); - if(myAnim->framesInGroup(ECreatureAnimType::TURN_L)) - { - myAnim->playOnce(ECreatureAnimType::TURN_L); - myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this); - } - else - { - setupSecondPart(); - } - return true; -} - -void BattleStackAnimation::rotateStack(BattleHex hex) -{ - setStackFacingRight(stack, !stackFacingRight(stack)); - - stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack)); -} - -void ReverseAnimation::setupSecondPart() -{ - assert(stack); - - if(!stack) - { - delete this; - return; - } - - rotateStack(nextHex); - - if(myAnim->framesInGroup(ECreatureAnimType::TURN_R)) - { - myAnim->playOnce(ECreatureAnimType::TURN_R); - myAnim->onAnimationReset += [&](){ delete this; }; - } - else - delete this; -} - -ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack): - StackActionAnimation(owner, _stack) -{ - setGroup(ECreatureAnimType::RESURRECTION); - logAnim->debug("Created ResurrectionAnimation for %s", stack->getName()); -} - -bool ColorTransformAnimation::init() -{ - return true; -} - -void ColorTransformAnimation::tick(uint32_t msPassed) -{ - float elapsed = msPassed / 1000.f; - float fullTime = AnimationControls::getFadeInDuration(); - float delta = elapsed / fullTime; - totalProgress += delta; - - size_t index = 0; - - while (index < timePoints.size() && timePoints[index] < totalProgress ) - ++index; - - if (index == timePoints.size()) - { - //end of animation. Apply ColorShifter using final values and die - const auto & shifter = steps[index - 1]; - owner.stacksController->setStackColorFilter(shifter, stack, spell, false); - delete this; - return; - } - - assert(index != 0); - - const auto & prevShifter = steps[index - 1]; - const auto & nextShifter = steps[index]; - - float prevPoint = timePoints[index-1]; - float nextPoint = timePoints[index]; - float localProgress = totalProgress - prevPoint; - float stepDuration = (nextPoint - prevPoint); - float factor = localProgress / stepDuration; - - auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor); - - owner.stacksController->setStackColorFilter(shifter, stack, spell, true); -} - -ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell): - BattleStackAnimation(owner, _stack), - spell(spell), - totalProgress(0.f) -{ - auto effect = owner.effectsController->getMuxerEffect(colorFilterName); - steps = effect.filters; - timePoints = effect.timePoints; - - assert(!steps.empty() && steps.size() == timePoints.size()); - - logAnim->debug("Created ColorTransformAnimation for %s", stack->getName()); -} - -RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) - : AttackAnimation(owner_, attacker, dest_, defender), - projectileEmitted(false) -{ - setSound(battle_sound(getCreature(), shoot)); -} - -bool RangedAttackAnimation::init() -{ - setAnimationGroup(); - initializeProjectile(); - - return AttackAnimation::init(); -} - -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) - static const double straightAngle = 0.2; - - double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); - - // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. - if (projectileAngle > straightAngle) - setGroup(getUpwardsGroup()); - else if (projectileAngle < -straightAngle) - setGroup(getDownwardsGroup()); - else - setGroup(getForwardGroup()); -} - -void RangedAttackAnimation::initializeProjectile() -{ - const CCreature *shooterInfo = getCreature(); - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225); - Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); - int multiplier = stackFacingRight(attackingStack) ? 1 : -1; - - if (getGroup() == getUpwardsGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; - } - else if (getGroup() == getDownwardsGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; - } - else if (getGroup() == getForwardGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; - } - else - { - assert(0); - } - - createProjectile(shotOrigin, shotTarget); -} - -void RangedAttackAnimation::emitProjectile() -{ - logAnim->debug("Ranged attack projectile emitted"); - owner.projectilesController->emitStackProjectile(attackingStack); - projectileEmitted = true; -} - -void RangedAttackAnimation::tick(uint32_t msPassed) -{ - // animation should be paused if there is an active projectile - if (projectileEmitted) - { - if (!owner.projectilesController->hasActiveProjectile(attackingStack, false)) - owner.executeAnimationStage(EAnimationEvents::HIT); - - } - - bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true); - - if (!projectileEmitted || stackHasProjectile) - stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame()); - else - stackAnimation(attackingStack)->playUntil(static_cast(-1)); - - AttackAnimation::tick(msPassed); - - if (!projectileEmitted) - { - // emit projectile once animation playback reached "climax" frame - if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() ) - { - emitProjectile(); - return; - } - } -} - -RangedAttackAnimation::~RangedAttackAnimation() -{ - assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false)); - assert(projectileEmitted); - - // FIXME: is this possible? Animation is over but we're yet to fire projectile? - if (!projectileEmitted) - { - logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now..."); - emitProjectile(); - } -} - -ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) - : RangedAttackAnimation(owner, attacker, _dest, _attacked) -{ - logAnim->debug("Created ShootingAnimation for %s", stack->getName()); -} - -void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const -{ - owner.projectilesController->createProjectile(attackingStack, from, dest); -} - -uint32_t ShootingAnimation::getAttackClimaxFrame() const -{ - const CCreature *shooterInfo = getCreature(); - - uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame; - uint32_t selectedFrame = std::clamp(shooterInfo->animation.attackClimaxFrame, 1, maxFrames); - - if (climaxFrame != selectedFrame) - logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames); - - return selectedFrame - 1; // H3 counts frames from 1 -} - -ECreatureAnimType ShootingAnimation::getUpwardsGroup() const -{ - return ECreatureAnimType::SHOOT_UP; -} - -ECreatureAnimType ShootingAnimation::getForwardGroup() const -{ - return ECreatureAnimType::SHOOT_FRONT; -} - -ECreatureAnimType ShootingAnimation::getDownwardsGroup() const -{ - return ECreatureAnimType::SHOOT_DOWN; -} - -CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) - : ShootingAnimation(owner, attacker, _dest, _attacked), - catapultDamage(_catapultDmg), - explosionEmitted(false) -{ - logAnim->debug("Created shooting anim for %s", stack->getName()); -} - -void CatapultAnimation::tick(uint32_t msPassed) -{ - ShootingAnimation::tick(msPassed); - - if ( explosionEmitted) - return; - - if ( !projectileEmitted) - return; - - if (owner.projectilesController->hasActiveProjectile(attackingStack, false)) - return; - - explosionEmitted = true; - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); - - std::string soundFilename = (catapultDamage > 0) ? "WALLHIT" : "WALLMISS"; - std::string effectFilename = (catapultDamage > 0) ? "SGEXPL" : "CSGRCK"; - - CCS->soundh->playSound( soundFilename ); - owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); -} - -void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const -{ - owner.projectilesController->createCatapultProjectile(attackingStack, from, dest); -} - -CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell) - : RangedAttackAnimation(owner_, attacker, dest, defender), - spell(spell) -{ - if(!dest.isValid()) - { - assert(spell->animationInfo.projectile.empty()); - - if (defender) - dest = defender->getPosition(); - else - dest = attacker->getPosition(); - } -} - -ECreatureAnimType CastAnimation::getUpwardsGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_UP, - ECreatureAnimType::SPECIAL_UP, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::SHOOT_UP, - ECreatureAnimType::ATTACK_UP - }); -} - -ECreatureAnimType CastAnimation::getForwardGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_FRONT, - ECreatureAnimType::SPECIAL_FRONT, - ECreatureAnimType::SHOOT_FRONT, - ECreatureAnimType::ATTACK_FRONT - }); -} - -ECreatureAnimType CastAnimation::getDownwardsGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_DOWN, - ECreatureAnimType::SPECIAL_DOWN, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::SHOOT_DOWN, - ECreatureAnimType::ATTACK_DOWN - }); -} - -void CastAnimation::createProjectile(const Point & from, const Point & dest) const -{ - if (!spell->animationInfo.projectile.empty()) - owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell); -} - -uint32_t CastAnimation::getAttackClimaxFrame() const -{ - //TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks - uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - - return maxFrames / 2; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, int effects, bool reversed): - BattleAnimation(owner), - animation(std::make_shared(animationName)), - effectFlags(effects), - effectFinished(false), - reversed(reversed) -{ - logAnim->debug("CPointEffectAnimation::init: effect %s", animationName); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - battlehexes = hex; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - assert(hex.isValid()); - battlehexes.push_back(hex); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - positions = pos; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - positions.push_back(pos); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - assert(hex.isValid()); - battlehexes.push_back(hex); - positions.push_back(pos); -} - -bool EffectAnimation::init() -{ - animation->preload(); - - auto first = animation->getImage(0, 0, true); - if(!first) - { - delete this; - return false; - } - - for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i) - { - size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i; - - animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE)); - } - - if (screenFill()) - { - for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i) - for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j) - positions.push_back(Point( i * first->width(), j * first->height())); - } - - BattleEffect be; - be.effectID = ID; - be.animation = animation; - be.currentFrame = 0; - be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT; - - for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i) - { - bool hasTile = i < battlehexes.size(); - bool hasPosition = i < positions.size(); - - if (hasTile && !forceOnTop()) - be.tile = battlehexes[i]; - else - be.tile = BattleHex::INVALID; - - if (hasPosition) - { - be.pos.x = positions[i].x; - be.pos.y = positions[i].y; - } - else - { - const auto * destStack = owner.getCurrentPlayerInterface()->cb->battleGetUnitByPos(battlehexes[i], false); - Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); - - be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; - - if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. - be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; - - if (alignToBottom()) - be.pos.y = tilePos.y + tilePos.h - first->height(); - else - be.pos.y = tilePos.y - first->height()/2; - } - owner.effectsController->battleEffects.push_back(be); - } - return true; -} - -void EffectAnimation::tick(uint32_t msPassed) -{ - playEffect(msPassed); - - if (effectFinished) - { - //remove visual effect itself only if sound has finished as well - necessary for obstacles like force field - clearEffect(); - delete this; - } -} - -bool EffectAnimation::alignToBottom() const -{ - return effectFlags & ALIGN_TO_BOTTOM; -} - -bool EffectAnimation::forceOnTop() const -{ - return effectFlags & FORCE_ON_TOP; -} - -bool EffectAnimation::screenFill() const -{ - return effectFlags & SCREEN_FILL; -} - -void EffectAnimation::onEffectFinished() -{ - effectFinished = true; -} - -void EffectAnimation::playEffect(uint32_t msPassed) -{ - if ( effectFinished ) - return; - - for(auto & elem : owner.effectsController->battleEffects) - { - if(elem.effectID == ID) - { - elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - - if(elem.currentFrame >= elem.animation->size()) - { - elem.currentFrame = elem.animation->size() - 1; - onEffectFinished(); - break; - } - } - } -} - -void EffectAnimation::clearEffect() -{ - auto & effects = owner.effectsController->battleEffects; - - vstd::erase_if(effects, [&](const BattleEffect & effect){ - return effect.effectID == ID; - }); -} - -EffectAnimation::~EffectAnimation() -{ - assert(effectFinished); -} - -HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell): - BattleAnimation(owner), - projectileEmitted(false), - hero(hero), - target(defender), - tile(dest), - spell(spell) -{ -} - -bool HeroCastAnimation::init() -{ - hero->setPhase(EHeroAnimType::CAST_SPELL); - - hero->onPhaseFinished([&](){ - delete this; - }); - - initializeProjectile(); - - return true; -} - -void HeroCastAnimation::initializeProjectile() -{ - // spell has no projectile to play, ignore this step - if (spell->animationInfo.projectile.empty()) - return; - - // targeted spells should have well, target - assert(tile.isValid()); - - Point srccoord = hero->pos.center() - hero->parent->pos.topLeft(); - Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile - - destcoord += Point(222, 265); // FIXME: what are these constants? - owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); -} - -void HeroCastAnimation::emitProjectile() -{ - if (projectileEmitted) - return; - - //spell has no projectile to play, skip this step and immediately play hit animations - if (spell->animationInfo.projectile.empty()) - emitAnimationEvent(); - else - owner.projectilesController->emitStackProjectile( nullptr ); - - projectileEmitted = true; -} - -void HeroCastAnimation::emitAnimationEvent() -{ - owner.executeAnimationStage(EAnimationEvents::HIT); -} - -void HeroCastAnimation::tick(uint32_t msPassed) -{ - float frame = hero->getFrame(); - - if (frame < 4.0f) // middle point of animation //TODO: un-hardcode - return; - - if (!projectileEmitted) - { - emitProjectile(); - hero->pause(); - return; - } - - if (!owner.projectilesController->hasActiveProjectile(nullptr, false)) - { - emitAnimationEvent(); - //TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile - hero->play(); - } -} +/* + * BattleAnimationClasses.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 "BattleAnimationClasses.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleProjectileController.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleEffectsController.h" +#include "BattleStacksController.h" +#include "CreatureAnimation.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" + +BattleAnimation::BattleAnimation(BattleInterface & owner) + : owner(owner), + ID(owner.stacksController->animIDhelper++), + initialized(false) +{ + logAnim->trace("Animation #%d created", ID); +} + +bool BattleAnimation::tryInitialize() +{ + assert(!initialized); + + if ( init() ) + { + initialized = true; + return true; + } + return false; +} + +bool BattleAnimation::isInitialized() +{ + return initialized; +} + +BattleAnimation::~BattleAnimation() +{ + logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); + for(auto & elem : pendingAnimations()) + { + if(elem == this) + elem = nullptr; + } + logAnim->trace("Animation #%d deleted", ID); +} + +std::vector & BattleAnimation::pendingAnimations() +{ + return owner.stacksController->currentAnimations; +} + +std::shared_ptr BattleAnimation::stackAnimation(const CStack * stack) const +{ + return owner.stacksController->stackAnimation[stack->unitId()]; +} + +bool BattleAnimation::stackFacingRight(const CStack * stack) +{ + return owner.stacksController->stackFacingRight[stack->unitId()]; +} + +void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) +{ + owner.stacksController->stackFacingRight[stack->unitId()] = facingRight; +} + +BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack) + : BattleAnimation(owner), + myAnim(stackAnimation(stack)), + stack(stack) +{ + assert(myAnim); +} + +StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack) + : BattleStackAnimation(owner, stack) + , nextGroup(ECreatureAnimType::HOLDING) + , currGroup(ECreatureAnimType::HOLDING) +{ +} + +ECreatureAnimType StackActionAnimation::getGroup() const +{ + return currGroup; +} + +void StackActionAnimation::setNextGroup( ECreatureAnimType group ) +{ + nextGroup = group; +} + +void StackActionAnimation::setGroup( ECreatureAnimType group ) +{ + currGroup = group; +} + +void StackActionAnimation::setSound( const AudioPath & sound ) +{ + this->sound = sound; +} + +bool StackActionAnimation::init() +{ + if (!sound.empty()) + CCS->soundh->playSound(sound); + + if (myAnim->framesInGroup(currGroup) > 0) + { + myAnim->playOnce(currGroup); + myAnim->onAnimationReset += [&](){ delete this; }; + } + else + delete this; + + return true; +} + +StackActionAnimation::~StackActionAnimation() +{ + if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED) + myAnim->setType(ECreatureAnimType::HOLDING); + else + myAnim->setType(nextGroup); + +} + +ECreatureAnimType AttackAnimation::findValidGroup( const std::vector candidates ) const +{ + for ( auto group : candidates) + { + if(myAnim->framesInGroup(group) > 0) + return group; + } + + assert(0); + return ECreatureAnimType::HOLDING; +} + +const CCreature * AttackAnimation::getCreature() const +{ + if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS) + return owner.siegeController->getTurretCreature(); + else + return attackingStack->unitType(); +} + + +AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) + : StackActionAnimation(owner, attacker), + dest(_dest), + defendingStack(defender), + attackingStack(attacker) +{ + assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); + attackingStackPosBeforeReturn = attackingStack->getPosition(); +} + +HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) +{ + setGroup(ECreatureAnimType::HITTED); + setSound(stack->unitType()->sounds.wince); + logAnim->debug("Created HittedAnimation for %s", stack->getName()); +} + +DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) +{ + setGroup(ECreatureAnimType::DEFENCE); + setSound(stack->unitType()->sounds.defend); + logAnim->debug("Created DefenceAnimation for %s", stack->getName()); +} + +DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): + StackActionAnimation(owner, stack) +{ + setSound(stack->unitType()->sounds.killed); + + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) + setGroup(ECreatureAnimType::DEATH_RANGED); + else + setGroup(ECreatureAnimType::DEATH); + + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0) + setNextGroup(ECreatureAnimType::DEAD_RANGED); + else + setNextGroup(ECreatureAnimType::DEAD); + + logAnim->debug("Created DeathAnimation for %s", stack->getName()); +} + +DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames) + : BattleAnimation(owner), + counter(0), + howMany(howManyFrames) +{ + logAnim->debug("Created dummy animation for %d frames", howManyFrames); +} + +bool DummyAnimation::init() +{ + return true; +} + +void DummyAnimation::tick(uint32_t msPassed) +{ + counter++; + if(counter > howMany) + delete this; +} + +ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_UP; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_UP + }); +} + +ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_FRONT; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::ATTACK_FRONT + }); +} + +ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_DOWN; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_DOWN + }); +} + +ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack) +{ + const ECreatureAnimType mutPosToGroup[] = + { + getUpwardsGroup (multiAttack), + getUpwardsGroup (multiAttack), + getForwardGroup (multiAttack), + getDownwardsGroup(multiAttack), + getDownwardsGroup(multiAttack), + getForwardGroup (multiAttack) + }; + + int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1); + + int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); + if(mutPos == -1 && attackingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition()); + } + if (mutPos == -1 && defendingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex()); + } + if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex()); + } + + assert(mutPos >= 0 && mutPos <=5); + + return mutPosToGroup[mutPos]; +} + +void MeleeAttackAnimation::tick(uint32_t msPassed) +{ + size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame(); + size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + + if ( currentFrame * 2 >= totalFrames ) + owner.executeAnimationStage(EAnimationEvents::HIT); + + AttackAnimation::tick(msPassed); +} + +MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack) + : AttackAnimation(owner, attacker, _dest, _attacked) +{ + logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); + setSound(getCreature()->sounds.attack); + setGroup(selectGroup(multiAttack)); +} + +StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex): + BattleStackAnimation(owner, _stack), + prevHex(prevHex), + nextHex(nextHex) +{ +} + +bool MovementAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0); + + if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0) + { + //no movement, end immediately + delete this; + return false; + } + + logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex); + + //reverse unit if necessary + if(owner.stacksController->shouldRotate(stack, prevHex, nextHex)) + { + // it seems that H3 does NOT plays full rotation animation during movement + // Logical since it takes quite a lot of time + rotateStack(prevHex); + } + + if(myAnim->getType() != ECreatureAnimType::MOVING) + { + myAnim->setType(ECreatureAnimType::MOVING); + } + + if (moveSoundHander == -1) + { + moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); + } + + Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); + Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); + + progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); + + begX = begPosition.x; + begY = begPosition.y; + //progress = 0; + distanceX = endPosition.x - begPosition.x; + distanceY = endPosition.y - begPosition.y; + + if (stack->hasBonus(Selector::type()(BonusType::FLYING))) + { + float distance = static_cast(sqrt(distanceX * distanceX + distanceY * distanceY)); + progressPerSecond = AnimationControls::getFlightDistance(stack->unitType()) / distance; + } + + return true; +} + +void MovementAnimation::tick(uint32_t msPassed) +{ + progress += float(msPassed) / 1000 * progressPerSecond; + + //moving instructions + myAnim->pos.x = begX + distanceX * progress; + myAnim->pos.y = begY + distanceY * progress; + + BattleAnimation::tick(msPassed); + + if(progress >= 1.0) + { + progress -= 1.0; + // Sets the position of the creature animation sprites + Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack); + myAnim->pos.moveTo(coords); + + // true if creature haven't reached the final destination hex + if ((curentMoveIndex + 1) < destTiles.size()) + { + // update the next hex field which has to be reached by the stack + curentMoveIndex++; + prevHex = nextHex; + nextHex = destTiles[curentMoveIndex]; + + // request re-initialization + initialized = false; + } + else + delete this; + } +} + +MovementAnimation::~MovementAnimation() +{ + assert(stack); + + if(moveSoundHander != -1) + CCS->soundh->stopSound(moveSoundHander); +} + +MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) + : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), + destTiles(_destTiles), + curentMoveIndex(0), + begX(0), begY(0), + distanceX(0), distanceY(0), + progressPerSecond(0.0), + moveSoundHander(-1), + progress(0.0) +{ + logAnim->debug("Created MovementAnimation for %s", stack->getName()); +} + +MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) +: StackMoveAnimation(owner, _stack, destTile, destTile) +{ + logAnim->debug("Created MovementEndAnimation for %s", stack->getName()); +} + +bool MovementEndAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + + if(!stack || myAnim->isDeadOrDying()) + { + delete this; + return false; + } + + logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); + myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); + + CCS->soundh->playSound(stack->unitType()->sounds.endMoving); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) + { + delete this; + return false; + } + + + myAnim->setType(ECreatureAnimType::MOVE_END); + myAnim->onAnimationReset += [&](){ delete this; }; + + return true; +} + +MovementEndAnimation::~MovementEndAnimation() +{ + if(myAnim->getType() != ECreatureAnimType::DEAD) + myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default + + CCS->curh->show(); +} + +MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack) + : StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition()) +{ + logAnim->debug("Created MovementStartAnimation for %s", stack->getName()); +} + +bool MovementStartAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + + if(!stack || myAnim->isDeadOrDying()) + { + delete this; + return false; + } + + logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); + CCS->soundh->playSound(stack->unitType()->sounds.startMoving); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) + { + delete this; + return false; + } + + myAnim->setType(ECreatureAnimType::MOVE_START); + myAnim->onAnimationReset += [&](){ delete this; }; + return true; +} + +ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest) + : StackMoveAnimation(owner, stack, dest, dest) +{ + logAnim->debug("Created ReverseAnimation for %s", stack->getName()); +} + +bool ReverseAnimation::init() +{ + assert(myAnim); + assert(!myAnim->isDeadOrDying()); + + if(myAnim == nullptr || myAnim->isDeadOrDying()) + { + delete this; + return false; //there is no such creature + } + + logAnim->debug("CReverseAnimation::init: stack %s", stack->getName()); + if(myAnim->framesInGroup(ECreatureAnimType::TURN_L)) + { + myAnim->playOnce(ECreatureAnimType::TURN_L); + myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this); + } + else + { + setupSecondPart(); + } + return true; +} + +void BattleStackAnimation::rotateStack(BattleHex hex) +{ + setStackFacingRight(stack, !stackFacingRight(stack)); + + stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack)); +} + +void ReverseAnimation::setupSecondPart() +{ + assert(stack); + + if(!stack) + { + delete this; + return; + } + + rotateStack(nextHex); + + if(myAnim->framesInGroup(ECreatureAnimType::TURN_R)) + { + myAnim->playOnce(ECreatureAnimType::TURN_R); + myAnim->onAnimationReset += [&](){ delete this; }; + } + else + delete this; +} + +ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack): + StackActionAnimation(owner, _stack) +{ + setGroup(ECreatureAnimType::RESURRECTION); + logAnim->debug("Created ResurrectionAnimation for %s", stack->getName()); +} + +bool ColorTransformAnimation::init() +{ + return true; +} + +void ColorTransformAnimation::tick(uint32_t msPassed) +{ + float elapsed = msPassed / 1000.f; + float fullTime = AnimationControls::getFadeInDuration(); + float delta = elapsed / fullTime; + totalProgress += delta; + + size_t index = 0; + + while (index < timePoints.size() && timePoints[index] < totalProgress ) + ++index; + + if (index == timePoints.size()) + { + //end of animation. Apply ColorShifter using final values and die + const auto & shifter = steps[index - 1]; + owner.stacksController->setStackColorFilter(shifter, stack, spell, false); + delete this; + return; + } + + assert(index != 0); + + const auto & prevShifter = steps[index - 1]; + const auto & nextShifter = steps[index]; + + float prevPoint = timePoints[index-1]; + float nextPoint = timePoints[index]; + float localProgress = totalProgress - prevPoint; + float stepDuration = (nextPoint - prevPoint); + float factor = localProgress / stepDuration; + + auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor); + + owner.stacksController->setStackColorFilter(shifter, stack, spell, true); +} + +ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell): + BattleStackAnimation(owner, _stack), + spell(spell), + totalProgress(0.f) +{ + auto effect = owner.effectsController->getMuxerEffect(colorFilterName); + steps = effect.filters; + timePoints = effect.timePoints; + + assert(!steps.empty() && steps.size() == timePoints.size()); + + logAnim->debug("Created ColorTransformAnimation for %s", stack->getName()); +} + +RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) + : AttackAnimation(owner_, attacker, dest_, defender), + projectileEmitted(false) +{ + setSound(getCreature()->sounds.shoot); +} + +bool RangedAttackAnimation::init() +{ + setAnimationGroup(); + initializeProjectile(); + + return AttackAnimation::init(); +} + +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) + static const double straightAngle = 0.2; + + double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); + + // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. + if (projectileAngle > straightAngle) + setGroup(getUpwardsGroup()); + else if (projectileAngle < -straightAngle) + setGroup(getDownwardsGroup()); + else + setGroup(getForwardGroup()); +} + +void RangedAttackAnimation::initializeProjectile() +{ + const CCreature *shooterInfo = getCreature(); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225); + Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); + int multiplier = stackFacingRight(attackingStack) ? 1 : -1; + + if (getGroup() == getUpwardsGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; + } + else if (getGroup() == getDownwardsGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; + } + else if (getGroup() == getForwardGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; + } + else + { + assert(0); + } + + createProjectile(shotOrigin, shotTarget); +} + +void RangedAttackAnimation::emitProjectile() +{ + logAnim->debug("Ranged attack projectile emitted"); + owner.projectilesController->emitStackProjectile(attackingStack); + projectileEmitted = true; +} + +void RangedAttackAnimation::tick(uint32_t msPassed) +{ + // animation should be paused if there is an active projectile + if (projectileEmitted) + { + if (!owner.projectilesController->hasActiveProjectile(attackingStack, false)) + owner.executeAnimationStage(EAnimationEvents::HIT); + + } + + bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true); + + if (!projectileEmitted || stackHasProjectile) + stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame()); + else + stackAnimation(attackingStack)->playUntil(static_cast(-1)); + + AttackAnimation::tick(msPassed); + + if (!projectileEmitted) + { + // emit projectile once animation playback reached "climax" frame + if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() ) + { + emitProjectile(); + return; + } + } +} + +RangedAttackAnimation::~RangedAttackAnimation() +{ + assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false)); + assert(projectileEmitted); + + // FIXME: is this possible? Animation is over but we're yet to fire projectile? + if (!projectileEmitted) + { + logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now..."); + emitProjectile(); + } +} + +ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) + : RangedAttackAnimation(owner, attacker, _dest, _attacked) +{ + logAnim->debug("Created ShootingAnimation for %s", stack->getName()); +} + +void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const +{ + owner.projectilesController->createProjectile(attackingStack, from, dest); +} + +uint32_t ShootingAnimation::getAttackClimaxFrame() const +{ + const CCreature *shooterInfo = getCreature(); + + uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame; + uint32_t selectedFrame = std::clamp(shooterInfo->animation.attackClimaxFrame, 1, maxFrames); + + if (climaxFrame != selectedFrame) + logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames); + + return selectedFrame - 1; // H3 counts frames from 1 +} + +ECreatureAnimType ShootingAnimation::getUpwardsGroup() const +{ + return ECreatureAnimType::SHOOT_UP; +} + +ECreatureAnimType ShootingAnimation::getForwardGroup() const +{ + return ECreatureAnimType::SHOOT_FRONT; +} + +ECreatureAnimType ShootingAnimation::getDownwardsGroup() const +{ + return ECreatureAnimType::SHOOT_DOWN; +} + +CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) + : ShootingAnimation(owner, attacker, _dest, _attacked), + catapultDamage(_catapultDmg), + explosionEmitted(false) +{ + logAnim->debug("Created shooting anim for %s", stack->getName()); +} + +void CatapultAnimation::tick(uint32_t msPassed) +{ + ShootingAnimation::tick(msPassed); + + if ( explosionEmitted) + return; + + if ( !projectileEmitted) + return; + + if (owner.projectilesController->hasActiveProjectile(attackingStack, false)) + return; + + explosionEmitted = true; + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); + + auto soundFilename = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS"); + AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK"); + + CCS->soundh->playSound( soundFilename ); + owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); +} + +void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const +{ + owner.projectilesController->createCatapultProjectile(attackingStack, from, dest); +} + +CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell) + : RangedAttackAnimation(owner_, attacker, dest, defender), + spell(spell) +{ + if(!dest.isValid()) + { + assert(spell->animationInfo.projectile.empty()); + + if (defender) + dest = defender->getPosition(); + else + dest = attacker->getPosition(); + } +} + +ECreatureAnimType CastAnimation::getUpwardsGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_UP, + ECreatureAnimType::ATTACK_UP + }); +} + +ECreatureAnimType CastAnimation::getForwardGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::SHOOT_FRONT, + ECreatureAnimType::ATTACK_FRONT + }); +} + +ECreatureAnimType CastAnimation::getDownwardsGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_DOWN, + ECreatureAnimType::ATTACK_DOWN + }); +} + +void CastAnimation::createProjectile(const Point & from, const Point & dest) const +{ + if (!spell->animationInfo.projectile.empty()) + owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell); +} + +uint32_t CastAnimation::getAttackClimaxFrame() const +{ + //TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks + uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + + return maxFrames / 2; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): + BattleAnimation(owner), + animation(GH.renderHandler().loadAnimation(animationName)), + effectFlags(effects), + effectFinished(false), + reversed(reversed) +{ + logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName()); +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + battlehexes = hex; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, 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) +{ + positions = pos; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, 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) +{ + assert(hex.isValid()); + battlehexes.push_back(hex); + positions.push_back(pos); +} + +bool EffectAnimation::init() +{ + animation->preload(); + + auto first = animation->getImage(0, 0, true); + if(!first) + { + delete this; + return false; + } + + for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i) + { + size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i; + + animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE)); + } + + if (screenFill()) + { + for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i) + for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j) + positions.push_back(Point( i * first->width(), j * first->height())); + } + + BattleEffect be; + be.effectID = ID; + be.animation = animation; + be.currentFrame = 0; + be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT; + + for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i) + { + bool hasTile = i < battlehexes.size(); + bool hasPosition = i < positions.size(); + + if (hasTile && !forceOnTop()) + be.tile = battlehexes[i]; + else + be.tile = BattleHex::INVALID; + + if (hasPosition) + { + be.pos.x = positions[i].x; + be.pos.y = positions[i].y; + } + else + { + const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false); + Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); + + be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; + + if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. + be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; + + if (alignToBottom()) + be.pos.y = tilePos.y + tilePos.h - first->height(); + else + be.pos.y = tilePos.y - first->height()/2; + } + owner.effectsController->battleEffects.push_back(be); + } + return true; +} + +void EffectAnimation::tick(uint32_t msPassed) +{ + playEffect(msPassed); + + if (effectFinished) + { + //remove visual effect itself only if sound has finished as well - necessary for obstacles like force field + clearEffect(); + delete this; + } +} + +bool EffectAnimation::alignToBottom() const +{ + return effectFlags & ALIGN_TO_BOTTOM; +} + +bool EffectAnimation::forceOnTop() const +{ + return effectFlags & FORCE_ON_TOP; +} + +bool EffectAnimation::screenFill() const +{ + return effectFlags & SCREEN_FILL; +} + +void EffectAnimation::onEffectFinished() +{ + effectFinished = true; +} + +void EffectAnimation::playEffect(uint32_t msPassed) +{ + if ( effectFinished ) + return; + + for(auto & elem : owner.effectsController->battleEffects) + { + if(elem.effectID == ID) + { + elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + + if(elem.currentFrame >= elem.animation->size()) + { + elem.currentFrame = elem.animation->size() - 1; + onEffectFinished(); + break; + } + } + } +} + +void EffectAnimation::clearEffect() +{ + auto & effects = owner.effectsController->battleEffects; + + vstd::erase_if(effects, [&](const BattleEffect & effect){ + return effect.effectID == ID; + }); +} + +EffectAnimation::~EffectAnimation() +{ + assert(effectFinished); +} + +HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell): + BattleAnimation(owner), + projectileEmitted(false), + hero(hero), + target(defender), + tile(dest), + spell(spell) +{ +} + +bool HeroCastAnimation::init() +{ + hero->setPhase(EHeroAnimType::CAST_SPELL); + + hero->onPhaseFinished([&](){ + delete this; + }); + + initializeProjectile(); + + return true; +} + +void HeroCastAnimation::initializeProjectile() +{ + // spell has no projectile to play, ignore this step + if (spell->animationInfo.projectile.empty()) + return; + + // targeted spells should have well, target + assert(tile.isValid()); + + Point srccoord = hero->pos.center() - hero->parent->pos.topLeft(); + Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile + + destcoord += Point(222, 265); // FIXME: what are these constants? + owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); +} + +void HeroCastAnimation::emitProjectile() +{ + if (projectileEmitted) + return; + + //spell has no projectile to play, skip this step and immediately play hit animations + if (spell->animationInfo.projectile.empty()) + emitAnimationEvent(); + else + owner.projectilesController->emitStackProjectile( nullptr ); + + projectileEmitted = true; +} + +void HeroCastAnimation::emitAnimationEvent() +{ + owner.executeAnimationStage(EAnimationEvents::HIT); +} + +void HeroCastAnimation::tick(uint32_t msPassed) +{ + float frame = hero->getFrame(); + + if (frame < 4.0f) // middle point of animation //TODO: un-hardcode + return; + + if (!projectileEmitted) + { + emitProjectile(); + hero->pause(); + return; + } + + if (!owner.projectilesController->hasActiveProjectile(nullptr, false)) + { + emitAnimationEvent(); + //TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile + hero->play(); + } +} diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index 48e9fa48b..00b68bf7a 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -1,372 +1,372 @@ -/* - * BattleAnimations.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/battle/BattleHex.h" -#include "BattleConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CCreature; -class CSpell; -class Point; - -VCMI_LIB_NAMESPACE_END - -struct SDL_Color; -class ColorFilter; -class BattleHero; -class CAnimation; -class BattleInterface; -class CreatureAnimation; -struct StackAttackedInfo; - -/// Base class of battle animations -class BattleAnimation -{ -protected: - BattleInterface & owner; - bool initialized; - - std::vector & pendingAnimations(); - std::shared_ptr stackAnimation(const CStack * stack) const; - bool stackFacingRight(const CStack * stack); - void setStackFacingRight(const CStack * stack, bool facingRight); - - virtual bool init() = 0; //to be called - if returned false, call again until returns true - -public: - ui32 ID; //unique identifier - - bool isInitialized(); - bool tryInitialize(); - virtual void tick(uint32_t msPassed) {} //call every new frame - virtual ~BattleAnimation(); - - BattleAnimation(BattleInterface & owner); -}; - -/// Sub-class which is responsible for managing the battle stack animation. -class BattleStackAnimation : public BattleAnimation -{ -public: - std::shared_ptr myAnim; //animation for our stack, managed by BattleInterface - const CStack * stack; //id of stack whose animation it is - - BattleStackAnimation(BattleInterface & owner, const CStack * _stack); - void rotateStack(BattleHex hex); -}; - -class StackActionAnimation : public BattleStackAnimation -{ - ECreatureAnimType nextGroup; - ECreatureAnimType currGroup; - std::string sound; -public: - void setNextGroup( ECreatureAnimType group ); - void setGroup( ECreatureAnimType group ); - void setSound( std::string sound ); - - ECreatureAnimType getGroup() const; - - StackActionAnimation(BattleInterface & owner, const CStack * _stack); - ~StackActionAnimation(); - - bool init() override; -}; - -/// Animation of a defending unit -class DefenceAnimation : public StackActionAnimation -{ -public: - DefenceAnimation(BattleInterface & owner, const CStack * stack); -}; - -/// Animation of a hit unit -class HittedAnimation : public StackActionAnimation -{ -public: - HittedAnimation(BattleInterface & owner, const CStack * stack); -}; - -/// Animation of a dying unit -class DeathAnimation : public StackActionAnimation -{ -public: - DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged); -}; - -/// Resurrects stack from dead state -class ResurrectionAnimation : public StackActionAnimation -{ -public: - ResurrectionAnimation(BattleInterface & owner, const CStack * _stack); -}; - -class ColorTransformAnimation : public BattleStackAnimation -{ - std::vector steps; - std::vector timePoints; - const CSpell * spell; - - float totalProgress; - - bool init() override; - void tick(uint32_t msPassed) override; - -public: - ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell); -}; - -/// Base class for all animations that play during stack movement -class StackMoveAnimation : public BattleStackAnimation -{ -public: - BattleHex nextHex; - BattleHex prevHex; - -protected: - StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex); -}; - -/// Move animation of a creature -class MovementAnimation : public StackMoveAnimation -{ -private: - int moveSoundHander; // sound handler used when moving a unit - - std::vector destTiles; //full path, includes already passed hexes - ui32 curentMoveIndex; // index of nextHex in destTiles - - double begX, begY; // starting position - double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft - - /// progress gain per second - double progressPerSecond; - - /// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends - double progress; - -public: - bool init() override; - void tick(uint32_t msPassed) override; - - MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); - ~MovementAnimation(); -}; - -/// Move end animation of a creature -class MovementEndAnimation : public StackMoveAnimation -{ -public: - bool init() override; - - MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); - ~MovementEndAnimation(); -}; - -/// Move start animation of a creature -class MovementStartAnimation : public StackMoveAnimation -{ -public: - bool init() override; - - MovementStartAnimation(BattleInterface & owner, const CStack * _stack); -}; - -/// Class responsible for animation of stack chaning direction (left <-> right) -class ReverseAnimation : public StackMoveAnimation -{ - void setupSecondPart(); -public: - bool init() override; - - ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest); -}; - -/// This class is responsible for managing the battle attack animation -class AttackAnimation : public StackActionAnimation -{ -protected: - BattleHex dest; //attacked hex - const CStack *defendingStack; - const CStack *attackingStack; - int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature - - const CCreature * getCreature() const; - ECreatureAnimType findValidGroup( const std::vector candidates ) const; - -public: - AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); -}; - -/// Hand-to-hand attack -class MeleeAttackAnimation : public AttackAnimation -{ - ECreatureAnimType getUpwardsGroup(bool multiAttack) const; - ECreatureAnimType getForwardGroup(bool multiAttack) const; - ECreatureAnimType getDownwardsGroup(bool multiAttack) const; - - ECreatureAnimType selectGroup(bool multiAttack); - -public: - MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack); - - void tick(uint32_t msPassed) override; -}; - - -class RangedAttackAnimation : public AttackAnimation -{ - void setAnimationGroup(); - void initializeProjectile(); - void emitProjectile(); - void emitExplosion(); - -protected: - bool projectileEmitted; - - virtual ECreatureAnimType getUpwardsGroup() const = 0; - virtual ECreatureAnimType getForwardGroup() const = 0; - virtual ECreatureAnimType getDownwardsGroup() const = 0; - - virtual void createProjectile(const Point & from, const Point & dest) const = 0; - virtual uint32_t getAttackClimaxFrame() const = 0; - -public: - RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); - ~RangedAttackAnimation(); - - bool init() override; - void tick(uint32_t msPassed) override; -}; - -/// Shooting attack -class ShootingAnimation : public RangedAttackAnimation -{ - ECreatureAnimType getUpwardsGroup() const override; - ECreatureAnimType getForwardGroup() const override; - ECreatureAnimType getDownwardsGroup() const override; - - void createProjectile(const Point & from, const Point & dest) const override; - uint32_t getAttackClimaxFrame() const override; - -public: - ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); - -}; - -/// Catapult attack -class CatapultAnimation : public ShootingAnimation -{ -private: - bool explosionEmitted; - int catapultDamage; - -public: - CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); - - void createProjectile(const Point & from, const Point & dest) const override; - void tick(uint32_t msPassed) override; -}; - -class CastAnimation : public RangedAttackAnimation -{ - const CSpell * spell; - - ECreatureAnimType getUpwardsGroup() const override; - ECreatureAnimType getForwardGroup() const override; - ECreatureAnimType getDownwardsGroup() const override; - - void createProjectile(const Point & from, const Point & dest) const override; - uint32_t getAttackClimaxFrame() const override; - -public: - CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); -}; - -class DummyAnimation : public BattleAnimation -{ -private: - int counter; - int howMany; -public: - bool init() override; - void tick(uint32_t msPassed) override; - - DummyAnimation(BattleInterface & owner, int howManyFrames); -}; - -/// Class that plays effect at one or more positions along with (single) sound effect -class EffectAnimation : public BattleAnimation -{ - std::string soundName; - bool effectFinished; - bool reversed; - int effectFlags; - - std::shared_ptr animation; - std::vector positions; - std::vector battlehexes; - - bool alignToBottom() const; - bool waitForSound() const; - bool forceOnTop() const; - bool screenFill() const; - - void onEffectFinished(); - void clearEffect(); - void playEffect(uint32_t msPassed); - -public: - enum EEffectFlags - { - ALIGN_TO_BOTTOM = 1, - FORCE_ON_TOP = 2, - SCREEN_FILL = 4, - }; - - /// Create animation with screen-wide effect - EffectAnimation(BattleInterface & owner, std::string animationName, int effects = 0, bool reversed = false); - - /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset - EffectAnimation(BattleInterface & owner, std::string animationName, Point pos , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos , int effects = 0, bool reversed = false); - - /// Create animation positioned at certain hex(es) - EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects = 0, bool reversed = false); - - EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); - ~EffectAnimation(); - - bool init() override; - void tick(uint32_t msPassed) override; -}; - -class HeroCastAnimation : public BattleAnimation -{ - std::shared_ptr hero; - const CStack * target; - const CSpell * spell; - BattleHex tile; - bool projectileEmitted; - - void initializeProjectile(); - void emitProjectile(); - void emitAnimationEvent(); - -public: - HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell); - - void tick(uint32_t msPassed) override; - bool init() override; -}; +/* + * BattleAnimations.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/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "BattleConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CCreature; +class CSpell; +class Point; + +VCMI_LIB_NAMESPACE_END + +class ColorFilter; +class BattleHero; +class CAnimation; +class BattleInterface; +class CreatureAnimation; +struct StackAttackedInfo; + +/// Base class of battle animations +class BattleAnimation +{ +protected: + BattleInterface & owner; + bool initialized; + + std::vector & pendingAnimations(); + std::shared_ptr stackAnimation(const CStack * stack) const; + bool stackFacingRight(const CStack * stack); + void setStackFacingRight(const CStack * stack, bool facingRight); + + virtual bool init() = 0; //to be called - if returned false, call again until returns true + +public: + ui32 ID; //unique identifier + + bool isInitialized(); + bool tryInitialize(); + virtual void tick(uint32_t msPassed) {} //call every new frame + virtual ~BattleAnimation(); + + BattleAnimation(BattleInterface & owner); +}; + +/// Sub-class which is responsible for managing the battle stack animation. +class BattleStackAnimation : public BattleAnimation +{ +public: + std::shared_ptr myAnim; //animation for our stack, managed by BattleInterface + const CStack * stack; //id of stack whose animation it is + + BattleStackAnimation(BattleInterface & owner, const CStack * _stack); + void rotateStack(BattleHex hex); +}; + +class StackActionAnimation : public BattleStackAnimation +{ + ECreatureAnimType nextGroup; + ECreatureAnimType currGroup; + AudioPath sound; +public: + void setNextGroup( ECreatureAnimType group ); + void setGroup( ECreatureAnimType group ); + void setSound( const AudioPath & sound ); + + ECreatureAnimType getGroup() const; + + StackActionAnimation(BattleInterface & owner, const CStack * _stack); + ~StackActionAnimation(); + + bool init() override; +}; + +/// Animation of a defending unit +class DefenceAnimation : public StackActionAnimation +{ +public: + DefenceAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a hit unit +class HittedAnimation : public StackActionAnimation +{ +public: + HittedAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a dying unit +class DeathAnimation : public StackActionAnimation +{ +public: + DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged); +}; + +/// Resurrects stack from dead state +class ResurrectionAnimation : public StackActionAnimation +{ +public: + ResurrectionAnimation(BattleInterface & owner, const CStack * _stack); +}; + +class ColorTransformAnimation : public BattleStackAnimation +{ + std::vector steps; + std::vector timePoints; + const CSpell * spell; + + float totalProgress; + + bool init() override; + void tick(uint32_t msPassed) override; + +public: + ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell); +}; + +/// Base class for all animations that play during stack movement +class StackMoveAnimation : public BattleStackAnimation +{ +public: + BattleHex nextHex; + BattleHex prevHex; + +protected: + StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex); +}; + +/// Move animation of a creature +class MovementAnimation : public StackMoveAnimation +{ +private: + int moveSoundHander; // sound handler used when moving a unit + + std::vector destTiles; //full path, includes already passed hexes + ui32 curentMoveIndex; // index of nextHex in destTiles + + double begX, begY; // starting position + double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft + + /// progress gain per second + double progressPerSecond; + + /// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends + double progress; + +public: + bool init() override; + void tick(uint32_t msPassed) override; + + MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); + ~MovementAnimation(); +}; + +/// Move end animation of a creature +class MovementEndAnimation : public StackMoveAnimation +{ +public: + bool init() override; + + MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); + ~MovementEndAnimation(); +}; + +/// Move start animation of a creature +class MovementStartAnimation : public StackMoveAnimation +{ +public: + bool init() override; + + MovementStartAnimation(BattleInterface & owner, const CStack * _stack); +}; + +/// Class responsible for animation of stack chaning direction (left <-> right) +class ReverseAnimation : public StackMoveAnimation +{ + void setupSecondPart(); +public: + bool init() override; + + ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest); +}; + +/// This class is responsible for managing the battle attack animation +class AttackAnimation : public StackActionAnimation +{ +protected: + BattleHex dest; //attacked hex + const CStack *defendingStack; + const CStack *attackingStack; + int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature + + const CCreature * getCreature() const; + ECreatureAnimType findValidGroup( const std::vector candidates ) const; + +public: + AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); +}; + +/// Hand-to-hand attack +class MeleeAttackAnimation : public AttackAnimation +{ + ECreatureAnimType getUpwardsGroup(bool multiAttack) const; + ECreatureAnimType getForwardGroup(bool multiAttack) const; + ECreatureAnimType getDownwardsGroup(bool multiAttack) const; + + ECreatureAnimType selectGroup(bool multiAttack); + +public: + MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack); + + void tick(uint32_t msPassed) override; +}; + + +class RangedAttackAnimation : public AttackAnimation +{ + void setAnimationGroup(); + void initializeProjectile(); + void emitProjectile(); + void emitExplosion(); + +protected: + bool projectileEmitted; + + virtual ECreatureAnimType getUpwardsGroup() const = 0; + virtual ECreatureAnimType getForwardGroup() const = 0; + virtual ECreatureAnimType getDownwardsGroup() const = 0; + + virtual void createProjectile(const Point & from, const Point & dest) const = 0; + virtual uint32_t getAttackClimaxFrame() const = 0; + +public: + RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + ~RangedAttackAnimation(); + + bool init() override; + void tick(uint32_t msPassed) override; +}; + +/// Shooting attack +class ShootingAnimation : public RangedAttackAnimation +{ + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; + + void createProjectile(const Point & from, const Point & dest) const override; + uint32_t getAttackClimaxFrame() const override; + +public: + ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + +}; + +/// Catapult attack +class CatapultAnimation : public ShootingAnimation +{ +private: + bool explosionEmitted; + int catapultDamage; + +public: + CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); + + void createProjectile(const Point & from, const Point & dest) const override; + void tick(uint32_t msPassed) override; +}; + +class CastAnimation : public RangedAttackAnimation +{ + const CSpell * spell; + + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; + + void createProjectile(const Point & from, const Point & dest) const override; + uint32_t getAttackClimaxFrame() const override; + +public: + CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); +}; + +class DummyAnimation : public BattleAnimation +{ +private: + int counter; + int howMany; +public: + bool init() override; + void tick(uint32_t msPassed) override; + + DummyAnimation(BattleInterface & owner, int howManyFrames); +}; + +/// Class that plays effect at one or more positions along with (single) sound effect +class EffectAnimation : public BattleAnimation +{ + std::string soundName; + bool effectFinished; + bool reversed; + int effectFlags; + + std::shared_ptr animation; + std::vector positions; + std::vector battlehexes; + + bool alignToBottom() const; + bool waitForSound() const; + bool forceOnTop() const; + bool screenFill() const; + + void onEffectFinished(); + void clearEffect(); + void playEffect(uint32_t msPassed); + +public: + enum EEffectFlags + { + ALIGN_TO_BOTTOM = 1, + FORCE_ON_TOP = 2, + SCREEN_FILL = 4, + }; + + /// Create animation with screen-wide effect + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, 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, 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); + ~EffectAnimation(); + + bool init() override; + void tick(uint32_t msPassed) override; +}; + +class HeroCastAnimation : public BattleAnimation +{ + std::shared_ptr hero; + const CStack * target; + const CSpell * spell; + BattleHex tile; + bool projectileEmitted; + + void initializeProjectile(); + void emitProjectile(); + void emitAnimationEvent(); + +public: + HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell); + + void tick(uint32_t msPassed) override; + bool init() override; +}; diff --git a/client/battle/BattleConstants.h b/client/battle/BattleConstants.h index fb6bb4baa..580e864bd 100644 --- a/client/battle/BattleConstants.h +++ b/client/battle/BattleConstants.h @@ -1,100 +1,100 @@ -/* - * BattleConstants.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 - -enum class EBattleEffect -{ - // list of battle effects that have hardcoded triggers - MAGIC_MIRROR = 3, - FIRE_SHIELD = 11, - FEAR = 15, - GOOD_LUCK = 18, - GOOD_MORALE = 20, - BAD_MORALE = 30, - BAD_LUCK = 48, - RESURRECT = 50, - DRAIN_LIFE = 52, - POISON = 67, - DEATH_BLOW = 73, - REGENERATION = 74, - MANA_DRAIN = 77, - RESISTANCE = 78, - - INVALID = -1, -}; - -enum class EAnimationEvents -{ - // any action - ROTATE, // stacks rotate before action - - // movement action - MOVE_START, // stack starts movement - MOVEMENT, // movement animation loop starts - MOVE_END, // stack end movement - - // attack/spellcast action - BEFORE_HIT, // attack and defence effects play, e.g. luck/death blow - ATTACK, // attack and defence animations are playing - HIT, // hit & death animations are playing - AFTER_HIT, // post-attack effect, e.g. phoenix rebirth - - COUNT -}; - -enum class EHeroAnimType -{ - HOLDING = 0, - IDLE = 1, // idling movement that happens from time to time - DEFEAT = 2, // played when army loses stack or on friendly fire - VICTORY = 3, // when enemy stack killed or huge damage is dealt - CAST_SPELL = 4 // spellcasting -}; - -enum class ECreatureAnimType -{ - INVALID = -1, - - MOVING = 0, - MOUSEON = 1, - HOLDING = 2, // base idling animation - HITTED = 3, // base animation for when stack is taking damage - DEFENCE = 4, // alternative animation for defending in melee if stack spent its action on defending - DEATH = 5, - DEATH_RANGED = 6, // Optional, alternative animation for when stack is killed by ranged attack - TURN_L = 7, - TURN_R = 8, - //TURN_L2 = 9, //unused - identical to TURN_L - //TURN_R2 = 10, //unused - identical to TURN_R - ATTACK_UP = 11, - ATTACK_FRONT = 12, - ATTACK_DOWN = 13, - SHOOT_UP = 14, // Shooters only - SHOOT_FRONT = 15, // Shooters only - SHOOT_DOWN = 16, // Shooters only - SPECIAL_UP = 17, // If empty, fallback to SPECIAL_FRONT - SPECIAL_FRONT = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability - SPECIAL_DOWN = 19, // If empty, fallback to SPECIAL_FRONT - MOVE_START = 20, // small animation to be played before MOVING - MOVE_END = 21, // small animation to be played after MOVING - - DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here - DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here - RESURRECTION = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here - FROZEN = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation - - CAST_UP = 30, - CAST_FRONT = 31, - CAST_DOWN = 32, - - GROUP_ATTACK_UP = 40, - GROUP_ATTACK_FRONT = 41, - GROUP_ATTACK_DOWN = 42 -}; +/* + * BattleConstants.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 + +enum class EBattleEffect +{ + // list of battle effects that have hardcoded triggers + MAGIC_MIRROR = 3, + FIRE_SHIELD = 11, + FEAR = 15, + GOOD_LUCK = 18, + GOOD_MORALE = 20, + BAD_MORALE = 30, + BAD_LUCK = 48, + RESURRECT = 50, + DRAIN_LIFE = 52, + POISON = 67, + DEATH_BLOW = 73, + REGENERATION = 74, + MANA_DRAIN = 77, + RESISTANCE = 78, + + INVALID = -1, +}; + +enum class EAnimationEvents +{ + // any action + ROTATE, // stacks rotate before action + + // movement action + MOVE_START, // stack starts movement + MOVEMENT, // movement animation loop starts + MOVE_END, // stack end movement + + // attack/spellcast action + BEFORE_HIT, // attack and defence effects play, e.g. luck/death blow + ATTACK, // attack and defence animations are playing + HIT, // hit & death animations are playing + AFTER_HIT, // post-attack effect, e.g. phoenix rebirth + + COUNT +}; + +enum class EHeroAnimType +{ + HOLDING = 0, + IDLE = 1, // idling movement that happens from time to time + DEFEAT = 2, // played when army loses stack or on friendly fire + VICTORY = 3, // when enemy stack killed or huge damage is dealt + CAST_SPELL = 4 // spellcasting +}; + +enum class ECreatureAnimType +{ + INVALID = -1, + + MOVING = 0, + MOUSEON = 1, + HOLDING = 2, // base idling animation + HITTED = 3, // base animation for when stack is taking damage + DEFENCE = 4, // alternative animation for defending in melee if stack spent its action on defending + DEATH = 5, + DEATH_RANGED = 6, // Optional, alternative animation for when stack is killed by ranged attack + TURN_L = 7, + TURN_R = 8, + //TURN_L2 = 9, //unused - identical to TURN_L + //TURN_R2 = 10, //unused - identical to TURN_R + ATTACK_UP = 11, + ATTACK_FRONT = 12, + ATTACK_DOWN = 13, + SHOOT_UP = 14, // Shooters only + SHOOT_FRONT = 15, // Shooters only + SHOOT_DOWN = 16, // Shooters only + SPECIAL_UP = 17, // If empty, fallback to SPECIAL_FRONT + SPECIAL_FRONT = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability + SPECIAL_DOWN = 19, // If empty, fallback to SPECIAL_FRONT + MOVE_START = 20, // small animation to be played before MOVING + MOVE_END = 21, // small animation to be played after MOVING + + DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here + DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here + RESURRECTION = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here + FROZEN = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation + + CAST_UP = 30, + CAST_FRONT = 31, + CAST_DOWN = 32, + + GROUP_ATTACK_UP = 40, + GROUP_ATTACK_FRONT = 41, + GROUP_ATTACK_DOWN = 42 +}; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index 1477234c5..f1db77c16 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -1,159 +1,160 @@ -/* - * BattleEffectsController.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 "BattleEffectsController.h" - -#include "BattleAnimationClasses.h" -#include "BattleWindow.h" -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#include "BattleRenderer.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/CAnimation.h" - -#include "../../CCallback.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/filesystem/ResourceID.h" -#include "../../lib/NetPacks.h" -#include "../../lib/CStack.h" -#include "../../lib/IGameEventsReceiver.h" -#include "../../lib/CGeneralTextHandler.h" - -BattleEffectsController::BattleEffectsController(BattleInterface & owner): - owner(owner) -{ - loadColorMuxers(); -} - -void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) -{ - displayEffect(effect, "", destTile); -} - -void BattleEffectsController::displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile) -{ - size_t effectID = static_cast(effect); - - std::string customAnim = graphics->battleACToDef[effectID][0]; - - CCS->soundh->playSound( soundFile ); - - owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); -} - -void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) -{ - owner.checkForAnimations(); - - const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID); - if(!stack) - { - logGlobal->error("Invalid stack ID %d", bte.stackID); - return; - } - //don't show animation when no HP is regenerated - switch(static_cast(bte.effect)) - { - case BonusType::HP_REGENERATION: - displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition()); - break; - case BonusType::MANA_DRAIN: - displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition()); - break; - case BonusType::POISON: - displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition()); - break; - case BonusType::FEAR: - displayEffect(EBattleEffect::FEAR, "FEAR", stack->getPosition()); - break; - case BonusType::MORALE: - { - std::string hlp = CGI->generaltexth->allTexts[33]; - boost::algorithm::replace_first(hlp,"%s",(stack->getName())); - displayEffect(EBattleEffect::GOOD_MORALE, "GOODMRLE", stack->getPosition()); - owner.appendBattleLog(hlp); - break; - } - default: - return; - } - owner.waitForAnimations(); -} - -void BattleEffectsController::startAction(const BattleAction* action) -{ - owner.checkForAnimations(); - - const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber); - - switch(action->actionType) - { - case EActionType::WAIT: - owner.appendBattleLog(stack->formatGeneralMessage(136)); - break; - case EActionType::BAD_MORALE: - owner.appendBattleLog(stack->formatGeneralMessage(-34)); - displayEffect(EBattleEffect::BAD_MORALE, "BADMRLE", stack->getPosition()); - break; - } - - owner.waitForAnimations(); -} - -void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (auto & elem : battleEffects) - { - renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas) - { - int currentFrame = static_cast(floor(elem.currentFrame)); - currentFrame %= elem.animation->size(); - - auto img = elem.animation->getImage(currentFrame, static_cast(elem.type)); - - canvas.draw(img, elem.pos); - }); - } -} - -void BattleEffectsController::loadColorMuxers() -{ - const JsonNode config(ResourceID("config/battleEffects.json")); - - for(auto & muxer : config["colorMuxers"].Struct()) - { - ColorMuxerEffect effect; - std::string identifier = muxer.first; - - for (const JsonNode & entry : muxer.second.Vector() ) - { - effect.timePoints.push_back(entry["time"].Float()); - effect.filters.push_back(ColorFilter::genFromJson(entry)); - } - colorMuxerEffects[identifier] = effect; - } -} - -const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name) -{ - static const ColorMuxerEffect emptyEffect; - - if (colorMuxerEffects.count(name)) - return colorMuxerEffects[name]; - - logAnim->error("Failed to find color muxer effect named '%s'!", name); - return emptyEffect; -} +/* + * BattleEffectsController.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 "BattleEffectsController.h" + +#include "BattleAnimationClasses.h" +#include "BattleWindow.h" +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#include "BattleRenderer.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../render/CAnimation.h" +#include "../render/Graphics.h" + +#include "../../CCallback.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/CStack.h" +#include "../../lib/IGameEventsReceiver.h" +#include "../../lib/CGeneralTextHandler.h" + +BattleEffectsController::BattleEffectsController(BattleInterface & owner): + owner(owner) +{ + loadColorMuxers(); +} + +void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) +{ + displayEffect(effect, AudioPath(), destTile); +} + +void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile) +{ + size_t effectID = static_cast(effect); + + AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]); + + CCS->soundh->playSound( soundFile ); + + owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); +} + +void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) +{ + owner.checkForAnimations(); + + const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID); + if(!stack) + { + logGlobal->error("Invalid stack ID %d", bte.stackID); + return; + } + //don't show animation when no HP is regenerated + switch(static_cast(bte.effect)) + { + case BonusType::HP_REGENERATION: + displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition()); + break; + case BonusType::MANA_DRAIN: + displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition()); + break; + case BonusType::POISON: + displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition()); + break; + case BonusType::FEAR: + displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition()); + break; + case BonusType::MORALE: + { + std::string hlp = CGI->generaltexth->allTexts[33]; + boost::algorithm::replace_first(hlp,"%s",(stack->getName())); + displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition()); + owner.appendBattleLog(hlp); + break; + } + default: + return; + } + owner.waitForAnimations(); +} + +void BattleEffectsController::startAction(const BattleAction & action) +{ + owner.checkForAnimations(); + + const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber); + + switch(action.actionType) + { + case EActionType::WAIT: + owner.appendBattleLog(stack->formatGeneralMessage(136)); + break; + case EActionType::BAD_MORALE: + owner.appendBattleLog(stack->formatGeneralMessage(-34)); + displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition()); + break; + } + + owner.waitForAnimations(); +} + +void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (auto & elem : battleEffects) + { + renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas) + { + int currentFrame = static_cast(floor(elem.currentFrame)); + currentFrame %= elem.animation->size(); + + auto img = elem.animation->getImage(currentFrame, static_cast(elem.type)); + + canvas.draw(img, elem.pos); + }); + } +} + +void BattleEffectsController::loadColorMuxers() +{ + const JsonNode config(JsonPath::builtin("config/battleEffects.json")); + + for(auto & muxer : config["colorMuxers"].Struct()) + { + ColorMuxerEffect effect; + std::string identifier = muxer.first; + + for (const JsonNode & entry : muxer.second.Vector() ) + { + effect.timePoints.push_back(entry["time"].Float()); + effect.filters.push_back(ColorFilter::genFromJson(entry)); + } + colorMuxerEffects[identifier] = effect; + } +} + +const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name) +{ + static const ColorMuxerEffect emptyEffect; + + if (colorMuxerEffects.count(name)) + return colorMuxerEffects[name]; + + logAnim->error("Failed to find color muxer effect named '%s'!", name); + return emptyEffect; +} diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 574b85212..5e551901a 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -1,74 +1,75 @@ -/* - * BattleEffectsController.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/battle/BattleHex.h" -#include "../../lib/Point.h" -#include "BattleConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BattleAction; -struct BattleTriggerEffect; - -VCMI_LIB_NAMESPACE_END - -struct ColorMuxerEffect; -class CAnimation; -class Canvas; -class BattleInterface; -class BattleRenderer; -class EffectAnimation; - -/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... -struct BattleEffect -{ - enum class AnimType : ui8 - { - DEFAULT = 0, //If we have such animation - REVERSE = 1 //Reverse DEFAULT will be used - }; - - AnimType type; - Point pos; //position on the screen - float currentFrame; - 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 -}; - -/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale -class BattleEffectsController -{ - BattleInterface & owner; - - /// list of current effects that are being displayed on screen (spells & creature abilities) - std::vector battleEffects; - - std::map colorMuxerEffects; - - void loadColorMuxers(); -public: - const ColorMuxerEffect &getMuxerEffect(const std::string & name); - - BattleEffectsController(BattleInterface & owner); - - void startAction(const BattleAction* action); - - //displays custom effect on the battlefield - void displayEffect(EBattleEffect effect, const BattleHex & destTile); - void displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile); - - void battleTriggerEffect(const BattleTriggerEffect & bte); - - void collectRenderableObjects(BattleRenderer & renderer); - - friend class EffectAnimation; -}; +/* + * BattleEffectsController.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/battle/BattleHex.h" +#include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "BattleConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleAction; +struct BattleTriggerEffect; + +VCMI_LIB_NAMESPACE_END + +struct ColorMuxerEffect; +class CAnimation; +class Canvas; +class BattleInterface; +class BattleRenderer; +class EffectAnimation; + +/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... +struct BattleEffect +{ + enum class AnimType : ui8 + { + DEFAULT = 0, //If we have such animation + REVERSE = 1 //Reverse DEFAULT will be used + }; + + AnimType type; + Point pos; //position on the screen + float currentFrame; + 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 +}; + +/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale +class BattleEffectsController +{ + BattleInterface & owner; + + /// list of current effects that are being displayed on screen (spells & creature abilities) + std::vector battleEffects; + + std::map colorMuxerEffects; + + void loadColorMuxers(); +public: + const ColorMuxerEffect &getMuxerEffect(const std::string & name); + + BattleEffectsController(BattleInterface & owner); + + void startAction(const BattleAction & action); + + //displays custom effect on the battlefield + void displayEffect(EBattleEffect effect, const BattleHex & destTile); + void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile); + + void battleTriggerEffect(const BattleTriggerEffect & bte); + + void collectRenderableObjects(BattleRenderer & renderer); + + friend class EffectAnimation; +}; diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index cb239a3eb..b0ee095b5 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -1,918 +1,919 @@ -/* - * BattleFieldController.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 "BattleFieldController.h" - -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleInterfaceClasses.h" -#include "BattleEffectsController.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "BattleObstacleController.h" -#include "BattleProjectileController.h" -#include "BattleRenderer.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../adventureMap/CInGameConsole.h" -#include "../client/render/CAnimation.h" - -#include "../../CCallback.h" -#include "../../lib/BattleFieldHandler.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CStack.h" -#include "../../lib/spells/ISpellMechanics.h" - -namespace HexMasks -{ - // mask definitions that has set to 1 the edges present in the hex edges highlight image - /* - /\ - 0 1 - / \ - | | - 5 2 - | | - \ / - 4 3 - \/ - */ - enum HexEdgeMasks { - empty = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed - topLeft = 0b000001, - topRight = 0b000010, - right = 0b000100, - bottomRight = 0b001000, - bottomLeft = 0b010000, - left = 0b100000, - - top = 0b000011, - bottom = 0b011000, - topRightHalfCorner = 0b000110, - bottomRightHalfCorner = 0b001100, - bottomLeftHalfCorner = 0b110000, - topLeftHalfCorner = 0b100001, - - rightTopAndBottom = 0b001010, // special case, right half can be drawn instead of only top and bottom - leftTopAndBottom = 0b010001, // special case, left half can be drawn instead of only top and bottom - - rightHalf = 0b001110, - leftHalf = 0b110001, - - topRightCorner = 0b000111, - bottomRightCorner = 0b011100, - bottomLeftCorner = 0b111000, - topLeftCorner = 0b100011 - }; -} - -std::map hexEdgeMaskToFrameIndex; - -// Maps HexEdgesMask to "Frame" indexes for range highligt images -void initializeHexEdgeMaskToFrameIndex() -{ - 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; -} - -BattleFieldController::BattleFieldController(BattleInterface & owner): - owner(owner) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - //preparing cells and hexes - cellBorder = IImage::createFromFile("CCELLGRD.BMP", EImageBlitMode::COLORKEY); - cellShade = IImage::createFromFile("CCELLSHD.BMP"); - cellUnitMovementHighlight = IImage::createFromFile("UnitMovementHighlight.PNG", EImageBlitMode::COLORKEY); - cellUnitMaxMovementHighlight = IImage::createFromFile("UnitMaxMovementHighlight.PNG", EImageBlitMode::COLORKEY); - - attackCursors = std::make_shared("CRCOMBAT"); - attackCursors->preload(); - - spellCursors = std::make_shared("CRSPELL"); - spellCursors->preload(); - - initializeHexEdgeMaskToFrameIndex(); - - rangedFullDamageLimitImages = std::make_shared("battle/rangeHighlights/rangeHighlightsGreen.json"); - rangedFullDamageLimitImages->preload(); - - shootingRangeLimitImages = std::make_shared("battle/rangeHighlights/rangeHighlightsRed.json"); - shootingRangeLimitImages->preload(); - - flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); - flipRangeLimitImagesIntoPositions(shootingRangeLimitImages); - - if(!owner.siegeController) - { - auto bfieldType = owner.curInt->cb->battleGetBattlefieldType(); - - if(bfieldType == BattleField::NONE) - logGlobal->error("Invalid battlefield returned for current battle"); - else - background = IImage::createFromFile(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); - } - else - { - std::string backgroundName = owner.siegeController->getBattleBackgroundName(); - background = IImage::createFromFile(backgroundName, EImageBlitMode::OPAQUE); - } - - pos.w = background->width(); - pos.h = background->height(); - - backgroundWithHexes = std::make_unique(Point(background->width(), background->height())); - - updateAccessibleHexes(); - addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); -} - -void BattleFieldController::activate() -{ - LOCPLINT->cingconsole->pos = this->pos; - CIntObject::activate(); -} - -void BattleFieldController::createHeroes() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - // create heroes as part of our constructor for correct positioning inside battlefield - if(owner.attackingHeroInstance) - owner.attackingHero = std::make_shared(owner, owner.attackingHeroInstance, false); - - if(owner.defendingHeroInstance) - owner.defendingHero = std::make_shared(owner, owner.defendingHeroInstance, true); -} - -void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition) -{ - if (!on && pos.isInside(finalPosition)) - clickPressed(finalPosition); -} - -void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) -{ - Point distance = currentPosition - initialPosition; - - if (distance.length() < settings["battle"]["swipeAttackDistance"].Float()) - hoveredHex = getHexAtPosition(initialPosition); - else - hoveredHex = BattleHex::INVALID; - - currentAttackOriginPoint = currentPosition; - - if (pos.isInside(initialPosition)) - owner.actionsController->onHexHovered(getHoveredHex()); -} - -void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - hoveredHex = getHexAtPosition(cursorPosition); - currentAttackOriginPoint = cursorPosition; - - if (pos.isInside(cursorPosition)) - owner.actionsController->onHexHovered(getHoveredHex()); - else - owner.actionsController->onHoverEnded(); -} - -void BattleFieldController::clickPressed(const Point & cursorPosition) -{ - BattleHex selectedHex = getHoveredHex(); - - if (selectedHex != BattleHex::INVALID) - owner.actionsController->onHexLeftClicked(selectedHex); -} - -void BattleFieldController::showPopupWindow(const Point & cursorPosition) -{ - BattleHex selectedHex = getHoveredHex(); - - if (selectedHex != BattleHex::INVALID) - owner.actionsController->onHexRightClicked(selectedHex); -} - -void BattleFieldController::renderBattlefield(Canvas & canvas) -{ - Canvas clippedCanvas(canvas, pos); - - showBackground(clippedCanvas); - - BattleRenderer renderer(owner); - - renderer.execute(clippedCanvas); - - owner.projectilesController->render(clippedCanvas); -} - -void BattleFieldController::showBackground(Canvas & canvas) -{ - if (owner.stacksController->getActiveStack() != nullptr ) - showBackgroundImageWithHexes(canvas); - else - showBackgroundImage(canvas); - - showHighlightedHexes(canvas); -} - -void BattleFieldController::showBackgroundImage(Canvas & canvas) -{ - canvas.draw(background, Point(0, 0)); - - owner.obstacleController->showAbsoluteObstacles(canvas); - if ( owner.siegeController ) - owner.siegeController->showAbsoluteObstacles(canvas); - - if (settings["battle"]["cellBorders"].Bool()) - { - for (int i=0; igetActiveStack(); - std::vector attackableHexes; - if(activeStack) - occupiableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); - - // prepare background graphic with hexes and shaded hexes - backgroundWithHexes->draw(background, Point(0,0)); - owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes); - if(owner.siegeController) - owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes); - - // show shaded hexes for active's stack valid movement and the hexes that it can attack - if(settings["battle"]["stackRange"].Bool()) - { - std::vector hexesToShade = occupiableHexes; - hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end()); - for(BattleHex hex : hexesToShade) - { - showHighlightedHex(*backgroundWithHexes, cellShade, hex, false); - } - } - - // draw cell borders - if(settings["battle"]["cellBorders"].Bool()) - { - for(int i=0; idraw(cellBorder, hexPositionLocal(i).topLeft()); - } - } -} - -void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr highlight, BattleHex hex, bool darkBorder) -{ - Point hexPos = hexPositionLocal(hex).topLeft(); - - canvas.draw(highlight, hexPos); - if(!darkBorder && settings["battle"]["cellBorders"].Bool()) - canvas.draw(cellBorder, hexPos); -} - -std::set BattleFieldController::getHighlightedHexesForActiveStack() -{ - std::set result; - - if(!owner.stacksController->getActiveStack()) - return result; - - if(!settings["battle"]["stackRange"].Bool()) - return result; - - auto hoveredHex = getHoveredHex(); - - std::set set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); - for(BattleHex hex : set) - result.insert(hex); - - return result; -} - -std::set BattleFieldController::getMovementRangeForHoveredStack() -{ - std::set result; - - if (!owner.stacksController->getActiveStack()) - return result; - - if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) - return result; - - auto hoveredHex = getHoveredHex(); - - // add possible movement hexes for stack under mouse - const CStack * const hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - if(hoveredStack) - { - std::vector v = owner.curInt->cb->battleGetAvailableHexes(hoveredStack, true, true, nullptr); - for(BattleHex hex : v) - result.insert(hex); - } - return result; -} - -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; - - spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(hoveredHex); - caster = owner.actionsController->getCurrentSpellcaster(); - - if(caster && spell) //when casting spell - { - // printing shaded hex(es) - spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell); - auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex); - - for(BattleHex shadedHex : shadedHexes) - { - if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1)) - result.insert(shadedHex); - } - } - return result; -} - -std::set BattleFieldController::getHighlightedHexesForMovementTarget() -{ - const CStack * stack = owner.stacksController->getActiveStack(); - auto hoveredHex = getHoveredHex(); - - if(!stack) - return {}; - - std::vector availableHexes = owner.curInt->cb->battleGetAvailableHexes(stack, false, false, nullptr); - - auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex)) - { - if(isTileAttackable(hoveredHex)) - { - BattleHex attackFromHex = fromWhichHexAttack(hoveredHex); - - if(stack->doubleWide()) - return {attackFromHex, stack->occupiedHex(attackFromHex)}; - else - return {attackFromHex}; - } - } - - if(vstd::contains(availableHexes, hoveredHex)) - { - if(stack->doubleWide()) - return {hoveredHex, stack->occupiedHex(hoveredHex)}; - else - return {hoveredHex}; - } - - if(stack->doubleWide()) - { - for(auto const & hex : availableHexes) - { - if(stack->occupiedHex(hex) == hoveredHex) - return {hoveredHex, hex}; - } - } - - return {}; -} - -// Range limit highlight helpers - -std::vector BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance) -{ - std::vector rangeHexes; - - if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) - return rangeHexes; - - // get only battlefield hexes that are within the given distance - for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++) - { - BattleHex hex(i); - if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance) - rangeHexes.push_back(hex); - } - - return rangeHexes; -} - -std::vector BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector rangeHexes, uint8_t distanceToLimit) -{ - std::vector rangeLimitHexes; - - // from range hexes get only the ones at the limit - for(auto & hex : rangeHexes) - { - if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit) - rangeLimitHexes.push_back(hex); - } - - return rangeLimitHexes; -} - -bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit) -{ - bool hexInRangeLimit = false; - - if(!rangeLimitHexes.empty()) - { - auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex); - *hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos); - hexInRangeLimit = pos != rangeLimitHexes.end(); - } - - return hexInRangeLimit; -} - -std::vector> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector wholeRangeHexes, std::vector rangeLimitHexes) -{ - std::vector> output; - - if(wholeRangeHexes.empty()) - return output; - - for(auto & hex : rangeLimitHexes) - { - // get all neighbours and their directions - - auto neighbouringTiles = hex.allNeighbouringTiles(); - - std::vector outsideNeighbourDirections; - - // for each neighbour add to output only the valid ones and only that are not found in range Hexes - for(auto direction = 0; direction < 6; direction++) - { - if(!neighbouringTiles[direction].isAvailable()) - continue; - - auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]); - - if(it == wholeRangeHexes.end()) - outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction - } - - output.push_back(outsideNeighbourDirections); - } - - return output; -} - -std::vector> BattleFieldController::calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages) -{ - std::vector> output; // if no image is to be shown an empty image is still added to help with traverssing the range - - if(hexesNeighbourDirections.empty()) - return output; - - for(auto & directions : hexesNeighbourDirections) - { - std::bitset<6> mask; - - // convert directions to mask - for(auto direction : directions) - mask.set(direction); - - uint8_t imageKey = static_cast(mask.to_ulong()); - output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey])); - } - - return output; -} - -void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts) -{ - 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(); -} - -void BattleFieldController::showHighlightedHexes(Canvas & canvas) -{ - std::vector rangedFullDamageLimitHexes; - std::vector shootingRangeLimitHexes; - - std::vector> rangedFullDamageLimitHexesHighligts; - std::vector> shootingRangeLimitHexesHighligts; - - std::set hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack(); - std::set hoveredSpellHexes = getHighlightedHexesForSpellRange(); - std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); - - BattleHex hoveredHex = getHoveredHex(); - if(hoveredHex == BattleHex::INVALID) - return; - - const CStack * hoveredStack = getHoveredStack(); - - // skip range limit calculations if unit hovered is not a shooter - if(hoveredStack && hoveredStack->isShooter()) - { - // calculate array with highlight images for ranged full damage limit - auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance(); - calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts); - - // calculate array with highlight images for shooting range limit - auto shootingRangeDistance = hoveredStack->getShootingRangeDistance(); - calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); - } - - auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; - - for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) - { - bool stackMovement = hoveredStackMovementRangeHexes.count(hex); - bool mouse = hoveredMouseHexes.count(hex); - - // calculate if hex is Ranged Full Damage Limit and its position in highlight array - int hexIndexInRangedFullDamageLimit = 0; - bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit); - - // calculate if hex is Shooting Range Limit and its position in highlight array - int hexIndexInShootingRangeLimit = 0; - bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit); - - if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well - { - showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); - showHighlightedHex(canvas, cellShade, hex, true); - } - if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded - { - showHighlightedHex(canvas, cellShade, hex, true); - } - if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight - { - showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); - } - if(hexInRangedFullDamageLimit) - { - showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false); - } - if(hexInShootingRangeLimit) - { - showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false); - } - } -} - -Rect BattleFieldController::hexPositionLocal(BattleHex hex) const -{ - int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX(); - int y = 86 + 42 *hex.getY(); - int w = cellShade->width(); - int h = cellShade->height(); - return Rect(x, y, w, h); -} - -Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const -{ - return hexPositionLocal(hex) + pos.topLeft(); -} - -bool BattleFieldController::isPixelInHex(Point const & position) -{ - return !cellShade->isTransparent(position); -} - -BattleHex BattleFieldController::getHoveredHex() -{ - return hoveredHex; -} - -const CStack* BattleFieldController::getHoveredStack() -{ - auto hoveredHex = getHoveredHex(); - const CStack* hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - - return hoveredStack; -} - -BattleHex BattleFieldController::getHexAtPosition(Point hoverPos) -{ - if (owner.attackingHero) - { - if (owner.attackingHero->pos.isInside(hoverPos)) - return BattleHex::HERO_ATTACKER; - } - - if (owner.defendingHero) - { - if (owner.attackingHero->pos.isInside(hoverPos)) - return BattleHex::HERO_DEFENDER; - } - - for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h) - { - Rect hexPosition = hexPositionAbsolute(h); - - if (!hexPosition.isInside(hoverPos)) - continue; - - if (isPixelInHex(hoverPos - hexPosition.topLeft())) - return h; - } - - return BattleHex::INVALID; -} - -BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber) -{ - const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); - auto neighbours = myNumber.allNeighbouringTiles(); - // 0 1 - // 5 x 2 - // 4 3 - - // if true - our current stack can move into this hex (and attack) - std::array attackAvailability; - - if (doubleWide) - { - // For double-hexes we need to ensure that both hexes needed for this direction are occupyable: - // | -0- | -1- | -2- | -3- | -4- | -5- | -6- | -7- - // | o o - | - o o | - - | - - | - - | - - | o o | - - - // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - - // | - - | - - | - - | - o o | o o - | - - | - - | o o - - for (size_t i : { 1, 2, 3}) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false)); - - for (size_t i : { 4, 5, 0}) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false)); - - attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]); - attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]); - } - else - { - for (size_t i = 0; i < 6; ++i) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]); - - attackAvailability[6] = false; - attackAvailability[7] = false; - } - - // Zero available tiles to attack from - if ( vstd::find(attackAvailability, true) == attackAvailability.end()) - { - logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber); - return BattleHex::NONE; - } - - // For each valid direction, select position to test against - std::array testPoint; - - for (size_t i = 0; i < 6; ++i) - if (attackAvailability[i]) - testPoint[i] = hexPositionAbsolute(neighbours[i]).center(); - - // For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them - if (attackAvailability[6]) - testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5); - - if (attackAvailability[7]) - testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0, 5); - - // Compute distance between tested position & cursor position and pick nearest - std::array distance2; - - for (size_t i = 0; i < 8; ++i) - if (attackAvailability[i]) - distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x); - - size_t nearest = -1; - for (size_t i = 0; i < 8; ++i) - if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) ) - nearest = i; - - assert(nearest != -1); - return BattleHex::EDir(nearest); -} - -BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget) -{ - BattleHex::EDir direction = selectAttackDirection(getHoveredHex()); - - const CStack * attacker = owner.stacksController->getActiveStack(); - - assert(direction != BattleHex::NONE); - assert(attacker); - - if (!attacker->doubleWide()) - { - assert(direction != BattleHex::BOTTOM); - assert(direction != BattleHex::TOP); - return attackTarget.cloneInDirection(direction); - } - else - { - // We need to find position of right hex of double-hex creature (or left for defending side) - // | TOP_LEFT |TOP_RIGHT | RIGHT |BOTTOM_RIGHT|BOTTOM_LEFT| LEFT | TOP |BOTTOM - // | o o - | - o o | - - | - - | - - | - - | o o | - - - // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - - // | - - | - - | - - | - o o | o o - | - - | - - | o o - - switch (direction) - { - case BattleHex::TOP_LEFT: - case BattleHex::LEFT: - case BattleHex::BOTTOM_LEFT: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(direction); - else - return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT); - } - - case BattleHex::TOP_RIGHT: - case BattleHex::RIGHT: - case BattleHex::BOTTOM_RIGHT: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT); - else - return attackTarget.cloneInDirection(direction); - } - - case BattleHex::TOP: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT); - else - return attackTarget.cloneInDirection(BattleHex::TOP_LEFT); - } - - case BattleHex::BOTTOM: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT); - else - return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT); - } - default: - assert(0); - return BattleHex::INVALID; - } - } -} - -bool BattleFieldController::isTileAttackable(const BattleHex & number) const -{ - for (auto & elem : occupiableHexes) - { - if (BattleHex::mutualPosition(elem, number) != -1 || elem == number) - return true; - } - return false; -} - -void BattleFieldController::updateAccessibleHexes() -{ - auto accessibility = owner.curInt->cb->getAccesibility(); - - for(int i = 0; i < accessibility.size(); i++) - stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); -} - -bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const -{ - return stackCountOutsideHexes[number]; -} - -void BattleFieldController::showAll(Canvas & to) -{ - show(to); -} - -void BattleFieldController::tick(uint32_t msPassed) -{ - updateAccessibleHexes(); - owner.stacksController->tick(msPassed); - owner.obstacleController->tick(msPassed); - owner.projectilesController->tick(msPassed); -} - -void BattleFieldController::show(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - - renderBattlefield(to); - - if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID) - { - auto combatCursorIndex = CCS->curh->get(); - if (combatCursorIndex) - { - auto combatImageIndex = static_cast(*combatCursorIndex); - to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex)); - return; - } - - auto spellCursorIndex = CCS->curh->get(); - if (spellCursorIndex) - { - auto spellImageIndex = static_cast(*spellCursorIndex); - to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast()); - return; - } - - } -} - -bool BattleFieldController::receiveEvent(const Point & position, int eventType) const -{ - if (eventType == HOVER) - return true; - return CIntObject::receiveEvent(position, eventType); -} +/* + * BattleFieldController.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 "BattleFieldController.h" + +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleInterfaceClasses.h" +#include "BattleEffectsController.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "BattleObstacleController.h" +#include "BattleProjectileController.h" +#include "BattleRenderer.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../renderSDL/SDL_Extensions.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../adventureMap/CInGameConsole.h" +#include "../client/render/CAnimation.h" + +#include "../../CCallback.h" +#include "../../lib/BattleFieldHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/spells/ISpellMechanics.h" + +namespace HexMasks +{ + // mask definitions that has set to 1 the edges present in the hex edges highlight image + /* + /\ + 0 1 + / \ + | | + 5 2 + | | + \ / + 4 3 + \/ + */ + enum HexEdgeMasks { + empty = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed + topLeft = 0b000001, + topRight = 0b000010, + right = 0b000100, + bottomRight = 0b001000, + bottomLeft = 0b010000, + left = 0b100000, + + top = 0b000011, + bottom = 0b011000, + topRightHalfCorner = 0b000110, + bottomRightHalfCorner = 0b001100, + bottomLeftHalfCorner = 0b110000, + topLeftHalfCorner = 0b100001, + + rightTopAndBottom = 0b001010, // special case, right half can be drawn instead of only top and bottom + leftTopAndBottom = 0b010001, // special case, left half can be drawn instead of only top and bottom + + rightHalf = 0b001110, + leftHalf = 0b110001, + + topRightCorner = 0b000111, + bottomRightCorner = 0b011100, + bottomLeftCorner = 0b111000, + topLeftCorner = 0b100011 + }; +} + +std::map hexEdgeMaskToFrameIndex; + +// Maps HexEdgesMask to "Frame" indexes for range highligt images +void initializeHexEdgeMaskToFrameIndex() +{ + 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; +} + +BattleFieldController::BattleFieldController(BattleInterface & owner): + owner(owner) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + //preparing cells and hexes + cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); + cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP")); + 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(); + + 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); + + if(!owner.siegeController) + { + auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); + + if(bfieldType == BattleField::NONE) + logGlobal->error("Invalid battlefield returned for current battle"); + else + background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); + } + else + { + auto backgroundName = owner.siegeController->getBattleBackgroundName(); + background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE); + } + + pos.w = background->width(); + pos.h = background->height(); + + backgroundWithHexes = std::make_unique(Point(background->width(), background->height())); + + updateAccessibleHexes(); + addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); +} + +void BattleFieldController::activate() +{ + LOCPLINT->cingconsole->pos = this->pos; + CIntObject::activate(); +} + +void BattleFieldController::createHeroes() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // create heroes as part of our constructor for correct positioning inside battlefield + if(owner.attackingHeroInstance) + owner.attackingHero = std::make_shared(owner, owner.attackingHeroInstance, false); + + if(owner.defendingHeroInstance) + owner.defendingHero = std::make_shared(owner, owner.defendingHeroInstance, true); +} + +void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if (!on && pos.isInside(finalPosition)) + clickPressed(finalPosition); +} + +void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + Point distance = currentPosition - initialPosition; + + if (distance.length() < settings["battle"]["swipeAttackDistance"].Float()) + hoveredHex = getHexAtPosition(initialPosition); + else + hoveredHex = BattleHex::INVALID; + + currentAttackOriginPoint = currentPosition; + + if (pos.isInside(initialPosition)) + owner.actionsController->onHexHovered(getHoveredHex()); +} + +void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + hoveredHex = getHexAtPosition(cursorPosition); + currentAttackOriginPoint = cursorPosition; + + if (pos.isInside(cursorPosition)) + owner.actionsController->onHexHovered(getHoveredHex()); + else + owner.actionsController->onHoverEnded(); +} + +void BattleFieldController::clickPressed(const Point & cursorPosition) +{ + BattleHex selectedHex = getHoveredHex(); + + if (selectedHex != BattleHex::INVALID) + owner.actionsController->onHexLeftClicked(selectedHex); +} + +void BattleFieldController::showPopupWindow(const Point & cursorPosition) +{ + BattleHex selectedHex = getHoveredHex(); + + if (selectedHex != BattleHex::INVALID) + owner.actionsController->onHexRightClicked(selectedHex); +} + +void BattleFieldController::renderBattlefield(Canvas & canvas) +{ + Canvas clippedCanvas(canvas, pos); + + showBackground(clippedCanvas); + + BattleRenderer renderer(owner); + + renderer.execute(clippedCanvas); + + owner.projectilesController->render(clippedCanvas); +} + +void BattleFieldController::showBackground(Canvas & canvas) +{ + if (owner.stacksController->getActiveStack() != nullptr ) + showBackgroundImageWithHexes(canvas); + else + showBackgroundImage(canvas); + + showHighlightedHexes(canvas); +} + +void BattleFieldController::showBackgroundImage(Canvas & canvas) +{ + canvas.draw(background, Point(0, 0)); + + owner.obstacleController->showAbsoluteObstacles(canvas); + if ( owner.siegeController ) + owner.siegeController->showAbsoluteObstacles(canvas); + + if (settings["battle"]["cellBorders"].Bool()) + { + for (int i=0; igetActiveStack(); + std::vector attackableHexes; + if(activeStack) + occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); + + // prepare background graphic with hexes and shaded hexes + backgroundWithHexes->draw(background, Point(0,0)); + owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes); + if(owner.siegeController) + owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes); + + // show shaded hexes for active's stack valid movement and the hexes that it can attack + if(settings["battle"]["stackRange"].Bool()) + { + std::vector hexesToShade = occupiableHexes; + hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end()); + for(BattleHex hex : hexesToShade) + { + showHighlightedHex(*backgroundWithHexes, cellShade, hex, false); + } + } + + // draw cell borders + if(settings["battle"]["cellBorders"].Bool()) + { + for(int i=0; idraw(cellBorder, hexPositionLocal(i).topLeft()); + } + } +} + +void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr highlight, BattleHex hex, bool darkBorder) +{ + Point hexPos = hexPositionLocal(hex).topLeft(); + + canvas.draw(highlight, hexPos); + if(!darkBorder && settings["battle"]["cellBorders"].Bool()) + canvas.draw(cellBorder, hexPos); +} + +std::set BattleFieldController::getHighlightedHexesForActiveStack() +{ + std::set result; + + if(!owner.stacksController->getActiveStack()) + return result; + + if(!settings["battle"]["stackRange"].Bool()) + return result; + + auto hoveredHex = getHoveredHex(); + + std::set set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); + for(BattleHex hex : set) + result.insert(hex); + + return result; +} + +std::set BattleFieldController::getMovementRangeForHoveredStack() +{ + std::set result; + + if (!owner.stacksController->getActiveStack()) + return result; + + if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) + return result; + + auto hoveredHex = getHoveredHex(); + + // add possible movement hexes for stack under mouse + const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(hoveredStack) + { + std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); + for(BattleHex hex : v) + result.insert(hex); + } + return result; +} + +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; + + spells::Mode mode = owner.actionsController->getCurrentCastMode(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); + caster = owner.actionsController->getCurrentSpellcaster(); + + if(caster && spell) //when casting spell + { + // printing shaded hex(es) + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); + auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex); + + for(BattleHex shadedHex : shadedHexes) + { + if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1)) + result.insert(shadedHex); + } + } + return result; +} + +std::set BattleFieldController::getHighlightedHexesForMovementTarget() +{ + const CStack * stack = owner.stacksController->getActiveStack(); + auto hoveredHex = getHoveredHex(); + + if(!stack) + return {}; + + std::vector availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr); + + auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex)) + { + if(isTileAttackable(hoveredHex)) + { + BattleHex attackFromHex = fromWhichHexAttack(hoveredHex); + + if(stack->doubleWide()) + return {attackFromHex, stack->occupiedHex(attackFromHex)}; + else + return {attackFromHex}; + } + } + + if(vstd::contains(availableHexes, hoveredHex)) + { + if(stack->doubleWide()) + return {hoveredHex, stack->occupiedHex(hoveredHex)}; + else + return {hoveredHex}; + } + + if(stack->doubleWide()) + { + for(auto const & hex : availableHexes) + { + if(stack->occupiedHex(hex) == hoveredHex) + return {hoveredHex, hex}; + } + } + + return {}; +} + +// Range limit highlight helpers + +std::vector BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance) +{ + std::vector rangeHexes; + + if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) + return rangeHexes; + + // get only battlefield hexes that are within the given distance + for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + BattleHex hex(i); + if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance) + rangeHexes.push_back(hex); + } + + return rangeHexes; +} + +std::vector BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector rangeHexes, uint8_t distanceToLimit) +{ + std::vector rangeLimitHexes; + + // from range hexes get only the ones at the limit + for(auto & hex : rangeHexes) + { + if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit) + rangeLimitHexes.push_back(hex); + } + + return rangeLimitHexes; +} + +bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit) +{ + bool hexInRangeLimit = false; + + if(!rangeLimitHexes.empty()) + { + auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex); + *hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos); + hexInRangeLimit = pos != rangeLimitHexes.end(); + } + + return hexInRangeLimit; +} + +std::vector> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector wholeRangeHexes, std::vector rangeLimitHexes) +{ + std::vector> output; + + if(wholeRangeHexes.empty()) + return output; + + for(auto & hex : rangeLimitHexes) + { + // get all neighbours and their directions + + auto neighbouringTiles = hex.allNeighbouringTiles(); + + std::vector outsideNeighbourDirections; + + // for each neighbour add to output only the valid ones and only that are not found in range Hexes + for(auto direction = 0; direction < 6; direction++) + { + if(!neighbouringTiles[direction].isAvailable()) + continue; + + auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]); + + if(it == wholeRangeHexes.end()) + outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction + } + + output.push_back(outsideNeighbourDirections); + } + + return output; +} + +std::vector> BattleFieldController::calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages) +{ + std::vector> output; // if no image is to be shown an empty image is still added to help with traverssing the range + + if(hexesNeighbourDirections.empty()) + return output; + + for(auto & directions : hexesNeighbourDirections) + { + std::bitset<6> mask; + + // convert directions to mask + for(auto direction : directions) + mask.set(direction); + + uint8_t imageKey = static_cast(mask.to_ulong()); + output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey])); + } + + return output; +} + +void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts) +{ + 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(); +} + +void BattleFieldController::showHighlightedHexes(Canvas & canvas) +{ + std::vector rangedFullDamageLimitHexes; + std::vector shootingRangeLimitHexes; + + std::vector> rangedFullDamageLimitHexesHighligts; + std::vector> shootingRangeLimitHexesHighligts; + + std::set hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack(); + std::set hoveredSpellHexes = getHighlightedHexesForSpellRange(); + std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); + + BattleHex hoveredHex = getHoveredHex(); + if(hoveredHex == BattleHex::INVALID) + return; + + const CStack * hoveredStack = getHoveredStack(); + + // skip range limit calculations if unit hovered is not a shooter + if(hoveredStack && hoveredStack->isShooter()) + { + // calculate array with highlight images for ranged full damage limit + auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance(); + calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts); + + // calculate array with highlight images for shooting range limit + auto shootingRangeDistance = hoveredStack->getShootingRangeDistance(); + calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); + } + + auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; + + for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) + { + bool stackMovement = hoveredStackMovementRangeHexes.count(hex); + bool mouse = hoveredMouseHexes.count(hex); + + // calculate if hex is Ranged Full Damage Limit and its position in highlight array + int hexIndexInRangedFullDamageLimit = 0; + bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit); + + // calculate if hex is Shooting Range Limit and its position in highlight array + int hexIndexInShootingRangeLimit = 0; + bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit); + + if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well + { + showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); + showHighlightedHex(canvas, cellShade, hex, true); + } + if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded + { + showHighlightedHex(canvas, cellShade, hex, true); + } + if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight + { + showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); + } + if(hexInRangedFullDamageLimit) + { + showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false); + } + if(hexInShootingRangeLimit) + { + showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false); + } + } +} + +Rect BattleFieldController::hexPositionLocal(BattleHex hex) const +{ + int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX(); + int y = 86 + 42 *hex.getY(); + int w = cellShade->width(); + int h = cellShade->height(); + return Rect(x, y, w, h); +} + +Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const +{ + return hexPositionLocal(hex) + pos.topLeft(); +} + +bool BattleFieldController::isPixelInHex(Point const & position) +{ + return !cellShade->isTransparent(position); +} + +BattleHex BattleFieldController::getHoveredHex() +{ + return hoveredHex; +} + +const CStack* BattleFieldController::getHoveredStack() +{ + auto hoveredHex = getHoveredHex(); + const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + + return hoveredStack; +} + +BattleHex BattleFieldController::getHexAtPosition(Point hoverPos) +{ + if (owner.attackingHero) + { + if (owner.attackingHero->pos.isInside(hoverPos)) + return BattleHex::HERO_ATTACKER; + } + + if (owner.defendingHero) + { + if (owner.attackingHero->pos.isInside(hoverPos)) + return BattleHex::HERO_DEFENDER; + } + + for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h) + { + Rect hexPosition = hexPositionAbsolute(h); + + if (!hexPosition.isInside(hoverPos)) + continue; + + if (isPixelInHex(hoverPos - hexPosition.topLeft())) + return h; + } + + return BattleHex::INVALID; +} + +BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber) +{ + const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); + auto neighbours = myNumber.allNeighbouringTiles(); + // 0 1 + // 5 x 2 + // 4 3 + + // if true - our current stack can move into this hex (and attack) + std::array attackAvailability; + + if (doubleWide) + { + // For double-hexes we need to ensure that both hexes needed for this direction are occupyable: + // | -0- | -1- | -2- | -3- | -4- | -5- | -6- | -7- + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + for (size_t i : { 1, 2, 3}) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false)); + + for (size_t i : { 4, 5, 0}) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false)); + + attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]); + attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]); + } + else + { + for (size_t i = 0; i < 6; ++i) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]); + + attackAvailability[6] = false; + attackAvailability[7] = false; + } + + // Zero available tiles to attack from + if ( vstd::find(attackAvailability, true) == attackAvailability.end()) + { + logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber); + return BattleHex::NONE; + } + + // For each valid direction, select position to test against + std::array testPoint; + + for (size_t i = 0; i < 6; ++i) + if (attackAvailability[i]) + testPoint[i] = hexPositionAbsolute(neighbours[i]).center(); + + // For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them + if (attackAvailability[6]) + testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5); + + if (attackAvailability[7]) + testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0, 5); + + // Compute distance between tested position & cursor position and pick nearest + std::array distance2; + + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i]) + distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x); + + size_t nearest = -1; + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) ) + nearest = i; + + assert(nearest != -1); + return BattleHex::EDir(nearest); +} + +BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget) +{ + BattleHex::EDir direction = selectAttackDirection(getHoveredHex()); + + const CStack * attacker = owner.stacksController->getActiveStack(); + + assert(direction != BattleHex::NONE); + assert(attacker); + + if (!attacker->doubleWide()) + { + assert(direction != BattleHex::BOTTOM); + assert(direction != BattleHex::TOP); + return attackTarget.cloneInDirection(direction); + } + else + { + // We need to find position of right hex of double-hex creature (or left for defending side) + // | TOP_LEFT |TOP_RIGHT | RIGHT |BOTTOM_RIGHT|BOTTOM_LEFT| LEFT | TOP |BOTTOM + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + switch (direction) + { + case BattleHex::TOP_LEFT: + case BattleHex::LEFT: + case BattleHex::BOTTOM_LEFT: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction); + else + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT); + } + + case BattleHex::TOP_RIGHT: + case BattleHex::RIGHT: + case BattleHex::BOTTOM_RIGHT: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT); + else + return attackTarget.cloneInDirection(direction); + } + + case BattleHex::TOP: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::TOP_LEFT); + } + + case BattleHex::BOTTOM: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT); + } + default: + assert(0); + return BattleHex::INVALID; + } + } +} + +bool BattleFieldController::isTileAttackable(const BattleHex & number) const +{ + for (auto & elem : occupiableHexes) + { + if (BattleHex::mutualPosition(elem, number) != -1 || elem == number) + return true; + } + return false; +} + +void BattleFieldController::updateAccessibleHexes() +{ + auto accessibility = owner.getBattle()->getAccesibility(); + + for(int i = 0; i < accessibility.size(); i++) + stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); +} + +bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const +{ + return stackCountOutsideHexes[number]; +} + +void BattleFieldController::showAll(Canvas & to) +{ + show(to); +} + +void BattleFieldController::tick(uint32_t msPassed) +{ + updateAccessibleHexes(); + owner.stacksController->tick(msPassed); + owner.obstacleController->tick(msPassed); + owner.projectilesController->tick(msPassed); +} + +void BattleFieldController::show(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + + renderBattlefield(to); + + if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID) + { + auto combatCursorIndex = CCS->curh->get(); + if (combatCursorIndex) + { + auto combatImageIndex = static_cast(*combatCursorIndex); + to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex)); + return; + } + + auto spellCursorIndex = CCS->curh->get(); + if (spellCursorIndex) + { + auto spellImageIndex = static_cast(*spellCursorIndex); + to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast()); + return; + } + + } +} + +bool BattleFieldController::receiveEvent(const Point & position, int eventType) const +{ + if (eventType == HOVER) + return true; + return CIntObject::receiveEvent(position, eventType); +} diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index b97649cc8..4a713a5de 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -1,143 +1,144 @@ -/* - * BattleFieldController.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/battle/BattleHex.h" -#include "../../lib/Point.h" -#include "../gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN -class CStack; -class Rect; -VCMI_LIB_NAMESPACE_END - -class BattleHero; -class Canvas; -class IImage; -class BattleInterface; - -/// Handles battlefield grid as well as rendering of background layer of battle interface -class BattleFieldController : public CIntObject -{ - BattleInterface & owner; - - std::shared_ptr background; - std::shared_ptr cellBorder; - std::shared_ptr cellUnitMovementHighlight; - std::shared_ptr cellUnitMaxMovementHighlight; - std::shared_ptr cellShade; - std::shared_ptr rangedFullDamageLimitImages; - std::shared_ptr shootingRangeLimitImages; - - std::shared_ptr attackCursors; - std::shared_ptr spellCursors; - - /// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack - std::unique_ptr backgroundWithHexes; - - /// direction which will be used to perform attack with current cursor position - Point currentAttackOriginPoint; - - /// hex currently under mouse hover - BattleHex hoveredHex; - - /// hexes to which currently active stack can move - std::vector occupiableHexes; - - /// hexes that when in front of a unit cause it's amount box to move back - std::array stackCountOutsideHexes; - - void showHighlightedHex(Canvas & to, std::shared_ptr highlight, BattleHex hex, bool darkBorder); - - std::set getHighlightedHexesForActiveStack(); - std::set getMovementRangeForHoveredStack(); - std::set getHighlightedHexesForSpellRange(); - std::set getHighlightedHexesForMovementTarget(); - - // Range limit highlight helpers - - /// get all hexes within a certain distance of given hex - std::vector getRangeHexes(BattleHex sourceHex, uint8_t distance); - - /// get only hexes at the limit of a range - std::vector getRangeLimitHexes(BattleHex hoveredHex, std::vector hexRange, uint8_t distanceToLimit); - - /// 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 - std::vector> getOutsideNeighbourDirectionsForLimitHexes(std::vector rangeHexes, std::vector rangeLimitHexes); - - /// calculates what image to use as range limit, depending on the direction of neighbors - /// a mask is used internally to mark the directions of all neighbours - /// based on this mask the corresponding image is selected - 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 showBackground(Canvas & canvas); - void showBackgroundImage(Canvas & canvas); - void showBackgroundImageWithHexes(Canvas & canvas); - void showHighlightedHexes(Canvas & canvas); - void updateAccessibleHexes(); - - BattleHex getHexAtPosition(Point hoverPosition); - - /// Checks whether selected pixel is transparent, uses local coordinates of a hex - bool isPixelInHex(Point const & position); - size_t selectBattleCursor(BattleHex myNumber); - - void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; - void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; - void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void activate() override; - - void showAll(Canvas & to) override; - void show(Canvas & to) override; - void tick(uint32_t msPassed) override; - - bool receiveEvent(const Point & position, int eventType) const override; - -public: - BattleFieldController(BattleInterface & owner); - - void createHeroes(); - - void redrawBackgroundWithHexes(); - void renderBattlefield(Canvas & canvas); - - /// Returns position of hex relative to owner (BattleInterface) - Rect hexPositionLocal(BattleHex hex) const; - - /// Returns position of hex relative to game window - Rect hexPositionAbsolute(BattleHex hex) const; - - /// Returns ID of currently hovered hex or BattleHex::INVALID if none - BattleHex getHoveredHex(); - - /// Returns the currently hovered stack - const CStack* getHoveredStack(); - - /// returns true if selected tile can be attacked in melee by current stack - bool isTileAttackable(const BattleHex & number) const; - - /// returns true if stack should render its stack count image in default position - outside own hex - bool stackCountOutsideHex(const BattleHex & number) const; - - BattleHex::EDir selectAttackDirection(BattleHex myNumber); - - BattleHex fromWhichHexAttack(BattleHex myNumber); -}; +/* + * BattleFieldController.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/battle/BattleHex.h" +#include "../../lib/Point.h" +#include "../gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CStack; +class Rect; +VCMI_LIB_NAMESPACE_END + +class BattleHero; +class CAnimation; +class Canvas; +class IImage; +class BattleInterface; + +/// Handles battlefield grid as well as rendering of background layer of battle interface +class BattleFieldController : public CIntObject +{ + BattleInterface & owner; + + std::shared_ptr background; + std::shared_ptr cellBorder; + std::shared_ptr cellUnitMovementHighlight; + std::shared_ptr cellUnitMaxMovementHighlight; + std::shared_ptr cellShade; + std::shared_ptr rangedFullDamageLimitImages; + std::shared_ptr shootingRangeLimitImages; + + std::shared_ptr attackCursors; + std::shared_ptr spellCursors; + + /// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack + std::unique_ptr backgroundWithHexes; + + /// direction which will be used to perform attack with current cursor position + Point currentAttackOriginPoint; + + /// hex currently under mouse hover + BattleHex hoveredHex; + + /// hexes to which currently active stack can move + std::vector occupiableHexes; + + /// hexes that when in front of a unit cause it's amount box to move back + std::array stackCountOutsideHexes; + + void showHighlightedHex(Canvas & to, std::shared_ptr highlight, BattleHex hex, bool darkBorder); + + std::set getHighlightedHexesForActiveStack(); + std::set getMovementRangeForHoveredStack(); + std::set getHighlightedHexesForSpellRange(); + std::set getHighlightedHexesForMovementTarget(); + + // Range limit highlight helpers + + /// get all hexes within a certain distance of given hex + std::vector getRangeHexes(BattleHex sourceHex, uint8_t distance); + + /// get only hexes at the limit of a range + std::vector getRangeLimitHexes(BattleHex hoveredHex, std::vector hexRange, uint8_t distanceToLimit); + + /// 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 + std::vector> getOutsideNeighbourDirectionsForLimitHexes(std::vector rangeHexes, std::vector rangeLimitHexes); + + /// calculates what image to use as range limit, depending on the direction of neighbors + /// a mask is used internally to mark the directions of all neighbours + /// based on this mask the corresponding image is selected + 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 showBackground(Canvas & canvas); + void showBackgroundImage(Canvas & canvas); + void showBackgroundImageWithHexes(Canvas & canvas); + void showHighlightedHexes(Canvas & canvas); + void updateAccessibleHexes(); + + BattleHex getHexAtPosition(Point hoverPosition); + + /// Checks whether selected pixel is transparent, uses local coordinates of a hex + bool isPixelInHex(Point const & position); + size_t selectBattleCursor(BattleHex myNumber); + + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void activate() override; + + void showAll(Canvas & to) override; + void show(Canvas & to) override; + void tick(uint32_t msPassed) override; + + bool receiveEvent(const Point & position, int eventType) const override; + +public: + BattleFieldController(BattleInterface & owner); + + void createHeroes(); + + void redrawBackgroundWithHexes(); + void renderBattlefield(Canvas & canvas); + + /// Returns position of hex relative to owner (BattleInterface) + Rect hexPositionLocal(BattleHex hex) const; + + /// Returns position of hex relative to game window + Rect hexPositionAbsolute(BattleHex hex) const; + + /// Returns ID of currently hovered hex or BattleHex::INVALID if none + BattleHex getHoveredHex(); + + /// Returns the currently hovered stack + const CStack* getHoveredStack(); + + /// returns true if selected tile can be attacked in melee by current stack + bool isTileAttackable(const BattleHex & number) const; + + /// returns true if stack should render its stack count image in default position - outside own hex + bool stackCountOutsideHex(const BattleHex & number) const; + + BattleHex::EDir selectAttackDirection(BattleHex myNumber); + + BattleHex fromWhichHexAttack(BattleHex myNumber); +}; diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 4159895de..ab7cffcb4 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -1,831 +1,831 @@ -/* - * BattleInterface.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 "BattleInterface.h" - -#include "BattleAnimationClasses.h" -#include "BattleActionsController.h" -#include "BattleInterfaceClasses.h" -#include "CreatureAnimation.h" -#include "BattleProjectileController.h" -#include "BattleEffectsController.h" -#include "BattleObstacleController.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleWindow.h" -#include "BattleStacksController.h" -#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 "../render/Canvas.h" -#include "../adventureMap/AdventureMapInterface.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/CondSh.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/NetPacks.h" -#include "../../lib/UnlockGuard.h" -#include "../../lib/TerrainHandler.h" - -BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, - std::shared_ptr att, - std::shared_ptr defen, - std::shared_ptr spectatorInt) - : attackingHeroInstance(hero1) - , defendingHeroInstance(hero2) - , attackerInt(att) - , defenderInt(defen) - , curInt(att) - , battleOpeningDelayActive(true) -{ - if(spectatorInt) - { - curInt = spectatorInt; - } - else if(!curInt) - { - //May happen when we are defending during network MP game -> attacker interface is just not present - curInt = defenderInt; - } - - //hot-seat -> check tactics for both players (defender may be local human) - if(attackerInt && attackerInt->cb->battleGetTacticDist()) - tacticianInterface = attackerInt; - else if(defenderInt && defenderInt->cb->battleGetTacticDist()) - tacticianInterface = defenderInt; - - //if we found interface of player with tactics, then enter tactics mode - tacticsMode = static_cast(tacticianInterface); - - //initializing armies - this->army1 = army1; - this->army2 = army2; - - const CGTownInstance *town = curInt->cb->battleGetDefendedTown(); - if(town && town->hasFort()) - siegeController.reset(new BattleSiegeController(*this, town)); - - windowObject = std::make_shared(*this); - projectilesController.reset(new BattleProjectileController(*this)); - stacksController.reset( new BattleStacksController(*this)); - actionsController.reset( new BattleActionsController(*this)); - effectsController.reset(new BattleEffectsController(*this)); - obstacleController.reset(new BattleObstacleController(*this)); - - adventureInt->onAudioPaused(); - ongoingAnimationsState.set(true); - - GH.windows().pushWindow(windowObject); - windowObject->blockUI(true); - windowObject->updateQueue(); - - playIntroSoundAndUnlockInterface(); -} - -void BattleInterface::playIntroSoundAndUnlockInterface() -{ - auto onIntroPlayed = [this]() - { - boost::unique_lock un(*CPlayerInterface::pim); - if(LOCPLINT->battleInt) - onIntroSoundPlayed(); - }; - - int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); - if (battleIntroSoundChannel != -1) - { - CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); - - if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool()) - openingEnd(); - } - else // failed to play sound - { - onIntroSoundPlayed(); - } -} - -bool BattleInterface::openingPlaying() -{ - return battleOpeningDelayActive; -} - -void BattleInterface::onIntroSoundPlayed() -{ - if (openingPlaying()) - openingEnd(); - - CCS->musich->playMusicFromSet("battle", true, true); -} - -void BattleInterface::openingEnd() -{ - assert(openingPlaying()); - if (!openingPlaying()) - return; - - onAnimationsFinished(); - if(tacticsMode) - tacticNextStack(nullptr); - activateStack(); - battleOpeningDelayActive = false; -} - -BattleInterface::~BattleInterface() -{ - CPlayerInterface::battleInt = nullptr; - - if (adventureInt) - adventureInt->onAudioResumed(); - - awaitingEvents.clear(); - onAnimationsFinished(); -} - -void BattleInterface::redrawBattlefield() -{ - fieldController->redrawBackgroundWithHexes(); - GH.windows().totalRedraw(); -} - -void BattleInterface::stackReset(const CStack * stack) -{ - stacksController->stackReset(stack); -} - -void BattleInterface::stackAdded(const CStack * stack) -{ - stacksController->stackAdded(stack, false); -} - -void BattleInterface::stackRemoved(uint32_t stackID) -{ - stacksController->stackRemoved(stackID); - fieldController->redrawBackgroundWithHexes(); - windowObject->updateQueue(); -} - -void BattleInterface::stackActivated(const CStack *stack) -{ - stacksController->stackActivated(stack); -} - -void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport) -{ - if (teleport) - stacksController->stackTeleported(stack, destHex, distance); - else - stacksController->stackMoved(stack, destHex, distance); -} - -void BattleInterface::stacksAreAttacked(std::vector attackedInfos) -{ - stacksController->stacksAreAttacked(attackedInfos); - - std::array killedBySide = {0, 0}; - - for(const StackAttackedInfo & attackedInfo : attackedInfos) - { - ui8 side = attackedInfo.defender->unitSide(); - killedBySide.at(side) += attackedInfo.amountKilled; - } - - for(ui8 side = 0; side < 2; side++) - { - if(killedBySide.at(side) > killedBySide.at(1-side)) - setHeroAnimation(side, EHeroAnimType::DEFEAT); - else if(killedBySide.at(side) < killedBySide.at(1-side)) - setHeroAnimation(side, EHeroAnimType::VICTORY); - } -} - -void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) -{ - stacksController->stackAttacking(attackInfo); -} - -void BattleInterface::newRoundFirst( int round ) -{ - waitForAnimations(); -} - -void BattleInterface::newRound(int number) -{ - console->addText(CGI->generaltexth->allTexts[412]); -} - -void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) -{ - const CStack * actor = nullptr; - if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) - { - actor = stacksController->getActiveStack(); - } - - auto side = curInt->cb->playerToSide(curInt->playerID); - if(!side) - { - logGlobal->error("Player %s is not in battle", curInt->playerID.getStr()); - return; - } - - BattleAction ba; - ba.side = side.value(); - ba.actionType = action; - ba.aimToHex(tile); - ba.actionSubtype = additional; - - sendCommand(ba, actor); -} - -void BattleInterface::sendCommand(BattleAction command, const CStack * actor) -{ - command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2); - - if(!tacticsMode) - { - logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); - stacksController->setActiveStack(nullptr); - LOCPLINT->cb->battleMakeUnitAction(command); - } - else - { - curInt->cb->battleMakeTacticAction(command); - stacksController->setActiveStack(nullptr); - //next stack will be activated when action ends - } - CCS->curh->set(Cursor::Combat::POINTER); -} - -const CGHeroInstance * BattleInterface::getActiveHero() -{ - const CStack *attacker = stacksController->getActiveStack(); - if(!attacker) - { - return nullptr; - } - - if(attacker->unitSide() == BattleSide::ATTACKER) - { - return attackingHeroInstance; - } - - return defendingHeroInstance; -} - -void BattleInterface::stackIsCatapulting(const CatapultAttack & ca) -{ - if (siegeController) - siegeController->stackIsCatapulting(ca); -} - -void BattleInterface::gateStateChanged(const EGateState state) -{ - if (siegeController) - siegeController->gateStateChanged(state); -} - -void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) -{ - checkForAnimations(); - stacksController->setActiveStack(nullptr); - - CCS->curh->set(Cursor::Map::POINTER); - curInt->waitWhileDialog(); - - if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) - { - curInt->cb->selectionMade(0, queryID); - windowObject->close(); - return; - } - - auto wnd = std::make_shared(br, *(this->curInt)); - wnd->resultCallback = [=](ui32 selection) - { - curInt->cb->selectionMade(selection, queryID); - }; - GH.windows().pushWindow(wnd); - - curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 - CPlayerInterface::battleInt = nullptr; -} - -void BattleInterface::spellCast(const BattleSpellCast * sc) -{ - // Do not deactivate anything in tactics mode - // This is battlefield setup spells - if(!tacticsMode) - { - windowObject->blockUI(true); - - // Disable current active stack duing the cast - // Store the current activeStack to stackToActivate - stacksController->deactivateStack(); - } - - CCS->curh->set(Cursor::Combat::BLOCKED); - - const SpellID spellID = sc->spellID; - const CSpell * spell = spellID.toSpell(); - auto targetedTile = sc->tile; - - assert(spell); - if(!spell) - return; - - const std::string & castSoundPath = spell->getCastSound(); - - if (!castSoundPath.empty()) - { - auto group = spell->animationInfo.projectile.empty() ? - EAnimationEvents::HIT: - EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning - - addToAnimationStage(group, [=]() { - CCS->soundh->playSound(castSoundPath); - }); - } - - if ( sc->activeCast ) - { - const CStack * casterStack = curInt->cb->battleGetStackByID(sc->casterStack); - - if(casterStack != nullptr ) - { - addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() - { - stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); - displaySpellCast(spell, casterStack->getPosition()); - }); - } - else - { - auto hero = sc->side ? defendingHero : attackingHero; - assert(hero); - - addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() - { - stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); - }); - } - } - - addToAnimationStage(EAnimationEvents::HIT, [=](){ - displaySpellHit(spell, targetedTile); - }); - - //queuing affect animation - for(auto & elem : sc->affectedCres) - { - auto stack = curInt->cb->battleGetStackByID(elem, false); - assert(stack); - if(stack) - { - addToAnimationStage(EAnimationEvents::HIT, [=](){ - displaySpellEffect(spell, stack->getPosition()); - }); - } - } - - for(auto & elem : sc->reflectedCres) - { - auto stack = curInt->cb->battleGetStackByID(elem, false); - assert(stack); - addToAnimationStage(EAnimationEvents::HIT, [=](){ - effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); - }); - } - - if (!sc->resistedCres.empty()) - { - addToAnimationStage(EAnimationEvents::HIT, [=](){ - CCS->soundh->playSound("MAGICRES"); - }); - } - - for(auto & elem : sc->resistedCres) - { - auto stack = curInt->cb->battleGetStackByID(elem, false); - assert(stack); - addToAnimationStage(EAnimationEvents::HIT, [=](){ - effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); - }); - } - - //mana absorption - if (sc->manaGained > 0) - { - Point leftHero = Point(15, 30); - Point rightHero = Point(755, 30); - bool side = sc->side; - - addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero)); - stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); - }); - } - - // animations will be executed by spell effects -} - -void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) -{ - if(stacksController->getActiveStack() != nullptr) - fieldController->redrawBackgroundWithHexes(); -} - -void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) -{ - if(side == BattleSide::ATTACKER) - { - if(attackingHero) - attackingHero->setPhase(phase); - } - else - { - if(defendingHero) - defendingHero->setPhase(phase); - } -} - -void BattleInterface::displayBattleLog(const std::vector & battleLog) -{ - for(const auto & line : battleLog) - { - std::string formatted = line.toString(); - boost::algorithm::trim(formatted); - appendBattleLog(formatted); - } -} - -void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) -{ - for(const CSpell::TAnimation & animation : q) - { - if(animation.pause > 0) - stacksController->addNewAnim(new DummyAnimation(*this, animation.pause)); - - if (!animation.effectName.empty()) - { - const CStack * destStack = getCurrentPlayerInterface()->cb->battleGetStackByPos(destinationTile, false); - - if (destStack) - stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); - } - - if(!animation.resourceName.empty()) - { - int flags = 0; - - if (isHit) - flags |= EffectAnimation::FORCE_ON_TOP; - - if (animation.verticalPosition == VerticalPosition::BOTTOM) - flags |= EffectAnimation::ALIGN_TO_BOTTOM; - - if (!destinationTile.isValid()) - flags |= EffectAnimation::SCREEN_FILL; - - if (!destinationTile.isValid()) - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); - else - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); - } - } -} - -void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false); -} - -void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false); -} - -void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true); -} - -CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const -{ - return curInt.get(); -} - -void BattleInterface::trySetActivePlayer( PlayerColor player ) -{ - if ( attackerInt && attackerInt->playerID == player ) - curInt = attackerInt; - - if ( defenderInt && defenderInt->playerID == player ) - curInt = defenderInt; -} - -void BattleInterface::activateStack() -{ - stacksController->activateStack(); - - const CStack * s = stacksController->getActiveStack(); - if(!s) - return; - - windowObject->updateQueue(); - windowObject->blockUI(false); - fieldController->redrawBackgroundWithHexes(); - actionsController->activateStack(); - GH.fakeMouseMove(); -} - -bool BattleInterface::makingTurn() const -{ - return stacksController->getActiveStack() != nullptr; -} - -void BattleInterface::endAction(const BattleAction* action) -{ - // it is possible that tactics mode ended while opening music is still playing - waitForAnimations(); - - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); - - // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast - activateStack(); - - stacksController->endAction(action); - windowObject->updateQueue(); - - //stack ended movement in tactics phase -> select the next one - if (tacticsMode) - tacticNextStack(stack); - - //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed - if(action->actionType == EActionType::HERO_SPELL) - fieldController->redrawBackgroundWithHexes(); -} - -void BattleInterface::appendBattleLog(const std::string & newEntry) -{ - console->addText(newEntry); -} - -void BattleInterface::startAction(const BattleAction* action) -{ - if(action->actionType == EActionType::END_TACTIC_PHASE) - { - windowObject->tacticPhaseEnded(); - return; - } - - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); - - if (stack) - { - windowObject->updateQueue(); - } - else - { - assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number - } - - stacksController->startAction(action); - - if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell - return; - - if (!stack) - { - logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber); - return; - } - - effectsController->startAction(action); -} - -void BattleInterface::tacticPhaseEnd() -{ - stacksController->setActiveStack(nullptr); - tacticsMode = false; - - auto side = tacticianInterface->cb->playerToSide(tacticianInterface->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); - - tacticianInterface->cb->battleMakeTacticAction(action); -} - -static bool immobile(const CStack *s) -{ - return !s->speed(0, true); //should bound stacks be immobile? -} - -void BattleInterface::tacticNextStack(const CStack * current) -{ - if (!current) - current = stacksController->getActiveStack(); - - //no switching stacks when the current one is moving - checkForAnimations(); - - TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE); - vstd::erase_if (stacksOfMine, &immobile); - if (stacksOfMine.empty()) - { - tacticPhaseEnd(); - return; - } - - auto it = vstd::find(stacksOfMine, current); - if (it != stacksOfMine.end() && ++it != stacksOfMine.end()) - stackActivated(*it); - else - stackActivated(stacksOfMine.front()); - -} - -void BattleInterface::obstaclePlaced(const std::vector> oi) -{ - obstacleController->obstaclePlaced(oi); -} - -void BattleInterface::obstacleRemoved(const std::vector & obstacles) -{ - obstacleController->obstacleRemoved(obstacles); -} - -const CGHeroInstance *BattleInterface::currentHero() const -{ - if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID) - return attackingHeroInstance; - - if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID) - return defendingHeroInstance; - - return nullptr; -} - -InfoAboutHero BattleInterface::enemyHero() const -{ - InfoAboutHero ret; - if (attackingHeroInstance->tempOwner == curInt->playerID) - curInt->cb->getHeroInfo(defendingHeroInstance, ret); - else - curInt->cb->getHeroInfo(attackingHeroInstance, ret); - - return ret; -} - -void BattleInterface::requestAutofightingAIToTakeAction() -{ - assert(curInt->isAutoFightOn); - - if(curInt->cb->battleIsFinished()) - { - return; // battle finished with spellcast - } - - if (tacticsMode) - { - // Always end tactics mode. Player interface is blocked currently, so it's not possible that - // the AI can take any action except end tactics phase (AI actions won't be triggered) - //TODO implement the possibility that the AI will be triggered for further actions - //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? - tacticPhaseEnd(); - stacksController->setActiveStack(nullptr); - } - else - { - const CStack* activeStack = stacksController->getActiveStack(); - - // If enemy is moving, activeStack can be null - if (activeStack) - { - stacksController->setActiveStack(nullptr); - - // 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([this, activeStack]() - { - curInt->autofightingAI->activeStack(activeStack); - }); - aiThread.detach(); - } - } -} - -void BattleInterface::castThisSpell(SpellID spellID) -{ - actionsController->castThisSpell(spellID); -} - -void BattleInterface::executeStagedAnimations() -{ - EAnimationEvents earliestStage = EAnimationEvents::COUNT; - - for(const auto & event : awaitingEvents) - earliestStage = std::min(earliestStage, event.event); - - if(earliestStage != EAnimationEvents::COUNT) - executeAnimationStage(earliestStage); -} - -void BattleInterface::executeAnimationStage(EAnimationEvents event) -{ - decltype(awaitingEvents) executingEvents; - - for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();) - { - if(it->event == event) - { - executingEvents.push_back(*it); - it = awaitingEvents.erase(it); - } - else - ++it; - } - for(const auto & event : executingEvents) - event.action(); -} - -void BattleInterface::onAnimationsStarted() -{ - ongoingAnimationsState.setn(true); -} - -void BattleInterface::onAnimationsFinished() -{ - ongoingAnimationsState.setn(false); -} - -void BattleInterface::waitForAnimations() -{ - { - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); - ongoingAnimationsState.waitUntil(false); - } - - assert(!hasAnimations()); - assert(awaitingEvents.empty()); - - if (!awaitingEvents.empty()) - { - logGlobal->error("Wait for animations finished but we still have awaiting events!"); - awaitingEvents.clear(); - } -} - -bool BattleInterface::hasAnimations() -{ - return ongoingAnimationsState.get(); -} - -void BattleInterface::checkForAnimations() -{ - assert(!hasAnimations()); - if(hasAnimations()) - logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!"); - - waitForAnimations(); -} - -void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action) -{ - awaitingEvents.push_back({action, event}); -} - -void BattleInterface::setBattleQueueVisibility(bool visible) -{ - windowObject->hideQueue(); - if(visible) - windowObject->showQueue(); -} - -void BattleInterface::setStickyHeroWindowsVisibility(bool visible) -{ - windowObject->hideStickyHeroWindows(); - if(visible) - windowObject->showStickyHeroWindows(); -} +/* + * BattleInterface.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 "BattleInterface.h" + +#include "BattleAnimationClasses.h" +#include "BattleActionsController.h" +#include "BattleInterfaceClasses.h" +#include "CreatureAnimation.h" +#include "BattleProjectileController.h" +#include "BattleEffectsController.h" +#include "BattleObstacleController.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleWindow.h" +#include "BattleStacksController.h" +#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 "../windows/CTutorialWindow.h" +#include "../render/Canvas.h" +#include "../adventureMap/AdventureMapInterface.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CondSh.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/CThreadHelper.h" + +BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, + std::shared_ptr att, + std::shared_ptr defen, + std::shared_ptr spectatorInt) + : attackingHeroInstance(hero1) + , defendingHeroInstance(hero2) + , attackerInt(att) + , defenderInt(defen) + , curInt(att) + , battleID(battleID) + , battleOpeningDelayActive(true) +{ + if(spectatorInt) + { + curInt = spectatorInt; + } + else if(!curInt) + { + //May happen when we are defending during network MP game -> attacker interface is just not present + curInt = defenderInt; + } + + //hot-seat -> check tactics for both players (defender may be local human) + if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist()) + tacticianInterface = attackerInt; + else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist()) + tacticianInterface = defenderInt; + + //if we found interface of player with tactics, then enter tactics mode + tacticsMode = static_cast(tacticianInterface); + + //initializing armies + this->army1 = army1; + this->army2 = army2; + + const CGTownInstance *town = getBattle()->battleGetDefendedTown(); + if(town && town->hasFort()) + siegeController.reset(new BattleSiegeController(*this, town)); + + windowObject = std::make_shared(*this); + projectilesController.reset(new BattleProjectileController(*this)); + stacksController.reset( new BattleStacksController(*this)); + actionsController.reset( new BattleActionsController(*this)); + effectsController.reset(new BattleEffectsController(*this)); + obstacleController.reset(new BattleObstacleController(*this)); + + adventureInt->onAudioPaused(); + ongoingAnimationsState.set(true); + + GH.windows().pushWindow(windowObject); + windowObject->blockUI(true); + windowObject->updateQueue(); + + playIntroSoundAndUnlockInterface(); +} + +void BattleInterface::playIntroSoundAndUnlockInterface() +{ + auto onIntroPlayed = [this]() + { + if(LOCPLINT->battleInt) + onIntroSoundPlayed(); + }; + + int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); + if (battleIntroSoundChannel != -1) + { + CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); + + if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool()) + openingEnd(); + } + else // failed to play sound + { + onIntroSoundPlayed(); + } +} + +bool BattleInterface::openingPlaying() +{ + return battleOpeningDelayActive; +} + +void BattleInterface::onIntroSoundPlayed() +{ + if (openingPlaying()) + openingEnd(); + + CCS->musich->playMusicFromSet("battle", true, true); +} + +void BattleInterface::openingEnd() +{ + assert(openingPlaying()); + if (!openingPlaying()) + return; + + onAnimationsFinished(); + if(tacticsMode) + tacticNextStack(nullptr); + activateStack(); + battleOpeningDelayActive = false; + + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_BATTLE); +} + +BattleInterface::~BattleInterface() +{ + CPlayerInterface::battleInt = nullptr; + + if (adventureInt) + adventureInt->onAudioResumed(); + + awaitingEvents.clear(); + onAnimationsFinished(); +} + +void BattleInterface::redrawBattlefield() +{ + fieldController->redrawBackgroundWithHexes(); + GH.windows().totalRedraw(); +} + +void BattleInterface::stackReset(const CStack * stack) +{ + stacksController->stackReset(stack); +} + +void BattleInterface::stackAdded(const CStack * stack) +{ + stacksController->stackAdded(stack, false); +} + +void BattleInterface::stackRemoved(uint32_t stackID) +{ + stacksController->stackRemoved(stackID); + fieldController->redrawBackgroundWithHexes(); + windowObject->updateQueue(); +} + +void BattleInterface::stackActivated(const CStack *stack) +{ + stacksController->stackActivated(stack); +} + +void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport) +{ + if (teleport) + stacksController->stackTeleported(stack, destHex, distance); + else + stacksController->stackMoved(stack, destHex, distance); +} + +void BattleInterface::stacksAreAttacked(std::vector attackedInfos) +{ + stacksController->stacksAreAttacked(attackedInfos); + + std::array killedBySide = {0, 0}; + + for(const StackAttackedInfo & attackedInfo : attackedInfos) + { + ui8 side = attackedInfo.defender->unitSide(); + killedBySide.at(side) += attackedInfo.amountKilled; + } + + for(ui8 side = 0; side < 2; side++) + { + if(killedBySide.at(side) > killedBySide.at(1-side)) + setHeroAnimation(side, EHeroAnimType::DEFEAT); + else if(killedBySide.at(side) < killedBySide.at(1-side)) + setHeroAnimation(side, EHeroAnimType::VICTORY); + } +} + +void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) +{ + stacksController->stackAttacking(attackInfo); +} + +void BattleInterface::newRoundFirst() +{ + waitForAnimations(); +} + +void BattleInterface::newRound() +{ + console->addText(CGI->generaltexth->allTexts[412]); +} + +void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) +{ + const CStack * actor = nullptr; + if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) + { + actor = stacksController->getActiveStack(); + } + + auto side = getBattle()->playerToSide(curInt->playerID); + if(!side) + { + logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); + return; + } + + BattleAction ba; + ba.side = side.value(); + ba.actionType = action; + ba.aimToHex(tile); + ba.spell = spell; + + sendCommand(ba, actor); +} + +void BattleInterface::sendCommand(BattleAction command, const CStack * actor) +{ + command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2); + + if(!tacticsMode) + { + logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); + stacksController->setActiveStack(nullptr); + curInt->cb->battleMakeUnitAction(battleID, command); + } + else + { + curInt->cb->battleMakeTacticAction(battleID, command); + stacksController->setActiveStack(nullptr); + //next stack will be activated when action ends + } + CCS->curh->set(Cursor::Combat::POINTER); +} + +const CGHeroInstance * BattleInterface::getActiveHero() +{ + const CStack *attacker = stacksController->getActiveStack(); + if(!attacker) + { + return nullptr; + } + + if(attacker->unitSide() == BattleSide::ATTACKER) + { + return attackingHeroInstance; + } + + return defendingHeroInstance; +} + +void BattleInterface::stackIsCatapulting(const CatapultAttack & ca) +{ + if (siegeController) + siegeController->stackIsCatapulting(ca); +} + +void BattleInterface::gateStateChanged(const EGateState state) +{ + if (siegeController) + siegeController->gateStateChanged(state); +} + +void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) +{ + checkForAnimations(); + stacksController->setActiveStack(nullptr); + + CCS->curh->set(Cursor::Map::POINTER); + curInt->waitWhileDialog(); + + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) + { + curInt->cb->selectionMade(0, queryID); + windowObject->close(); + return; + } + + auto wnd = std::make_shared(br, *(this->curInt)); + wnd->resultCallback = [=](ui32 selection) + { + curInt->cb->selectionMade(selection, queryID); + }; + GH.windows().pushWindow(wnd); + + curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 + CPlayerInterface::battleInt = nullptr; +} + +void BattleInterface::spellCast(const BattleSpellCast * sc) +{ + // Do not deactivate anything in tactics mode + // This is battlefield setup spells + if(!tacticsMode) + { + windowObject->blockUI(true); + + // Disable current active stack duing the cast + // Store the current activeStack to stackToActivate + stacksController->deactivateStack(); + } + + CCS->curh->set(Cursor::Combat::BLOCKED); + + const SpellID spellID = sc->spellID; + const CSpell * spell = spellID.toSpell(); + auto targetedTile = sc->tile; + + assert(spell); + if(!spell) + return; + + const AudioPath & castSoundPath = spell->getCastSound(); + + if (!castSoundPath.empty()) + { + auto group = spell->animationInfo.projectile.empty() ? + EAnimationEvents::HIT: + EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning + + addToAnimationStage(group, [=]() { + CCS->soundh->playSound(castSoundPath); + }); + } + + if ( sc->activeCast ) + { + const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack); + + if(casterStack != nullptr ) + { + addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() + { + stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); + displaySpellCast(spell, casterStack->getPosition()); + }); + } + else + { + auto hero = sc->side ? defendingHero : attackingHero; + assert(hero); + + addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() + { + stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); + }); + } + } + + addToAnimationStage(EAnimationEvents::HIT, [=](){ + displaySpellHit(spell, targetedTile); + }); + + //queuing affect animation + for(auto & elem : sc->affectedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + if(stack) + { + addToAnimationStage(EAnimationEvents::HIT, [=](){ + displaySpellEffect(spell, stack->getPosition()); + }); + } + } + + for(auto & elem : sc->reflectedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + addToAnimationStage(EAnimationEvents::HIT, [=](){ + effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); + }); + } + + if (!sc->resistedCres.empty()) + { + addToAnimationStage(EAnimationEvents::HIT, [=](){ + CCS->soundh->playSound(AudioPath::builtin("MAGICRES")); + }); + } + + for(auto & elem : sc->resistedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + addToAnimationStage(EAnimationEvents::HIT, [=](){ + effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); + }); + } + + //mana absorption + if (sc->manaGained > 0) + { + Point leftHero = Point(15, 30); + Point rightHero = Point(755, 30); + bool 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)); + }); + } + + // animations will be executed by spell effects +} + +void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) +{ + if(stacksController->getActiveStack() != nullptr) + fieldController->redrawBackgroundWithHexes(); +} + +void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) +{ + if(side == BattleSide::ATTACKER) + { + if(attackingHero) + attackingHero->setPhase(phase); + } + else + { + if(defendingHero) + defendingHero->setPhase(phase); + } +} + +void BattleInterface::displayBattleLog(const std::vector & battleLog) +{ + for(const auto & line : battleLog) + { + std::string formatted = line.toString(); + boost::algorithm::trim(formatted); + appendBattleLog(formatted); + } +} + +void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) +{ + for(const CSpell::TAnimation & animation : q) + { + if(animation.pause > 0) + stacksController->addNewAnim(new DummyAnimation(*this, animation.pause)); + + if (!animation.effectName.empty()) + { + const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false); + + if (destStack) + stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); + } + + if(!animation.resourceName.empty()) + { + int flags = 0; + + if (isHit) + flags |= EffectAnimation::FORCE_ON_TOP; + + if (animation.verticalPosition == VerticalPosition::BOTTOM) + flags |= EffectAnimation::ALIGN_TO_BOTTOM; + + if (!destinationTile.isValid()) + flags |= EffectAnimation::SCREEN_FILL; + + if (!destinationTile.isValid()) + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); + else + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); + } + } +} + +void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false); +} + +void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false); +} + +void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true); +} + +CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const +{ + return curInt.get(); +} + +void BattleInterface::trySetActivePlayer( PlayerColor player ) +{ + if ( attackerInt && attackerInt->playerID == player ) + curInt = attackerInt; + + if ( defenderInt && defenderInt->playerID == player ) + curInt = defenderInt; +} + +void BattleInterface::activateStack() +{ + stacksController->activateStack(); + + const CStack * s = stacksController->getActiveStack(); + if(!s) + return; + + windowObject->updateQueue(); + windowObject->blockUI(false); + fieldController->redrawBackgroundWithHexes(); + actionsController->activateStack(); + GH.fakeMouseMove(); +} + +bool BattleInterface::makingTurn() const +{ + return stacksController->getActiveStack() != nullptr; +} + +BattleID BattleInterface::getBattleID() const +{ + return battleID; +} + +std::shared_ptr BattleInterface::getBattle() const +{ + return curInt->cb->getBattle(battleID); +} + +void BattleInterface::endAction(const BattleAction &action) +{ + // it is possible that tactics mode ended while opening music is still playing + waitForAnimations(); + + const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber); + + // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast + activateStack(); + + stacksController->endAction(action); + windowObject->updateQueue(); + + //stack ended movement in tactics phase -> select the next one + if (tacticsMode) + tacticNextStack(stack); + + //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed + if(action.actionType == EActionType::HERO_SPELL) + fieldController->redrawBackgroundWithHexes(); +} + +void BattleInterface::appendBattleLog(const std::string & newEntry) +{ + console->addText(newEntry); +} + +void BattleInterface::startAction(const BattleAction & action) +{ + if(action.actionType == EActionType::END_TACTIC_PHASE) + { + windowObject->tacticPhaseEnded(); + return; + } + + stacksController->startAction(action); + + if (!action.isUnitAction()) + return; + + assert(getBattle()->battleGetStackByID(action.stackNumber)); + windowObject->updateQueue(); + effectsController->startAction(action); +} + +void BattleInterface::tacticPhaseEnd() +{ + stacksController->setActiveStack(nullptr); + tacticsMode = false; + + auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); + auto action = BattleAction::makeEndOFTacticPhase(*side); + + tacticianInterface->cb->battleMakeTacticAction(battleID, action); +} + +static bool immobile(const CStack *s) +{ + return !s->speed(0, true); //should bound stacks be immobile? +} + +void BattleInterface::tacticNextStack(const CStack * current) +{ + if (!current) + current = stacksController->getActiveStack(); + + //no switching stacks when the current one is moving + checkForAnimations(); + + TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE); + vstd::erase_if (stacksOfMine, &immobile); + if (stacksOfMine.empty()) + { + tacticPhaseEnd(); + return; + } + + auto it = vstd::find(stacksOfMine, current); + if (it != stacksOfMine.end() && ++it != stacksOfMine.end()) + stackActivated(*it); + else + stackActivated(stacksOfMine.front()); + +} + +void BattleInterface::obstaclePlaced(const std::vector> oi) +{ + obstacleController->obstaclePlaced(oi); +} + +void BattleInterface::obstacleRemoved(const std::vector & obstacles) +{ + obstacleController->obstacleRemoved(obstacles); +} + +const CGHeroInstance *BattleInterface::currentHero() const +{ + if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID) + return attackingHeroInstance; + + if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID) + return defendingHeroInstance; + + return nullptr; +} + +InfoAboutHero BattleInterface::enemyHero() const +{ + InfoAboutHero ret; + if (attackingHeroInstance->tempOwner == curInt->playerID) + curInt->cb->getHeroInfo(defendingHeroInstance, ret); + else + curInt->cb->getHeroInfo(attackingHeroInstance, ret); + + return ret; +} + +void BattleInterface::requestAutofightingAIToTakeAction() +{ + assert(curInt->isAutoFightOn); + + if(getBattle()->battleIsFinished()) + { + return; // battle finished with spellcast + } + + if (tacticsMode) + { + // Always end tactics mode. Player interface is blocked currently, so it's not possible that + // the AI can take any action except end tactics phase (AI actions won't be triggered) + //TODO implement the possibility that the AI will be triggered for further actions + //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? + tacticPhaseEnd(); + stacksController->setActiveStack(nullptr); + } + else + { + const CStack* activeStack = stacksController->getActiveStack(); + + // If enemy is moving, activeStack can be null + if (activeStack) + { + stacksController->setActiveStack(nullptr); + + // 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([this, activeStack]() + { + setThreadName("autofightingAI"); + curInt->autofightingAI->activeStack(battleID, activeStack); + }); + aiThread.detach(); + } + } +} + +void BattleInterface::castThisSpell(SpellID spellID) +{ + actionsController->castThisSpell(spellID); +} + +void BattleInterface::executeStagedAnimations() +{ + EAnimationEvents earliestStage = EAnimationEvents::COUNT; + + for(const auto & event : awaitingEvents) + earliestStage = std::min(earliestStage, event.event); + + if(earliestStage != EAnimationEvents::COUNT) + executeAnimationStage(earliestStage); +} + +void BattleInterface::executeAnimationStage(EAnimationEvents event) +{ + decltype(awaitingEvents) executingEvents; + + for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();) + { + if(it->event == event) + { + executingEvents.push_back(*it); + it = awaitingEvents.erase(it); + } + else + ++it; + } + for(const auto & event : executingEvents) + event.action(); +} + +void BattleInterface::onAnimationsStarted() +{ + ongoingAnimationsState.setn(true); +} + +void BattleInterface::onAnimationsFinished() +{ + ongoingAnimationsState.setn(false); +} + +void BattleInterface::waitForAnimations() +{ + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + ongoingAnimationsState.waitUntil(false); + } + + assert(!hasAnimations()); + assert(awaitingEvents.empty()); + + if (!awaitingEvents.empty()) + { + logGlobal->error("Wait for animations finished but we still have awaiting events!"); + awaitingEvents.clear(); + } +} + +bool BattleInterface::hasAnimations() +{ + return ongoingAnimationsState.get(); +} + +void BattleInterface::checkForAnimations() +{ + assert(!hasAnimations()); + if(hasAnimations()) + logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!"); + + waitForAnimations(); +} + +void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action) +{ + awaitingEvents.push_back({action, event}); +} + +void BattleInterface::setBattleQueueVisibility(bool visible) +{ + windowObject->hideQueue(); + if(visible) + windowObject->showQueue(); +} + +void BattleInterface::setStickyHeroWindowsVisibility(bool visible) +{ + windowObject->hideStickyHeroWindows(); + if(visible) + windowObject->showStickyHeroWindows(); +} diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index fabd68984..31e64ef29 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -1,223 +1,230 @@ -/* - * BattleInterface.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 "BattleConstants.h" -#include "../gui/CIntObject.h" -#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation -#include "../../lib/CondSh.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCreatureSet; -class CGHeroInstance; -class CStack; -struct BattleResult; -struct BattleSpellCast; -struct CObstacleInstance; -struct SetStackEffect; -class BattleAction; -class CGTownInstance; -struct CatapultAttack; -struct BattleTriggerEffect; -struct BattleHex; -struct InfoAboutHero; -class ObstacleChanges; - -VCMI_LIB_NAMESPACE_END - -class BattleHero; -class Canvas; -class BattleResultWindow; -class StackQueue; -class CPlayerInterface; -class CAnimation; -struct BattleEffect; -class IImage; -class StackQueue; - -class BattleProjectileController; -class BattleSiegeController; -class BattleObstacleController; -class BattleFieldController; -class BattleRenderer; -class BattleWindow; -class BattleStacksController; -class BattleActionsController; -class BattleEffectsController; -class BattleConsole; - -/// Small struct which contains information about the id of the attacked stack, the damage dealt,... -struct StackAttackedInfo -{ - const CStack *defender; - const CStack *attacker; - - int64_t damageDealt; - uint32_t amountKilled; - SpellID spellEffect; - - bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack - bool killed; //if true, stack has been killed - bool rebirth; //if true, play rebirth animation after all - bool cloneKilled; - bool fireShield; -}; - -struct StackAttackInfo -{ - const CStack *attacker; - const CStack *defender; - std::vector< const CStack *> secondaryDefender; - - SpellID spellEffect; - BattleHex tile; - - bool indirectAttack; - bool lucky; - bool unlucky; - bool deathBlow; - bool lifeDrain; -}; - -/// Main class for battles, responsible for relaying information from server to various battle entities -class BattleInterface -{ - using AwaitingAnimationAction = std::function; - - struct AwaitingAnimationEvents { - AwaitingAnimationAction action; - EAnimationEvents event; - }; - - /// Conditional variables that are set depending on ongoing animations on the battlefield - CondSh ongoingAnimationsState; - - /// List of events that are waiting to be triggered - std::vector awaitingEvents; - - /// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players - std::shared_ptr tacticianInterface; - - /// attacker interface, not null if attacker is human in our vcmiclient - std::shared_ptr attackerInt; - - /// defender interface, not null if attacker is human in our vcmiclient - std::shared_ptr defenderInt; - - /// if set to true, battle is still starting and waiting for intro sound to end / key press from player - bool battleOpeningDelayActive; - - void playIntroSoundAndUnlockInterface(); - void onIntroSoundPlayed(); -public: - /// copy of initial armies (for result window) - const CCreatureSet *army1; - const CCreatureSet *army2; - - std::shared_ptr windowObject; - std::shared_ptr console; - - /// currently active player interface - std::shared_ptr curInt; - - const CGHeroInstance *attackingHeroInstance; - const CGHeroInstance *defendingHeroInstance; - - bool tacticsMode; - - std::unique_ptr projectilesController; - std::unique_ptr siegeController; - std::unique_ptr obstacleController; - std::unique_ptr fieldController; - std::unique_ptr stacksController; - std::unique_ptr actionsController; - std::unique_ptr effectsController; - - std::shared_ptr attackingHero; - std::shared_ptr defendingHero; - - bool openingPlaying(); - void openingEnd(); - - bool makingTurn() const; - - BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); - ~BattleInterface(); - - void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player - void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all - void requestAutofightingAIToTakeAction(); - - void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1); - void sendCommand(BattleAction command, const CStack * actor = nullptr); - - const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell - - void showInterface(Canvas & to); - - void setHeroAnimation(ui8 side, EHeroAnimType phase); - - void executeSpellCast(); //called when a hero casts a spell - - void appendBattleLog(const std::string & newEntry); - - void redrawBattlefield(); //refresh GUI after changing stack range / grid settings - CPlayerInterface *getCurrentPlayerInterface() const; - - void tacticNextStack(const CStack *current); - void tacticPhaseEnd(); - - void setBattleQueueVisibility(bool visible); - void setStickyHeroWindowsVisibility(bool visible); - - void executeStagedAnimations(); - void executeAnimationStage( EAnimationEvents event); - void onAnimationsStarted(); - void onAnimationsFinished(); - void waitForAnimations(); - bool hasAnimations(); - void checkForAnimations(); - void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); - - //call-ins - void startAction(const BattleAction* action); - void stackReset(const CStack * stack); - void stackAdded(const CStack * stack); //new stack appeared on battlefield - void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled - void stackActivated(const CStack *stack); //active stack has been changed - void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex - 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( int round ); - void newRound(int number); //caled when round is ended; number is the number of round - 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 - void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks - void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook - - void displayBattleLog(const std::vector & battleLog); - - void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); - void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation - void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - - void endAction(const BattleAction* action); - - void obstaclePlaced(const std::vector> oi); - void obstacleRemoved(const std::vector & obstacles); - - void gateStateChanged(const EGateState state); - - const CGHeroInstance *currentHero() const; - InfoAboutHero enemyHero() const; -}; +/* + * BattleInterface.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 "BattleConstants.h" +#include "../gui/CIntObject.h" +#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation +#include "../../lib/CondSh.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCreatureSet; +class CGHeroInstance; +class CStack; +struct BattleResult; +struct BattleSpellCast; +struct CObstacleInstance; +struct SetStackEffect; +class BattleAction; +class CGTownInstance; +struct CatapultAttack; +struct BattleTriggerEffect; +struct BattleHex; +struct InfoAboutHero; +class ObstacleChanges; +class CPlayerBattleCallback; + +VCMI_LIB_NAMESPACE_END + +class BattleHero; +class Canvas; +class BattleResultWindow; +class StackQueue; +class CPlayerInterface; +class CAnimation; +struct BattleEffect; +class IImage; +class StackQueue; + +class BattleProjectileController; +class BattleSiegeController; +class BattleObstacleController; +class BattleFieldController; +class BattleRenderer; +class BattleWindow; +class BattleStacksController; +class BattleActionsController; +class BattleEffectsController; +class BattleConsole; + +/// Small struct which contains information about the id of the attacked stack, the damage dealt,... +struct StackAttackedInfo +{ + const CStack *defender; + const CStack *attacker; + + int64_t damageDealt; + uint32_t amountKilled; + SpellID spellEffect; + + bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack + bool killed; //if true, stack has been killed + bool rebirth; //if true, play rebirth animation after all + bool cloneKilled; + bool fireShield; +}; + +struct StackAttackInfo +{ + const CStack *attacker; + const CStack *defender; + std::vector< const CStack *> secondaryDefender; + + SpellID spellEffect; + BattleHex tile; + + bool indirectAttack; + bool lucky; + bool unlucky; + bool deathBlow; + bool lifeDrain; +}; + +/// Main class for battles, responsible for relaying information from server to various battle entities +class BattleInterface +{ + using AwaitingAnimationAction = std::function; + + struct AwaitingAnimationEvents { + AwaitingAnimationAction action; + EAnimationEvents event; + }; + + /// Conditional variables that are set depending on ongoing animations on the battlefield + CondSh ongoingAnimationsState; + + /// List of events that are waiting to be triggered + std::vector awaitingEvents; + + /// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players + std::shared_ptr tacticianInterface; + + /// attacker interface, not null if attacker is human in our vcmiclient + std::shared_ptr attackerInt; + + /// defender interface, not null if attacker is human in our vcmiclient + std::shared_ptr defenderInt; + + /// if set to true, battle is still starting and waiting for intro sound to end / key press from player + bool battleOpeningDelayActive; + + /// ID of ongoing battle + BattleID battleID; + + void playIntroSoundAndUnlockInterface(); + void onIntroSoundPlayed(); +public: + /// copy of initial armies (for result window) + const CCreatureSet *army1; + const CCreatureSet *army2; + + std::shared_ptr windowObject; + std::shared_ptr console; + + /// currently active player interface + std::shared_ptr curInt; + + const CGHeroInstance *attackingHeroInstance; + const CGHeroInstance *defendingHeroInstance; + + bool tacticsMode; + + std::unique_ptr projectilesController; + std::unique_ptr siegeController; + std::unique_ptr obstacleController; + std::unique_ptr fieldController; + std::unique_ptr stacksController; + std::unique_ptr actionsController; + std::unique_ptr effectsController; + + std::shared_ptr attackingHero; + std::shared_ptr defendingHero; + + bool openingPlaying(); + void openingEnd(); + + bool makingTurn() const; + + BattleID getBattleID() const; + std::shared_ptr getBattle() const; + + BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); + ~BattleInterface(); + + void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player + void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all + void requestAutofightingAIToTakeAction(); + + void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE); + void sendCommand(BattleAction command, const CStack * actor = nullptr); + + const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell + + void showInterface(Canvas & to); + + void setHeroAnimation(ui8 side, EHeroAnimType phase); + + void executeSpellCast(); //called when a hero casts a spell + + void appendBattleLog(const std::string & newEntry); + + void redrawBattlefield(); //refresh GUI after changing stack range / grid settings + CPlayerInterface *getCurrentPlayerInterface() const; + + void tacticNextStack(const CStack *current); + void tacticPhaseEnd(); + + void setBattleQueueVisibility(bool visible); + void setStickyHeroWindowsVisibility(bool visible); + + void executeStagedAnimations(); + void executeAnimationStage( EAnimationEvents event); + void onAnimationsStarted(); + void onAnimationsFinished(); + void waitForAnimations(); + bool hasAnimations(); + void checkForAnimations(); + void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); + + //call-ins + void startAction(const BattleAction & action); + void stackReset(const CStack * stack); + void stackAdded(const CStack * stack); //new stack appeared on battlefield + void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled + void stackActivated(const CStack *stack); //active stack has been changed + void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex + 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 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 + void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks + void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook + + void displayBattleLog(const std::vector & battleLog); + + void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); + void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation + void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation + void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation + + void endAction(const BattleAction & action); + + void obstaclePlaced(const std::vector> oi); + void obstacleRemoved(const std::vector & obstacles); + + void gateStateChanged(const EGateState state); + + const CGHeroInstance *currentHero() const; + InfoAboutHero enemyHero() const; +}; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 7f2f0647d..8a5ce5261 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -1,849 +1,921 @@ -/* - * BattleInterfaceClasses.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 "BattleInterfaceClasses.h" - -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleRenderer.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#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 "../render/Canvas.h" -#include "../render/IImage.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" -#include "../windows/CMessage.h" -#include "../windows/CSpellWindow.h" -#include "../render/CAnimation.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/NetPacks.h" -#include "../../lib/StartInfo.h" -#include "../../lib/CondSh.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/TextOperations.h" - -void BattleConsole::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - Point line1 (pos.x + pos.w/2, pos.y + 8); - Point line2 (pos.x + pos.w/2, pos.y + 24); - - auto visibleText = getVisibleText(); - - if(visibleText.size() > 0) - to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]); - - if(visibleText.size() > 1) - to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]); -} - -std::vector BattleConsole::getVisibleText() -{ - // high priority texts that hide battle log entries - for(const auto & text : {consoleText, hoverText}) - { - if (text.empty()) - continue; - - auto result = CMessage::breakText(text, pos.w, FONT_SMALL); - - if(result.size() > 2) - result.resize(2); - return result; - } - - // log is small enough to fit entirely - display it as such - if (logEntries.size() < 3) - return logEntries; - - return { logEntries[scrollPosition - 1], logEntries[scrollPosition] }; -} - -std::vector BattleConsole::splitText(const std::string &text) -{ - std::vector lines; - std::vector output; - - boost::split(lines, text, boost::is_any_of("\n")); - - for(const auto & line : lines) - { - if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) - { - output.push_back(line); - } - else - { - std::vector substrings = CMessage::breakText(line, pos.w, FONT_SMALL); - output.insert(output.end(), substrings.begin(), substrings.end()); - } - } - return output; -} - -bool BattleConsole::addText(const std::string & text) -{ - logGlobal->trace("CBattleConsole message: %s", text); - - auto newLines = splitText(text); - - logEntries.insert(logEntries.end(), newLines.begin(), newLines.end()); - scrollPosition = (int)logEntries.size()-1; - redraw(); - return true; -} -void BattleConsole::scrollUp(ui32 by) -{ - if(scrollPosition > static_cast(by)) - scrollPosition -= by; - redraw(); -} - -void BattleConsole::scrollDown(ui32 by) -{ - if(scrollPosition + by < logEntries.size()) - scrollPosition += by; - redraw(); -} - -BattleConsole::BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size) - : scrollPosition(-1) - , enteringText(false) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += objectPos; - pos.w = size.x; - pos.h = size.y; - - background = std::make_shared(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 ); -} - -void BattleConsole::deactivate() -{ - if (enteringText) - LOCPLINT->cingconsole->endEnteringText(false); - - CIntObject::deactivate(); -} - -void BattleConsole::setEnteringMode(bool on) -{ - consoleText.clear(); - - if (on) - { - assert(enteringText == false); - GH.startTextInput(pos); - } - else - { - assert(enteringText == true); - GH.stopTextInput(); - } - enteringText = on; - redraw(); -} - -void BattleConsole::setEnteredText(const std::string & text) -{ - assert(enteringText == true); - consoleText = text; - redraw(); -} - -void BattleConsole::write(const std::string & Text) -{ - hoverText = Text; - redraw(); -} - -void BattleConsole::clearIfMatching(const std::string & Text) -{ - if (hoverText == Text) - clear(); -} - -void BattleConsole::clear() -{ - write({}); -} - -const CGHeroInstance * BattleHero::instance() -{ - return hero; -} - -void BattleHero::tick(uint32_t msPassed) -{ - size_t groupIndex = static_cast(phase); - - float timePassed = msPassed / 1000.f; - - flagCurrentFrame += currentSpeed * timePassed; - currentFrame += currentSpeed * timePassed; - - if(flagCurrentFrame >= flagAnimation->size(0)) - flagCurrentFrame -= flagAnimation->size(0); - - if(currentFrame >= animation->size(groupIndex)) - { - currentFrame -= animation->size(groupIndex); - switchToNextPhase(); - } -} - -void BattleHero::render(Canvas & canvas) -{ - size_t groupIndex = static_cast(phase); - - auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true); - auto heroFrame = animation->getImage(currentFrame, groupIndex, true); - - Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2; - Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2; - - if(defender) - flagPosition += Point(-4, -41); - else - flagPosition += Point(4, -41); - - canvas.draw(flagFrame, flagPosition); - canvas.draw(heroFrame, heroPosition); -} - -void BattleHero::pause() -{ - currentSpeed = 0.f; -} - -void BattleHero::play() -{ - //H3 speed: 10 fps ( 100 ms per frame) - currentSpeed = 10.f; -} - -float BattleHero::getFrame() const -{ - return currentFrame; -} - -void BattleHero::collectRenderableObjects(BattleRenderer & renderer) -{ - auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0); - - renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas) - { - render(canvas); - }); -} - -void BattleHero::onPhaseFinished(const std::function & callback) -{ - phaseFinishedCallback = callback; -} - -void BattleHero::setPhase(EHeroAnimType newPhase) -{ - nextPhase = newPhase; - switchToNextPhase(); //immediately switch to next phase and then restore idling phase - nextPhase = EHeroAnimType::HOLDING; -} - -void BattleHero::heroLeftClicked() -{ - if(owner.actionsController->spellcastingModeActive()) //we are casting a spell - return; - - if(!hero || !owner.makingTurn()) - return; - - if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions - { - CCS->curh->set(Cursor::Map::POINTER); - GH.windows().createAndPushWindow(hero, owner.getCurrentPlayerInterface()); - } -} - -void BattleHero::heroRightClicked() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool()) - return; - - Point windowPosition; - if(GH.screenDimensions().x < 1000) - { - windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79; - windowPosition.y = owner.fieldController->pos.y + 135; - } - else - { - windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15; - windowPosition.y = owner.fieldController->pos.y; - } - - InfoAboutHero targetHero; - if(owner.makingTurn() || settings["session"]["spectate"].Bool()) - { - auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance; - targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); - GH.windows().createAndPushWindow(targetHero, &windowPosition); - } -} - -void BattleHero::switchToNextPhase() -{ - phase = nextPhase; - currentFrame = 0.f; - - auto copy = phaseFinishedCallback; - phaseFinishedCallback.clear(); - copy(); -} - -BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender): - defender(defender), - hero(hero), - owner(owner), - phase(EHeroAnimType::HOLDING), - nextPhase(EHeroAnimType::HOLDING), - currentSpeed(0.f), - currentFrame(0.f), - flagCurrentFrame(0.f) -{ - std::string animationPath; - - if(!hero->type->battleImage.empty()) - animationPath = hero->type->battleImage; - else - if(hero->gender == EHeroGender::FEMALE) - animationPath = hero->type->heroClass->imageBattleFemale; - else - animationPath = hero->type->heroClass->imageBattleMale; - - animation = std::make_shared(animationPath); - animation->preload(); - - pos.w = 64; - pos.h = 136; - pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0); - pos.y = owner.fieldController->pos.y; - - if(defender) - animation->verticalFlip(); - - if(defender) - flagAnimation = std::make_shared("CMFLAGR"); - else - flagAnimation = std::make_shared("CMFLAGL"); - - flagAnimation->preload(); - flagAnimation->playerColored(hero->tempOwner); - - switchToNextPhase(); - play(); - - addUsedEvents(TIME); -} - -HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground) - : CIntObject(0) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if (position != nullptr) - moveTo(*position); - - if(initializeBackground) - { - background = std::make_shared("CHRPOP"); - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - background->colorize(hero.owner); - } - - initializeData(hero); -} - -void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto attack = hero.details->primskills[0]; - auto defense = hero.details->primskills[1]; - auto power = hero.details->primskills[2]; - auto knowledge = hero.details->primskills[3]; - auto morale = hero.details->morale; - auto luck = hero.details->luck; - auto currentSpellPoints = hero.details->mana; - auto maxSpellPoints = hero.details->manaLimit; - - icons.push_back(std::make_shared("PortraitsLarge", hero.portrait, 0, 10, 6)); - - //primary stats - labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); - labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); - labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":")); - labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":")); - - labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack))); - labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense))); - labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power))); - labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge))); - - //morale+luck - labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); - labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); - - icons.push_back(std::make_shared("IMRL22", morale + 3, 0, 47, 131)); - icons.push_back(std::make_shared("ILCK22", luck + 3, 0, 47, 143)); - - //spell points - labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); - labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); -} - -void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo) -{ - icons.clear(); - labels.clear(); - - initializeData(updatedInfo); -} - -void HeroInfoBasicPanel::show(Canvas & to) -{ - showAll(to); - CIntObject::show(to); -} - -HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) - : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, "CHRPOP") -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if (position != nullptr) - moveTo(*position); - - background->colorize(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) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - background = std::make_shared("CPRESULT"); - background->colorize(owner.playerID); - pos = center(background->pos); - - exit = std::make_shared(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); - exit->setBorderColor(Colors::METALLIC_GOLD); - - if(allowReplay) - { - repeat = std::make_shared(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); - repeat->setBorderColor(Colors::METALLIC_GOLD); - 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 - { - labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); - } - else - { - labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); - } - - if(br.winner == 1) - { - labels.push_back(std::make_shared(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); - } - else - { - labels.push_back(std::make_shared(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); - } - - labels.push_back(std::make_shared(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407])); - labels.push_back(std::make_shared(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408])); - labels.push_back(std::make_shared(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409])); - - std::string sideNames[2] = {"N/A", "N/A"}; - - for(int i = 0; i < 2; i++) - { - auto heroInfo = owner.cb->battleGetHeroInfo(i); - const int xs[] = {21, 392}; - - if(heroInfo.portrait >= 0) //attacking hero - { - icons.push_back(std::make_shared("PortraitsLarge", heroInfo.portrait, 0, xs[i], 38)); - sideNames[i] = heroInfo.name; - } - else - { - auto stacks = owner.cb->battleGetAllStacks(); - vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison - { - return stack->unitSide() != i || !stack->base; - }); - - auto best = vstd::maxElementByFun(stacks, [](const CStack * stack) - { - return stack->unitType()->getAIValue(); - }); - - if(best != stacks.end()) //should be always but to be safe... - { - icons.push_back(std::make_shared("TWCRPORT", (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); - sideNames[i] = (*best)->unitType()->getNamePluralTranslated(); - } - } - } - - //printing attacker and defender's names - labels.push_back(std::make_shared(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0])); - 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) - { - 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])); - } - 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; - for(auto & elem : br.casualties[step]) - { - auto creature = CGI->creatures()->getByIndex(elem.first); - if (creature->getId() == CreatureID::ARROW_TOWERS ) - continue; // do not show destroyed towers in battle results - - icons.push_back(std::make_shared("CPRSMALL", creature->getIconIndex(), 0, xPos, yPos)); - std::ostringstream amount; - amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); - xPos += 42; - } - } - } - //printing result description - bool weAreAttacker = !(owner.cb->battleGetMySide()); - if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won - { - int text = 304; - switch(br.result) - { - case BattleResult::NORMAL: - break; - case BattleResult::ESCAPE: - text = 303; - break; - case BattleResult::SURRENDER: - text = 302; - break; - default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; - } - - CCS->musich->playMusic("Music/Win Battle", false, true); - CCS->videoh->open("WIN3.BIK"); - std::string str = CGI->generaltexth->allTexts[text]; - - const CGHeroInstance * ourHero = owner.cb->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])); - } - - description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - else // we lose - { - int text = 311; - std::string musicName = "Music/LoseCombat"; - std::string videoName = "LBSTART.BIK"; - switch(br.result) - { - case BattleResult::NORMAL: - break; - case BattleResult::ESCAPE: - musicName = "Music/Retreat Battle"; - videoName = "RTSTART.BIK"; - text = 310; - break; - case BattleResult::SURRENDER: - musicName = "Music/Surrender Battle"; - videoName = "SURRENDER.BIK"; - text = 309; - break; - default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; - } - CCS->musich->playMusic(musicName, false, true); - CCS->videoh->open(videoName); - - labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); - } -} - -void BattleResultWindow::activate() -{ - owner.showingDialog->set(true); - CIntObject::activate(); -} - -void BattleResultWindow::show(Canvas & to) -{ - CIntObject::show(to); - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); -} - -void BattleResultWindow::buttonPressed(int button) -{ - if (resultCallback) - resultCallback(button); - - CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon - - close(); - - if(GH.windows().topWindow()) - GH.windows().popWindows(1); //pop battle interface if present - - //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->setn(false); - CCS->videoh->close(); -} - -void BattleResultWindow::bExitf() -{ - buttonPressed(0); -} - -void BattleResultWindow::bRepeatf() -{ - buttonPressed(1); -} - -StackQueue::StackQueue(bool Embedded, BattleInterface & owner) - : embedded(Embedded), - owner(owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(embedded) - { - pos.w = QUEUE_SIZE * 41; - pos.h = 49; - pos.x += parent->pos.w/2 - pos.w/2; - pos.y += 10; - - icons = std::make_shared("CPRSMALL"); - stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); - } - else - { - pos.w = 800; - pos.h = 85; - pos.x += 0; - pos.y -= pos.h; - - background = std::make_shared("DIBOXBCK", Rect(0, 0, pos.w, pos.h)); - - icons = std::make_shared("TWCRPORT"); - stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); - //TODO: where use big icons? - //stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESBIG"); - } - stateIcons->preload(); - - stackBoxes.resize(QUEUE_SIZE); - for (int i = 0; i < stackBoxes.size(); i++) - { - stackBoxes[i] = std::make_shared(this); - stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0)); - } -} - -void StackQueue::show(Canvas & to) -{ - if (embedded) - showAll(to); - CIntObject::show(to); -} - -void StackQueue::update() -{ - std::vector queueData; - - owner.getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0); - - size_t boxIndex = 0; - - for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) - { - for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) - stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); - } - - for(; boxIndex < stackBoxes.size(); boxIndex++) - stackBoxes[boxIndex]->setUnit(nullptr); -} - -int32_t StackQueue::getSiegeShooterIconID() -{ - return owner.siegeController->getSiegedTown()->town->faction->getIndex(); -} - -std::optional StackQueue::getHoveredUnitIdIfAny() const -{ - for(const auto & stackBox : stackBoxes) - { - if(stackBox->isHovered()) - { - return stackBox->getBoundUnitID(); - } - } - - return std::nullopt; -} - -StackQueue::StackBox::StackBox(StackQueue * owner): - CIntObject(SHOW_POPUP | HOVER), owner(owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"); - - pos.w = background->pos.w; - pos.h = background->pos.h; - - if(owner->embedded) - { - icon = std::make_shared(owner->icons, 0, 0, 5, 2); - amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - else - { - icon = std::make_shared(owner->icons, 0, 0, 9, 1); - amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - - int icon_x = pos.w - 17; - int icon_y = pos.h - 18; - - stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); - stateIcon->visible = false; - } -} - -void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) -{ - if(unit) - { - boundUnitID = unit->unitId(); - background->colorize(unit->unitOwner()); - icon->visible = true; - - // temporary code for mod compatibility: - // first, set icon that should definitely exist (arrow tower icon in base vcmi mod) - // second, try to switch to icon that should be provided by mod - // if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image - // for 1.2 release & later next line should be moved into 'else' block - icon->setFrame(unit->creatureIconIndex(), 0); - if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) - icon->setFrame(owner->getSiegeShooterIconID(), 1); - - amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); - - if(stateIcon) - { - 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; - } - } - } - else - { - boundUnitID = std::nullopt; - background->colorize(PlayerColor::NEUTRAL); - icon->visible = false; - icon->setFrame(0); - amount->setText(""); - - if(stateIcon) - stateIcon->visible = false; - } -} - -std::optional StackQueue::StackBox::getBoundUnitID() const -{ - return boundUnitID; -} - -bool StackQueue::StackBox::isBoundUnitHighlighted() const -{ - auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds(); - return vstd::contains(unitIdsToHighlight, getBoundUnitID()); -} - -void StackQueue::StackBox::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - if(isBoundUnitHighlighted()) - to.drawBorder(background->pos, Colors::CYAN, 2); -} - -void StackQueue::StackBox::show(Canvas & to) -{ - CIntObject::show(to); - - if(isBoundUnitHighlighted()) - to.drawBorder(background->pos, Colors::CYAN, 2); -} +/* + * BattleInterfaceClasses.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 "BattleInterfaceClasses.h" + +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleRenderer.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#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 "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IFont.h" +#include "../render/Graphics.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../windows/CMessage.h" +#include "../windows/CSpellWindow.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/StartInfo.h" +#include "../../lib/CondSh.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/TextOperations.h" + +void BattleConsole::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + Point line1 (pos.x + pos.w/2, pos.y + 8); + Point line2 (pos.x + pos.w/2, pos.y + 24); + + auto visibleText = getVisibleText(); + + if(visibleText.size() > 0) + to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]); + + if(visibleText.size() > 1) + to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]); +} + +std::vector BattleConsole::getVisibleText() +{ + // high priority texts that hide battle log entries + for(const auto & text : {consoleText, hoverText}) + { + if (text.empty()) + continue; + + auto result = CMessage::breakText(text, pos.w, FONT_SMALL); + + if(result.size() > 2) + result.resize(2); + return result; + } + + // log is small enough to fit entirely - display it as such + if (logEntries.size() < 3) + return logEntries; + + return { logEntries[scrollPosition - 1], logEntries[scrollPosition] }; +} + +std::vector BattleConsole::splitText(const std::string &text) +{ + std::vector lines; + std::vector output; + + boost::split(lines, text, boost::is_any_of("\n")); + + for(const auto & line : lines) + { + if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) + { + output.push_back(line); + } + else + { + std::vector substrings = CMessage::breakText(line, pos.w, FONT_SMALL); + output.insert(output.end(), substrings.begin(), substrings.end()); + } + } + return output; +} + +bool BattleConsole::addText(const std::string & text) +{ + logGlobal->trace("CBattleConsole message: %s", text); + + auto newLines = splitText(text); + + logEntries.insert(logEntries.end(), newLines.begin(), newLines.end()); + scrollPosition = (int)logEntries.size()-1; + redraw(); + return true; +} +void BattleConsole::scrollUp(ui32 by) +{ + if(scrollPosition > static_cast(by)) + scrollPosition -= by; + redraw(); +} + +void BattleConsole::scrollDown(ui32 by) +{ + if(scrollPosition + by < logEntries.size()) + scrollPosition += by; + redraw(); +} + +BattleConsole::BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size) + : scrollPosition(-1) + , enteringText(false) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += objectPos; + pos.w = size.x; + pos.h = size.y; + + background = std::make_shared(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 ); +} + +void BattleConsole::deactivate() +{ + if (enteringText) + LOCPLINT->cingconsole->endEnteringText(false); + + CIntObject::deactivate(); +} + +void BattleConsole::setEnteringMode(bool on) +{ + consoleText.clear(); + + if (on) + { + assert(enteringText == false); + GH.startTextInput(pos); + } + else + { + assert(enteringText == true); + GH.stopTextInput(); + } + enteringText = on; + redraw(); +} + +void BattleConsole::setEnteredText(const std::string & text) +{ + assert(enteringText == true); + consoleText = text; + redraw(); +} + +void BattleConsole::write(const std::string & Text) +{ + hoverText = Text; + redraw(); +} + +void BattleConsole::clearIfMatching(const std::string & Text) +{ + if (hoverText == Text) + clear(); +} + +void BattleConsole::clear() +{ + write({}); +} + +const CGHeroInstance * BattleHero::instance() +{ + return hero; +} + +void BattleHero::tick(uint32_t msPassed) +{ + size_t groupIndex = static_cast(phase); + + float timePassed = msPassed / 1000.f; + + flagCurrentFrame += currentSpeed * timePassed; + currentFrame += currentSpeed * timePassed; + + if(flagCurrentFrame >= flagAnimation->size(0)) + flagCurrentFrame -= flagAnimation->size(0); + + if(currentFrame >= animation->size(groupIndex)) + { + currentFrame -= animation->size(groupIndex); + switchToNextPhase(); + } +} + +void BattleHero::render(Canvas & canvas) +{ + size_t groupIndex = static_cast(phase); + + auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true); + auto heroFrame = animation->getImage(currentFrame, groupIndex, true); + + Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2; + Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2; + + if(defender) + flagPosition += Point(-4, -41); + else + flagPosition += Point(4, -41); + + canvas.draw(flagFrame, flagPosition); + canvas.draw(heroFrame, heroPosition); +} + +void BattleHero::pause() +{ + currentSpeed = 0.f; +} + +void BattleHero::play() +{ + //H3 speed: 10 fps ( 100 ms per frame) + currentSpeed = 10.f; +} + +float BattleHero::getFrame() const +{ + return currentFrame; +} + +void BattleHero::collectRenderableObjects(BattleRenderer & renderer) +{ + auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0); + + renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas) + { + render(canvas); + }); +} + +void BattleHero::onPhaseFinished(const std::function & callback) +{ + phaseFinishedCallback = callback; +} + +void BattleHero::setPhase(EHeroAnimType newPhase) +{ + nextPhase = newPhase; + switchToNextPhase(); //immediately switch to next phase and then restore idling phase + nextPhase = EHeroAnimType::HOLDING; +} + +void BattleHero::heroLeftClicked() +{ + if(owner.actionsController->spellcastingModeActive()) //we are casting a spell + return; + + if(!hero || !owner.makingTurn()) + return; + + if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions + { + CCS->curh->set(Cursor::Map::POINTER); + GH.windows().createAndPushWindow(hero, owner.getCurrentPlayerInterface()); + } +} + +void BattleHero::heroRightClicked() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool()) + return; + + Point windowPosition; + if(GH.screenDimensions().x < 1000) + { + windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79; + windowPosition.y = owner.fieldController->pos.y + 135; + } + else + { + windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15; + windowPosition.y = owner.fieldController->pos.y; + } + + InfoAboutHero targetHero; + if(owner.makingTurn() || settings["session"]["spectate"].Bool()) + { + auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance; + targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); + GH.windows().createAndPushWindow(targetHero, &windowPosition); + } +} + +void BattleHero::switchToNextPhase() +{ + phase = nextPhase; + currentFrame = 0.f; + + auto copy = phaseFinishedCallback; + phaseFinishedCallback.clear(); + copy(); +} + +BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender): + defender(defender), + hero(hero), + owner(owner), + phase(EHeroAnimType::HOLDING), + nextPhase(EHeroAnimType::HOLDING), + currentSpeed(0.f), + currentFrame(0.f), + flagCurrentFrame(0.f) +{ + AnimationPath animationPath; + + if(!hero->type->battleImage.empty()) + animationPath = hero->type->battleImage; + else + if(hero->gender == EHeroGender::FEMALE) + animationPath = hero->type->heroClass->imageBattleFemale; + else + animationPath = hero->type->heroClass->imageBattleMale; + + animation = GH.renderHandler().loadAnimation(animationPath); + animation->preload(); + + pos.w = 64; + pos.h = 136; + pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0); + pos.y = owner.fieldController->pos.y; + + if(defender) + animation->verticalFlip(); + + if(defender) + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR")); + else + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL")); + + flagAnimation->preload(); + flagAnimation->playerColored(hero->tempOwner); + + switchToNextPhase(); + play(); + + addUsedEvents(TIME); +} + +HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground) + : CIntObject(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if (position != nullptr) + moveTo(*position); + + if(initializeBackground) + { + background = std::make_shared(ImagePath::builtin("CHRPOP")); + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background->colorize(hero.owner); + } + + initializeData(hero); +} + +void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + auto attack = hero.details->primskills[0]; + auto defense = hero.details->primskills[1]; + auto power = hero.details->primskills[2]; + auto knowledge = hero.details->primskills[3]; + auto morale = hero.details->morale; + auto luck = hero.details->luck; + auto currentSpellPoints = hero.details->mana; + auto maxSpellPoints = hero.details->manaLimit; + + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6)); + + //primary stats + labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); + labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); + labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":")); + labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":")); + + labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack))); + labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense))); + labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power))); + labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge))); + + //morale+luck + labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); + labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); + + icons.push_back(std::make_shared(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131)); + icons.push_back(std::make_shared(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143)); + + //spell points + labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); + labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); +} + +void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo) +{ + icons.clear(); + labels.clear(); + + initializeData(updatedInfo); +} + +void HeroInfoBasicPanel::show(Canvas & to) +{ + showAll(to); + CIntObject::show(to); +} + +HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) + : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if (position != nullptr) + moveTo(*position); + + background->colorize(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) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + background = std::make_shared(ImagePath::builtin("CPRESULT")); + background->colorize(owner.playerID); + pos = center(background->pos); + + exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); + exit->setBorderColor(Colors::METALLIC_GOLD); + + if(allowReplay) + { + repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); + repeat->setBorderColor(Colors::METALLIC_GOLD); + 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 + { + labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); + } + else + { + labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); + } + + if(br.winner == 1) + { + labels.push_back(std::make_shared(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); + } + else + { + labels.push_back(std::make_shared(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); + } + + labels.push_back(std::make_shared(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407])); + labels.push_back(std::make_shared(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408])); + labels.push_back(std::make_shared(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409])); + + std::string sideNames[2] = {"N/A", "N/A"}; + + for(int i = 0; i < 2; i++) + { + 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; + } + else + { + auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks(); + vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison + { + return stack->unitSide() != i || !stack->base; + }); + + auto best = vstd::maxElementByFun(stacks, [](const CStack * stack) + { + return stack->unitType()->getAIValue(); + }); + + 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(); + } + } + } + + //printing attacker and defender's names + labels.push_back(std::make_shared(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0])); + 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) + { + 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])); + } + 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; + for(auto & elem : br.casualties[step]) + { + auto creature = CGI->creatures()->getByIndex(elem.first); + if (creature->getId() == CreatureID::ARROW_TOWERS ) + continue; // do not show destroyed towers in battle results + + icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos)); + std::ostringstream amount; + amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); + xPos += 42; + } + } + } + //printing result description + bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); + if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won + { + int text = 304; + currentVideo = BattleResultVideo::WIN; + switch(br.result) + { + case EBattleResult::NORMAL: + if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) + currentVideo = BattleResultVideo::WIN_SIEGE; + break; + case EBattleResult::ESCAPE: + text = 303; + break; + case EBattleResult::SURRENDER: + text = 302; + break; + default: + logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); + break; + } + 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])); + } + + 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; + break; + case EBattleResult::ESCAPE: + currentVideo = BattleResultVideo::RETREAT; + text = 310; + break; + case EBattleResult::SURRENDER: + currentVideo = BattleResultVideo::SURRENDER; + text = 309; + break; + default: + logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); + break; + } + playVideo(); + + labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); + } +} + +void BattleResultWindow::activate() +{ + owner.showingDialog->set(true); + 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) + resultCallback(button); + + CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon + + close(); + + if(GH.windows().topWindow()) + GH.windows().popWindows(1); //pop battle interface if present + + //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->setn(false); + CCS->videoh->close(); +} + +void BattleResultWindow::bExitf() +{ + buttonPressed(0); +} + +void BattleResultWindow::bRepeatf() +{ + buttonPressed(1); +} + +StackQueue::StackQueue(bool Embedded, BattleInterface & owner) + : embedded(Embedded), + owner(owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(embedded) + { + pos.w = QUEUE_SIZE * 41; + pos.h = 49; + pos.x += parent->pos.w/2 - pos.w/2; + pos.y += 10; + + icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); + stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); + } + else + { + pos.w = 800; + pos.h = 85; + pos.x += 0; + 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(QUEUE_SIZE); + for (int i = 0; i < stackBoxes.size(); i++) + { + stackBoxes[i] = std::make_shared(this); + stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0)); + } +} + +void StackQueue::show(Canvas & to) +{ + if (embedded) + showAll(to); + CIntObject::show(to); +} + +void StackQueue::update() +{ + std::vector queueData; + + owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0); + + size_t boxIndex = 0; + + for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) + { + for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) + stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); + } + + for(; boxIndex < stackBoxes.size(); boxIndex++) + stackBoxes[boxIndex]->setUnit(nullptr); +} + +int32_t StackQueue::getSiegeShooterIconID() +{ + return owner.siegeController->getSiegedTown()->town->faction->getIndex(); +} + +std::optional StackQueue::getHoveredUnitIdIfAny() const +{ + for(const auto & stackBox : stackBoxes) + { + if(stackBox->isHovered()) + { + return stackBox->getBoundUnitID(); + } + } + + return std::nullopt; +} + +StackQueue::StackBox::StackBox(StackQueue * owner): + CIntObject(SHOW_POPUP | HOVER), owner(owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + background = std::make_shared(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge")); + + pos.w = background->pos.w; + pos.h = background->pos.h; + + if(owner->embedded) + { + icon = std::make_shared(owner->icons, 0, 0, 5, 2); + amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + } + else + { + icon = std::make_shared(owner->icons, 0, 0, 9, 1); + amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + + int icon_x = pos.w - 17; + int icon_y = pos.h - 18; + + stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); + stateIcon->visible = false; + } +} + +void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) +{ + if(unit) + { + boundUnitID = unit->unitId(); + background->colorize(unit->unitOwner()); + icon->visible = true; + + // temporary code for mod compatibility: + // first, set icon that should definitely exist (arrow tower icon in base vcmi mod) + // second, try to switch to icon that should be provided by mod + // if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image + // for 1.2 release & later next line should be moved into 'else' block + icon->setFrame(unit->creatureIconIndex(), 0); + if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) + icon->setFrame(owner->getSiegeShooterIconID(), 1); + + amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); + + if(stateIcon) + { + 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; + } + } + } + else + { + boundUnitID = std::nullopt; + background->colorize(PlayerColor::NEUTRAL); + icon->visible = false; + icon->setFrame(0); + amount->setText(""); + + if(stateIcon) + stateIcon->visible = false; + } +} + +std::optional StackQueue::StackBox::getBoundUnitID() const +{ + return boundUnitID; +} + +bool StackQueue::StackBox::isBoundUnitHighlighted() const +{ + auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds(); + return vstd::contains(unitIdsToHighlight, getBoundUnitID()); +} + +void StackQueue::StackBox::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + if(isBoundUnitHighlighted()) + to.drawBorder(background->pos, Colors::CYAN, 2); +} + +void StackQueue::StackBox::show(Canvas & to) +{ + CIntObject::show(to); + + if(isBoundUnitHighlighted()) + to.drawBorder(background->pos, Colors::CYAN, 2); +} diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index aa5668bc5..ac2431e94 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -1,218 +1,238 @@ -/* - * BattleInterfaceClasses.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 "BattleConstants.h" -#include "../gui/CIntObject.h" -#include "../../lib/FunctionList.h" -#include "../../lib/battle/BattleHex.h" -#include "../windows/CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -struct BattleResult; -class CStack; - -namespace battle -{ -class Unit; -} - -VCMI_LIB_NAMESPACE_END - -class Canvas; -class BattleInterface; -class CPicture; -class CFilledTexture; -class CButton; -class CToggleButton; -class CLabel; -class CTextBox; -class CAnimImage; -class CPlayerInterface; -class BattleRenderer; - -/// 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 -{ -private: - std::shared_ptr background; - - /// List of all texts added during battle, essentially - log of entire battle - std::vector< std::string > logEntries; - - /// Current scrolling position, to allow showing older entries via scroll buttons - int scrollPosition; - - /// current hover text set on mouse move, takes priority over log entries - std::string hoverText; - - /// current text entered via in-game console, takes priority over both log entries and hover text - std::string consoleText; - - /// if true then we are currently entering console text - bool enteringText; - - /// splits text into individual strings for battle log - std::vector splitText(const std::string &text); - - /// select line(s) that will be visible in UI - std::vector getVisibleText(); -public: - BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); - - void showAll(Canvas & to) override; - void deactivate() override; - - bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters) - void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions - void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions - - // IStatusBar interface - void write(const std::string & Text) override; - void clearIfMatching(const std::string & Text) override; - void clear() override; - void setEnteringMode(bool on) override; - void setEnteredText(const std::string & text) override; -}; - -/// Hero battle animation -class BattleHero : public CIntObject -{ - bool defender; - - CFunctionList phaseFinishedCallback; - - std::shared_ptr animation; - std::shared_ptr flagAnimation; - - const CGHeroInstance * hero; //this animation's hero instance - const BattleInterface & owner; //battle interface to which this animation is assigned - - EHeroAnimType phase; //stage of animation - EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed - - float currentSpeed; - float currentFrame; //frame of animation - float flagCurrentFrame; - - void switchToNextPhase(); - - void render(Canvas & canvas); //prints next frame of animation to to -public: - const CGHeroInstance * instance(); - - void setPhase(EHeroAnimType newPhase); //sets phase of hero animation - - void collectRenderableObjects(BattleRenderer & renderer); - void tick(uint32_t msPassed) override; - - float getFrame() const; - void onPhaseFinished(const std::function &); - - void pause(); - void play(); - - void heroLeftClicked(); - void heroRightClicked(); - - BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); -}; - -class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element -{ -private: - std::shared_ptr background; - std::vector> labels; - std::vector> icons; -public: - HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); - - void show(Canvas & to) override; - - void initializeData(const InfoAboutHero & hero); - void update(const InfoAboutHero & updatedInfo); -}; - -class HeroInfoWindow : public CWindowObject -{ -private: - std::shared_ptr content; -public: - HeroInfoWindow(const InfoAboutHero & hero, Point * position); -}; - -/// Class which is responsible for showing the battle result window -class BattleResultWindow : public WindowBase -{ -private: - std::shared_ptr background; - std::vector> labels; - std::shared_ptr exit; - std::shared_ptr repeat; - std::vector> icons; - std::shared_ptr description; - CPlayerInterface & owner; - - void buttonPressed(int button); //internal function for button callbacks -public: - BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); - - void bExitf(); //exit button callback - void bRepeatf(); //repeat button callback - std::function resultCallback; //callback receiving which button was pressed - - void activate() override; - void show(Canvas & to) override; -}; - -/// Shows the stack queue -class StackQueue : public CIntObject -{ - class StackBox : public CIntObject - { - StackQueue * owner; - std::optional boundUnitID; - - std::shared_ptr background; - std::shared_ptr icon; - std::shared_ptr amount; - std::shared_ptr stateIcon; - - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - bool isBoundUnitHighlighted() const; - public: - StackBox(StackQueue * owner); - void setUnit(const battle::Unit * unit, size_t turn = 0); - std::optional getBoundUnitID() const; - - }; - - static const int QUEUE_SIZE = 10; - std::shared_ptr background; - std::vector> stackBoxes; - BattleInterface & owner; - - std::shared_ptr icons; - std::shared_ptr stateIcons; - - int32_t getSiegeShooterIconID(); -public: - const bool embedded; - - StackQueue(bool Embedded, BattleInterface & owner); - void update(); - std::optional getHoveredUnitIdIfAny() const; - - void show(Canvas & to) override; -}; +/* + * BattleInterfaceClasses.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 "BattleConstants.h" +#include "../gui/CIntObject.h" +#include "../../lib/FunctionList.h" +#include "../../lib/battle/BattleHex.h" +#include "../windows/CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +struct BattleResult; +struct InfoAboutHero; +class CStack; + +namespace battle +{ +class Unit; +} + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class Canvas; +class BattleInterface; +class CPicture; +class CFilledTexture; +class CButton; +class CToggleButton; +class CLabel; +class CTextBox; +class CAnimImage; +class CPlayerInterface; +class BattleRenderer; + +/// 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 +{ +private: + std::shared_ptr background; + + /// List of all texts added during battle, essentially - log of entire battle + std::vector< std::string > logEntries; + + /// Current scrolling position, to allow showing older entries via scroll buttons + int scrollPosition; + + /// current hover text set on mouse move, takes priority over log entries + std::string hoverText; + + /// current text entered via in-game console, takes priority over both log entries and hover text + std::string consoleText; + + /// if true then we are currently entering console text + bool enteringText; + + /// splits text into individual strings for battle log + std::vector splitText(const std::string &text); + + /// select line(s) that will be visible in UI + std::vector getVisibleText(); +public: + BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); + + void showAll(Canvas & to) override; + void deactivate() override; + + bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters) + void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions + void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions + + // IStatusBar interface + void write(const std::string & Text) override; + void clearIfMatching(const std::string & Text) override; + void clear() override; + void setEnteringMode(bool on) override; + void setEnteredText(const std::string & text) override; +}; + +/// Hero battle animation +class BattleHero : public CIntObject +{ + bool defender; + + CFunctionList phaseFinishedCallback; + + std::shared_ptr animation; + std::shared_ptr flagAnimation; + + const CGHeroInstance * hero; //this animation's hero instance + const BattleInterface & owner; //battle interface to which this animation is assigned + + EHeroAnimType phase; //stage of animation + EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed + + float currentSpeed; + float currentFrame; //frame of animation + float flagCurrentFrame; + + void switchToNextPhase(); + + void render(Canvas & canvas); //prints next frame of animation to to +public: + const CGHeroInstance * instance(); + + void setPhase(EHeroAnimType newPhase); //sets phase of hero animation + + void collectRenderableObjects(BattleRenderer & renderer); + void tick(uint32_t msPassed) override; + + float getFrame() const; + void onPhaseFinished(const std::function &); + + void pause(); + void play(); + + void heroLeftClicked(); + void heroRightClicked(); + + BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); +}; + +class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element +{ +private: + std::shared_ptr background; + std::vector> labels; + std::vector> icons; +public: + HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); + + void show(Canvas & to) override; + + void initializeData(const InfoAboutHero & hero); + void update(const InfoAboutHero & updatedInfo); +}; + +class HeroInfoWindow : public CWindowObject +{ +private: + std::shared_ptr content; +public: + HeroInfoWindow(const InfoAboutHero & hero, Point * position); +}; + +/// Class which is responsible for showing the battle result window +class BattleResultWindow : public WindowBase +{ +private: + std::shared_ptr background; + std::vector> labels; + std::shared_ptr exit; + std::shared_ptr repeat; + std::vector> icons; + std::shared_ptr description; + 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); + + void buttonPressed(int button); //internal function for button callbacks +public: + BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); + + void bExitf(); //exit button callback + void bRepeatf(); //repeat button callback + std::function resultCallback; //callback receiving which button was pressed + + void activate() override; + void show(Canvas & to) override; +}; + +/// Shows the stack queue +class StackQueue : public CIntObject +{ + class StackBox : public CIntObject + { + StackQueue * owner; + std::optional boundUnitID; + + std::shared_ptr background; + std::shared_ptr icon; + std::shared_ptr amount; + std::shared_ptr stateIcon; + + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + bool isBoundUnitHighlighted() const; + public: + StackBox(StackQueue * owner); + void setUnit(const battle::Unit * unit, size_t turn = 0); + std::optional getBoundUnitID() const; + + }; + + static const int QUEUE_SIZE = 10; + std::shared_ptr background; + std::vector> stackBoxes; + BattleInterface & owner; + + std::shared_ptr icons; + std::shared_ptr stateIcons; + + int32_t getSiegeShooterIconID(); +public: + const bool embedded; + + StackQueue(bool Embedded, BattleInterface & owner); + void update(); + std::optional getHoveredUnitIdIfAny() const; + + void show(Canvas & to) override; +}; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index d41b09a66..0c6e7e809 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -1,227 +1,236 @@ -/* - * BattleObstacleController.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 "BattleObstacleController.h" - -#include "BattleInterface.h" -#include "BattleFieldController.h" -#include "BattleAnimationClasses.h" -#include "BattleStacksController.h" -#include "BattleRenderer.h" -#include "CreatureAnimation.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CGuiHandler.h" -#include "../render/Canvas.h" - -#include "../../CCallback.h" -#include "../../lib/battle/CObstacleInstance.h" -#include "../../lib/ObstacleHandler.h" - -BattleObstacleController::BattleObstacleController(BattleInterface & owner): - owner(owner), - timePassed(0.f) -{ - auto obst = owner.curInt->cb->battleGetAllObstacles(); - for(auto & elem : obst) - { - if ( elem->obstacleType == CObstacleInstance::MOAT ) - continue; // handled by siege controller; - loadObstacleImage(*elem); - } -} - -void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) -{ - std::string animationName = oi.getAnimation(); - - if (animationsCache.count(animationName) == 0) - { - if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - { - // obstacle uses single bitmap image for animations - auto animation = std::make_shared(); - animation->setCustom(animationName, 0, 0); - animationsCache[animationName] = animation; - animation->preload(); - } - else - { - auto animation = std::make_shared(animationName); - animationsCache[animationName] = animation; - animation->preload(); - } - } - obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; -} - -void BattleObstacleController::obstacleRemoved(const std::vector & obstacles) -{ - for(const auto & oi : obstacles) - { - auto & obstacle = oi.data["obstacle"]; - - if (!obstacle.isStruct()) - { - logGlobal->error("I don't know how to animate removal of this obstacle"); - continue; - } - - auto animation = std::make_shared(obstacle["appearAnimation"].String()); - animation->preload(); - - auto first = animation->getImage(0, 0); - if(!first) - continue; - - //we assume here that effect graphics have the same size as the usual obstacle image - // -> if we know how to blit obstacle, let's blit the effect in the same place - Point whereTo = getObstaclePosition(first, obstacle); - //AFAIK, in H3 there is no sound of obstacle removal - owner.stacksController->addNewAnim(new EffectAnimation(owner, obstacle["appearAnimation"].String(), whereTo, obstacle["position"].Integer(), 0, true)); - - obstacleAnimations.erase(oi.id); - //so when multiple obstacles are removed, they show up one after another - owner.waitForAnimations(); - } -} - -void BattleObstacleController::obstaclePlaced(const std::vector> & obstacles) -{ - for(const auto & oi : obstacles) - { - auto side = owner.curInt->cb->playerToSide(owner.curInt->playerID); - - if(!oi->visibleForSide(side.value(), owner.curInt->cb->battleHasNativeStack(side.value()))) - continue; - - auto animation = std::make_shared(oi->getAppearAnimation()); - animation->preload(); - - auto first = animation->getImage(0, 0); - if(!first) - continue; - - //we assume here that effect graphics have the same size as the usual obstacle image - // -> if we know how to blit obstacle, let's blit the effect in the same place - Point whereTo = getObstaclePosition(first, *oi); - CCS->soundh->playSound( oi->getAppearSound() ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos)); - - //so when multiple obstacles are added, they show up one after another - owner.waitForAnimations(); - - loadObstacleImage(*oi); - } -} - -void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) -{ - //Blit absolute obstacles - for(auto & obstacle : owner.curInt->cb->battleGetAllObstacles()) - { - if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - { - auto img = getObstacleImage(*obstacle); - if(img) - canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height)); - } - - if (obstacle->obstacleType == CObstacleInstance::USUAL) - { - if (obstacle->getInfo().isForegroundObstacle) - continue; - - auto img = getObstacleImage(*obstacle); - if(img) - { - Point p = getObstaclePosition(img, *obstacle); - canvas.draw(img, p); - } - } - } -} - -void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (auto obstacle : owner.curInt->cb->battleGetAllObstacles()) - { - if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - continue; - - if (obstacle->obstacleType == CObstacleInstance::MOAT) - continue; - - if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle) - continue; - - renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ - auto img = getObstacleImage(*obstacle); - if(img) - { - Point p = getObstaclePosition(img, *obstacle); - canvas.draw(img, p); - } - }); - } -} - -void BattleObstacleController::tick(uint32_t msPassed) -{ - timePassed += msPassed / 1000.f; -} - -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) - return nullptr; - - animation = obstacleAnimations[oi.uniqueID]; - assert(animation); - - if(animation) - { - int frameIndex = framesCount % animation->size(0); - return animation->getImage(frameIndex, 0); - } - return nullptr; -} - -Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) -{ - int offset = obstacle.getAnimationYOffset(image->height()); - - Rect r = owner.fieldController->hexPositionLocal(obstacle.pos); - r.y += 42 - image->height() + offset; - - return r.topLeft(); -} - -Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle) -{ - auto animationYOffset = obstacle["animationYOffset"].Integer(); - auto offset = image->height() % 42; - - if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37) - animationYOffset -= 42; - - offset += animationYOffset; - - Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer()); - r.y += 42 - image->height() + offset; - - return r.topLeft(); -} +/* + * BattleObstacleController.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 "BattleObstacleController.h" + +#include "BattleInterface.h" +#include "BattleFieldController.h" +#include "BattleAnimationClasses.h" +#include "BattleStacksController.h" +#include "BattleRenderer.h" +#include "CreatureAnimation.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/ObstacleHandler.h" +#include "../../lib/serializer/JsonDeserializer.h" + +BattleObstacleController::BattleObstacleController(BattleInterface & owner): + owner(owner), + timePassed(0.f) +{ + auto obst = owner.getBattle()->battleGetAllObstacles(); + for(auto & elem : obst) + { + if ( elem->obstacleType == CObstacleInstance::MOAT ) + continue; // handled by siege controller; + loadObstacleImage(*elem); + } +} + +void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) +{ + AnimationPath animationName = oi.getAnimation(); + + if (animationsCache.count(animationName) == 0) + { + 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(); + } + } + obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; +} + +void BattleObstacleController::obstacleRemoved(const std::vector & obstacles) +{ + for(const auto & oi : obstacles) + { + auto & obstacle = oi.data["obstacle"]; + + if (!obstacle.isStruct()) + { + logGlobal->error("I don't know how to animate removal of this obstacle"); + continue; + } + + AnimationPath animationPath; + JsonDeserializer ser(nullptr, obstacle); + ser.serializeStruct("appearAnimation", animationPath); + + if(animationPath.empty()) + continue; + + auto animation = GH.renderHandler().loadAnimation(animationPath); + animation->preload(); + + auto first = animation->getImage(0, 0); + if(!first) + continue; + + //we assume here that effect graphics have the same size as the usual obstacle image + // -> if we know how to blit obstacle, let's blit the effect in the same place + Point whereTo = getObstaclePosition(first, obstacle); + //AFAIK, in H3 there is no sound of obstacle removal + owner.stacksController->addNewAnim(new EffectAnimation(owner, animationPath, whereTo, obstacle["position"].Integer(), 0, true)); + + obstacleAnimations.erase(oi.id); + //so when multiple obstacles are removed, they show up one after another + owner.waitForAnimations(); + } +} + +void BattleObstacleController::obstaclePlaced(const std::vector> & obstacles) +{ + for(const auto & oi : obstacles) + { + auto side = owner.getBattle()->playerToSide(owner.curInt->playerID); + + if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value()))) + continue; + + auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation()); + animation->preload(); + + auto first = animation->getImage(0, 0); + if(!first) + continue; + + //we assume here that effect graphics have the same size as the usual obstacle image + // -> if we know how to blit obstacle, let's blit the effect in the same place + Point whereTo = getObstaclePosition(first, *oi); + CCS->soundh->playSound( oi->getAppearSound() ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos)); + + //so when multiple obstacles are added, they show up one after another + owner.waitForAnimations(); + + loadObstacleImage(*oi); + } +} + +void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) +{ + //Blit absolute obstacles + for(auto & obstacle : owner.getBattle()->battleGetAllObstacles()) + { + if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + { + auto img = getObstacleImage(*obstacle); + if(img) + canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height)); + } + + if (obstacle->obstacleType == CObstacleInstance::USUAL) + { + if (obstacle->getInfo().isForegroundObstacle) + continue; + + auto img = getObstacleImage(*obstacle); + if(img) + { + Point p = getObstaclePosition(img, *obstacle); + canvas.draw(img, p); + } + } + } +} + +void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (auto obstacle : owner.getBattle()->battleGetAllObstacles()) + { + if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + continue; + + if (obstacle->obstacleType == CObstacleInstance::MOAT) + continue; + + if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle) + continue; + + renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ + auto img = getObstacleImage(*obstacle); + if(img) + { + Point p = getObstaclePosition(img, *obstacle); + canvas.draw(img, p); + } + }); + } +} + +void BattleObstacleController::tick(uint32_t msPassed) +{ + timePassed += msPassed / 1000.f; +} + +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) + return nullptr; + + animation = obstacleAnimations[oi.uniqueID]; + assert(animation); + + if(animation) + { + int frameIndex = framesCount % animation->size(0); + return animation->getImage(frameIndex, 0); + } + return nullptr; +} + +Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) +{ + int offset = obstacle.getAnimationYOffset(image->height()); + + Rect r = owner.fieldController->hexPositionLocal(obstacle.pos); + r.y += 42 - image->height() + offset; + + return r.topLeft(); +} + +Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle) +{ + auto animationYOffset = obstacle["animationYOffset"].Integer(); + auto offset = image->height() % 42; + + if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37) + animationYOffset -= 42; + + offset += animationYOffset; + + Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer()); + r.y += 42 - image->height() + offset; + + return r.topLeft(); +} diff --git a/client/battle/BattleObstacleController.h b/client/battle/BattleObstacleController.h index 0f33626f9..c4a7467a4 100644 --- a/client/battle/BattleObstacleController.h +++ b/client/battle/BattleObstacleController.h @@ -1,66 +1,68 @@ -/* - * BattleObstacleController.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 BattleHex; -struct CObstacleInstance; -class JsonNode; -class ObstacleChanges; -class Point; - -VCMI_LIB_NAMESPACE_END - -class IImage; -class Canvas; -class CAnimation; -class BattleInterface; -class BattleRenderer; - -/// Controls all currently active projectiles on the battlefield -/// (with exception of moat, which is apparently handled by siege controller) -class BattleObstacleController -{ - BattleInterface & owner; - - /// 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; - - void loadObstacleImage(const CObstacleInstance & oi); - - std::shared_ptr getObstacleImage(const CObstacleInstance & oi); - Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); - Point getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle); - -public: - BattleObstacleController(BattleInterface & owner); - - /// called every frame - void tick(uint32_t msPassed); - - /// call-in from network pack, add newly placed obstacles with any required animations - void obstaclePlaced(const std::vector> & oi); - - /// call-in from network pack, remove required obstacles with any required animations - void obstacleRemoved(const std::vector & obstacles); - - /// renders all "absolute" obstacles - void showAbsoluteObstacles(Canvas & canvas); - - /// adds all non-"absolute" visible obstacles for rendering - void collectRenderableObjects(BattleRenderer & renderer); -}; +/* + * BattleObstacleController.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" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleHex; +struct CObstacleInstance; +class JsonNode; +class ObstacleChanges; +class Point; + +VCMI_LIB_NAMESPACE_END + +class IImage; +class Canvas; +class CAnimation; +class BattleInterface; +class BattleRenderer; + +/// Controls all currently active projectiles on the battlefield +/// (with exception of moat, which is apparently handled by siege controller) +class BattleObstacleController +{ + BattleInterface & owner; + + /// 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; + + void loadObstacleImage(const CObstacleInstance & oi); + + std::shared_ptr getObstacleImage(const CObstacleInstance & oi); + Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); + Point getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle); + +public: + BattleObstacleController(BattleInterface & owner); + + /// called every frame + void tick(uint32_t msPassed); + + /// call-in from network pack, add newly placed obstacles with any required animations + void obstaclePlaced(const std::vector> & oi); + + /// call-in from network pack, remove required obstacles with any required animations + void obstacleRemoved(const std::vector & obstacles); + + /// renders all "absolute" obstacles + void showAbsoluteObstacles(Canvas & canvas); + + /// adds all non-"absolute" visible obstacles for rendering + void collectRenderableObjects(BattleRenderer & renderer); +}; diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index c2b2715e0..404540c82 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -1,385 +1,386 @@ -/* - * BattleProjectileController.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 "BattleProjectileController.h" - -#include "BattleInterface.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "CreatureAnimation.h" - -#include "../render/Canvas.h" -#include "../gui/CGuiHandler.h" -#include "../CGameInfo.h" - -#include "../../lib/CStack.h" -#include "../../lib/mapObjects/CGTownInstance.h" - -static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x) -{ - double facA = 0.005; // seems to be constant - - // system of 2 linear equations, solutions of which are missing coefficients - // for quadratic equation a*x*x + b*x + c - double eq[2][3] = { - { static_cast(from.x), 1.0, from.y - facA*from.x*from.x }, - { static_cast(dest.x), 1.0, dest.y - facA*dest.x*dest.x } - }; - - // solve system via determinants - double det = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1]; - double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; - double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; - - double facB = detB / det; - double facC = detC / det; - - return facA *pow(x, 2.0) + facB *x + facC; -} - -void ProjectileMissile::show(Canvas & canvas) -{ - size_t group = reverse ? 1 : 0; - auto image = animation->getImage(frameNum, group, true); - - if(image) - { - Point pos { - vstd::lerp(from.x, dest.x, progress) - image->width() / 2, - vstd::lerp(from.y, dest.y, progress) - image->height() / 2, - }; - - canvas.draw(image, pos); - } -} - -void ProjectileMissile::tick(uint32_t msPassed) -{ - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -void ProjectileAnimatedMissile::tick(uint32_t msPassed) -{ - ProjectileMissile::tick(msPassed); - frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - size_t animationSize = animation->size(reverse ? 1 : 0); - while (frameProgress > animationSize) - frameProgress -= animationSize; - - frameNum = std::floor(frameProgress); -} - -void ProjectileCatapult::tick(uint32_t msPassed) -{ - frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -void ProjectileCatapult::show(Canvas & canvas) -{ - int frameCounter = std::floor(frameProgress); - int frameIndex = (frameCounter + 1) % animation->size(0); - - auto image = animation->getImage(frameIndex, 0, true); - - if(image) - { - int posX = vstd::lerp(from.x, dest.x, progress); - int posY = calculateCatapultParabolaY(from, dest, posX); - Point pos(posX, posY); - - canvas.draw(image, pos); - } -} - -void ProjectileRay::show(Canvas & canvas) -{ - Point curr { - vstd::lerp(from.x, dest.x, progress), - vstd::lerp(from.y, dest.y, progress), - }; - - Point length = curr - from; - - //select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other - - if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis - { - int y1 = from.y - rayConfig.size() / 2; - int y2 = curr.y - rayConfig.size() / 2; - - int x1 = from.x; - int x2 = curr.x; - - for (size_t i = 0; i < rayConfig.size(); ++i) - { - auto ray = rayConfig[i]; - canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end); - } - } - else // draw in vertical axis - { - int x1 = from.x - rayConfig.size() / 2; - int x2 = curr.x - rayConfig.size() / 2; - - int y1 = from.y; - int y2 = curr.y; - - for (size_t i = 0; i < rayConfig.size(); ++i) - { - auto ray = rayConfig[i]; - - canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end); - } - } -} - -void ProjectileRay::tick(uint32_t msPassed) -{ - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -BattleProjectileController::BattleProjectileController(BattleInterface & owner): - owner(owner) -{} - -const CCreature & BattleProjectileController::getShooter(const CStack * stack) const -{ - const CCreature * creature = stack->unitType(); - - if(creature->getId() == CreatureID::ARROW_TOWERS) - creature = owner.siegeController->getTurretCreature(); - - if(creature->animation.missleFrameAngles.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]; - } - - return *creature; -} - -bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const -{ - return !getShooter(stack).animation.projectileRay.empty(); -} - -bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const -{ - return !getShooter(stack).animation.projectileImageName.empty(); -} - -void BattleProjectileController::initStackProjectile(const CStack * stack) -{ - if (!stackUsesMissileProjectile(stack)) - return; - - const CCreature & creature = getShooter(stack); - projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName); -} - -std::shared_ptr BattleProjectileController::createProjectileImage(const std::string & path ) -{ - std::shared_ptr projectile = std::make_shared(path); - projectile->preload(); - - if(projectile->size(1) != 0) - logAnim->error("Expected empty group 1 in stack projectile"); - else - projectile->createFlippedGroup(0, 1); - - return projectile; -} - -std::shared_ptr BattleProjectileController::getProjectileImage(const CStack * stack) -{ - const CCreature & creature = getShooter(stack); - std::string imageName = creature.animation.projectileImageName; - - if (!projectilesCache.count(imageName)) - initStackProjectile(stack); - - return projectilesCache[imageName]; -} - -void BattleProjectileController::emitStackProjectile(const CStack * stack) -{ - int stackID = stack ? stack->unitId() : -1; - - for (auto projectile : projectiles) - { - if ( !projectile->playing && projectile->shooterID == stackID) - { - projectile->playing = true; - return; - } - } -} - -void BattleProjectileController::render(Canvas & canvas) -{ - for ( auto projectile: projectiles) - { - if ( projectile->playing ) - projectile->show(canvas); - } -} - -void BattleProjectileController::tick(uint32_t msPassed) -{ - for ( auto projectile: projectiles) - { - if ( projectile->playing ) - projectile->tick(msPassed); - } - - vstd::erase_if(projectiles, [&](const std::shared_ptr & projectile){ - return projectile->progress > 1.0f; - }); -} - -bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const -{ - int stackID = stack ? stack->unitId() : -1; - - for(auto const & instance : projectiles) - { - if(instance->shooterID == stackID && (instance->playing || !emittedOnly)) - { - return true; - } - } - return false; -} - -float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) -{ - float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); - float distance = sqrt(distanceSquared); - - assert(distance > 1.f); - - return animSpeed / std::max( 1.f, distance); -} - -int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack) -{ - const CCreature & creature = getShooter(stack); - - auto & angles = creature.animation.missleFrameAngles; - 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 - size_t maxFrame = std::min(angles.size(), animation->size(0)); - - assert(maxFrame > 0); - double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x)); - - // values in angles array indicate position from which this frame was rendered, in degrees. - // possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots - // find frame that has closest angle to one that we need for this shot - int bestID = 0; - double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle ); - - for (int i=1; ianimation = getProjectileImage(shooter); - catapultProjectile->progress = 0; - catapultProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); - catapultProjectile->from = from; - catapultProjectile->dest = dest; - catapultProjectile->shooterID = shooter->unitId(); - catapultProjectile->playing = false; - catapultProjectile->frameProgress = 0.f; - - projectiles.push_back(std::shared_ptr(catapultProjectile)); -} - -void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest) -{ - const CCreature & shooterInfo = getShooter(shooter); - - std::shared_ptr projectile; - if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter)) - { - logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated()); - } - - if (stackUsesRayProjectile(shooter)) - { - auto rayProjectile = new ProjectileRay(); - projectile.reset(rayProjectile); - - rayProjectile->rayConfig = shooterInfo.animation.projectileRay; - rayProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed()); - } - else if (stackUsesMissileProjectile(shooter)) - { - auto missileProjectile = new ProjectileMissile(); - projectile.reset(missileProjectile); - - missileProjectile->animation = getProjectileImage(shooter); - missileProjectile->reverse = !owner.stacksController->facingRight(shooter); - missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter); - missileProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); - } - - - projectile->from = from; - projectile->dest = dest; - projectile->shooterID = shooter->unitId(); - projectile->progress = 0; - projectile->playing = false; - - projectiles.push_back(projectile); -} - -void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell) -{ - double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y)); - std::string animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); - - assert(!animToDisplay.empty()); - - if(!animToDisplay.empty()) - { - auto projectile = new ProjectileAnimatedMissile(); - - projectile->animation = createProjectileImage(animToDisplay); - projectile->frameProgress = 0; - projectile->frameNum = 0; - projectile->reverse = from.x > dest.x; - projectile->from = from; - projectile->dest = dest; - projectile->shooterID = shooter ? shooter->unitId() : -1; - projectile->progress = 0; - projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); - projectile->playing = false; - - projectiles.push_back(std::shared_ptr(projectile)); - } -} +/* + * BattleProjectileController.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 "BattleProjectileController.h" + +#include "BattleInterface.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "CreatureAnimation.h" + +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" +#include "../CGameInfo.h" + +#include "../../lib/CStack.h" +#include "../../lib/mapObjects/CGTownInstance.h" + +static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x) +{ + double facA = 0.005; // seems to be constant + + // system of 2 linear equations, solutions of which are missing coefficients + // for quadratic equation a*x*x + b*x + c + double eq[2][3] = { + { static_cast(from.x), 1.0, from.y - facA*from.x*from.x }, + { static_cast(dest.x), 1.0, dest.y - facA*dest.x*dest.x } + }; + + // solve system via determinants + double det = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1]; + double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; + double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; + + double facB = detB / det; + double facC = detC / det; + + return facA *pow(x, 2.0) + facB *x + facC; +} + +void ProjectileMissile::show(Canvas & canvas) +{ + size_t group = reverse ? 1 : 0; + auto image = animation->getImage(frameNum, group, true); + + if(image) + { + Point pos { + vstd::lerp(from.x, dest.x, progress) - image->width() / 2, + vstd::lerp(from.y, dest.y, progress) - image->height() / 2, + }; + + canvas.draw(image, pos); + } +} + +void ProjectileMissile::tick(uint32_t msPassed) +{ + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +void ProjectileAnimatedMissile::tick(uint32_t msPassed) +{ + ProjectileMissile::tick(msPassed); + frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + size_t animationSize = animation->size(reverse ? 1 : 0); + while (frameProgress > animationSize) + frameProgress -= animationSize; + + frameNum = std::floor(frameProgress); +} + +void ProjectileCatapult::tick(uint32_t msPassed) +{ + frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +void ProjectileCatapult::show(Canvas & canvas) +{ + int frameCounter = std::floor(frameProgress); + int frameIndex = (frameCounter + 1) % animation->size(0); + + auto image = animation->getImage(frameIndex, 0, true); + + if(image) + { + int posX = vstd::lerp(from.x, dest.x, progress); + int posY = calculateCatapultParabolaY(from, dest, posX); + Point pos(posX, posY); + + canvas.draw(image, pos); + } +} + +void ProjectileRay::show(Canvas & canvas) +{ + Point curr { + vstd::lerp(from.x, dest.x, progress), + vstd::lerp(from.y, dest.y, progress), + }; + + Point length = curr - from; + + //select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other + + if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis + { + int y1 = from.y - rayConfig.size() / 2; + int y2 = curr.y - rayConfig.size() / 2; + + int x1 = from.x; + int x2 = curr.x; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end); + } + } + else // draw in vertical axis + { + int x1 = from.x - rayConfig.size() / 2; + int x2 = curr.x - rayConfig.size() / 2; + + int y1 = from.y; + int y2 = curr.y; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + + canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end); + } + } +} + +void ProjectileRay::tick(uint32_t msPassed) +{ + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +BattleProjectileController::BattleProjectileController(BattleInterface & owner): + owner(owner) +{} + +const CCreature & BattleProjectileController::getShooter(const CStack * stack) const +{ + const CCreature * creature = stack->unitType(); + + if(creature->getId() == CreatureID::ARROW_TOWERS) + creature = owner.siegeController->getTurretCreature(); + + if(creature->animation.missleFrameAngles.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]; + } + + return *creature; +} + +bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const +{ + return !getShooter(stack).animation.projectileRay.empty(); +} + +bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const +{ + return !getShooter(stack).animation.projectileImageName.empty(); +} + +void BattleProjectileController::initStackProjectile(const CStack * stack) +{ + if (!stackUsesMissileProjectile(stack)) + return; + + const CCreature & creature = getShooter(stack); + projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName); +} + +std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) +{ + std::shared_ptr projectile = GH.renderHandler().loadAnimation(path); + projectile->preload(); + + if(projectile->size(1) != 0) + logAnim->error("Expected empty group 1 in stack projectile"); + else + projectile->createFlippedGroup(0, 1); + + return projectile; +} + +std::shared_ptr BattleProjectileController::getProjectileImage(const CStack * stack) +{ + const CCreature & creature = getShooter(stack); + AnimationPath imageName = creature.animation.projectileImageName; + + if (!projectilesCache.count(imageName)) + initStackProjectile(stack); + + return projectilesCache[imageName]; +} + +void BattleProjectileController::emitStackProjectile(const CStack * stack) +{ + int stackID = stack ? stack->unitId() : -1; + + for (auto projectile : projectiles) + { + if ( !projectile->playing && projectile->shooterID == stackID) + { + projectile->playing = true; + return; + } + } +} + +void BattleProjectileController::render(Canvas & canvas) +{ + for ( auto projectile: projectiles) + { + if ( projectile->playing ) + projectile->show(canvas); + } +} + +void BattleProjectileController::tick(uint32_t msPassed) +{ + for ( auto projectile: projectiles) + { + if ( projectile->playing ) + projectile->tick(msPassed); + } + + vstd::erase_if(projectiles, [&](const std::shared_ptr & projectile){ + return projectile->progress > 1.0f; + }); +} + +bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const +{ + int stackID = stack ? stack->unitId() : -1; + + for(auto const & instance : projectiles) + { + if(instance->shooterID == stackID && (instance->playing || !emittedOnly)) + { + return true; + } + } + return false; +} + +float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) +{ + float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); + float distance = sqrt(distanceSquared); + + assert(distance > 1.f); + + return animSpeed / std::max( 1.f, distance); +} + +int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack) +{ + const CCreature & creature = getShooter(stack); + + auto & angles = creature.animation.missleFrameAngles; + 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 + size_t maxFrame = std::min(angles.size(), animation->size(0)); + + assert(maxFrame > 0); + double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x)); + + // values in angles array indicate position from which this frame was rendered, in degrees. + // possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots + // find frame that has closest angle to one that we need for this shot + int bestID = 0; + double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle ); + + for (int i=1; ianimation = getProjectileImage(shooter); + catapultProjectile->progress = 0; + catapultProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); + catapultProjectile->from = from; + catapultProjectile->dest = dest; + catapultProjectile->shooterID = shooter->unitId(); + catapultProjectile->playing = false; + catapultProjectile->frameProgress = 0.f; + + projectiles.push_back(std::shared_ptr(catapultProjectile)); +} + +void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest) +{ + const CCreature & shooterInfo = getShooter(shooter); + + std::shared_ptr projectile; + if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter)) + { + logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated()); + } + + if (stackUsesRayProjectile(shooter)) + { + auto rayProjectile = new ProjectileRay(); + projectile.reset(rayProjectile); + + rayProjectile->rayConfig = shooterInfo.animation.projectileRay; + rayProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed()); + } + else if (stackUsesMissileProjectile(shooter)) + { + auto missileProjectile = new ProjectileMissile(); + projectile.reset(missileProjectile); + + missileProjectile->animation = getProjectileImage(shooter); + missileProjectile->reverse = !owner.stacksController->facingRight(shooter); + missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter); + missileProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); + } + + + projectile->from = from; + projectile->dest = dest; + projectile->shooterID = shooter->unitId(); + projectile->progress = 0; + projectile->playing = false; + + projectiles.push_back(projectile); +} + +void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell) +{ + double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y)); + AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); + + assert(!animToDisplay.empty()); + + if(!animToDisplay.empty()) + { + auto projectile = new ProjectileAnimatedMissile(); + + projectile->animation = createProjectileImage(animToDisplay); + projectile->frameProgress = 0; + projectile->frameNum = 0; + projectile->reverse = from.x > dest.x; + projectile->from = from; + projectile->dest = dest; + projectile->shooterID = shooter ? shooter->unitId() : -1; + projectile->progress = 0; + projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); + projectile->playing = false; + + projectiles.push_back(std::shared_ptr(projectile)); + } +} diff --git a/client/battle/BattleProjectileController.h b/client/battle/BattleProjectileController.h index a9622c230..d2b3a04e1 100644 --- a/client/battle/BattleProjectileController.h +++ b/client/battle/BattleProjectileController.h @@ -1,124 +1,125 @@ -/* - * BattleSiegeController.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/CCreatureHandler.h" -#include "../../lib/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CSpell; - -VCMI_LIB_NAMESPACE_END - -class CAnimation; -class Canvas; -class BattleInterface; - -/// Base class for projectiles -struct ProjectileBase -{ - virtual ~ProjectileBase() = default; - virtual void show(Canvas & canvas) = 0; - virtual void tick(uint32_t msPassed) = 0; - - Point from; // initial position on the screen - Point dest; // target position on the screen - - float progress; // current position of projectile on from->dest line - float speed; // how much progress is gained per second - int shooterID; // ID of shooter stack - bool playing; // if set to true, projectile animation is playing, e.g. flying to target -}; - -/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination -struct ProjectileMissile : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::shared_ptr animation; - int frameNum; // frame to display from projectile animation - bool reverse; // if true, projectile will be flipped by vertical axis -}; - -/// Projectile for spell - render animation moving in straight line from origin to destination -struct ProjectileAnimatedMissile : ProjectileMissile -{ - void tick(uint32_t msPassed) override; - float frameProgress; -}; - -/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination -struct ProjectileCatapult : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::shared_ptr animation; - float frameProgress; -}; - -/// Projectile for mages/evil eye - render ray expanding from origin position to destination -struct ProjectileRay : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::vector rayConfig; -}; - -/// Class that manages all ongoing projectiles in the game -/// ... even though in H3 only 1 projectile can be on screen at any point of time -class BattleProjectileController -{ - BattleInterface & owner; - - /// all projectiles loaded during current battle - std::map> projectilesCache; - - /// projectiles currently flying on battlefield - std::vector> projectiles; - - std::shared_ptr getProjectileImage(const CStack * stack); - std::shared_ptr createProjectileImage(const std::string & path ); - void initStackProjectile(const CStack * stack); - - bool stackUsesRayProjectile(const CStack * stack) const; - bool stackUsesMissileProjectile(const CStack * stack) const; - - void showProjectile(Canvas & canvas, std::shared_ptr projectile); - - const CCreature & getShooter(const CStack * stack) const; - - int computeProjectileFrameID( Point from, Point dest, const CStack * stack); - float computeProjectileFlightTime( Point from, Point dest, double speed); - -public: - BattleProjectileController(BattleInterface & owner); - - /// renders all currently active projectiles - void render(Canvas & canvas); - - /// updates positioning / animations of all projectiles - void tick(uint32_t msPassed); - - /// returns true if stack has projectile that is yet to hit target - bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const; - - /// starts rendering previously created projectile - void emitStackProjectile(const CStack * stack); - - /// creates (but not emits!) projectile and initializes it based on parameters - void createProjectile(const CStack * shooter, Point from, Point dest); - void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell); - void createCatapultProjectile(const CStack * shooter, Point from, Point dest); -}; +/* + * BattleSiegeController.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/CCreatureHandler.h" +#include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class Canvas; +class BattleInterface; + +/// Base class for projectiles +struct ProjectileBase +{ + virtual ~ProjectileBase() = default; + virtual void show(Canvas & canvas) = 0; + virtual void tick(uint32_t msPassed) = 0; + + Point from; // initial position on the screen + Point dest; // target position on the screen + + float progress; // current position of projectile on from->dest line + float speed; // how much progress is gained per second + int shooterID; // ID of shooter stack + bool playing; // if set to true, projectile animation is playing, e.g. flying to target +}; + +/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination +struct ProjectileMissile : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::shared_ptr animation; + int frameNum; // frame to display from projectile animation + bool reverse; // if true, projectile will be flipped by vertical axis +}; + +/// Projectile for spell - render animation moving in straight line from origin to destination +struct ProjectileAnimatedMissile : ProjectileMissile +{ + void tick(uint32_t msPassed) override; + float frameProgress; +}; + +/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination +struct ProjectileCatapult : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::shared_ptr animation; + float frameProgress; +}; + +/// Projectile for mages/evil eye - render ray expanding from origin position to destination +struct ProjectileRay : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::vector rayConfig; +}; + +/// Class that manages all ongoing projectiles in the game +/// ... even though in H3 only 1 projectile can be on screen at any point of time +class BattleProjectileController +{ + BattleInterface & owner; + + /// all projectiles loaded during current battle + std::map> projectilesCache; + + /// projectiles currently flying on battlefield + std::vector> projectiles; + + std::shared_ptr getProjectileImage(const CStack * stack); + std::shared_ptr createProjectileImage(const AnimationPath & path ); + void initStackProjectile(const CStack * stack); + + bool stackUsesRayProjectile(const CStack * stack) const; + bool stackUsesMissileProjectile(const CStack * stack) const; + + void showProjectile(Canvas & canvas, std::shared_ptr projectile); + + const CCreature & getShooter(const CStack * stack) const; + + int computeProjectileFrameID( Point from, Point dest, const CStack * stack); + float computeProjectileFlightTime( Point from, Point dest, double speed); + +public: + BattleProjectileController(BattleInterface & owner); + + /// renders all currently active projectiles + void render(Canvas & canvas); + + /// updates positioning / animations of all projectiles + void tick(uint32_t msPassed); + + /// returns true if stack has projectile that is yet to hit target + bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const; + + /// starts rendering previously created projectile + void emitStackProjectile(const CStack * stack); + + /// creates (but not emits!) projectile and initializes it based on parameters + void createProjectile(const CStack * shooter, Point from, Point dest); + void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell); + void createCatapultProjectile(const CStack * shooter, Point from, Point dest); +}; diff --git a/client/battle/BattleRenderer.cpp b/client/battle/BattleRenderer.cpp index 9e5971749..fbbe3e943 100644 --- a/client/battle/BattleRenderer.cpp +++ b/client/battle/BattleRenderer.cpp @@ -1,76 +1,76 @@ -/* - * BattleFieldController.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 "BattleRenderer.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleEffectsController.h" -#include "BattleWindow.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "BattleObstacleController.h" - -void BattleRenderer::collectObjects() -{ - owner.effectsController->collectRenderableObjects(*this); - owner.obstacleController->collectRenderableObjects(*this); - owner.stacksController->collectRenderableObjects(*this); - if (owner.siegeController) - owner.siegeController->collectRenderableObjects(*this); - if (owner.defendingHero) - owner.defendingHero->collectRenderableObjects(*this); - if (owner.attackingHero) - owner.attackingHero->collectRenderableObjects(*this); -} - -void BattleRenderer::sortObjects() -{ - auto getRow = [](const RenderableInstance & object) -> int - { - if (object.tile.isValid()) - return object.tile.getY(); - - if ( object.tile == BattleHex::HEX_BEFORE_ALL ) - return -1; - - assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID); - return GameConstants::BFIELD_HEIGHT; - }; - - std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){ - if ( getRow(left) != getRow(right)) - return getRow(left) < getRow(right); - return left.layer < right.layer; - }); -} - -void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas) -{ - for (auto const & object : objects) - object.functor(targetCanvas); -} - -BattleRenderer::BattleRenderer(BattleInterface & owner): - owner(owner) -{ -} - -void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor) -{ - objects.push_back({functor, layer, tile}); -} - -void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas) -{ - collectObjects(); - sortObjects(); - renderObjects(targetCanvas); -} +/* + * BattleFieldController.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 "BattleRenderer.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleEffectsController.h" +#include "BattleWindow.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "BattleObstacleController.h" + +void BattleRenderer::collectObjects() +{ + owner.effectsController->collectRenderableObjects(*this); + owner.obstacleController->collectRenderableObjects(*this); + owner.stacksController->collectRenderableObjects(*this); + if (owner.siegeController) + owner.siegeController->collectRenderableObjects(*this); + if (owner.defendingHero) + owner.defendingHero->collectRenderableObjects(*this); + if (owner.attackingHero) + owner.attackingHero->collectRenderableObjects(*this); +} + +void BattleRenderer::sortObjects() +{ + auto getRow = [](const RenderableInstance & object) -> int + { + if (object.tile.isValid()) + return object.tile.getY(); + + if ( object.tile == BattleHex::HEX_BEFORE_ALL ) + return -1; + + assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID); + return GameConstants::BFIELD_HEIGHT; + }; + + std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){ + if ( getRow(left) != getRow(right)) + return getRow(left) < getRow(right); + return left.layer < right.layer; + }); +} + +void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas) +{ + for (auto const & object : objects) + object.functor(targetCanvas); +} + +BattleRenderer::BattleRenderer(BattleInterface & owner): + owner(owner) +{ +} + +void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor) +{ + objects.push_back({functor, layer, tile}); +} + +void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas) +{ + collectObjects(); + sortObjects(); + renderObjects(targetCanvas); +} diff --git a/client/battle/BattleRenderer.h b/client/battle/BattleRenderer.h index 0dbab110b..9a5f20dd8 100644 --- a/client/battle/BattleRenderer.h +++ b/client/battle/BattleRenderer.h @@ -1,53 +1,53 @@ -/* - * BattleFieldController.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/battle/BattleHex.h" - -class Canvas; -class BattleInterface; - -enum class EBattleFieldLayer { - // confirmed ordering requirements: - CORPSES = 0, - WALLS = 1, - HEROES = 2, - STACKS = 2, // after corpses, obstacles, walls - OBSTACLES = 3, // after stacks - STACK_AMOUNTS = 3, // after stacks, obstacles, corpses - EFFECTS = 4, // after obstacles, battlements -}; - -class BattleRenderer -{ -public: - using RendererRef = Canvas &; - using RenderFunctor = std::function; - -private: - BattleInterface & owner; - - struct RenderableInstance - { - RenderFunctor functor; - EBattleFieldLayer layer; - BattleHex tile; - }; - std::vector objects; - - void collectObjects(); - void sortObjects(); - void renderObjects(RendererRef targetCanvas); -public: - BattleRenderer(BattleInterface & owner); - - void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor); - void execute(RendererRef targetCanvas); -}; +/* + * BattleFieldController.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/battle/BattleHex.h" + +class Canvas; +class BattleInterface; + +enum class EBattleFieldLayer { + // confirmed ordering requirements: + CORPSES = 0, + WALLS = 1, + HEROES = 2, + STACKS = 2, // after corpses, obstacles, walls + OBSTACLES = 3, // after stacks + STACK_AMOUNTS = 3, // after stacks, obstacles, corpses + EFFECTS = 4, // after obstacles, battlements +}; + +class BattleRenderer +{ +public: + using RendererRef = Canvas &; + using RenderFunctor = std::function; + +private: + BattleInterface & owner; + + struct RenderableInstance + { + RenderFunctor functor; + EBattleFieldLayer layer; + BattleHex tile; + }; + std::vector objects; + + void collectObjects(); + void sortObjects(); + void renderObjects(RendererRef targetCanvas); +public: + BattleRenderer(BattleInterface & owner); + + void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor); + void execute(RendererRef targetCanvas); +}; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 63d5bd9e5..660440f1e 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -1,369 +1,367 @@ -/* - * BattleSiegeController.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 "BattleSiegeController.h" - -#include "BattleAnimationClasses.h" -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleStacksController.h" -#include "BattleFieldController.h" -#include "BattleRenderer.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" - -#include "../../CCallback.h" -#include "../../lib/NetPacks.h" -#include "../../lib/CStack.h" -#include "../../lib/mapObjects/CGTownInstance.h" - -std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const -{ - auto getImageIndex = [&]() -> int - { - bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER); - - switch (state) - { - 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 - return 3; - } - return 1; - }; - - const std::string & prefix = town->town->clientInfo.siegePrefix; - std::string addit = std::to_string(getImageIndex()); - - switch(what) - { - case EWallVisual::BACKGROUND_WALL: - { - switch(town->town->faction->getIndex()) - { - case ETownType::RAMPART: - case ETownType::NECROPOLIS: - case ETownType::DUNGEON: - case ETownType::STRONGHOLD: - return prefix + "TPW1.BMP"; - default: - return prefix + "TPWL.BMP"; - } - } - case EWallVisual::KEEP: - return prefix + "MAN" + addit + ".BMP"; - case EWallVisual::BOTTOM_TOWER: - return prefix + "TW1" + addit + ".BMP"; - case EWallVisual::BOTTOM_WALL: - return prefix + "WA1" + addit + ".BMP"; - case EWallVisual::WALL_BELLOW_GATE: - return prefix + "WA3" + addit + ".BMP"; - case EWallVisual::WALL_OVER_GATE: - return prefix + "WA4" + addit + ".BMP"; - case EWallVisual::UPPER_WALL: - return prefix + "WA6" + addit + ".BMP"; - case EWallVisual::UPPER_TOWER: - return prefix + "TW2" + addit + ".BMP"; - case EWallVisual::GATE: - return prefix + "DRW" + addit + ".BMP"; - case EWallVisual::GATE_ARCH: - return prefix + "ARCH.BMP"; - case EWallVisual::BOTTOM_STATIC_WALL: - return prefix + "WA2.BMP"; - case EWallVisual::UPPER_STATIC_WALL: - return prefix + "WA5.BMP"; - case EWallVisual::MOAT: - return prefix + "MOAT.BMP"; - case EWallVisual::MOAT_BANK: - return prefix + "MLIP.BMP"; - case EWallVisual::KEEP_BATTLEMENT: - return prefix + "MANC.BMP"; - case EWallVisual::BOTTOM_BATTLEMENT: - return prefix + "TW1C.BMP"; - case EWallVisual::UPPER_BATTLEMENT: - return prefix + "TW2C.BMP"; - default: - return ""; - } -} - -void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) -{ - auto & ci = town->town->clientInfo; - auto const & pos = ci.siegePositions[what]; - - if ( wallPieceImages[what] && pos.isValid()) - canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); -} - -std::string BattleSiegeController::getBattleBackgroundName() const -{ - const std::string & prefix = town->town->clientInfo.siegePrefix; - return prefix + "BACK.BMP"; -} - -bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const -{ - //FIXME: use this instead of buildings test? - //ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel(); - - 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.curInt->cb->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; - case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; - case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; - default: return true; - } -} - -BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const -{ - static const std::array wallsPositions = { - BattleHex::INVALID, // BACKGROUND, // handled separately - BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL, - 135, // KEEP, - BattleHex::HEX_AFTER_ALL, // BOTTOM_TOWER, - 182, // BOTTOM_WALL, - 130, // WALL_BELLOW_GATE, - 62, // WALL_OVER_GATE, - 12, // UPPER_WALL, - BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER, - BattleHex::HEX_BEFORE_ALL, // GATE, // 94 - 112, // GATE_ARCH, - 165, // BOTTOM_STATIC_WALL, - 45, // UPPER_STATIC_WALL, - BattleHex::INVALID, // MOAT, // printed as absolute obstacle - BattleHex::INVALID, // MOAT_BANK, // printed as absolute obstacle - 135, // KEEP_BATTLEMENT, - BattleHex::HEX_AFTER_ALL, // BOTTOM_BATTLEMENT, - BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT, - }; - - return wallsPositions[what]; -} - -BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown): - owner(owner), - town(siegeTown) -{ - assert(owner.fieldController.get() == nullptr); // must be created after this - - for (int g = 0; g < wallPieceImages.size(); ++g) - { - if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state - continue; - - if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) - continue; - - wallPieceImages[g] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); - } -} - -const CCreature *BattleSiegeController::getTurretCreature() const -{ - return CGI->creh->objects[town->town->clientInfo.siegeShooter]; -} - -Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const -{ - // Turret positions are read out of the config/wall_pos.txt - int posID = 0; - switch (position) - { - case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature - posID = EWallVisual::CREATURE_KEEP; - break; - case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature - posID = EWallVisual::CREATURE_BOTTOM_TOWER; - break; - case BattleHex::CASTLE_UPPER_TOWER: // upper creature - posID = EWallVisual::CREATURE_UPPER_TOWER; - break; - } - - if (posID != 0) - { - return { - town->town->clientInfo.siegePositions[posID].x, - town->town->clientInfo.siegePositions[posID].y - }; - } - - assert(0); - return Point(0,0); -} - -void BattleSiegeController::gateStateChanged(const EGateState state) -{ - auto oldState = owner.curInt->cb->battleGetGateState(); - bool playSound = false; - auto stateId = EWallState::NONE; - switch(state) - { - case EGateState::CLOSED: - if (oldState != EGateState::BLOCKED) - playSound = true; - break; - case EGateState::BLOCKED: - if (oldState != EGateState::CLOSED) - playSound = true; - break; - case EGateState::OPENED: - playSound = true; - stateId = EWallState::DAMAGED; - break; - case EGateState::DESTROYED: - stateId = EWallState::DESTROYED; - break; - } - - if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED) - wallPieceImages[EWallVisual::GATE] = nullptr; - - if (stateId != EWallState::NONE) - wallPieceImages[EWallVisual::GATE] = IImage::createFromFile(getWallPieceImageName(EWallVisual::GATE, stateId)); - - if (playSound) - CCS->soundh->playSound(soundBase::DRAWBRG); -} - -void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) -{ - if (getWallPieceExistance(EWallVisual::MOAT)) - showWallPiece(canvas, EWallVisual::MOAT); - - if (getWallPieceExistance(EWallVisual::MOAT_BANK)) - showWallPiece(canvas, EWallVisual::MOAT_BANK); -} - -BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const -{ - switch(wallPiece) - { - case EWallVisual::KEEP_BATTLEMENT: return BattleHex::CASTLE_CENTRAL_TOWER; - case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER; - case EWallVisual::UPPER_BATTLEMENT: return BattleHex::CASTLE_UPPER_TOWER; - } - assert(0); - return BattleHex::INVALID; -} - -const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const -{ - for (auto & stack : owner.curInt->cb->battleGetAllStacks(true)) - { - if ( stack->initialPosition == getTurretBattleHex(wallPiece)) - return stack; - } - assert(0); - return nullptr; -} - -void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i) - { - auto wallPiece = EWallVisual::EWallVisual(i); - - if ( !getWallPieceExistance(wallPiece)) - continue; - - if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID) - continue; - - if (wallPiece == EWallVisual::KEEP_BATTLEMENT || - wallPiece == EWallVisual::BOTTOM_BATTLEMENT || - wallPiece == EWallVisual::UPPER_BATTLEMENT) - { - renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); - }); - renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece); - }); - } - renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece); - }); - } -} - -bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const -{ - if (owner.tacticsMode) - return false; - - auto wallPart = owner.curInt->cb->battleHexToWallPart(hex); - return owner.curInt->cb->isWallPartAttackable(wallPart); -} - -void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) -{ - if (ca.attacker != -1) - { - const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker); - for (auto attackInfo : ca.attackedParts) - { - owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); - } - } - else - { - std::vector positions; - - //no attacker stack, assume spell-related (earthquake) - only hit animation - for (auto attackInfo : ca.attackedParts) - positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); - - CCS->soundh->playSound( "WALLHIT" ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions)); - } - - owner.waitForAnimations(); - - for (auto attackInfo : ca.attackedParts) - { - int wallId = static_cast(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST; - //gate state changing handled separately - if (wallId == EWallVisual::GATE) - continue; - - auto wallState = EWallState(owner.curInt->cb->battleGetWallState(attackInfo.attackedPart)); - - wallPieceImages[wallId] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); - } -} - -const CGTownInstance *BattleSiegeController::getSiegedTown() const -{ - return town; -} +/* + * BattleSiegeController.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 "BattleSiegeController.h" + +#include "BattleAnimationClasses.h" +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleStacksController.h" +#include "BattleFieldController.h" +#include "BattleRenderer.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" + +ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const +{ + auto getImageIndex = [&]() -> int + { + bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER); + + switch (state) + { + 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 + return 3; + } + return 1; + }; + + const std::string & prefix = town->town->clientInfo.siegePrefix; + std::string addit = std::to_string(getImageIndex()); + + switch(what) + { + case EWallVisual::BACKGROUND_WALL: + { + auto faction = town->town->faction->getIndex(); + + if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) + return ImagePath::builtinTODO(prefix + "TPW1.BMP"); + else + return ImagePath::builtinTODO(prefix + "TPWL.BMP"); + } + case EWallVisual::KEEP: + return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP"); + case EWallVisual::BOTTOM_TOWER: + return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP"); + case EWallVisual::BOTTOM_WALL: + return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP"); + case EWallVisual::WALL_BELLOW_GATE: + return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP"); + case EWallVisual::WALL_OVER_GATE: + return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP"); + case EWallVisual::UPPER_WALL: + return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP"); + case EWallVisual::UPPER_TOWER: + return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP"); + case EWallVisual::GATE: + return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP"); + case EWallVisual::GATE_ARCH: + return ImagePath::builtinTODO(prefix + "ARCH.BMP"); + case EWallVisual::BOTTOM_STATIC_WALL: + return ImagePath::builtinTODO(prefix + "WA2.BMP"); + case EWallVisual::UPPER_STATIC_WALL: + return ImagePath::builtinTODO(prefix + "WA5.BMP"); + case EWallVisual::MOAT: + return ImagePath::builtinTODO(prefix + "MOAT.BMP"); + case EWallVisual::MOAT_BANK: + return ImagePath::builtinTODO(prefix + "MLIP.BMP"); + case EWallVisual::KEEP_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "MANC.BMP"); + case EWallVisual::BOTTOM_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "TW1C.BMP"); + case EWallVisual::UPPER_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "TW2C.BMP"); + default: + return ImagePath(); + } +} + +void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) +{ + auto & ci = town->town->clientInfo; + auto const & pos = ci.siegePositions[what]; + + if ( wallPieceImages[what] && pos.isValid()) + canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); +} + +ImagePath BattleSiegeController::getBattleBackgroundName() const +{ + const std::string & prefix = town->town->clientInfo.siegePrefix; + return ImagePath::builtinTODO(prefix + "BACK.BMP"); +} + +bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const +{ + //FIXME: use this instead of buildings test? + //ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel(); + + 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; + default: return true; + } +} + +BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const +{ + static const std::array wallsPositions = { + BattleHex::INVALID, // BACKGROUND, // handled separately + BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL, + 135, // KEEP, + BattleHex::HEX_AFTER_ALL, // BOTTOM_TOWER, + 182, // BOTTOM_WALL, + 130, // WALL_BELLOW_GATE, + 62, // WALL_OVER_GATE, + 12, // UPPER_WALL, + BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER, + BattleHex::HEX_BEFORE_ALL, // GATE, // 94 + 112, // GATE_ARCH, + 165, // BOTTOM_STATIC_WALL, + 45, // UPPER_STATIC_WALL, + BattleHex::INVALID, // MOAT, // printed as absolute obstacle + BattleHex::INVALID, // MOAT_BANK, // printed as absolute obstacle + 135, // KEEP_BATTLEMENT, + BattleHex::HEX_AFTER_ALL, // BOTTOM_BATTLEMENT, + BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT, + }; + + return wallsPositions[what]; +} + +BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown): + owner(owner), + town(siegeTown) +{ + assert(owner.fieldController.get() == nullptr); // must be created after this + + for (int g = 0; g < wallPieceImages.size(); ++g) + { + if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state + continue; + + if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) + continue; + + wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); + } +} + +const CCreature *BattleSiegeController::getTurretCreature() const +{ + return CGI->creh->objects[town->town->clientInfo.siegeShooter]; +} + +Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const +{ + // Turret positions are read out of the config/wall_pos.txt + int posID = 0; + switch (position) + { + case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature + posID = EWallVisual::CREATURE_KEEP; + break; + case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature + posID = EWallVisual::CREATURE_BOTTOM_TOWER; + break; + case BattleHex::CASTLE_UPPER_TOWER: // upper creature + posID = EWallVisual::CREATURE_UPPER_TOWER; + break; + } + + if (posID != 0) + { + return { + town->town->clientInfo.siegePositions[posID].x, + town->town->clientInfo.siegePositions[posID].y + }; + } + + assert(0); + return Point(0,0); +} + +void BattleSiegeController::gateStateChanged(const EGateState state) +{ + auto oldState = owner.getBattle()->battleGetGateState(); + bool playSound = false; + auto stateId = EWallState::NONE; + switch(state) + { + case EGateState::CLOSED: + if (oldState != EGateState::BLOCKED) + playSound = true; + break; + case EGateState::BLOCKED: + if (oldState != EGateState::CLOSED) + playSound = true; + break; + case EGateState::OPENED: + playSound = true; + stateId = EWallState::DAMAGED; + break; + case EGateState::DESTROYED: + stateId = EWallState::DESTROYED; + break; + } + + if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED) + wallPieceImages[EWallVisual::GATE] = nullptr; + + if (stateId != EWallState::NONE) + wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId)); + + if (playSound) + CCS->soundh->playSound(soundBase::DRAWBRG); +} + +void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) +{ + if (getWallPieceExistance(EWallVisual::MOAT)) + showWallPiece(canvas, EWallVisual::MOAT); + + if (getWallPieceExistance(EWallVisual::MOAT_BANK)) + showWallPiece(canvas, EWallVisual::MOAT_BANK); +} + +BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const +{ + switch(wallPiece) + { + case EWallVisual::KEEP_BATTLEMENT: return BattleHex::CASTLE_CENTRAL_TOWER; + case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER; + case EWallVisual::UPPER_BATTLEMENT: return BattleHex::CASTLE_UPPER_TOWER; + } + assert(0); + return BattleHex::INVALID; +} + +const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const +{ + for (auto & stack : owner.getBattle()->battleGetAllStacks(true)) + { + if ( stack->initialPosition == getTurretBattleHex(wallPiece)) + return stack; + } + assert(0); + return nullptr; +} + +void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i) + { + auto wallPiece = EWallVisual::EWallVisual(i); + + if ( !getWallPieceExistance(wallPiece)) + continue; + + if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID) + continue; + + if (wallPiece == EWallVisual::KEEP_BATTLEMENT || + wallPiece == EWallVisual::BOTTOM_BATTLEMENT || + wallPiece == EWallVisual::UPPER_BATTLEMENT) + { + renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); + }); + renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + showWallPiece(canvas, wallPiece); + }); + } + renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + showWallPiece(canvas, wallPiece); + }); + } +} + +bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const +{ + if (owner.tacticsMode) + return false; + + auto wallPart = owner.getBattle()->battleHexToWallPart(hex); + return owner.getBattle()->isWallPartAttackable(wallPart); +} + +void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) +{ + if (ca.attacker != -1) + { + const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker); + for (auto attackInfo : ca.attackedParts) + { + owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); + } + } + else + { + std::vector positions; + + //no attacker stack, assume spell-related (earthquake) - only hit animation + for (auto attackInfo : ca.attackedParts) + positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); + + CCS->soundh->playSound( AudioPath::builtin("WALLHIT") ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions)); + } + + owner.waitForAnimations(); + + for (auto attackInfo : ca.attackedParts) + { + int wallId = static_cast(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST; + //gate state changing handled separately + if (wallId == EWallVisual::GATE) + continue; + + auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart)); + + wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); + } +} + +const CGTownInstance *BattleSiegeController::getSiegedTown() const +{ + return town; +} diff --git a/client/battle/BattleSiegeController.h b/client/battle/BattleSiegeController.h index 262b78b76..196967296 100644 --- a/client/battle/BattleSiegeController.h +++ b/client/battle/BattleSiegeController.h @@ -1,110 +1,111 @@ -/* - * BattleObstacleController.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/GameConstants.h" -#include "../../lib/battle/BattleHex.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct CatapultAttack; -class CCreature; -class CStack; -class CGTownInstance; -class Point; - -VCMI_LIB_NAMESPACE_END - -class Canvas; -class BattleInterface; -class BattleRenderer; -class IImage; - -namespace EWallVisual -{ - enum EWallVisual - { - BACKGROUND, - BACKGROUND_WALL, - - KEEP, - BOTTOM_TOWER, - BOTTOM_WALL, - WALL_BELLOW_GATE, - WALL_OVER_GATE, - UPPER_WALL, - UPPER_TOWER, - GATE, - - GATE_ARCH, - BOTTOM_STATIC_WALL, - UPPER_STATIC_WALL, - MOAT, - MOAT_BANK, - KEEP_BATTLEMENT, - BOTTOM_BATTLEMENT, - UPPER_BATTLEMENT, - - CREATURE_KEEP, - CREATURE_BOTTOM_TOWER, - CREATURE_UPPER_TOWER, - - WALL_FIRST = BACKGROUND_WALL, - WALL_LAST = UPPER_BATTLEMENT, - - // these entries are mapped to EWallPart enum - DESTRUCTIBLE_FIRST = KEEP, - DESTRUCTIBLE_LAST = GATE, - }; -} - -class BattleSiegeController -{ - BattleInterface & owner; - - /// besieged town - const CGTownInstance *town; - - /// sections of castle walls, in their currently visible state - std::array, EWallVisual::WALL_LAST + 1> wallPieceImages; - - /// return URI for image for a wall piece - std::string getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; - - /// returns BattleHex to which chosen wall piece is bound - BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; - - /// returns true if chosen wall piece should be present in current battle - bool getWallPieceExistance(EWallVisual::EWallVisual what) const; - - void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); - - BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const; - const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const; - -public: - BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown); - - /// call-ins from server - void gateStateChanged(const EGateState state); - void stackIsCatapulting(const CatapultAttack & ca); - - /// call-ins from other battle controllers - void showAbsoluteObstacles(Canvas & canvas); - void collectRenderableObjects(BattleRenderer & renderer); - - /// queries from other battle controllers - bool isAttackableByCatapult(BattleHex hex) const; - std::string getBattleBackgroundName() const; - const CCreature *getTurretCreature() const; - Point getTurretCreaturePosition( BattleHex position ) const; - - const CGTownInstance *getSiegedTown() const; -}; +/* + * BattleObstacleController.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/GameConstants.h" +#include "../../lib/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CatapultAttack; +class CCreature; +class CStack; +class CGTownInstance; +class Point; + +VCMI_LIB_NAMESPACE_END + +class Canvas; +class BattleInterface; +class BattleRenderer; +class IImage; + +namespace EWallVisual +{ + enum EWallVisual + { + BACKGROUND, + BACKGROUND_WALL, + + KEEP, + BOTTOM_TOWER, + BOTTOM_WALL, + WALL_BELLOW_GATE, + WALL_OVER_GATE, + UPPER_WALL, + UPPER_TOWER, + GATE, + + GATE_ARCH, + BOTTOM_STATIC_WALL, + UPPER_STATIC_WALL, + MOAT, + MOAT_BANK, + KEEP_BATTLEMENT, + BOTTOM_BATTLEMENT, + UPPER_BATTLEMENT, + + CREATURE_KEEP, + CREATURE_BOTTOM_TOWER, + CREATURE_UPPER_TOWER, + + WALL_FIRST = BACKGROUND_WALL, + WALL_LAST = UPPER_BATTLEMENT, + + // these entries are mapped to EWallPart enum + DESTRUCTIBLE_FIRST = KEEP, + DESTRUCTIBLE_LAST = GATE, + }; +} + +class BattleSiegeController +{ + BattleInterface & owner; + + /// besieged town + const CGTownInstance *town; + + /// sections of castle walls, in their currently visible state + std::array, EWallVisual::WALL_LAST + 1> wallPieceImages; + + /// return URI for image for a wall piece + ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; + + /// returns BattleHex to which chosen wall piece is bound + BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; + + /// returns true if chosen wall piece should be present in current battle + bool getWallPieceExistance(EWallVisual::EWallVisual what) const; + + void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); + + BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const; + const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const; + +public: + BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown); + + /// call-ins from server + void gateStateChanged(const EGateState state); + void stackIsCatapulting(const CatapultAttack & ca); + + /// call-ins from other battle controllers + void showAbsoluteObstacles(Canvas & canvas); + void collectRenderableObjects(BattleRenderer & renderer); + + /// queries from other battle controllers + bool isAttackableByCatapult(BattleHex hex) const; + ImagePath getBattleBackgroundName() const; + const CCreature *getTurretCreature() const; + Point getTurretCreaturePosition( BattleHex position ) const; + + const CGTownInstance *getSiegedTown() const; +}; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 4abc8b6ed..85454fb83 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -1,893 +1,894 @@ -/* - * BattleStacksController.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 "BattleStacksController.h" - -#include "BattleSiegeController.h" -#include "BattleInterfaceClasses.h" -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleAnimationClasses.h" -#include "BattleFieldController.h" -#include "BattleEffectsController.h" -#include "BattleProjectileController.h" -#include "BattleWindow.h" -#include "BattleRenderer.h" -#include "CreatureAnimation.h" - -#include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../gui/CGuiHandler.h" -#include "../render/Colors.h" -#include "../render/Canvas.h" - -#include "../../CCallback.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/battle/BattleHex.h" -#include "../../lib/CStack.h" -#include "../../lib/CondSh.h" -#include "../../lib/TextOperations.h" - -static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) -{ - std::shared_ptr animation = anim.lock(); - if(!animation) - return; - - if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN) - animation->setType(ECreatureAnimType::HOLDING); - - if (animation->isIdle()) - { - const CCreature *creature = stack->unitType(); - - if (stack->isFrozen()) - animation->setType(ECreatureAnimType::FROZEN); - else - if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0) - { - if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) - animation->playOnce(ECreatureAnimType::MOUSEON); - else - animation->setType(ECreatureAnimType::HOLDING); - } - else - { - animation->setType(ECreatureAnimType::HOLDING); - } - } - // always reset callback - animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim); -} - -BattleStacksController::BattleStacksController(BattleInterface & owner): - owner(owner), - activeStack(nullptr), - stackToActivate(nullptr), - animIDhelper(0) -{ - //preparing graphics for displaying amounts of creatures - amountNormal = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountPositive = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountNegative = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountEffNeutral = IImage::createFromFile("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); - - std::vector stacks = owner.curInt->cb->battleGetAllStacks(true); - for(const CStack * s : stacks) - { - stackAdded(s, true); - } -} - -BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const -{ - if ( !stackAnimation.at(stack->unitId())->isMoving()) - return stack->getPosition(); - - if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING ) - return BattleHex::HEX_AFTER_ALL; - - for (auto & anim : currentAnimations) - { - // certainly ugly workaround but fixes quite annoying bug - // stack position will be updated only *after* movement is finished - // before this - stack is always at its initial position. Thus we need to find - // its current position. Which can be found only in this class - if (StackMoveAnimation *move = dynamic_cast(anim)) - { - if (move->stack == stack) - return std::max(move->prevHex, move->nextHex); - } - } - return stack->getPosition(); -} - -void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) -{ - auto stacks = owner.curInt->cb->battleGetAllStacks(false); - - for (auto stack : stacks) - { - if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks - continue; - - //FIXME: hack to ignore ghost stacks - if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost()) - continue; - - auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS; - auto location = getStackCurrentPosition(stack); - - renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){ - showStack(renderer, stack); - }); - - if (stackNeedsAmountBox(stack)) - { - renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){ - showStackAmountBox(renderer, stack); - }); - } - } -} - -void BattleStacksController::stackReset(const CStack * stack) -{ - auto iter = stackAnimation.find(stack->unitId()); - - if(iter == stackAnimation.end()) - { - logGlobal->error("Unit %d have no animation", stack->unitId()); - return; - } - - auto animation = iter->second; - - if(stack->alive() && animation->isDeadOrDying()) - { - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - addNewAnim(new ResurrectionAnimation(owner, stack)); - }); - } -} - -void BattleStacksController::stackAdded(const CStack * stack, bool instant) -{ - // Tower shooters have only their upper half visible - static const int turretCreatureAnimationHeight = 232; - - stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position - - Point coords = getStackPositionAtHex(stack->getPosition(), stack); - - if(stack->initialPosition < 0) //turret - { - assert(owner.siegeController); - - const CCreature *turretCreature = owner.siegeController->getTurretCreature(); - - stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature); - stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight; - stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); - - // FIXME: workaround for visible animation of Medusa tails (animation disabled in H3) - if (turretCreature->getId() == CreatureID::MEDUSA ) - stackAnimation[stack->unitId()]->pos.w = 250; - - coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition); - } - else - { - stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType()); - stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]); - stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight(); - stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); - } - stackAnimation[stack->unitId()]->pos.x = coords.x; - stackAnimation[stack->unitId()]->pos.y = coords.y; - stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING); - - if (!instant) - { - // immediately make stack transparent, giving correct shifter time to start - auto shifterFade = ColorFilter::genAlphaShifter(0); - setStackColorFilter(shifterFade, stack, nullptr, true); - - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr)); - if (stack->isClone()) - addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() )); - }); - } -} - -void BattleStacksController::setActiveStack(const CStack *stack) -{ - if (activeStack) // update UI - stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); - - activeStack = stack; - - if (activeStack) // update UI - stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); - - owner.windowObject->blockUI(activeStack == nullptr); - - if (activeStack) - stackAmountBoxHidden.clear(); -} - -bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const -{ - //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature - if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1) - return false; - - if(!stack->alive()) - return false; - - //hide box when target is going to die anyway - do not display "0 creatures" - if(stack->getCount() == 0) - return false; - - // if stack has any ongoing animation - hide the box - if (stackAmountBoxHidden.count(stack->unitId())) - return false; - - return true; -} - -std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * stack) -{ - std::vector activeSpells = stack->activeSpells(); - - if ( activeSpells.empty()) - return amountNormal; - - int effectsPositivness = 0; - - for(const auto & spellID : activeSpells) - effectsPositivness += CGI->spellh->objects.at(spellID)->positiveness; - - if (effectsPositivness > 0) - return amountPositive; - - if (effectsPositivness < 0) - return amountNegative; - - return amountEffNeutral; -} - -void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack) -{ - auto amountBG = getStackAmountBox(stack); - - bool doubleWide = stack->doubleWide(); - bool turnedRight = facingRight(stack); - bool attacker = stack->unitSide() == BattleSide::ATTACKER; - - BattleHex stackPos = stack->getPosition(); - - // double-wide unit turned around - use opposite hex for stack label - if (doubleWide && turnedRight != attacker) - stackPos = stack->occupiedHex(); - - BattleHex frontPos = turnedRight ? - stackPos.cloneInDirection(BattleHex::RIGHT) : - stackPos.cloneInDirection(BattleHex::LEFT); - - bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos); - - Point boxPosition; - - if (moveInside) - { - boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1); - } - else - { - if (turnedRight) - boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1); - else - boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); - } - - Point textPosition = amountBG->dimensions()/2 + boxPosition; - - canvas.draw(amountBG, boxPosition); - canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); -} - -void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) -{ - ColorFilter fullFilter = ColorFilter::genEmptyShifter(); - for(const auto & filter : stackFilterEffects) - { - if (filter.target == stack) - fullFilter = ColorFilter::genCombined(fullFilter, filter.effect); - } - - stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit -} - -void BattleStacksController::tick(uint32_t msPassed) -{ - updateHoveredStacks(); - updateBattleAnimations(msPassed); -} - -void BattleStacksController::initializeBattleAnimations() -{ - auto copiedVector = currentAnimations; - for (auto & elem : copiedVector) - if (elem && !elem->isInitialized()) - elem->tryInitialize(); -} - -void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed) -{ - for (auto stack : owner.curInt->cb->battleGetAllStacks(true)) - { - if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks - continue; - - stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f); - } - - // operate on copy - to prevent potential iterator invalidation due to push_back's - // FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing) - - auto copiedVector = currentAnimations; - for (auto & elem : copiedVector) - if (elem && elem->isInitialized()) - elem->tick(msPassed); -} - -void BattleStacksController::updateBattleAnimations(uint32_t msPassed) -{ - bool hadAnimations = !currentAnimations.empty(); - initializeBattleAnimations(); - tickFrameBattleAnimations(msPassed); - vstd::erase(currentAnimations, nullptr); - - if (currentAnimations.empty()) - owner.executeStagedAnimations(); - - if (hadAnimations && currentAnimations.empty()) - owner.onAnimationsFinished(); - - initializeBattleAnimations(); -} - -void BattleStacksController::addNewAnim(BattleAnimation *anim) -{ - if (currentAnimations.empty()) - stackAmountBoxHidden.clear(); - - owner.onAnimationsStarted(); - currentAnimations.push_back(anim); - - auto stackAnimation = dynamic_cast(anim); - if(stackAnimation) - stackAmountBoxHidden.insert(stackAnimation->stack->unitId()); -} - -void BattleStacksController::stackRemoved(uint32_t stackID) -{ - if (getActiveStack() && getActiveStack()->unitId() == stackID) - { - BattleAction action; - action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - action.actionType = EActionType::CANCEL; - action.stackNumber = getActiveStack()->unitId(); - - LOCPLINT->cb->battleMakeUnitAction(action); - setActiveStack(nullptr); - } -} - -void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) -{ - owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ - // remove any potentially erased petrification effect - removeExpiredColorFilters(); - }); - - for(auto & attackedInfo : attackedInfos) - { - if (!attackedInfo.attacker) - continue; - - // In H3, attacked stack will not reverse on ranged attack - if (attackedInfo.indirectAttack) - continue; - - // Another type of indirect attack - dragon breath - if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender)) - continue; - - // 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 - // if (needsReverse && !attackedInfo.defender->isFrozen()) - if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() - { - addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition())); - }); - } - } - - for(auto & attackedInfo : attackedInfos) - { - bool useDeathAnim = attackedInfo.killed; - bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed; - - EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT; - - owner.addToAnimationStage(usedEvent, [=]() - { - if (useDeathAnim) - addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack)); - else if(useDefenceAnim) - addNewAnim(new DefenceAnimation(owner, attackedInfo.defender)); - else - addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); - - if (attackedInfo.fireShield) - owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, "FIRESHIE", attackedInfo.attacker->getPosition()); - - if (attackedInfo.spellEffect != SpellID::NONE) - { - auto spell = attackedInfo.spellEffect.toSpell(); - if (!spell->getCastSound().empty()) - CCS->soundh->playSound(spell->getCastSound()); - - - owner.displaySpellEffect(spell, attackedInfo.defender->getPosition()); - } - }); - } - - for (auto & attackedInfo : attackedInfos) - { - if (attackedInfo.rebirth) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - owner.effectsController->displayEffect(EBattleEffect::RESURRECT, "RESURECT", attackedInfo.defender->getPosition()); - addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); - }); - } - - if (attackedInfo.killed && attackedInfo.defender->summoned) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr)); - stackRemoved(attackedInfo.defender->unitId()); - }); - } - } - owner.executeStagedAnimations(); - owner.waitForAnimations(); -} - -void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) -{ - assert(destHex.size() > 0); - //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed - - owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ - addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); - }); - - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack)); - addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) ); - }); - - // animations will be executed by spell -} - -void BattleStacksController::stackMoved(const CStack *stack, std::vector destHex, int distance) -{ - assert(destHex.size() > 0); - owner.checkForAnimations(); - - if(shouldRotate(stack, stack->getPosition(), destHex[0])) - { - owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]() - { - addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition())); - }); - } - - owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]() - { - addNewAnim(new MovementStartAnimation(owner, stack)); - }); - - if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, 1))) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() - { - addNewAnim(new MovementAnimation(owner, stack, destHex, distance)); - }); - } - - owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]() - { - addNewAnim(new MovementEndAnimation(owner, stack, destHex.back())); - }); - - owner.executeStagedAnimations(); - owner.waitForAnimations(); -} - -bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender) -{ - bool mustReverse = owner.curInt->cb->isToReverse( - attacker, - defender); - - if (attacker->unitSide() == BattleSide::ATTACKER) - return !mustReverse; - else - return mustReverse; -} - -void BattleStacksController::stackAttacking( const StackAttackInfo & info ) -{ - owner.checkForAnimations(); - - auto attacker = info.attacker; - auto defender = info.defender; - auto tile = info.tile; - auto spellEffect = info.spellEffect; - auto multiAttack = !info.secondaryDefender.empty(); - bool needsReverse = false; - - if (info.indirectAttack) - { - needsReverse = shouldRotate(attacker, attacker->position, info.tile); - } - else - { - needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker); - } - - if (needsReverse) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() - { - addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition())); - }); - } - - if(info.lucky) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); - owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, "GOODLUCK", attacker->getPosition()); - }); - } - - if(info.unlucky) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); - owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, "BADLUCK", attacker->getPosition()); - }); - } - - if(info.deathBlow) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); - owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, "DEATHBLO", defender->getPosition()); - }); - - for(auto elem : info.secondaryDefender) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition()); - }); - } - } - - owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]() - { - if (info.indirectAttack) - { - addNewAnim(new ShootingAnimation(owner, attacker, tile, defender)); - } - else - { - addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack)); - } - }); - - if (info.spellEffect != SpellID::NONE) - { - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - owner.displaySpellHit(spellEffect.toSpell(), tile); - }); - } - - if (info.lifeDrain) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() - { - owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, "DRAINLIF", attacker->getPosition()); - }); - } - - //return, animation playback will be handled by stacksAreAttacked -} - -bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const -{ - Point begPosition = getStackPositionAtHex(oldPos,stack); - Point endPosition = getStackPositionAtHex(nextHex, stack); - - if((begPosition.x > endPosition.x) && facingRight(stack)) - return true; - else if((begPosition.x < endPosition.x) && !facingRight(stack)) - return true; - - return false; -} - -void BattleStacksController::endAction(const BattleAction* action) -{ - owner.checkForAnimations(); - - //check if we should reverse stacks - TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY); - - for (const CStack *s : stacks) - { - bool shouldFaceRight = s && s->unitSide() == BattleSide::ATTACKER; - - if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle()) - { - addNewAnim(new ReverseAnimation(owner, s, s->getPosition())); - } - } - owner.executeStagedAnimations(); - owner.waitForAnimations(); - - stackAmountBoxHidden.clear(); - - owner.windowObject->blockUI(activeStack == nullptr); - removeExpiredColorFilters(); -} - -void BattleStacksController::startAction(const BattleAction* action) -{ - removeExpiredColorFilters(); -} - -void BattleStacksController::stackActivated(const CStack *stack) -{ - stackToActivate = stack; - owner.waitForAnimations(); - logAnim->debug("Activating next stack"); - owner.activateStack(); -} - -void BattleStacksController::deactivateStack() -{ - if (!activeStack) { - return; - } - stackToActivate = activeStack; - setActiveStack(nullptr); -} - -void BattleStacksController::activateStack() -{ - if ( !currentAnimations.empty()) - return; - - if ( !stackToActivate) - return; - - owner.trySetActivePlayer(stackToActivate->unitOwner()); - - setActiveStack(stackToActivate); - stackToActivate = nullptr; - - const CStack * s = getActiveStack(); - if(!s) - return; -} - -const CStack* BattleStacksController::getActiveStack() const -{ - return activeStack; -} - -bool BattleStacksController::facingRight(const CStack * stack) const -{ - return stackFacingRight.at(stack->unitId()); -} - -Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const -{ - Point ret(-500, -500); //returned value - if(stack && stack->initialPosition < 0) //creatures in turrets - return owner.siegeController->getTurretCreaturePosition(stack->initialPosition); - - static const Point basePos(-189, -139); // position of creature in topleft corner - static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left - - ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX(); - ret.y = basePos.y + 42 * hexNum.getY(); - - if (stack) - { - if(facingRight(stack)) - ret.x += imageShiftX; - else - ret.x -= imageShiftX; - - //shifting position for double - hex creatures - if(stack->doubleWide()) - { - if(stack->unitSide() == BattleSide::ATTACKER) - { - if(facingRight(stack)) - ret.x -= 44; - } - else - { - if(!facingRight(stack)) - ret.x += 44; - } - } - } - //returning - return ret; -} - -void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent) -{ - for (auto & filter : stackFilterEffects) - { - if (filter.target == target && filter.source == source) - { - filter.effect = effect; - filter.persistent = persistent; - return; - } - } - stackFilterEffects.push_back({ effect, target, source, persistent }); -} - -void BattleStacksController::removeExpiredColorFilters() -{ - vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter) - { - if (!filter.persistent) - { - if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, filter.source->id), Selector::all)) - return true; - if (filter.effect == ColorFilter::genEmptyShifter()) - return true; - } - return false; - }); -} - -void BattleStacksController::updateHoveredStacks() -{ - auto newStacks = selectHoveredStacks(); - - for(const auto * stack : mouseHoveredStacks) - { - if (vstd::contains(newStacks, stack)) - continue; - - if (stack == activeStack) - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); - else - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); - } - - for(const auto * stack : newStacks) - { - if (vstd::contains(mouseHoveredStacks, stack)) - continue; - - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); - if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) - stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); - } - - mouseHoveredStacks = newStacks; -} - -std::vector BattleStacksController::selectHoveredStacks() -{ - // only allow during our turn - do not try to highlight creatures while they are in the middle of actions - if (!activeStack) - return {}; - - if(owner.hasAnimations()) - return {}; - - auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId(); - if(hoveredQueueUnitId.has_value()) - { - return { owner.curInt->cb->battleGetStackByID(hoveredQueueUnitId.value(), true) }; - } - - auto hoveredHex = owner.fieldController->getHoveredHex(); - - if (!hoveredHex.isValid()) - return {}; - - const spells::Caster *caster = nullptr; - const CSpell *spell = nullptr; - - spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(hoveredHex); - caster = owner.actionsController->getCurrentSpellcaster(); - - if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell - { - spells::Target target; - target.emplace_back(hoveredHex); - - spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell); - auto mechanics = spell->battleMechanics(&event); - return mechanics->getAffectedStacks(target); - } - - if(hoveredHex.isValid()) - { - const CStack * const stack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - - if (stack) - return {stack}; - } - - return {}; -} - -const std::vector BattleStacksController::getHoveredStacksUnitIds() const -{ - auto result = std::vector(); - for(const auto * stack : mouseHoveredStacks) - { - result.push_back(stack->unitId()); - } - - return result; -} +/* + * BattleStacksController.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 "BattleStacksController.h" + +#include "BattleSiegeController.h" +#include "BattleInterfaceClasses.h" +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleAnimationClasses.h" +#include "BattleFieldController.h" +#include "BattleEffectsController.h" +#include "BattleProjectileController.h" +#include "BattleWindow.h" +#include "BattleRenderer.h" +#include "CreatureAnimation.h" + +#include "../CPlayerInterface.h" +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../render/Colors.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleHex.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/TextOperations.h" + +static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) +{ + std::shared_ptr animation = anim.lock(); + if(!animation) + return; + + if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN) + animation->setType(ECreatureAnimType::HOLDING); + + if (animation->isIdle()) + { + const CCreature *creature = stack->unitType(); + + if (stack->isFrozen()) + animation->setType(ECreatureAnimType::FROZEN); + else + if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0) + { + if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) + animation->playOnce(ECreatureAnimType::MOUSEON); + else + animation->setType(ECreatureAnimType::HOLDING); + } + else + { + animation->setType(ECreatureAnimType::HOLDING); + } + } + // always reset callback + animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim); +} + +BattleStacksController::BattleStacksController(BattleInterface & owner): + owner(owner), + activeStack(nullptr), + stackToActivate(nullptr), + animIDhelper(0) +{ + //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); + + std::vector stacks = owner.getBattle()->battleGetAllStacks(true); + for(const CStack * s : stacks) + { + stackAdded(s, true); + } +} + +BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const +{ + if ( !stackAnimation.at(stack->unitId())->isMoving()) + return stack->getPosition(); + + if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING ) + return BattleHex::HEX_AFTER_ALL; + + for (auto & anim : currentAnimations) + { + // certainly ugly workaround but fixes quite annoying bug + // stack position will be updated only *after* movement is finished + // before this - stack is always at its initial position. Thus we need to find + // its current position. Which can be found only in this class + if (StackMoveAnimation *move = dynamic_cast(anim)) + { + if (move->stack == stack) + return std::max(move->prevHex, move->nextHex); + } + } + return stack->getPosition(); +} + +void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) +{ + auto stacks = owner.getBattle()->battleGetAllStacks(false); + + for (auto stack : stacks) + { + if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks + continue; + + //FIXME: hack to ignore ghost stacks + if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost()) + continue; + + auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS; + auto location = getStackCurrentPosition(stack); + + renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){ + showStack(renderer, stack); + }); + + if (stackNeedsAmountBox(stack)) + { + renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){ + showStackAmountBox(renderer, stack); + }); + } + } +} + +void BattleStacksController::stackReset(const CStack * stack) +{ + auto iter = stackAnimation.find(stack->unitId()); + + if(iter == stackAnimation.end()) + { + logGlobal->error("Unit %d have no animation", stack->unitId()); + return; + } + + auto animation = iter->second; + + if(stack->alive() && animation->isDeadOrDying()) + { + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + addNewAnim(new ResurrectionAnimation(owner, stack)); + }); + } +} + +void BattleStacksController::stackAdded(const CStack * stack, bool instant) +{ + // Tower shooters have only their upper half visible + static const int turretCreatureAnimationHeight = 232; + + stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position + + Point coords = getStackPositionAtHex(stack->getPosition(), stack); + + if(stack->initialPosition < 0) //turret + { + assert(owner.siegeController); + + const CCreature *turretCreature = owner.siegeController->getTurretCreature(); + + stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature); + stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight; + stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); + + // FIXME: workaround for visible animation of Medusa tails (animation disabled in H3) + if (turretCreature->getId() == CreatureID::MEDUSA ) + stackAnimation[stack->unitId()]->pos.w = 250; + + coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition); + } + else + { + stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType()); + stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]); + stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight(); + stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); + } + stackAnimation[stack->unitId()]->pos.x = coords.x; + stackAnimation[stack->unitId()]->pos.y = coords.y; + stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING); + + if (!instant) + { + // immediately make stack transparent, giving correct shifter time to start + auto shifterFade = ColorFilter::genAlphaShifter(0); + setStackColorFilter(shifterFade, stack, nullptr, true); + + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr)); + if (stack->isClone()) + addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() )); + }); + } +} + +void BattleStacksController::setActiveStack(const CStack *stack) +{ + if (activeStack) // update UI + stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); + + activeStack = stack; + + if (activeStack) // update UI + stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); + + owner.windowObject->blockUI(activeStack == nullptr); + + if (activeStack) + stackAmountBoxHidden.clear(); +} + +bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const +{ + //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature + if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1) + return false; + + if(!stack->alive()) + return false; + + //hide box when target is going to die anyway - do not display "0 creatures" + if(stack->getCount() == 0) + return false; + + // if stack has any ongoing animation - hide the box + if (stackAmountBoxHidden.count(stack->unitId())) + return false; + + return true; +} + +std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * stack) +{ + std::vector activeSpells = stack->activeSpells(); + + if ( activeSpells.empty()) + return amountNormal; + + int effectsPositivness = 0; + + for(const auto & spellID : activeSpells) + { + auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness(); + if(!boost::logic::indeterminate(positiveness)) + { + if(positiveness) + effectsPositivness++; + else + effectsPositivness--; + } + } + + if (effectsPositivness > 0) + return amountPositive; + + if (effectsPositivness < 0) + return amountNegative; + + return amountEffNeutral; +} + +void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack) +{ + auto amountBG = getStackAmountBox(stack); + + bool doubleWide = stack->doubleWide(); + bool turnedRight = facingRight(stack); + bool attacker = stack->unitSide() == BattleSide::ATTACKER; + + BattleHex stackPos = stack->getPosition(); + + // double-wide unit turned around - use opposite hex for stack label + if (doubleWide && turnedRight != attacker) + stackPos = stack->occupiedHex(); + + BattleHex frontPos = turnedRight ? + stackPos.cloneInDirection(BattleHex::RIGHT) : + stackPos.cloneInDirection(BattleHex::LEFT); + + bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos); + + Point boxPosition; + + if (moveInside) + { + boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1); + } + else + { + if (turnedRight) + boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1); + else + boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); + } + + Point textPosition = amountBG->dimensions()/2 + boxPosition; + + canvas.draw(amountBG, boxPosition); + canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); +} + +void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) +{ + ColorFilter fullFilter = ColorFilter::genEmptyShifter(); + for(const auto & filter : stackFilterEffects) + { + if (filter.target == stack) + fullFilter = ColorFilter::genCombined(fullFilter, filter.effect); + } + + stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit +} + +void BattleStacksController::tick(uint32_t msPassed) +{ + updateHoveredStacks(); + updateBattleAnimations(msPassed); +} + +void BattleStacksController::initializeBattleAnimations() +{ + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && !elem->isInitialized()) + elem->tryInitialize(); +} + +void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed) +{ + for (auto stack : owner.getBattle()->battleGetAllStacks(true)) + { + if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks + continue; + + stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f); + } + + // operate on copy - to prevent potential iterator invalidation due to push_back's + // FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing) + + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && elem->isInitialized()) + elem->tick(msPassed); +} + +void BattleStacksController::updateBattleAnimations(uint32_t msPassed) +{ + bool hadAnimations = !currentAnimations.empty(); + initializeBattleAnimations(); + tickFrameBattleAnimations(msPassed); + vstd::erase(currentAnimations, nullptr); + + if (currentAnimations.empty()) + owner.executeStagedAnimations(); + + if (hadAnimations && currentAnimations.empty()) + owner.onAnimationsFinished(); + + initializeBattleAnimations(); +} + +void BattleStacksController::addNewAnim(BattleAnimation *anim) +{ + if (currentAnimations.empty()) + stackAmountBoxHidden.clear(); + + owner.onAnimationsStarted(); + currentAnimations.push_back(anim); + + auto stackAnimation = dynamic_cast(anim); + if(stackAnimation) + stackAmountBoxHidden.insert(stackAnimation->stack->unitId()); +} + +void BattleStacksController::stackRemoved(uint32_t stackID) +{ + if (getActiveStack() && getActiveStack()->unitId() == stackID) + setActiveStack(nullptr); +} + +void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) +{ + owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ + // remove any potentially erased petrification effect + removeExpiredColorFilters(); + }); + + for(auto & attackedInfo : attackedInfos) + { + if (!attackedInfo.attacker) + continue; + + // In H3, attacked stack will not reverse on ranged attack + if (attackedInfo.indirectAttack) + continue; + + // Another type of indirect attack - dragon breath + if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender)) + continue; + + // 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 + // if (needsReverse && !attackedInfo.defender->isFrozen()) + if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() + { + addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition())); + }); + } + } + + for(auto & attackedInfo : attackedInfos) + { + bool useDeathAnim = attackedInfo.killed; + bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed; + + EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT; + + owner.addToAnimationStage(usedEvent, [=]() + { + if (useDeathAnim) + addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack)); + else if(useDefenceAnim) + addNewAnim(new DefenceAnimation(owner, attackedInfo.defender)); + else + addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); + + if (attackedInfo.fireShield) + owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition()); + + if (attackedInfo.spellEffect != SpellID::NONE) + { + auto spell = attackedInfo.spellEffect.toSpell(); + if (!spell->getCastSound().empty()) + CCS->soundh->playSound(spell->getCastSound()); + + + owner.displaySpellEffect(spell, attackedInfo.defender->getPosition()); + } + }); + } + + for (auto & attackedInfo : attackedInfos) + { + if (attackedInfo.rebirth) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition()); + addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); + }); + } + + if (attackedInfo.killed && attackedInfo.defender->summoned) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr)); + stackRemoved(attackedInfo.defender->unitId()); + }); + } + } + owner.executeStagedAnimations(); + owner.waitForAnimations(); +} + +void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) +{ + assert(destHex.size() > 0); + //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed + + owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); + }); + + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack)); + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) ); + }); + + // animations will be executed by spell +} + +void BattleStacksController::stackMoved(const CStack *stack, std::vector destHex, int distance) +{ + assert(destHex.size() > 0); + owner.checkForAnimations(); + + if(shouldRotate(stack, stack->getPosition(), destHex[0])) + { + owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]() + { + addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition())); + }); + } + + owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]() + { + addNewAnim(new MovementStartAnimation(owner, stack)); + }); + + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementTeleporting))) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() + { + addNewAnim(new MovementAnimation(owner, stack, destHex, distance)); + }); + } + + owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]() + { + addNewAnim(new MovementEndAnimation(owner, stack, destHex.back())); + }); + + owner.executeStagedAnimations(); + owner.waitForAnimations(); +} + +bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender) +{ + bool mustReverse = owner.getBattle()->isToReverse(attacker, defender); + + if (attacker->unitSide() == BattleSide::ATTACKER) + return !mustReverse; + else + return mustReverse; +} + +void BattleStacksController::stackAttacking( const StackAttackInfo & info ) +{ + owner.checkForAnimations(); + + auto attacker = info.attacker; + auto defender = info.defender; + auto tile = info.tile; + auto spellEffect = info.spellEffect; + auto multiAttack = !info.secondaryDefender.empty(); + bool needsReverse = false; + + if (info.indirectAttack) + { + needsReverse = shouldRotate(attacker, attacker->position, info.tile); + } + else + { + needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker); + } + + if (needsReverse) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() + { + addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition())); + }); + } + + if(info.lucky) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); + owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition()); + }); + } + + if(info.unlucky) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); + owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition()); + }); + } + + if(info.deathBlow) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition()); + }); + + for(auto elem : info.secondaryDefender) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition()); + }); + } + } + + owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]() + { + if (info.indirectAttack) + { + addNewAnim(new ShootingAnimation(owner, attacker, tile, defender)); + } + else + { + addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack)); + } + }); + + if (info.spellEffect != SpellID::NONE) + { + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + owner.displaySpellHit(spellEffect.toSpell(), tile); + }); + } + + if (info.lifeDrain) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() + { + owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition()); + }); + } + + //return, animation playback will be handled by stacksAreAttacked +} + +bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const +{ + Point begPosition = getStackPositionAtHex(oldPos,stack); + Point endPosition = getStackPositionAtHex(nextHex, stack); + + if((begPosition.x > endPosition.x) && facingRight(stack)) + return true; + else if((begPosition.x < endPosition.x) && !facingRight(stack)) + return true; + + return false; +} + +void BattleStacksController::endAction(const BattleAction & action) +{ + owner.checkForAnimations(); + + //check if we should reverse stacks + TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY); + + for (const CStack *s : stacks) + { + bool shouldFaceRight = s && s->unitSide() == BattleSide::ATTACKER; + + if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle()) + { + addNewAnim(new ReverseAnimation(owner, s, s->getPosition())); + } + } + owner.executeStagedAnimations(); + owner.waitForAnimations(); + + stackAmountBoxHidden.clear(); + + owner.windowObject->blockUI(activeStack == nullptr); + removeExpiredColorFilters(); +} + +void BattleStacksController::startAction(const BattleAction & action) +{ + removeExpiredColorFilters(); +} + +void BattleStacksController::stackActivated(const CStack *stack) +{ + stackToActivate = stack; + owner.waitForAnimations(); + logAnim->debug("Activating next stack"); + owner.activateStack(); +} + +void BattleStacksController::deactivateStack() +{ + if (!activeStack) { + return; + } + stackToActivate = activeStack; + setActiveStack(nullptr); +} + +void BattleStacksController::activateStack() +{ + if ( !currentAnimations.empty()) + return; + + if ( !stackToActivate) + return; + + owner.trySetActivePlayer(stackToActivate->unitOwner()); + + setActiveStack(stackToActivate); + stackToActivate = nullptr; + + const CStack * s = getActiveStack(); + if(!s) + return; +} + +const CStack* BattleStacksController::getActiveStack() const +{ + return activeStack; +} + +bool BattleStacksController::facingRight(const CStack * stack) const +{ + return stackFacingRight.at(stack->unitId()); +} + +Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const +{ + Point ret(-500, -500); //returned value + if(stack && stack->initialPosition < 0) //creatures in turrets + return owner.siegeController->getTurretCreaturePosition(stack->initialPosition); + + static const Point basePos(-189, -139); // position of creature in topleft corner + static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left + + ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX(); + ret.y = basePos.y + 42 * hexNum.getY(); + + if (stack) + { + if(facingRight(stack)) + ret.x += imageShiftX; + else + ret.x -= imageShiftX; + + //shifting position for double - hex creatures + if(stack->doubleWide()) + { + if(stack->unitSide() == BattleSide::ATTACKER) + { + if(facingRight(stack)) + ret.x -= 44; + } + else + { + if(!facingRight(stack)) + ret.x += 44; + } + } + } + //returning + return ret; +} + +void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent) +{ + for (auto & filter : stackFilterEffects) + { + if (filter.target == target && filter.source == source) + { + filter.effect = effect; + filter.persistent = persistent; + return; + } + } + stackFilterEffects.push_back({ effect, target, source, persistent }); +} + +void BattleStacksController::removeExpiredColorFilters() +{ + vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter) + { + if (!filter.persistent) + { + if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all)) + return true; + if (filter.effect == ColorFilter::genEmptyShifter()) + return true; + } + return false; + }); +} + +void BattleStacksController::updateHoveredStacks() +{ + auto newStacks = selectHoveredStacks(); + + for(const auto * stack : mouseHoveredStacks) + { + if (vstd::contains(newStacks, stack)) + continue; + + if (stack == activeStack) + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); + else + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); + } + + for(const auto * stack : newStacks) + { + if (vstd::contains(mouseHoveredStacks, stack)) + continue; + + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); + if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) + stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); + } + + mouseHoveredStacks = newStacks; +} + +std::vector BattleStacksController::selectHoveredStacks() +{ + // only allow during our turn - do not try to highlight creatures while they are in the middle of actions + if (!activeStack) + return {}; + + if(owner.hasAnimations()) + return {}; + + auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId(); + if(hoveredQueueUnitId.has_value()) + { + return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) }; + } + + auto hoveredHex = owner.fieldController->getHoveredHex(); + + if (!hoveredHex.isValid()) + return {}; + + const spells::Caster *caster = nullptr; + const CSpell *spell = nullptr; + + spells::Mode mode = owner.actionsController->getCurrentCastMode(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); + caster = owner.actionsController->getCurrentSpellcaster(); + + if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell + { + spells::Target target; + target.emplace_back(hoveredHex); + + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); + auto mechanics = spell->battleMechanics(&event); + return mechanics->getAffectedStacks(target); + } + + if(hoveredHex.isValid()) + { + const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + + if (stack) + return {stack}; + } + + return {}; +} + +const std::vector BattleStacksController::getHoveredStacksUnitIds() const +{ + auto result = std::vector(); + for(const auto * stack : mouseHoveredStacks) + { + result.push_back(stack->unitId()); + } + + return result; +} diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 0be41d287..ccf66c5a9 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -1,147 +1,147 @@ -/* - * BattleStacksController.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/ColorFilter.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct BattleHex; -class BattleAction; -class CStack; -class CSpell; -class SpellID; -class Point; - -VCMI_LIB_NAMESPACE_END - -struct StackAttackedInfo; -struct StackAttackInfo; - -class ColorFilter; -class Canvas; -class BattleInterface; -class BattleAnimation; -class CreatureAnimation; -class BattleAnimation; -class BattleRenderer; -class IImage; - -struct BattleStackFilterEffect -{ - ColorFilter effect; - const CStack * target; - const CSpell * source; - bool persistent; -}; - -/// Class responsible for handling stacks in battle -/// Handles ordering of stacks animation -/// As well as rendering of stacks, their amount boxes -/// And any other effect applied to stacks -class BattleStacksController -{ - BattleInterface & owner; - - std::shared_ptr amountNormal; - std::shared_ptr amountNegative; - std::shared_ptr amountPositive; - std::shared_ptr amountEffNeutral; - - /// currently displayed animations - std::vector currentAnimations; - - /// currently active color effects on stacks, in order of their addition (which corresponds to their apply order) - std::vector stackFilterEffects; - - /// animations of creatures from fighting armies (order by BattleInfo's stacks' ID) - std::map> stackAnimation; - - /// //TODO: move it to battle callback - std::map stackFacingRight; - - /// Stacks have amount box hidden due to ongoing animations - std::set stackAmountBoxHidden; - - /// currently active stack; nullptr - no one - const CStack *activeStack; - - /// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation - std::vector mouseHoveredStacks; - - ///when animation is playing, we should wait till the end to make the next stack active; nullptr of none - const CStack *stackToActivate; - - /// for giving IDs for animations - ui32 animIDhelper; - - bool stackNeedsAmountBox(const CStack * stack) const; - void showStackAmountBox(Canvas & canvas, const CStack * stack); - BattleHex getStackCurrentPosition(const CStack * stack) const; - - std::shared_ptr getStackAmountBox(const CStack * stack); - - void removeExpiredColorFilters(); - - void initializeBattleAnimations(); - void tickFrameBattleAnimations(uint32_t msPassed); - - void updateBattleAnimations(uint32_t msPassed); - void updateHoveredStacks(); - - std::vector selectHoveredStacks(); - - bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender); - -public: - BattleStacksController(BattleInterface & owner); - - bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const; - bool facingRight(const CStack * stack) const; - - void stackReset(const CStack * stack); - void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield - void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled - void stackActivated(const CStack *stack); //active stack has been changed - void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex - void stackTeleported(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex - void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked - void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest - - void startAction(const BattleAction* action); - void endAction(const BattleAction* action); - - void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack - - void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack - - void setActiveStack(const CStack *stack); - - void showAliveStack(Canvas & canvas, const CStack * stack); - void showStack(Canvas & canvas, const CStack * stack); - - void collectRenderableObjects(BattleRenderer & renderer); - - /// Adds new color filter effect targeting stack - /// Effect will last as long as stack is affected by specified spell (unless effect is persistent) - /// If effect from same (target, source) already exists, it will be updated - void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent); - void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims - - const CStack* getActiveStack() const; - const std::vector getHoveredStacksUnitIds() const; - - void tick(uint32_t msPassed); - - /// returns position of animation needed to place stack in specific hex - Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const; - - friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations -}; +/* + * BattleStacksController.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/ColorFilter.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleHex; +class BattleAction; +class CStack; +class CSpell; +class SpellID; +class Point; + +VCMI_LIB_NAMESPACE_END + +struct StackAttackedInfo; +struct StackAttackInfo; + +class ColorFilter; +class Canvas; +class BattleInterface; +class BattleAnimation; +class CreatureAnimation; +class BattleAnimation; +class BattleRenderer; +class IImage; + +struct BattleStackFilterEffect +{ + ColorFilter effect; + const CStack * target; + const CSpell * source; + bool persistent; +}; + +/// Class responsible for handling stacks in battle +/// Handles ordering of stacks animation +/// As well as rendering of stacks, their amount boxes +/// And any other effect applied to stacks +class BattleStacksController +{ + BattleInterface & owner; + + std::shared_ptr amountNormal; + std::shared_ptr amountNegative; + std::shared_ptr amountPositive; + std::shared_ptr amountEffNeutral; + + /// currently displayed animations + std::vector currentAnimations; + + /// currently active color effects on stacks, in order of their addition (which corresponds to their apply order) + std::vector stackFilterEffects; + + /// animations of creatures from fighting armies (order by BattleInfo's stacks' ID) + std::map> stackAnimation; + + /// //TODO: move it to battle callback + std::map stackFacingRight; + + /// Stacks have amount box hidden due to ongoing animations + std::set stackAmountBoxHidden; + + /// currently active stack; nullptr - no one + const CStack *activeStack; + + /// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation + std::vector mouseHoveredStacks; + + ///when animation is playing, we should wait till the end to make the next stack active; nullptr of none + const CStack *stackToActivate; + + /// for giving IDs for animations + ui32 animIDhelper; + + bool stackNeedsAmountBox(const CStack * stack) const; + void showStackAmountBox(Canvas & canvas, const CStack * stack); + BattleHex getStackCurrentPosition(const CStack * stack) const; + + std::shared_ptr getStackAmountBox(const CStack * stack); + + void removeExpiredColorFilters(); + + void initializeBattleAnimations(); + void tickFrameBattleAnimations(uint32_t msPassed); + + void updateBattleAnimations(uint32_t msPassed); + void updateHoveredStacks(); + + std::vector selectHoveredStacks(); + + bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender); + +public: + BattleStacksController(BattleInterface & owner); + + bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const; + bool facingRight(const CStack * stack) const; + + void stackReset(const CStack * stack); + void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield + void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled + void stackActivated(const CStack *stack); //active stack has been changed + void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stackTeleported(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked + void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest + + void startAction(const BattleAction & action); + void endAction(const BattleAction & action); + + void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack + + void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack + + void setActiveStack(const CStack *stack); + + void showAliveStack(Canvas & canvas, const CStack * stack); + void showStack(Canvas & canvas, const CStack * stack); + + void collectRenderableObjects(BattleRenderer & renderer); + + /// Adds new color filter effect targeting stack + /// Effect will last as long as stack is affected by specified spell (unless effect is persistent) + /// If effect from same (target, source) already exists, it will be updated + void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent); + void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims + + const CStack* getActiveStack() const; + const std::vector getHoveredStacksUnitIds() const; + + void tick(uint32_t msPassed); + + /// returns position of animation needed to place stack in specific hex + Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const; + + friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations +}; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 78a5f690c..c5f1734d9 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -1,686 +1,687 @@ -/* - * BattleWindow.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 "BattleWindow.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#include "BattleActionsController.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../windows/CSpellWindow.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../windows/CMessage.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/filesystem/ResourceID.h" -#include "../windows/settings/SettingsMainWindow.h" - -BattleWindow::BattleWindow(BattleInterface & owner): - owner(owner), - defaultAction(PossiblePlayerBattleAction::INVALID) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.w = 800; - pos.h = 600; - pos = center(); - - REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - - const JsonNode config(ResourceID("config/widgets/BattleWindow2.json")); - - 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)); - addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this)); - addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this)); - addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this)); - addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this)); - addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this)); - addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this)); - addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this)); - addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this)); - addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this)); - - addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();}); - addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();}); - addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); }); - addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); }); - - build(config); - - console = widget("console"); - - owner.console = console; - - owner.fieldController.reset( new BattleFieldController(owner)); - owner.fieldController->createHeroes(); - - createQueue(); - createStickyHeroInfoWindows(); - - if ( owner.tacticsMode ) - tacticPhaseStarted(); - else - tacticPhaseEnded(); - - addUsedEvents(LCLICK | KEYBOARD); -} - -void BattleWindow::createQueue() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - //create stack queue and adjust our own position - bool embedQueue; - bool showQueue = settings["battle"]["showQueue"].Bool(); - std::string queueSize = settings["battle"]["queueSize"].String(); - - if(queueSize == "auto") - embedQueue = GH.screenDimensions().y < 700; - else - embedQueue = GH.screenDimensions().y < 700 || queueSize == "small"; - - queue = std::make_shared(embedQueue, owner); - if(!embedQueue && showQueue) - { - //re-center, taking into account stack queue position - pos.y -= queue->pos.h; - pos.h += queue->pos.h; - pos = center(); - } - - if (!showQueue) - queue->disable(); -} - -void BattleWindow::createStickyHeroInfoWindows() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - if(owner.defendingHeroInstance) - { - InfoAboutHero info; - info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x + pos.w + 15, pos.y) - : Point(pos.x + pos.w -79, pos.y + 135); - defenderHeroWindow = std::make_shared(info, &position); - } - if(owner.attackingHeroInstance) - { - InfoAboutHero info; - info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x - 93, pos.y) - : Point(pos.x + 1, pos.y + 135); - attackerHeroWindow = std::make_shared(info, &position); - } - - bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); - - if(!showInfoWindows) - { - if(attackerHeroWindow) - attackerHeroWindow->disable(); - - if(defenderHeroWindow) - defenderHeroWindow->disable(); - } -} - -BattleWindow::~BattleWindow() -{ - CPlayerInterface::battleInt = nullptr; -} - -std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & config) const -{ - auto rect = readRect(config["rect"]); - auto offset = readPosition(config["imagePosition"]); - auto background = widget("menuBattle"); - return std::make_shared(background, rect.topLeft(), offset, rect.dimensions() ); -} - -void BattleWindow::toggleQueueVisibility() -{ - if(settings["battle"]["showQueue"].Bool()) - hideQueue(); - else - showQueue(); -} - -void BattleWindow::hideQueue() -{ - if(settings["battle"]["showQueue"].Bool() == false) - return; - - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = false; - - queue->disable(); - - if (!queue->embedded) - { - //re-center, taking into account stack queue position - pos.y += queue->pos.h; - pos.h -= queue->pos.h; - pos = center(); - } - GH.windows().totalRedraw(); -} - -void BattleWindow::showQueue() -{ - if(settings["battle"]["showQueue"].Bool() == true) - return; - - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = true; - - createQueue(); - updateQueue(); - GH.windows().totalRedraw(); -} - -void BattleWindow::toggleStickyHeroWindowsVisibility() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool()) - hideStickyHeroWindows(); - else - showStickyHeroWindows(); -} - -void BattleWindow::hideStickyHeroWindows() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false) - return; - - Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; - showStickyHeroInfoWindows->Bool() = false; - - if(attackerHeroWindow) - attackerHeroWindow->disable(); - - if(defenderHeroWindow) - defenderHeroWindow->disable(); - - GH.windows().totalRedraw(); -} - -void BattleWindow::showStickyHeroWindows() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) - return; - - Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; - showStickyHeroInfoWindows->Bool() = true; - - - createStickyHeroInfoWindows(); - GH.windows().totalRedraw(); -} - -void BattleWindow::updateQueue() -{ - queue->update(); -} - -void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) -{ - std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; - panelToUpdate->update(hero); -} - -void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) -{ - if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) - { - InfoAboutHero heroInfo = InfoAboutHero(); - heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE); - - updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo); - } - else - { - logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window"); - } -} - -void BattleWindow::activate() -{ - GH.setStatusbar(console); - CIntObject::activate(); - LOCPLINT->cingconsole->activate(); -} - -void BattleWindow::deactivate() -{ - GH.setStatusbar(nullptr); - CIntObject::deactivate(); - LOCPLINT->cingconsole->deactivate(); -} - -bool BattleWindow::captureThisKey(EShortcut key) -{ - return owner.openingPlaying(); -} - -void BattleWindow::keyPressed(EShortcut key) -{ - if (owner.openingPlaying()) - { - owner.openingEnd(); - return; - } - InterfaceObjectConfigurable::keyPressed(key); -} - -void BattleWindow::clickPressed(const Point & cursorPosition) -{ - if (owner.openingPlaying()) - { - owner.openingEnd(); - return; - } - InterfaceObjectConfigurable::clickPressed(cursorPosition); -} - -void BattleWindow::tacticPhaseStarted() -{ - auto menuBattle = widget("menuBattle"); - auto console = widget("console"); - auto menuTactics = widget("menuTactics"); - auto tacticNext = widget("tacticNext"); - auto tacticEnd = widget("tacticEnd"); - auto alternativeAction = widget("alternativeAction"); - - menuBattle->disable(); - console->disable(); - if (alternativeAction) - alternativeAction->disable(); - - menuTactics->enable(); - tacticNext->enable(); - tacticEnd->enable(); - - redraw(); -} - -void BattleWindow::tacticPhaseEnded() -{ - auto menuBattle = widget("menuBattle"); - auto console = widget("console"); - auto menuTactics = widget("menuTactics"); - auto tacticNext = widget("tacticNext"); - auto tacticEnd = widget("tacticEnd"); - auto alternativeAction = widget("alternativeAction"); - - menuBattle->enable(); - console->enable(); - if (alternativeAction) - alternativeAction->enable(); - - menuTactics->disable(); - tacticNext->disable(); - tacticEnd->disable(); - - redraw(); -} - -void BattleWindow::bOptionsf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - CCS->curh->set(Cursor::Map::POINTER); - - GH.windows().createAndPushWindow(&owner); -} - -void BattleWindow::bSurrenderf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - int cost = owner.curInt->cb->battleGetSurrenderCost(); - if(cost >= 0) - { - std::string enemyHeroName = owner.curInt->cb->battleGetEnemyHero().name; - if(enemyHeroName.empty()) - { - logGlobal->warn("Surrender performed without enemy hero, should not happen!"); - enemyHeroName = "#ENEMY#"; - } - - std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold." - owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr); - } -} - -void BattleWindow::bFleef() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if ( owner.curInt->cb->battleCanFlee() ) - { - CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); - owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? - } - else - { - std::vector> comps; - std::string heroName; - //calculating fleeing hero's name - if (owner.attackingHeroInstance) - if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getMyColor()) - heroName = owner.attackingHeroInstance->getNameTranslated(); - if (owner.defendingHeroInstance) - if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getMyColor()) - heroName = owner.defendingHeroInstance->getNameTranslated(); - //calculating text - auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! - - //printing message - owner.curInt->showInfoDialog(boost::to_string(txt), comps); - } -} - -void BattleWindow::reallyFlee() -{ - owner.giveCommand(EActionType::RETREAT); - CCS->curh->set(Cursor::Map::POINTER); -} - -void BattleWindow::reallySurrender() -{ - if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.curInt->cb->battleGetSurrenderCost()) - { - owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! - } - else - { - owner.giveCommand(EActionType::SURRENDER); - CCS->curh->set(Cursor::Map::POINTER); - } -} - -void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) -{ - auto w = widget("alternativeAction"); - if(!w) - return; - - std::string iconName = variables["actionIconDefault"].String(); - switch(action.get()) - { - case PossiblePlayerBattleAction::ATTACK: - iconName = variables["actionIconAttack"].String(); - break; - - case PossiblePlayerBattleAction::SHOOT: - iconName = variables["actionIconShoot"].String(); - break; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - iconName = variables["actionIconSpell"].String(); - break; - - case PossiblePlayerBattleAction::ANY_LOCATION: - iconName = variables["actionIconSpell"].String(); - break; - - //TODO: figure out purpose of this icon - //case PossiblePlayerBattleAction::???: - //iconName = variables["actionIconWalk"].String(); - //break; - - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - iconName = variables["actionIconReturn"].String(); - break; - - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - iconName = variables["actionIconNoReturn"].String(); - break; - } - - auto anim = std::make_shared(iconName); - w->setImage(anim); - w->redraw(); -} - -void BattleWindow::setAlternativeActions(const std::list & actions) -{ - alternativeActions = actions; - defaultAction = PossiblePlayerBattleAction::INVALID; - if(alternativeActions.size() > 1) - defaultAction = alternativeActions.back(); - if(!alternativeActions.empty()) - showAlternativeActionIcon(alternativeActions.front()); - else - showAlternativeActionIcon(defaultAction); -} - -void BattleWindow::bAutofightf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - //Stop auto-fight mode - if(owner.curInt->isAutoFightOn) - { - assert(owner.curInt->autofightingAI); - owner.curInt->isAutoFightOn = false; - logGlobal->trace("Stopping the autofight..."); - } - else if(!owner.curInt->autofightingAI) - { - owner.curInt->isAutoFightOn = true; - blockUI(true); - - auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); - - AutocombatPreferences autocombatPreferences = AutocombatPreferences(); - autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); - - ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); - ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false); - owner.curInt->autofightingAI = ai; - owner.curInt->cb->registerBattleInterface(ai); - - owner.requestAutofightingAIToTakeAction(); - } -} - -void BattleWindow::bSpellf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (!owner.makingTurn()) - return; - - auto myHero = owner.currentHero(); - if(!myHero) - return; - - CCS->curh->set(Cursor::Map::POINTER); - - ESpellCastProblem::ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); - - if(spellCastProblem == ESpellCastProblem::OK) - { - GH.windows().createAndPushWindow(myHero, owner.curInt.get()); - } - else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) - { - //TODO: move to spell mechanics, add more information to spell cast problem - //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible - auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); - if (!blockingBonus) - return; - - if (blockingBonus->source == BonusSource::ARTIFACT) - { - const auto artID = ArtifactID(blockingBonus->sid); - //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; - - //%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]) - % heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated())); - } - } -} - -void BattleWindow::bSwitchActionf() -{ - if(alternativeActions.empty()) - return; - - if(alternativeActions.front() == defaultAction) - { - alternativeActions.push_back(alternativeActions.front()); - alternativeActions.pop_front(); - } - - auto actions = owner.actionsController->getPossibleActions(); - if(!actions.empty() && actions.front() == alternativeActions.front()) - { - owner.actionsController->removePossibleAction(alternativeActions.front()); - showAlternativeActionIcon(defaultAction); - } - else - { - owner.actionsController->pushFrontPossibleAction(alternativeActions.front()); - showAlternativeActionIcon(alternativeActions.front()); - } - - alternativeActions.push_back(alternativeActions.front()); - alternativeActions.pop_front(); -} - -void BattleWindow::bWaitf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (owner.stacksController->getActiveStack() != nullptr) - owner.giveCommand(EActionType::WAIT); -} - -void BattleWindow::bDefencef() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (owner.stacksController->getActiveStack() != nullptr) - owner.giveCommand(EActionType::DEFEND); -} - -void BattleWindow::bConsoleUpf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - console->scrollUp(); -} - -void BattleWindow::bConsoleDownf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - console->scrollDown(); -} - -void BattleWindow::bTacticNextStack() -{ - owner.tacticNextStack(nullptr); -} - -void BattleWindow::bTacticPhaseEnd() -{ - owner.tacticPhaseEnd(); -} - -void BattleWindow::blockUI(bool on) -{ - bool canCastSpells = false; - auto hero = owner.curInt->cb->battleGetMyHero(); - - if(hero) - { - ESpellCastProblem::ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO); - - //if magic is blocked, we leave button active, so the message can be displayed after button click - canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; - } - - bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false; - - setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on); - setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.curInt->cb->battleCanFlee()); - setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.curInt->cb->battleGetSurrenderCost() < 0); - setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells); - 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, owner.actionsController->spellcastingModeActive()); - 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); -} - -std::optional BattleWindow::getQueueHoveredUnitId() -{ - return queue->getHoveredUnitIdIfAny(); -} - -void BattleWindow::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600) - CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); -} - -void BattleWindow::show(Canvas & to) -{ - CIntObject::show(to); - LOCPLINT->cingconsole->show(to); -} - -void BattleWindow::close() -{ - if(!GH.windows().isTopWindow(this)) - logGlobal->error("Only top interface must be closed"); - GH.windows().popWindows(1); -} +/* + * BattleWindow.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 "BattleWindow.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#include "BattleActionsController.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../CMusicHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../windows/CSpellWindow.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../windows/CMessage.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../CCallback.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../windows/settings/SettingsMainWindow.h" + +BattleWindow::BattleWindow(BattleInterface & owner): + owner(owner), + defaultAction(PossiblePlayerBattleAction::INVALID) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.w = 800; + pos.h = 600; + pos = center(); + + REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); + + const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); + + 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)); + addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this)); + addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this)); + addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this)); + addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this)); + addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this)); + addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this)); + addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this)); + addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this)); + addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this)); + + addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();}); + addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();}); + addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); }); + addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); }); + + build(config); + + console = widget("console"); + + owner.console = console; + + owner.fieldController.reset( new BattleFieldController(owner)); + owner.fieldController->createHeroes(); + + createQueue(); + createStickyHeroInfoWindows(); + + if ( owner.tacticsMode ) + tacticPhaseStarted(); + else + tacticPhaseEnded(); + + addUsedEvents(LCLICK | KEYBOARD); +} + +void BattleWindow::createQueue() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + //create stack queue and adjust our own position + bool embedQueue; + bool showQueue = settings["battle"]["showQueue"].Bool(); + std::string queueSize = settings["battle"]["queueSize"].String(); + + if(queueSize == "auto") + embedQueue = GH.screenDimensions().y < 700; + else + embedQueue = GH.screenDimensions().y < 700 || queueSize == "small"; + + queue = std::make_shared(embedQueue, owner); + if(!embedQueue && showQueue) + { + //re-center, taking into account stack queue position + pos.y -= queue->pos.h; + pos.h += queue->pos.h; + pos = center(); + } + + if (!showQueue) + queue->disable(); +} + +void BattleWindow::createStickyHeroInfoWindows() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(owner.defendingHeroInstance) + { + InfoAboutHero info; + info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x + pos.w + 15, pos.y) + : Point(pos.x + pos.w -79, pos.y + 135); + defenderHeroWindow = std::make_shared(info, &position); + } + if(owner.attackingHeroInstance) + { + InfoAboutHero info; + info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x - 93, pos.y) + : Point(pos.x + 1, pos.y + 135); + attackerHeroWindow = std::make_shared(info, &position); + } + + bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); + + if(!showInfoWindows) + { + if(attackerHeroWindow) + attackerHeroWindow->disable(); + + if(defenderHeroWindow) + defenderHeroWindow->disable(); + } +} + +BattleWindow::~BattleWindow() +{ + CPlayerInterface::battleInt = nullptr; +} + +std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & config) const +{ + auto rect = readRect(config["rect"]); + auto offset = readPosition(config["imagePosition"]); + auto background = widget("menuBattle"); + return std::make_shared(background, rect.topLeft(), offset, rect.dimensions() ); +} + +void BattleWindow::toggleQueueVisibility() +{ + if(settings["battle"]["showQueue"].Bool()) + hideQueue(); + else + showQueue(); +} + +void BattleWindow::hideQueue() +{ + if(settings["battle"]["showQueue"].Bool() == false) + return; + + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = false; + + queue->disable(); + + if (!queue->embedded) + { + //re-center, taking into account stack queue position + pos.y += queue->pos.h; + pos.h -= queue->pos.h; + pos = center(); + } + GH.windows().totalRedraw(); +} + +void BattleWindow::showQueue() +{ + if(settings["battle"]["showQueue"].Bool() == true) + return; + + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = true; + + createQueue(); + updateQueue(); + GH.windows().totalRedraw(); +} + +void BattleWindow::toggleStickyHeroWindowsVisibility() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool()) + hideStickyHeroWindows(); + else + showStickyHeroWindows(); +} + +void BattleWindow::hideStickyHeroWindows() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false) + return; + + Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; + showStickyHeroInfoWindows->Bool() = false; + + if(attackerHeroWindow) + attackerHeroWindow->disable(); + + if(defenderHeroWindow) + defenderHeroWindow->disable(); + + GH.windows().totalRedraw(); +} + +void BattleWindow::showStickyHeroWindows() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) + return; + + Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; + showStickyHeroInfoWindows->Bool() = true; + + + createStickyHeroInfoWindows(); + GH.windows().totalRedraw(); +} + +void BattleWindow::updateQueue() +{ + queue->update(); +} + +void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) +{ + std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; + panelToUpdate->update(hero); +} + +void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) +{ + if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) + { + InfoAboutHero heroInfo = InfoAboutHero(); + heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE); + + updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo); + } + else + { + logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window"); + } +} + +void BattleWindow::activate() +{ + GH.setStatusbar(console); + CIntObject::activate(); + LOCPLINT->cingconsole->activate(); +} + +void BattleWindow::deactivate() +{ + GH.setStatusbar(nullptr); + CIntObject::deactivate(); + LOCPLINT->cingconsole->deactivate(); +} + +bool BattleWindow::captureThisKey(EShortcut key) +{ + return owner.openingPlaying(); +} + +void BattleWindow::keyPressed(EShortcut key) +{ + if (owner.openingPlaying()) + { + owner.openingEnd(); + return; + } + InterfaceObjectConfigurable::keyPressed(key); +} + +void BattleWindow::clickPressed(const Point & cursorPosition) +{ + if (owner.openingPlaying()) + { + owner.openingEnd(); + return; + } + InterfaceObjectConfigurable::clickPressed(cursorPosition); +} + +void BattleWindow::tacticPhaseStarted() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + auto alternativeAction = widget("alternativeAction"); + + menuBattle->disable(); + console->disable(); + if (alternativeAction) + alternativeAction->disable(); + + menuTactics->enable(); + tacticNext->enable(); + tacticEnd->enable(); + + redraw(); +} + +void BattleWindow::tacticPhaseEnded() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + auto alternativeAction = widget("alternativeAction"); + + menuBattle->enable(); + console->enable(); + if (alternativeAction) + alternativeAction->enable(); + + menuTactics->disable(); + tacticNext->disable(); + tacticEnd->disable(); + + redraw(); +} + +void BattleWindow::bOptionsf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + CCS->curh->set(Cursor::Map::POINTER); + + GH.windows().createAndPushWindow(&owner); +} + +void BattleWindow::bSurrenderf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + int cost = owner.getBattle()->battleGetSurrenderCost(); + if(cost >= 0) + { + std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name; + if(enemyHeroName.empty()) + { + logGlobal->warn("Surrender performed without enemy hero, should not happen!"); + enemyHeroName = "#ENEMY#"; + } + + std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold." + owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr); + } +} + +void BattleWindow::bFleef() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if ( owner.getBattle()->battleCanFlee() ) + { + CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); + owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? + } + else + { + std::vector> comps; + std::string heroName; + //calculating fleeing hero's name + if (owner.attackingHeroInstance) + if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) + heroName = owner.attackingHeroInstance->getNameTranslated(); + if (owner.defendingHeroInstance) + if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) + heroName = owner.defendingHeroInstance->getNameTranslated(); + //calculating text + auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! + + //printing message + owner.curInt->showInfoDialog(boost::str(txt), comps); + } +} + +void BattleWindow::reallyFlee() +{ + owner.giveCommand(EActionType::RETREAT); + CCS->curh->set(Cursor::Map::POINTER); +} + +void BattleWindow::reallySurrender() +{ + if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost()) + { + owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! + } + else + { + owner.giveCommand(EActionType::SURRENDER); + CCS->curh->set(Cursor::Map::POINTER); + } +} + +void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) +{ + auto w = widget("alternativeAction"); + if(!w) + return; + + AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]); + switch(action.get()) + { + case PossiblePlayerBattleAction::ATTACK: + iconName = AnimationPath::fromJson(variables["actionIconAttack"]); + break; + + case PossiblePlayerBattleAction::SHOOT: + iconName = AnimationPath::fromJson(variables["actionIconShoot"]); + break; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); + break; + + case PossiblePlayerBattleAction::ANY_LOCATION: + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); + break; + + //TODO: figure out purpose of this icon + //case PossiblePlayerBattleAction::???: + //iconName = variables["actionIconWalk"].String(); + //break; + + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + iconName = AnimationPath::fromJson(variables["actionIconReturn"]); + break; + + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]); + break; + } + + auto anim = GH.renderHandler().loadAnimation(iconName); + w->setImage(anim); + w->redraw(); +} + +void BattleWindow::setAlternativeActions(const std::list & actions) +{ + alternativeActions = actions; + defaultAction = PossiblePlayerBattleAction::INVALID; + if(alternativeActions.size() > 1) + defaultAction = alternativeActions.back(); + if(!alternativeActions.empty()) + showAlternativeActionIcon(alternativeActions.front()); + else + showAlternativeActionIcon(defaultAction); +} + +void BattleWindow::bAutofightf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + //Stop auto-fight mode + if(owner.curInt->isAutoFightOn) + { + assert(owner.curInt->autofightingAI); + owner.curInt->isAutoFightOn = false; + logGlobal->trace("Stopping the autofight..."); + } + else if(!owner.curInt->autofightingAI) + { + owner.curInt->isAutoFightOn = true; + blockUI(true); + + auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); + + AutocombatPreferences autocombatPreferences = AutocombatPreferences(); + autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); + + ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); + ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false); + owner.curInt->autofightingAI = ai; + owner.curInt->cb->registerBattleInterface(ai); + + owner.requestAutofightingAIToTakeAction(); + } +} + +void BattleWindow::bSpellf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (!owner.makingTurn()) + return; + + auto myHero = owner.currentHero(); + if(!myHero) + return; + + CCS->curh->set(Cursor::Map::POINTER); + + ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO); + + if(spellCastProblem == ESpellCastProblem::OK) + { + GH.windows().createAndPushWindow(myHero, owner.curInt.get()); + } + else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) + { + //TODO: move to spell mechanics, add more information to spell cast problem + //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible + auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); + if (!blockingBonus) + return; + + if (blockingBonus->source == BonusSource::ARTIFACT) + { + 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; + + //%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]) + % heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated())); + } + } +} + +void BattleWindow::bSwitchActionf() +{ + if(alternativeActions.empty()) + return; + + if(alternativeActions.front() == defaultAction) + { + alternativeActions.push_back(alternativeActions.front()); + alternativeActions.pop_front(); + } + + auto actions = owner.actionsController->getPossibleActions(); + if(!actions.empty() && actions.front() == alternativeActions.front()) + { + owner.actionsController->removePossibleAction(alternativeActions.front()); + showAlternativeActionIcon(defaultAction); + } + else + { + owner.actionsController->pushFrontPossibleAction(alternativeActions.front()); + showAlternativeActionIcon(alternativeActions.front()); + } + + alternativeActions.push_back(alternativeActions.front()); + alternativeActions.pop_front(); +} + +void BattleWindow::bWaitf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (owner.stacksController->getActiveStack() != nullptr) + owner.giveCommand(EActionType::WAIT); +} + +void BattleWindow::bDefencef() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (owner.stacksController->getActiveStack() != nullptr) + owner.giveCommand(EActionType::DEFEND); +} + +void BattleWindow::bConsoleUpf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + console->scrollUp(); +} + +void BattleWindow::bConsoleDownf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + console->scrollDown(); +} + +void BattleWindow::bTacticNextStack() +{ + owner.tacticNextStack(nullptr); +} + +void BattleWindow::bTacticPhaseEnd() +{ + owner.tacticPhaseEnd(); +} + +void BattleWindow::blockUI(bool on) +{ + bool canCastSpells = false; + auto hero = owner.getBattle()->battleGetMyHero(); + + if(hero) + { + ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO); + + //if magic is blocked, we leave button active, so the message can be displayed after button click + canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; + } + + bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false; + + setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on); + setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee()); + setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0); + setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells); + 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, owner.actionsController->spellcastingModeActive()); + 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); +} + +std::optional BattleWindow::getQueueHoveredUnitId() +{ + return queue->getHoveredUnitIdIfAny(); +} + +void BattleWindow::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600) + CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); +} + +void BattleWindow::show(Canvas & to) +{ + CIntObject::show(to); + LOCPLINT->cingconsole->show(to); +} + +void BattleWindow::close() +{ + if(!GH.windows().isTopWindow(this)) + logGlobal->error("Only top interface must be closed"); + GH.windows().popWindows(1); +} diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 6241e3f57..761d5183b 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -1,118 +1,118 @@ -/* - * BattleWindow.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 "../gui/InterfaceObjectConfigurable.h" -#include "../../lib/battle/CBattleInfoCallback.h" -#include "../../lib/battle/PossiblePlayerBattleAction.h" - -VCMI_LIB_NAMESPACE_BEGIN -class CStack; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class BattleInterface; -class BattleConsole; -class BattleRenderer; -class StackQueue; -class HeroInfoBasicPanel; - -/// GUI object that handles functionality of panel at the bottom of combat screen -class BattleWindow : public InterfaceObjectConfigurable -{ - BattleInterface & owner; - - std::shared_ptr queue; - std::shared_ptr console; - std::shared_ptr attackerHeroWindow; - std::shared_ptr defenderHeroWindow; - - /// button press handling functions - void bOptionsf(); - void bSurrenderf(); - void bFleef(); - void bAutofightf(); - void bSpellf(); - void bWaitf(); - void bSwitchActionf(); - void bDefencef(); - void bConsoleUpf(); - void bConsoleDownf(); - void bTacticNextStack(); - void bTacticPhaseEnd(); - - /// functions for handling actions after they were confirmed by popup window - void reallyFlee(); - void reallySurrender(); - - /// management of alternative actions - std::list alternativeActions; - PossiblePlayerBattleAction defaultAction; - void showAlternativeActionIcon(PossiblePlayerBattleAction); - - /// flip battle queue visibility to opposite - void toggleQueueVisibility(); - void createQueue(); - - void toggleStickyHeroWindowsVisibility(); - void createStickyHeroInfoWindows(); - - std::shared_ptr buildBattleConsole(const JsonNode &) const; - -public: - BattleWindow(BattleInterface & owner ); - ~BattleWindow(); - - /// Closes window once battle finished - void close(); - - /// Toggle StackQueue visibility - void hideQueue(); - void showQueue(); - - /// Toggle permanent hero info windows visibility (HD mod feature) - void hideStickyHeroWindows(); - void showStickyHeroWindows(); - - /// Event handler for netpack changing hero mana points - void heroManaPointsChanged(const CGHeroInstance * hero); - - /// block all UI elements when player is not allowed to act, e.g. during enemy turn - void blockUI(bool on); - - /// Refresh queue after turn order changes - void updateQueue(); - - /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side - void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); - - /// Get mouse-hovered battle queue unit ID if any found - std::optional getQueueHoveredUnitId(); - - void activate() override; - void deactivate() override; - void keyPressed(EShortcut key) override; - bool captureThisKey(EShortcut key) override; - void clickPressed(const Point & cursorPosition) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - /// Toggle UI to displaying tactics phase - void tacticPhaseStarted(); - - /// Toggle UI to displaying battle log in place of tactics UI - void tacticPhaseEnded(); - - /// Set possible alternative options. If more than 1 - the last will be considered as default option - void setAlternativeActions(const std::list &); -}; - +/* + * BattleWindow.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 "../gui/InterfaceObjectConfigurable.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/PossiblePlayerBattleAction.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CStack; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class BattleInterface; +class BattleConsole; +class BattleRenderer; +class StackQueue; +class HeroInfoBasicPanel; + +/// GUI object that handles functionality of panel at the bottom of combat screen +class BattleWindow : public InterfaceObjectConfigurable +{ + BattleInterface & owner; + + std::shared_ptr queue; + std::shared_ptr console; + std::shared_ptr attackerHeroWindow; + std::shared_ptr defenderHeroWindow; + + /// button press handling functions + void bOptionsf(); + void bSurrenderf(); + void bFleef(); + void bAutofightf(); + void bSpellf(); + void bWaitf(); + void bSwitchActionf(); + void bDefencef(); + void bConsoleUpf(); + void bConsoleDownf(); + void bTacticNextStack(); + void bTacticPhaseEnd(); + + /// functions for handling actions after they were confirmed by popup window + void reallyFlee(); + void reallySurrender(); + + /// management of alternative actions + std::list alternativeActions; + PossiblePlayerBattleAction defaultAction; + void showAlternativeActionIcon(PossiblePlayerBattleAction); + + /// flip battle queue visibility to opposite + void toggleQueueVisibility(); + void createQueue(); + + void toggleStickyHeroWindowsVisibility(); + void createStickyHeroInfoWindows(); + + std::shared_ptr buildBattleConsole(const JsonNode &) const; + +public: + BattleWindow(BattleInterface & owner ); + ~BattleWindow(); + + /// Closes window once battle finished + void close(); + + /// Toggle StackQueue visibility + void hideQueue(); + void showQueue(); + + /// Toggle permanent hero info windows visibility (HD mod feature) + void hideStickyHeroWindows(); + void showStickyHeroWindows(); + + /// Event handler for netpack changing hero mana points + void heroManaPointsChanged(const CGHeroInstance * hero); + + /// block all UI elements when player is not allowed to act, e.g. during enemy turn + void blockUI(bool on); + + /// Refresh queue after turn order changes + void updateQueue(); + + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side + void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); + + /// Get mouse-hovered battle queue unit ID if any found + std::optional getQueueHoveredUnitId(); + + void activate() override; + void deactivate() override; + void keyPressed(EShortcut key) override; + bool captureThisKey(EShortcut key) override; + void clickPressed(const Point & cursorPosition) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + /// Toggle UI to displaying tactics phase + void tacticPhaseStarted(); + + /// Toggle UI to displaying battle log in place of tactics UI + void tacticPhaseEnded(); + + /// Set possible alternative options. If more than 1 - the last will be considered as default option + void setAlternativeActions(const std::list &); +}; + diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index c534f02b2..d3c6f7632 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -1,433 +1,433 @@ -/* - * CCreatureAnimation.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 "CreatureAnimation.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/CCreatureHandler.h" - -#include "../render/Canvas.h" -#include "../render/ColorFilter.h" -#include "../renderSDL/SDL_Extensions.h" - -static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 }; -static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 }; -static const SDL_Color creatureNoBorder = { 0, 0, 0, 0 }; - -static SDL_Color genShadow(ui8 alpha) -{ - return CSDL_Ext::makeColor(0, 0, 0, alpha); -} - -SDL_Color AnimationControls::getBlueBorder() -{ - return creatureBlueBorder; -} - -SDL_Color AnimationControls::getGoldBorder() -{ - return creatureGoldBorder; -} - -SDL_Color AnimationControls::getNoBorder() -{ - return creatureNoBorder; -} - -std::shared_ptr AnimationControls::getAnimation(const CCreature * creature) -{ - auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); - return std::make_shared(creature->animDefName, func); -} - -float AnimationControls::getAnimationSpeedFactor() -{ - // according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666) - // exact value is hard to tell due to large rounding errors - // however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays: - // with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames - return settings["battle"]["speedFactor"].Float(); -} - -float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type) -{ - assert(creature->animation.walkAnimationTime != 0); - assert(creature->animation.attackAnimationTime != 0); - assert(anim->framesInGroup(type) != 0); - - // possible new fields for creature format: - //split "Attack time" into "Shoot Time" and "Cast Time" - - // base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame) - const float baseSpeed = 10.f; - const float speed = baseSpeed * getAnimationSpeedFactor(); - - switch (type) - { - case ECreatureAnimType::MOVING: - return speed / creature->animation.walkAnimationTime; - - case ECreatureAnimType::MOUSEON: - return baseSpeed; - - case ECreatureAnimType::HOLDING: - return creature->animation.idleAnimationTime; - - case ECreatureAnimType::SHOOT_UP: - case ECreatureAnimType::SHOOT_FRONT: - case ECreatureAnimType::SHOOT_DOWN: - case ECreatureAnimType::SPECIAL_UP: - case ECreatureAnimType::SPECIAL_FRONT: - case ECreatureAnimType::SPECIAL_DOWN: - case ECreatureAnimType::CAST_DOWN: - case ECreatureAnimType::CAST_FRONT: - case ECreatureAnimType::CAST_UP: - return speed / creature->animation.attackAnimationTime; - - // as strange as it looks like "attackAnimationTime" does not affects melee attacks - // necessary because length of these animations must be same for all creatures for synchronization - case ECreatureAnimType::ATTACK_UP: - case ECreatureAnimType::ATTACK_FRONT: - case ECreatureAnimType::ATTACK_DOWN: - case ECreatureAnimType::HITTED: - case ECreatureAnimType::DEFENCE: - case ECreatureAnimType::DEATH: - case ECreatureAnimType::DEATH_RANGED: - case ECreatureAnimType::RESURRECTION: - case ECreatureAnimType::GROUP_ATTACK_DOWN: - case ECreatureAnimType::GROUP_ATTACK_FRONT: - case ECreatureAnimType::GROUP_ATTACK_UP: - return speed; - - case ECreatureAnimType::TURN_L: - case ECreatureAnimType::TURN_R: - return speed; - - case ECreatureAnimType::MOVE_START: - case ECreatureAnimType::MOVE_END: - return speed; - - case ECreatureAnimType::DEAD: - case ECreatureAnimType::DEAD_RANGED: - return speed; - - default: - return speed; - } -} - -float AnimationControls::getProjectileSpeed() -{ - // H3 speed: 1250/2500/3750 pixels per second - return static_cast(getAnimationSpeedFactor() * 1250); -} - -float AnimationControls::getRayProjectileSpeed() -{ - // H3 speed: 4000/8000/12000 pixels per second - return static_cast(getAnimationSpeedFactor() * 4000); -} - -float AnimationControls::getCatapultSpeed() -{ - // H3 speed: 200/400/600 pixels per second - return static_cast(getAnimationSpeedFactor() * 200); -} - -float AnimationControls::getSpellEffectSpeed() -{ - // H3 speed: 10/20/30 frames per second - return static_cast(getAnimationSpeedFactor() * 10); -} - -float AnimationControls::getMovementDistance(const CCreature * creature) -{ - // H3 speed: 2/4/6 tiles per second - return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); -} - -float AnimationControls::getFlightDistance(const CCreature * creature) -{ - // Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists - // H3 speed: 250/500/750 pixels per second - return static_cast( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); -} - -float AnimationControls::getFadeInDuration() -{ - // H3 speed: 500/250/166 ms - return 0.5f / getAnimationSpeedFactor(); -} - -float AnimationControls::getObstaclesSpeed() -{ - // H3 speed: 20 frames per second, irregardless of speed setting. - return 20.f; -} - -ECreatureAnimType CreatureAnimation::getType() const -{ - return type; -} - -void CreatureAnimation::setType(ECreatureAnimType type) -{ - this->type = type; - currentFrame = 0; - once = false; - - speed = speedController(this, type); -} - -CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller) - : name(name_), - speed(0.1f), - shadowAlpha(128), - currentFrame(0), - animationEnd(-1), - elapsedTime(0), - type(ECreatureAnimType::HOLDING), - border(CSDL_Ext::makeColor(0, 0, 0, 0)), - speedController(controller), - once(false) -{ - forward = std::make_shared(name_); - reverse = std::make_shared(name_); - - //todo: optimize - forward->preload(); - reverse->preload(); - - // if necessary, add one frame into vcmi-only group DEAD - if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); - } - - if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); - } - - if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); - reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); - } - - if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0) - { - for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i) - { - size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i; - - forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); - } - } - - //TODO: get dimensions form CAnimation - auto first = forward->getImage(0, size_t(type), true); - - if(!first) - { - fullWidth = 0; - fullHeight = 0; - return; - } - fullWidth = first->width(); - fullHeight = first->height(); - - reverse->verticalFlip(); - - speed = speedController(this, type); -} - -void CreatureAnimation::endAnimation() -{ - once = false; - auto copy = onAnimationReset; - onAnimationReset.clear(); - copy(); -} - -bool CreatureAnimation::incrementFrame(float timePassed) -{ - elapsedTime += timePassed; - currentFrame += timePassed * speed; - if (animationEnd >= 0) - currentFrame = std::min(currentFrame, animationEnd); - - const auto framesNumber = framesInGroup(type); - - if(framesNumber <= 0) - { - endAnimation(); - } - else if(currentFrame >= float(framesNumber)) - { - // just in case of extremely low fps (or insanely high speed) - while(currentFrame >= float(framesNumber)) - currentFrame -= framesNumber; - - if(once) - setType(ECreatureAnimType::HOLDING); - - endAnimation(); - return true; - } - return false; -} - -void CreatureAnimation::setBorderColor(SDL_Color palette) -{ - border = palette; -} - -int CreatureAnimation::getWidth() const -{ - return fullWidth; -} - -int CreatureAnimation::getHeight() const -{ - return fullHeight; -} - -float CreatureAnimation::getCurrentFrame() const -{ - return currentFrame; -} - -void CreatureAnimation::playOnce( ECreatureAnimType type ) -{ - setType(type); - once = true; -} - -inline int getBorderStrength(float time) -{ - float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1 - - return static_cast(borderStrength * 155 + 100); // scale to 0-255 -} - -static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base) -{ - return CSDL_Ext::makeColor(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 SDL_Color addColors(const SDL_Color & base, const SDL_Color & over) -{ - return CSDL_Ext::makeColor( - 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) -{ - SDL_Color shadowTest = shifter.shiftColor(genShadow(128)); - shadowAlpha = shadowTest.a; - - size_t frame = static_cast(floor(currentFrame)); - - std::shared_ptr image; - - if(facingRight) - image = forward->getImage(frame, size_t(type)); - else - image = reverse->getImage(frame, size_t(type)); - - if(image) - { - IImage::SpecialPalette SpecialPalette; - genSpecialPalette(SpecialPalette); - - image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); - image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); - - canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); - - } -} - -void CreatureAnimation::playUntil(size_t frameIndex) -{ - animationEnd = frameIndex; -} - -int CreatureAnimation::framesInGroup(ECreatureAnimType group) const -{ - return static_cast(forward->size(size_t(group))); -} - -bool CreatureAnimation::isDead() const -{ - return getType() == ECreatureAnimType::DEAD - || getType() == ECreatureAnimType::DEAD_RANGED; -} - -bool CreatureAnimation::isDying() const -{ - return getType() == ECreatureAnimType::DEATH - || getType() == ECreatureAnimType::DEATH_RANGED; -} - -bool CreatureAnimation::isDeadOrDying() const -{ - return getType() == ECreatureAnimType::DEAD - || getType() == ECreatureAnimType::DEATH - || getType() == ECreatureAnimType::DEAD_RANGED - || getType() == ECreatureAnimType::DEATH_RANGED; -} - -bool CreatureAnimation::isIdle() const -{ - return getType() == ECreatureAnimType::HOLDING - || getType() == ECreatureAnimType::MOUSEON; -} - -bool CreatureAnimation::isMoving() const -{ - return getType() == ECreatureAnimType::MOVE_START - || getType() == ECreatureAnimType::MOVING - || getType() == ECreatureAnimType::MOVE_END - || getType() == ECreatureAnimType::TURN_L - || getType() == ECreatureAnimType::TURN_R; -} - -bool CreatureAnimation::isShooting() const -{ - return getType() == ECreatureAnimType::SHOOT_UP - || getType() == ECreatureAnimType::SHOOT_FRONT - || getType() == ECreatureAnimType::SHOOT_DOWN; -} +/* + * CCreatureAnimation.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 "CreatureAnimation.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/ColorFilter.h" +#include "../render/IRenderHandler.h" + +static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; +static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 }; +static const ColorRGBA creatureNoBorder = { 0, 0, 0, 0 }; + +static ColorRGBA genShadow(ui8 alpha) +{ + return ColorRGBA(0, 0, 0, alpha); +} + +ColorRGBA AnimationControls::getBlueBorder() +{ + return creatureBlueBorder; +} + +ColorRGBA AnimationControls::getGoldBorder() +{ + return creatureGoldBorder; +} + +ColorRGBA AnimationControls::getNoBorder() +{ + return creatureNoBorder; +} + +std::shared_ptr AnimationControls::getAnimation(const CCreature * creature) +{ + auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); + return std::make_shared(creature->animDefName, func); +} + +float AnimationControls::getAnimationSpeedFactor() +{ + // according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666) + // exact value is hard to tell due to large rounding errors + // however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays: + // with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames + return settings["battle"]["speedFactor"].Float(); +} + +float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type) +{ + assert(creature->animation.walkAnimationTime != 0); + assert(creature->animation.attackAnimationTime != 0); + assert(anim->framesInGroup(type) != 0); + + // possible new fields for creature format: + //split "Attack time" into "Shoot Time" and "Cast Time" + + // base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame) + const float baseSpeed = 10.f; + const float speed = baseSpeed * getAnimationSpeedFactor(); + + switch (type) + { + case ECreatureAnimType::MOVING: + return speed / creature->animation.walkAnimationTime; + + case ECreatureAnimType::MOUSEON: + return baseSpeed; + + case ECreatureAnimType::HOLDING: + return creature->animation.idleAnimationTime; + + case ECreatureAnimType::SHOOT_UP: + case ECreatureAnimType::SHOOT_FRONT: + case ECreatureAnimType::SHOOT_DOWN: + case ECreatureAnimType::SPECIAL_UP: + case ECreatureAnimType::SPECIAL_FRONT: + case ECreatureAnimType::SPECIAL_DOWN: + case ECreatureAnimType::CAST_DOWN: + case ECreatureAnimType::CAST_FRONT: + case ECreatureAnimType::CAST_UP: + return speed / creature->animation.attackAnimationTime; + + // as strange as it looks like "attackAnimationTime" does not affects melee attacks + // necessary because length of these animations must be same for all creatures for synchronization + case ECreatureAnimType::ATTACK_UP: + case ECreatureAnimType::ATTACK_FRONT: + case ECreatureAnimType::ATTACK_DOWN: + case ECreatureAnimType::HITTED: + case ECreatureAnimType::DEFENCE: + case ECreatureAnimType::DEATH: + case ECreatureAnimType::DEATH_RANGED: + case ECreatureAnimType::RESURRECTION: + case ECreatureAnimType::GROUP_ATTACK_DOWN: + case ECreatureAnimType::GROUP_ATTACK_FRONT: + case ECreatureAnimType::GROUP_ATTACK_UP: + return speed; + + case ECreatureAnimType::TURN_L: + case ECreatureAnimType::TURN_R: + return speed; + + case ECreatureAnimType::MOVE_START: + case ECreatureAnimType::MOVE_END: + return speed; + + case ECreatureAnimType::DEAD: + case ECreatureAnimType::DEAD_RANGED: + return speed; + + default: + return speed; + } +} + +float AnimationControls::getProjectileSpeed() +{ + // H3 speed: 1250/2500/3750 pixels per second + return static_cast(getAnimationSpeedFactor() * 1250); +} + +float AnimationControls::getRayProjectileSpeed() +{ + // H3 speed: 4000/8000/12000 pixels per second + return static_cast(getAnimationSpeedFactor() * 4000); +} + +float AnimationControls::getCatapultSpeed() +{ + // H3 speed: 200/400/600 pixels per second + return static_cast(getAnimationSpeedFactor() * 200); +} + +float AnimationControls::getSpellEffectSpeed() +{ + // H3 speed: 10/20/30 frames per second + return static_cast(getAnimationSpeedFactor() * 10); +} + +float AnimationControls::getMovementDistance(const CCreature * creature) +{ + // H3 speed: 2/4/6 tiles per second + return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); +} + +float AnimationControls::getFlightDistance(const CCreature * creature) +{ + // Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists + // H3 speed: 250/500/750 pixels per second + return static_cast( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); +} + +float AnimationControls::getFadeInDuration() +{ + // H3 speed: 500/250/166 ms + return 0.5f / getAnimationSpeedFactor(); +} + +float AnimationControls::getObstaclesSpeed() +{ + // H3 speed: 20 frames per second, irregardless of speed setting. + return 20.f; +} + +ECreatureAnimType CreatureAnimation::getType() const +{ + return type; +} + +void CreatureAnimation::setType(ECreatureAnimType type) +{ + this->type = type; + currentFrame = 0; + once = false; + + speed = speedController(this, type); +} + +CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller) + : name(name_), + speed(0.1f), + shadowAlpha(128), + currentFrame(0), + animationEnd(-1), + elapsedTime(0), + type(ECreatureAnimType::HOLDING), + speedController(controller), + once(false) +{ + forward = GH.renderHandler().loadAnimation(name_); + reverse = GH.renderHandler().loadAnimation(name_); + + //todo: optimize + forward->preload(); + reverse->preload(); + + // if necessary, add one frame into vcmi-only group DEAD + if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); + } + + if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + } + + if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + } + + if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0) + { + for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i) + { + size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i; + + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + } + } + + //TODO: get dimensions form CAnimation + auto first = forward->getImage(0, size_t(type), true); + + if(!first) + { + fullWidth = 0; + fullHeight = 0; + return; + } + fullWidth = first->width(); + fullHeight = first->height(); + + reverse->verticalFlip(); + + speed = speedController(this, type); +} + +void CreatureAnimation::endAnimation() +{ + once = false; + auto copy = onAnimationReset; + onAnimationReset.clear(); + copy(); +} + +bool CreatureAnimation::incrementFrame(float timePassed) +{ + elapsedTime += timePassed; + currentFrame += timePassed * speed; + if (animationEnd >= 0) + currentFrame = std::min(currentFrame, animationEnd); + + const auto framesNumber = framesInGroup(type); + + if(framesNumber <= 0) + { + endAnimation(); + } + else if(currentFrame >= float(framesNumber)) + { + // just in case of extremely low fps (or insanely high speed) + while(currentFrame >= float(framesNumber)) + currentFrame -= framesNumber; + + if(once) + setType(ECreatureAnimType::HOLDING); + + endAnimation(); + return true; + } + return false; +} + +void CreatureAnimation::setBorderColor(ColorRGBA palette) +{ + border = palette; +} + +int CreatureAnimation::getWidth() const +{ + return fullWidth; +} + +int CreatureAnimation::getHeight() const +{ + return fullHeight; +} + +float CreatureAnimation::getCurrentFrame() const +{ + return currentFrame; +} + +void CreatureAnimation::playOnce( ECreatureAnimType type ) +{ + setType(type); + once = true; +} + +inline int getBorderStrength(float time) +{ + float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1 + + return static_cast(borderStrength * 155 + 100); // scale to 0-255 +} + +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)); + shadowAlpha = shadowTest.a; + + size_t frame = static_cast(floor(currentFrame)); + + std::shared_ptr image; + + if(facingRight) + image = forward->getImage(frame, size_t(type)); + else + image = reverse->getImage(frame, size_t(type)); + + if(image) + { + IImage::SpecialPalette SpecialPalette; + genSpecialPalette(SpecialPalette); + + image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); + image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); + + canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); + + } +} + +void CreatureAnimation::playUntil(size_t frameIndex) +{ + animationEnd = frameIndex; +} + +int CreatureAnimation::framesInGroup(ECreatureAnimType group) const +{ + return static_cast(forward->size(size_t(group))); +} + +bool CreatureAnimation::isDead() const +{ + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEAD_RANGED; +} + +bool CreatureAnimation::isDying() const +{ + return getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEATH_RANGED; +} + +bool CreatureAnimation::isDeadOrDying() const +{ + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEAD_RANGED + || getType() == ECreatureAnimType::DEATH_RANGED; +} + +bool CreatureAnimation::isIdle() const +{ + return getType() == ECreatureAnimType::HOLDING + || getType() == ECreatureAnimType::MOUSEON; +} + +bool CreatureAnimation::isMoving() const +{ + return getType() == ECreatureAnimType::MOVE_START + || getType() == ECreatureAnimType::MOVING + || getType() == ECreatureAnimType::MOVE_END + || getType() == ECreatureAnimType::TURN_L + || getType() == ECreatureAnimType::TURN_R; +} + +bool CreatureAnimation::isShooting() const +{ + return getType() == ECreatureAnimType::SHOOT_UP + || getType() == ECreatureAnimType::SHOOT_FRONT + || getType() == ECreatureAnimType::SHOOT_DOWN; +} diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 2e7ac4a7f..9029f7437 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -1,159 +1,158 @@ -/* - * CCreatureAnimation.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/FunctionList.h" -#include "../widgets/Images.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" - -#include - -class CIntObject; -class CreatureAnimation; -class Canvas; - -/// Namespace for some common controls of animations -namespace AnimationControls -{ - /// get SDL_Color for creature selection borders - SDL_Color getBlueBorder(); - SDL_Color getGoldBorder(); - SDL_Color getNoBorder(); - - /// returns animation speed factor according to game settings, - /// slow speed is considered to be "base speed" and will return 1.0 - float getAnimationSpeedFactor(); - - /// creates animation object with preset speed control - std::shared_ptr getAnimation(const CCreature * creature); - - /// returns animation speed of specific group, taking in mind game setting (in frames per second) - float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID); - - /// returns how far projectile should move per second, in pixels per second - float getProjectileSpeed(); - - /// returns how far projectile should move per second, in pixels per second - float getRayProjectileSpeed(); - - /// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction - float getCatapultSpeed(); - - /// returns speed of any spell effects, including any special effects like morale (in frames per second) - float getSpellEffectSpeed(); - - /// returns speed of movement animation across the screen, in tiles per second - float getMovementDistance(const CCreature * creature); - - /// returns speed of movement animation across the screen, in pixels per seconds - float getFlightDistance(const CCreature * creature); - - /// Returns total time for full fade-in effect on newly summoned creatures, in seconds - float getFadeInDuration(); - - /// Returns animation speed for obstacles, in frames per second - float getObstaclesSpeed(); -} - -/// Class which manages animations of creatures/units inside battles -/// TODO: split into constant image container and class that does *control* of animation -class CreatureAnimation : public CIntObject -{ -public: - using TSpeedController = std::function; - -private: - std::string name; - - /// animation for rendering stack in default orientation - facing right - std::shared_ptr forward; - - /// animation that has all its frames flipped for rendering stack facing left - std::shared_ptr reverse; - - int fullWidth; - int fullHeight; - - /// speed of animation, measure in frames per second - float speed; - - /// currently displayed frame. Float to allow H3-style animations where frames - /// don't display for integer number of frames - float currentFrame; - float animationEnd; - - /// cumulative, real-time duration of animation. Used for effects like selection border - float elapsedTime; - - ///type of animation being displayed - ECreatureAnimType type; - - /// current value of shadow transparency - uint8_t shadowAlpha; - - /// border color, disabled if alpha = 0 - SDL_Color border; - - TSpeedController speedController; - - /// animation will be played once and the reset to idling - bool once; - - 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; - - int getWidth() const; - int getHeight() const; - - /// Constructor - /// name - path to .def file, relative to SPRITES/ directory - /// controller - function that will return for how long *each* frame - /// in specified group of animation should be played, measured in seconds - CreatureAnimation(const std::string & name_, TSpeedController speedController); - - /// sets type of animation and resets framecount - void setType(ECreatureAnimType type); - - /// returns currently rendered type of animation - ECreatureAnimType getType() const; - - void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight); - - /// should be called every frame, return true when animation was reset to beginning - bool incrementFrame(float timePassed); - - void setBorderColor(SDL_Color palette); - - /// Gets the current frame ID within current group. - float getCurrentFrame() const; - - /// plays once given type of animation, then resets to idle - void playOnce(ECreatureAnimType type); - - /// returns number of frames in selected animation type - int framesInGroup(ECreatureAnimType group) const; - - void playUntil(size_t frameIndex); - - /// helpers to classify current type of animation - bool isDead() const; - bool isDying() const; - bool isDeadOrDying() const; - bool isIdle() const; - bool isMoving() const; - bool isShooting() const; -}; +/* + * CCreatureAnimation.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/FunctionList.h" +#include "../../lib/Color.h" +#include "../widgets/Images.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" + +class CIntObject; +class CreatureAnimation; +class Canvas; + +/// Namespace for some common controls of animations +namespace AnimationControls +{ + /// get color for creature selection borders + ColorRGBA getBlueBorder(); + ColorRGBA getGoldBorder(); + ColorRGBA getNoBorder(); + + /// returns animation speed factor according to game settings, + /// slow speed is considered to be "base speed" and will return 1.0 + float getAnimationSpeedFactor(); + + /// creates animation object with preset speed control + std::shared_ptr getAnimation(const CCreature * creature); + + /// returns animation speed of specific group, taking in mind game setting (in frames per second) + float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID); + + /// returns how far projectile should move per second, in pixels per second + float getProjectileSpeed(); + + /// returns how far projectile should move per second, in pixels per second + float getRayProjectileSpeed(); + + /// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction + float getCatapultSpeed(); + + /// returns speed of any spell effects, including any special effects like morale (in frames per second) + float getSpellEffectSpeed(); + + /// returns speed of movement animation across the screen, in tiles per second + float getMovementDistance(const CCreature * creature); + + /// returns speed of movement animation across the screen, in pixels per seconds + float getFlightDistance(const CCreature * creature); + + /// Returns total time for full fade-in effect on newly summoned creatures, in seconds + float getFadeInDuration(); + + /// Returns animation speed for obstacles, in frames per second + float getObstaclesSpeed(); +} + +/// Class which manages animations of creatures/units inside battles +/// TODO: split into constant image container and class that does *control* of animation +class CreatureAnimation : public CIntObject +{ +public: + using TSpeedController = std::function; + +private: + AnimationPath name; + + /// animation for rendering stack in default orientation - facing right + std::shared_ptr forward; + + /// animation that has all its frames flipped for rendering stack facing left + std::shared_ptr reverse; + + int fullWidth; + int fullHeight; + + /// speed of animation, measure in frames per second + float speed; + + /// currently displayed frame. Float to allow H3-style animations where frames + /// don't display for integer number of frames + float currentFrame; + float animationEnd; + + /// cumulative, real-time duration of animation. Used for effects like selection border + float elapsedTime; + + ///type of animation being displayed + ECreatureAnimType type; + + /// current value of shadow transparency + uint8_t shadowAlpha; + + /// border color, disabled if alpha = 0 + ColorRGBA border; + + TSpeedController speedController; + + /// animation will be played once and the reset to idling + bool once; + + 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; + + int getWidth() const; + int getHeight() const; + + /// Constructor + /// name - path to .def file, relative to SPRITES/ directory + /// controller - function that will return for how long *each* frame + /// in specified group of animation should be played, measured in seconds + CreatureAnimation(const AnimationPath & name_, TSpeedController speedController); + + /// sets type of animation and resets framecount + void setType(ECreatureAnimType type); + + /// returns currently rendered type of animation + ECreatureAnimType getType() const; + + void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight); + + /// should be called every frame, return true when animation was reset to beginning + bool incrementFrame(float timePassed); + + void setBorderColor(ColorRGBA palette); + + /// Gets the current frame ID within current group. + float getCurrentFrame() const; + + /// plays once given type of animation, then resets to idle + void playOnce(ECreatureAnimType type); + + /// returns number of frames in selected animation type + int framesInGroup(ECreatureAnimType group) const; + + void playUntil(size_t frameIndex); + + /// helpers to classify current type of animation + bool isDead() const; + bool isDying() const; + bool isDeadOrDying() const; + bool isIdle() const; + bool isMoving() const; + bool isShooting() const; +}; diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 698616d7b..2db1020a9 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -24,10 +24,12 @@ #include "../CMT.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" +#include "../CMusicHandler.h" #include "../../lib/CConfigHandler.h" #include +#include InputHandler::InputHandler() : mouseHandler(std::make_unique()) @@ -111,32 +113,45 @@ bool InputHandler::ignoreEventsUntilInput() void InputHandler::preprocessEvent(const SDL_Event & ev) { - if((ev.type==SDL_QUIT) ||(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT))) + if(ev.type == SDL_QUIT) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); #ifdef VCMI_ANDROID handleQuit(false); #else - handleQuit(); -#endif - return; - } -#ifdef VCMI_ANDROID - else if (ev.type == SDL_KEYDOWN && ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK) - { handleQuit(true); - } #endif - else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4) - { - boost::unique_lock lock(*CPlayerInterface::pim); - Settings full = settings.write["video"]["fullscreen"]; - full->Bool() = !full->Bool(); - - GH.onScreenResize(); return; } + else if(ev.type == SDL_KEYDOWN) + { + if(ev.key.keysym.sym == SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT)) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + handleQuit(true); + return; + } + + if(ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + handleQuit(true); + return; + } + + if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F4) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + Settings full = settings.write["video"]["fullscreen"]; + full->Bool() = !full->Bool(); + + GH.onScreenResize(); + return; + } + } else if(ev.type == SDL_USEREVENT) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); handleUserEvent(ev.user); return; @@ -147,16 +162,35 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) case SDL_WINDOWEVENT_RESTORED: #ifndef VCMI_IOS { - boost::unique_lock lock(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); GH.onScreenResize(); } #endif break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(settings["general"]["music"].Integer()); + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + } + } + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(0); + CCS->soundh->setVolume(0); + } + } + break; } return; } else if(ev.type == SDL_SYSWMEVENT) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if(!settings["session"]["headless"].Bool() && settings["general"]["notifications"].Bool()) { NotificationHandler::handleSdlEvent(ev); @@ -166,6 +200,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) //preprocessing if(ev.type == SDL_MOUSEMOTION) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if (CCS && CCS->curh) CCS->curh->cursorMove(ev.motion.x, ev.motion.y); } @@ -254,6 +289,11 @@ void InputHandler::hapticFeedback() fingerHandler->hapticFeedback(); } +uint32_t InputHandler::getTicks() +{ + return SDL_GetTicks(); +} + bool InputHandler::hasTouchInputDevice() const { return fingerHandler->hasTouchInputDevice(); @@ -276,6 +316,8 @@ void InputHandler::handleUserEvent(const SDL_UserEvent & current) auto heapFunctor = static_cast*>(current.data1); (*heapFunctor)(); + + delete heapFunctor; } const Point & InputHandler::getCursorPosition() const diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index ac245f247..0584de677 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -68,6 +68,9 @@ public: /// do a haptic feedback void hapticFeedback(); + /// Get the number of milliseconds since SDL library initialization + uint32_t getTicks(); + /// returns true if system has active touchscreen bool hasTouchInputDevice() const; diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 618372aba..01b7c990e 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -59,14 +59,6 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) Settings s = settings.write["session"]; switch(key.keysym.sym) { - case SDLK_F5: - if(settings["session"]["spectate-locked-pim"].Bool()) - CPlayerInterface::pim->unlock(); - else - CPlayerInterface::pim->lock(); - s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool(); - break; - case SDLK_F6: s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool(); break; diff --git a/client/eventsSDL/InputSourceMouse.cpp b/client/eventsSDL/InputSourceMouse.cpp index ec971c645..dc9d8227f 100644 --- a/client/eventsSDL/InputSourceMouse.cpp +++ b/client/eventsSDL/InputSourceMouse.cpp @@ -48,7 +48,7 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b { case SDL_BUTTON_LEFT: if(button.clicks > 1) - GH.events().dispatchMouseDoubleClick(position); + GH.events().dispatchMouseDoubleClick(position, 0); else GH.events().dispatchMouseLeftButtonPressed(position, 0); break; diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 456d9cfc7..ef80833f0 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -33,7 +33,7 @@ #include InputSourceTouch::InputSourceTouch() - : lastTapTimeTicks(0) + : lastTapTimeTicks(0), lastLeftClickTimeTicks(0) { params.useRelativeMode = settings["general"]["userRelativePointer"].Bool(); params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float(); @@ -181,8 +181,18 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) case TouchState::TAP_DOWN_SHORT: { GH.input().setCursorPosition(convertTouchToMouse(tfinger)); - GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); - GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + if(tfinger.timestamp - lastLeftClickTimeTicks < params.doubleTouchTimeMilliseconds && (convertTouchToMouse(tfinger) - lastLeftClickPosition).length() < params.doubleTouchToleranceDistance) + { + GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger), params.touchToleranceDistance); + GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + } + else + { + GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); + GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + lastLeftClickTimeTicks = tfinger.timestamp; + lastLeftClickPosition = convertTouchToMouse(tfinger); + } state = TouchState::IDLE; break; } diff --git a/client/eventsSDL/InputSourceTouch.h b/client/eventsSDL/InputSourceTouch.h index 56d112dc7..19a8cca50 100644 --- a/client/eventsSDL/InputSourceTouch.h +++ b/client/eventsSDL/InputSourceTouch.h @@ -72,6 +72,12 @@ struct TouchInputParameters /// tap for period longer than specified here will be qualified as "long tap", triggering corresponding gesture uint32_t longTouchTimeMilliseconds = 750; + /// time span in where the second tap has to happen for qualifing as "double click" + uint32_t doubleTouchTimeMilliseconds = 500; + + /// max distance in where the second tap has to happen for qualifing as "double click" + 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; @@ -94,6 +100,9 @@ class InputSourceTouch uint32_t lastTapTimeTicks; Point lastTapPosition; + uint32_t lastLeftClickTimeTicks; + Point lastLeftClickPosition; + Point convertTouchToMouse(const SDL_TouchFingerEvent & current); Point convertTouchToMouse(float x, float y); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 27c97895a..72bc72c37 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -1,255 +1,258 @@ -/* - * CGuiHandler.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 "CGuiHandler.h" -#include "../lib/CondSh.h" - -#include "CIntObject.h" -#include "CursorHandler.h" -#include "ShortcutHandler.h" -#include "FramerateManager.h" -#include "WindowHandler.h" -#include "EventDispatcher.h" -#include "../eventsSDL/InputHandler.h" - -#include "../CGameInfo.h" -#include "../render/Colors.h" -#include "../renderSDL/ScreenHandler.h" -#include "../CMT.h" -#include "../CPlayerInterface.h" -#include "../battle/BattleInterface.h" - -#include "../../lib/CThreadHelper.h" -#include "../../lib/CConfigHandler.h" - -#include - -CGuiHandler GH; - -boost::thread_specific_ptr inGuiThread; - -SObjectConstruction::SObjectConstruction(CIntObject *obj) -:myObj(obj) -{ - GH.createdObj.push_front(obj); - GH.captureChildren = true; -} - -SObjectConstruction::~SObjectConstruction() -{ - assert(GH.createdObj.size()); - assert(GH.createdObj.front() == myObj); - 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; -} - -void CGuiHandler::init() -{ - inputHandlerInstance = std::make_unique(); - eventDispatcherInstance = std::make_unique(); - windowHandlerInstance = std::make_unique(); - screenHandlerInstance = std::make_unique(); - shortcutsHandlerInstance = std::make_unique(); - framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); -} - -void CGuiHandler::handleEvents() -{ - events().dispatchTimer(framerate().getElapsedMilliseconds()); - - //player interface may want special event handling - if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents()) - return; - - input().processEvents(); -} - -void CGuiHandler::fakeMouseMove() -{ - dispatchMainThread([](){ - assert(CPlayerInterface::pim); - boost::unique_lock lock(*CPlayerInterface::pim); - GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); - }); -} - -void CGuiHandler::startTextInput(const Rect & whereInput) -{ - input().startTextInput(whereInput); -} - -void CGuiHandler::stopTextInput() -{ - input().stopTextInput(); -} - -void CGuiHandler::renderFrame() -{ - // Updating GUI requires locking pim mutex (that protects screen and GUI state). - // During game: - // When ending the game, the pim mutex might be hold by other thread, - // that will notify us about the ending game by setting terminate_cond flag. - //in PreGame terminate_cond stay false - - bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded - while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate - boost::this_thread::sleep(boost::posix_time::milliseconds(1)); - - if(acquiredTheLockOnPim) - { - // If we are here, pim mutex has been successfully locked - let's store it in a safe RAII lock. - boost::unique_lock un(*CPlayerInterface::pim, boost::adopt_lock); - - if(nullptr != curInt) - curInt->update(); - - if(settings["video"]["showfps"].Bool()) - drawFPSCounter(); - - SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); - - SDL_RenderClear(mainRenderer); - SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); - - CCS->curh->render(); - - SDL_RenderPresent(mainRenderer); - - windows().onFrameRendered(); - } - - framerate().framerateDelay(); // holds a constant FPS -} - -CGuiHandler::CGuiHandler() - : defActionsDef(0) - , captureChildren(false) - , curInt(nullptr) - , fakeStatusBar(std::make_shared()) - , terminate_cond (new CondSh(false)) -{ -} - -CGuiHandler::~CGuiHandler() -{ - delete terminate_cond; -} - -ShortcutHandler & CGuiHandler::shortcuts() -{ - assert(shortcutsHandlerInstance); - return *shortcutsHandlerInstance; -} - -FramerateManager & CGuiHandler::framerate() -{ - assert(framerateManagerInstance); - return *framerateManagerInstance; -} - -bool CGuiHandler::isKeyboardCtrlDown() const -{ - return inputHandlerInstance->isKeyboardCtrlDown(); -} - -bool CGuiHandler::isKeyboardAltDown() const -{ - return inputHandlerInstance->isKeyboardAltDown(); -} - -bool CGuiHandler::isKeyboardShiftDown() const -{ - return inputHandlerInstance->isKeyboardShiftDown(); -} - -const Point & CGuiHandler::getCursorPosition() const -{ - return inputHandlerInstance->getCursorPosition(); -} - -Point CGuiHandler::screenDimensions() const -{ - return Point(screen->w, screen->h); -} - -void CGuiHandler::drawFPSCounter() -{ - static SDL_Rect overlay = { 0, 0, 64, 32}; - uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); - SDL_FillRect(screen, &overlay, black); - std::string fps = std::to_string(framerate().getFramerate()); - graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(10, 10)); -} - -bool CGuiHandler::amIGuiThread() -{ - return inGuiThread.get() && *inGuiThread; -} - -void CGuiHandler::dispatchMainThread(const std::function & functor) -{ - inputHandlerInstance->dispatchMainThread(functor); -} - -IScreenHandler & CGuiHandler::screenHandler() -{ - return *screenHandlerInstance; -} - -EventDispatcher & CGuiHandler::events() -{ - return *eventDispatcherInstance; -} - -InputHandler & CGuiHandler::input() -{ - return *inputHandlerInstance; -} - -WindowHandler & CGuiHandler::windows() -{ - assert(windowHandlerInstance); - return *windowHandlerInstance; -} - -std::shared_ptr CGuiHandler::statusbar() -{ - auto locked = currentStatusBar.lock(); - - if (!locked) - return fakeStatusBar; - - return locked; -} - -void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) -{ - currentStatusBar = newStatusBar; -} - -void CGuiHandler::onScreenResize() -{ - screenHandler().onScreenResize(); - windows().onScreenResize(); -} +/* + * CGuiHandler.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 "CGuiHandler.h" +#include "../lib/CondSh.h" + +#include "CIntObject.h" +#include "CursorHandler.h" +#include "ShortcutHandler.h" +#include "FramerateManager.h" +#include "WindowHandler.h" +#include "EventDispatcher.h" +#include "../eventsSDL/InputHandler.h" + +#include "../CGameInfo.h" +#include "../render/Colors.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../render/EFont.h" +#include "../renderSDL/ScreenHandler.h" +#include "../renderSDL/RenderHandler.h" +#include "../CMT.h" +#include "../CPlayerInterface.h" +#include "../battle/BattleInterface.h" + +#include "../../lib/CThreadHelper.h" +#include "../../lib/CConfigHandler.h" + +#include + +CGuiHandler GH; + +static thread_local bool inGuiThread = false; + +SObjectConstruction::SObjectConstruction(CIntObject *obj) +:myObj(obj) +{ + GH.createdObj.push_front(obj); + GH.captureChildren = true; +} + +SObjectConstruction::~SObjectConstruction() +{ + assert(GH.createdObj.size()); + assert(GH.createdObj.front() == myObj); + 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; +} + +void CGuiHandler::init() +{ + inGuiThread = true; + + inputHandlerInstance = std::make_unique(); + eventDispatcherInstance = std::make_unique(); + windowHandlerInstance = std::make_unique(); + screenHandlerInstance = std::make_unique(); + renderHandlerInstance = std::make_unique(); + shortcutsHandlerInstance = std::make_unique(); + framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); +} + +void CGuiHandler::handleEvents() +{ + events().dispatchTimer(framerate().getElapsedMilliseconds()); + + //player interface may want special event handling + if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents()) + return; + + input().processEvents(); +} + +void CGuiHandler::fakeMouseMove() +{ + dispatchMainThread([](){ + GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); + }); +} + +void CGuiHandler::startTextInput(const Rect & whereInput) +{ + input().startTextInput(whereInput); +} + +void CGuiHandler::stopTextInput() +{ + input().stopTextInput(); +} + +void CGuiHandler::renderFrame() +{ + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if (nullptr != curInt) + curInt->update(); + + if (settings["video"]["showfps"].Bool()) + drawFPSCounter(); + } + + SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); + + SDL_RenderClear(mainRenderer); + SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + CCS->curh->render(); + + windows().onFrameRendered(); + } + + SDL_RenderPresent(mainRenderer); + framerate().framerateDelay(); // holds a constant FPS +} + +CGuiHandler::CGuiHandler() + : defActionsDef(0) + , captureChildren(false) + , curInt(nullptr) + , fakeStatusBar(std::make_shared()) +{ +} + +CGuiHandler::~CGuiHandler() = default; + +ShortcutHandler & CGuiHandler::shortcuts() +{ + assert(shortcutsHandlerInstance); + return *shortcutsHandlerInstance; +} + +FramerateManager & CGuiHandler::framerate() +{ + assert(framerateManagerInstance); + return *framerateManagerInstance; +} + +bool CGuiHandler::isKeyboardCtrlDown() const +{ + return inputHandlerInstance->isKeyboardCtrlDown(); +} + +bool CGuiHandler::isKeyboardAltDown() const +{ + return inputHandlerInstance->isKeyboardAltDown(); +} + +bool CGuiHandler::isKeyboardShiftDown() const +{ + return inputHandlerInstance->isKeyboardShiftDown(); +} + +const Point & CGuiHandler::getCursorPosition() const +{ + return inputHandlerInstance->getCursorPosition(); +} + +Point CGuiHandler::screenDimensions() const +{ + return Point(screen->w, screen->h); +} + +void CGuiHandler::drawFPSCounter() +{ + int x = 7; + int y = screen->h-20; + int width3digitFPSIncludingPadding = 48; + int heightFPSTextIncludingPadding = 11; + static 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)); +} + +bool CGuiHandler::amIGuiThread() +{ + return inGuiThread; +} + +void CGuiHandler::dispatchMainThread(const std::function & functor) +{ + inputHandlerInstance->dispatchMainThread(functor); +} + +IScreenHandler & CGuiHandler::screenHandler() +{ + return *screenHandlerInstance; +} + +IRenderHandler & CGuiHandler::renderHandler() +{ + return *renderHandlerInstance; +} + +EventDispatcher & CGuiHandler::events() +{ + return *eventDispatcherInstance; +} + +InputHandler & CGuiHandler::input() +{ + return *inputHandlerInstance; +} + +WindowHandler & CGuiHandler::windows() +{ + assert(windowHandlerInstance); + return *windowHandlerInstance; +} + +std::shared_ptr CGuiHandler::statusbar() +{ + auto locked = currentStatusBar.lock(); + + if (!locked) + return fakeStatusBar; + + return locked; +} + +void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) +{ + currentStatusBar = newStatusBar; +} + +void CGuiHandler::onScreenResize() +{ + screenHandler().onScreenResize(); + windows().onScreenResize(); +} diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index b88aaadb0..4f651c159 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -1,128 +1,130 @@ -/* - * CGuiHandler.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 -template struct CondSh; -class Point; -class Rect; -VCMI_LIB_NAMESPACE_END - -enum class MouseButton; -class ShortcutHandler; -class FramerateManager; -class IStatusBar; -class CIntObject; -class IUpdateable; -class IShowActivatable; -class IScreenHandler; -class WindowHandler; -class EventDispatcher; -class InputHandler; - -// Handles GUI logic and drawing -class CGuiHandler -{ -private: - /// Fake no-op version status bar, for use in windows that have no status bar - std::shared_ptr fakeStatusBar; - - /// Status bar of current window, if any. Uses weak_ptr to allow potential hanging reference after owned window has been deleted - std::weak_ptr currentStatusBar; - - std::unique_ptr shortcutsHandlerInstance; - std::unique_ptr windowHandlerInstance; - - std::unique_ptr screenHandlerInstance; - std::unique_ptr framerateManagerInstance; - std::unique_ptr eventDispatcherInstance; - std::unique_ptr inputHandlerInstance; - -public: - /// returns current position of mouse cursor, relative to vcmi window - const Point & getCursorPosition() const; - - ShortcutHandler & shortcuts(); - FramerateManager & framerate(); - EventDispatcher & events(); - InputHandler & input(); - - /// Returns current logical screen dimensions - /// May not match size of window if user has UI scaling different from 100% - Point screenDimensions() const; - - /// returns true if chosen keyboard key is currently pressed down - bool isKeyboardAltDown() const; - bool isKeyboardCtrlDown() const; - bool isKeyboardShiftDown() const; - - void startTextInput(const Rect & where); - void stopTextInput(); - - IScreenHandler & screenHandler(); - - WindowHandler & windows(); - - /// Returns currently active status bar. Guaranteed to be non-null - std::shared_ptr statusbar(); - - /// Set currently active status bar - void setStatusbar(std::shared_ptr); - - 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 - - CGuiHandler(); - ~CGuiHandler(); - - void init(); - void renderFrame(); - - /// called whenever user selects different resolution, requiring to center/resize all windows - void onScreenResize(); - - void handleEvents(); //takes events from queue and calls interested objects - void fakeMouseMove(); - void drawFPSCounter(); // draws the FPS to the upper left corner of the screen - - bool amIGuiThread(); - - /// Calls provided functor in main thread on next execution frame - void dispatchMainThread(const std::function & functor); - - CondSh * terminate_cond; // confirm termination -}; - -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) +/* + * CGuiHandler.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 +template struct CondSh; +class Point; +class Rect; +VCMI_LIB_NAMESPACE_END + +enum class MouseButton; +class ShortcutHandler; +class FramerateManager; +class IStatusBar; +class CIntObject; +class IUpdateable; +class IShowActivatable; +class IRenderHandler; +class IScreenHandler; +class WindowHandler; +class EventDispatcher; +class InputHandler; + +// Handles GUI logic and drawing +class CGuiHandler +{ +private: + /// Fake no-op version status bar, for use in windows that have no status bar + std::shared_ptr fakeStatusBar; + + /// Status bar of current window, if any. Uses weak_ptr to allow potential hanging reference after owned window has been deleted + std::weak_ptr currentStatusBar; + + std::unique_ptr shortcutsHandlerInstance; + std::unique_ptr windowHandlerInstance; + + std::unique_ptr screenHandlerInstance; + std::unique_ptr renderHandlerInstance; + std::unique_ptr framerateManagerInstance; + std::unique_ptr eventDispatcherInstance; + std::unique_ptr inputHandlerInstance; + +public: + boost::mutex interfaceMutex; + + /// returns current position of mouse cursor, relative to vcmi window + const Point & getCursorPosition() const; + + ShortcutHandler & shortcuts(); + FramerateManager & framerate(); + EventDispatcher & events(); + InputHandler & input(); + + /// Returns current logical screen dimensions + /// May not match size of window if user has UI scaling different from 100% + Point screenDimensions() const; + + /// returns true if chosen keyboard key is currently pressed down + bool isKeyboardAltDown() const; + bool isKeyboardCtrlDown() const; + bool isKeyboardShiftDown() const; + + void startTextInput(const Rect & where); + void stopTextInput(); + + IScreenHandler & screenHandler(); + IRenderHandler & renderHandler(); + WindowHandler & windows(); + + /// Returns currently active status bar. Guaranteed to be non-null + std::shared_ptr statusbar(); + + /// Set currently active status bar + void setStatusbar(std::shared_ptr); + + 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 + + CGuiHandler(); + ~CGuiHandler(); + + void init(); + void renderFrame(); + + /// called whenever user selects different resolution, requiring to center/resize all windows + void onScreenResize(); + + void handleEvents(); //takes events from queue and calls interested objects + void fakeMouseMove(); + void drawFPSCounter(); // draws the FPS to the upper left corner of the screen + + bool amIGuiThread(); + + /// Calls provided functor in main thread on next execution frame + void dispatchMainThread(const std::function & functor); +}; + +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 74c946d16..3f136151e 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -1,346 +1,346 @@ -/* - * CIntObject.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 "CIntObject.h" - -#include "CGuiHandler.h" -#include "WindowHandler.h" -#include "EventDispatcher.h" -#include "Shortcut.h" -#include "../render/Canvas.h" -#include "../windows/CMessage.h" -#include "../CMT.h" - -CIntObject::CIntObject(int used_, Point pos_): - parent_m(nullptr), - parent(parent_m), - redrawParent(false), - inputEnabled(true), - used(used_), - recActions(GH.defActionsDef), - defActions(GH.defActionsDef), - pos(pos_, Point()) -{ - if(GH.captureChildren) - GH.createdObj.front()->addChild(this, true); -} - -CIntObject::~CIntObject() -{ - if(isActive()) - deactivate(); - - while(!children.empty()) - { - if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) - delete children.front(); - else - removeChild(children.front()); - } - - if(parent_m) - parent_m->removeChild(this); -} - -void CIntObject::show(Canvas & to) -{ - if(defActions & UPDATE) - 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); - } -} - -void CIntObject::activate() -{ - if (isActive()) - return; - - if (inputEnabled) - activateEvents(used | GENERAL); - else - activateEvents(GENERAL); - - assert(isActive()); - - if(defActions & ACTIVATE) - for(auto & elem : children) - if(elem->recActions & ACTIVATE) - elem->activate(); -} - -void CIntObject::deactivate() -{ - if (!isActive()) - return; - - deactivateEvents(used | GENERAL); - - assert(!isActive()); - - if(defActions & DEACTIVATE) - for(auto & elem : children) - if(elem->recActions & DEACTIVATE) - elem->deactivate(); -} - -void CIntObject::addUsedEvents(ui16 newActions) -{ - if (isActive() && inputEnabled) - activateEvents(~used & newActions); - used |= newActions; -} - -void CIntObject::removeUsedEvents(ui16 newActions) -{ - if (isActive()) - deactivateEvents(used & newActions); - used &= ~newActions; -} - -void CIntObject::disable() -{ - if(isActive()) - deactivate(); - - recActions = DISPOSE; -} - -void CIntObject::enable() -{ - if(!isActive() && (!parent_m || parent_m->isActive())) - { - activate(); - redraw(); - } - - recActions = 255; -} - -void CIntObject::setEnabled(bool on) -{ - if (on) - enable(); - else - disable(); -} - -void CIntObject::setInputEnabled(bool on) -{ - if (inputEnabled == on) - return; - - inputEnabled = on; - - if (isActive()) - { - assert((used & GENERAL) == 0); - - if (on) - activateEvents(used); - else - deactivateEvents(used); - } - - for(auto & elem : children) - elem->setInputEnabled(on); -} - -void CIntObject::setRedrawParent(bool on) -{ - redrawParent = on; -} - -void CIntObject::fitToScreen(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); - if (newPos != pos.topLeft()) - moveTo(newPos, propagate); -} - -void CIntObject::moveBy(const Point & p, bool propagate) -{ - pos.x += p.x; - pos.y += p.y; - if(propagate) - for(auto & elem : children) - elem->moveBy(p, propagate); -} - -void CIntObject::moveTo(const Point & p, bool propagate) -{ - moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); -} - -void CIntObject::addChild(CIntObject * child, bool adjustPosition) -{ - if (vstd::contains(children, child)) - { - return; - } - if (child->parent_m) - { - child->parent_m->removeChild(child, adjustPosition); - } - children.push_back(child); - child->parent_m = this; - if(adjustPosition) - child->moveBy(pos.topLeft(), adjustPosition); - - if (inputEnabled != child->inputEnabled) - child->setInputEnabled(inputEnabled); - - if (!isActive() && child->isActive()) - child->deactivate(); - if (isActive()&& !child->isActive()) - child->activate(); -} - -void CIntObject::removeChild(CIntObject * child, bool adjustPosition) -{ - if (!child) - return; - - if(!vstd::contains(children, child)) - throw std::runtime_error("Wrong child object"); - - if(child->parent_m != this) - throw std::runtime_error("Wrong child object"); - - children -= child; - child->parent_m = nullptr; - if(adjustPosition) - child->pos -= pos.topLeft(); -} - -void CIntObject::redraw() -{ - //currently most of calls come from active objects so this check won't affect them - //it should fix glitches when called by inactive elements located below active window - if (isActive()) - { - if (parent_m && redrawParent) - { - parent_m->redraw(); - } - else - { - Canvas buffer = Canvas::createFromSurface(screenBuf); - - showAll(buffer); - if(screenBuf != screen) - { - Canvas screenBuffer = Canvas::createFromSurface(screen); - - showAll(screenBuffer); - } - } - } -} - -bool CIntObject::receiveEvent(const Point & position, int eventType) const -{ - return pos.isInside(position); -} - -const Rect & CIntObject::getPosition() const -{ - return pos; -} - -void CIntObject::onScreenResize() -{ - center(pos, true); -} - -bool CIntObject::isPopupWindow() const -{ - return false; -} - -const Rect & CIntObject::center( const Rect &r, bool propagate ) -{ - pos.w = r.w; - pos.h = r.h; - return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate); -} - -const Rect & CIntObject::center( bool propagate ) -{ - return center(pos, propagate); -} - -const Rect & CIntObject::center(const Point & p, bool propagate) -{ - moveBy(Point(p.x - pos.w/2 - pos.x, - p.y - pos.h/2 - pos.y), - propagate); - return pos; -} - -bool CIntObject::captureThisKey(EShortcut key) -{ - return false; -} - -CKeyShortcut::CKeyShortcut() - : assignedKey(EShortcut::NONE) - , shortcutPressed(false) -{} - -CKeyShortcut::CKeyShortcut(EShortcut key) - : assignedKey(key) - , shortcutPressed(false) -{ -} - -void CKeyShortcut::keyPressed(EShortcut key) -{ - if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed) - { - shortcutPressed = true; - clickPressed(GH.getCursorPosition()); - } -} - -void CKeyShortcut::keyReleased(EShortcut key) -{ - if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed) - { - shortcutPressed = false; - clickReleased(GH.getCursorPosition()); - } -} - -WindowBase::WindowBase(int used_, Point pos_) - : CIntObject(used_, pos_) -{ - -} - -void WindowBase::close() -{ - if(!GH.windows().isTopWindow(this)) - logGlobal->error("Only top interface must be closed"); - GH.windows().popWindows(1); -} +/* + * CIntObject.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 "CIntObject.h" + +#include "CGuiHandler.h" +#include "WindowHandler.h" +#include "EventDispatcher.h" +#include "Shortcut.h" +#include "../render/Canvas.h" +#include "../windows/CMessage.h" +#include "../CMT.h" + +CIntObject::CIntObject(int used_, Point pos_): + parent_m(nullptr), + parent(parent_m), + redrawParent(false), + inputEnabled(true), + used(used_), + recActions(GH.defActionsDef), + defActions(GH.defActionsDef), + pos(pos_, Point()) +{ + if(GH.captureChildren) + GH.createdObj.front()->addChild(this, true); +} + +CIntObject::~CIntObject() +{ + if(isActive()) + deactivate(); + + while(!children.empty()) + { + if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) + delete children.front(); + else + removeChild(children.front()); + } + + if(parent_m) + parent_m->removeChild(this); +} + +void CIntObject::show(Canvas & to) +{ + if(defActions & UPDATE) + 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); + } +} + +void CIntObject::activate() +{ + if (isActive()) + return; + + if (inputEnabled) + activateEvents(used | GENERAL); + else + activateEvents(GENERAL); + + assert(isActive()); + + if(defActions & ACTIVATE) + for(auto & elem : children) + if(elem->recActions & ACTIVATE) + elem->activate(); +} + +void CIntObject::deactivate() +{ + if (!isActive()) + return; + + deactivateEvents(used | GENERAL); + + assert(!isActive()); + + if(defActions & DEACTIVATE) + for(auto & elem : children) + if(elem->recActions & DEACTIVATE) + elem->deactivate(); +} + +void CIntObject::addUsedEvents(ui16 newActions) +{ + if (isActive() && inputEnabled) + activateEvents(~used & newActions); + used |= newActions; +} + +void CIntObject::removeUsedEvents(ui16 newActions) +{ + if (isActive()) + deactivateEvents(used & newActions); + used &= ~newActions; +} + +void CIntObject::disable() +{ + if(isActive()) + deactivate(); + + recActions = DISPOSE; +} + +void CIntObject::enable() +{ + if(!isActive() && (!parent_m || parent_m->isActive())) + { + activate(); + redraw(); + } + + recActions = 255; +} + +void CIntObject::setEnabled(bool on) +{ + if (on) + enable(); + else + disable(); +} + +void CIntObject::setInputEnabled(bool on) +{ + if (inputEnabled == on) + return; + + inputEnabled = on; + + if (isActive()) + { + assert((used & GENERAL) == 0); + + if (on) + activateEvents(used); + else + deactivateEvents(used); + } + + for(auto & elem : children) + elem->setInputEnabled(on); +} + +void CIntObject::setRedrawParent(bool on) +{ + redrawParent = on; +} + +void CIntObject::fitToScreen(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); + if (newPos != pos.topLeft()) + moveTo(newPos, propagate); +} + +void CIntObject::moveBy(const Point & p, bool propagate) +{ + pos.x += p.x; + pos.y += p.y; + if(propagate) + for(auto & elem : children) + elem->moveBy(p, propagate); +} + +void CIntObject::moveTo(const Point & p, bool propagate) +{ + moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); +} + +void CIntObject::addChild(CIntObject * child, bool adjustPosition) +{ + if (vstd::contains(children, child)) + { + return; + } + if (child->parent_m) + { + child->parent_m->removeChild(child, adjustPosition); + } + children.push_back(child); + child->parent_m = this; + if(adjustPosition) + child->moveBy(pos.topLeft(), adjustPosition); + + if (inputEnabled != child->inputEnabled) + child->setInputEnabled(inputEnabled); + + if (!isActive() && child->isActive()) + child->deactivate(); + if (isActive()&& !child->isActive()) + child->activate(); +} + +void CIntObject::removeChild(CIntObject * child, bool adjustPosition) +{ + if (!child) + return; + + if(!vstd::contains(children, child)) + throw std::runtime_error("Wrong child object"); + + if(child->parent_m != this) + throw std::runtime_error("Wrong child object"); + + children -= child; + child->parent_m = nullptr; + if(adjustPosition) + child->pos -= pos.topLeft(); +} + +void CIntObject::redraw() +{ + //currently most of calls come from active objects so this check won't affect them + //it should fix glitches when called by inactive elements located below active window + if (isActive()) + { + if (parent_m && redrawParent) + { + parent_m->redraw(); + } + else + { + Canvas buffer = Canvas::createFromSurface(screenBuf); + + showAll(buffer); + if(screenBuf != screen) + { + Canvas screenBuffer = Canvas::createFromSurface(screen); + + showAll(screenBuffer); + } + } + } +} + +bool CIntObject::receiveEvent(const Point & position, int eventType) const +{ + return pos.isInside(position); +} + +const Rect & CIntObject::getPosition() const +{ + return pos; +} + +void CIntObject::onScreenResize() +{ + center(pos, true); +} + +bool CIntObject::isPopupWindow() const +{ + return false; +} + +const Rect & CIntObject::center( const Rect &r, bool propagate ) +{ + pos.w = r.w; + pos.h = r.h; + return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate); +} + +const Rect & CIntObject::center( bool propagate ) +{ + return center(pos, propagate); +} + +const Rect & CIntObject::center(const Point & p, bool propagate) +{ + moveBy(Point(p.x - pos.w/2 - pos.x, + p.y - pos.h/2 - pos.y), + propagate); + return pos; +} + +bool CIntObject::captureThisKey(EShortcut key) +{ + return false; +} + +CKeyShortcut::CKeyShortcut() + : assignedKey(EShortcut::NONE) + , shortcutPressed(false) +{} + +CKeyShortcut::CKeyShortcut(EShortcut key) + : assignedKey(key) + , shortcutPressed(false) +{ +} + +void CKeyShortcut::keyPressed(EShortcut key) +{ + if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed) + { + shortcutPressed = true; + clickPressed(GH.getCursorPosition()); + } +} + +void CKeyShortcut::keyReleased(EShortcut key) +{ + if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed) + { + shortcutPressed = false; + clickReleased(GH.getCursorPosition()); + } +} + +WindowBase::WindowBase(int used_, Point pos_) + : CIntObject(used_, pos_) +{ + +} + +void WindowBase::close() +{ + if(!GH.windows().isTopWindow(this)) + logGlobal->error("Only top interface must be closed"); + GH.windows().popWindows(1); +} diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index c1bddd586..7909e72e7 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -1,183 +1,204 @@ -/* - * CIntObject.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/Graphics.h" -#include "../../lib/Rect.h" -#include "EventsReceiver.h" - -class CGuiHandler; -class CPicture; -class Canvas; - -class IUpdateable -{ -public: - virtual void update()=0; - virtual ~IUpdateable() = default; -}; - -class IShowActivatable -{ -public: - virtual void activate()=0; - virtual void deactivate()=0; - - virtual void redraw()=0; - virtual void show(Canvas & to) = 0; - virtual void showAll(Canvas & to) = 0; - - virtual bool isPopupWindow() const = 0; - virtual void onScreenResize() = 0; - virtual ~IShowActivatable() = default; -}; - -// Base UI element -class CIntObject : public IShowActivatable, public AEventsReceiver //interface object -{ - ui16 used; - - //non-const versions of fields to allow changing them in CIntObject - CIntObject *parent_m; //parent object - - bool inputEnabled; - bool redrawParent; - -public: - std::vector children; - - /// read-only parent access. May not be a "clean" solution but allows some compatibility - CIntObject * const & parent; - - /// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead - /*const*/ Rect pos; - - CIntObject(int used=0, Point offset=Point()); - virtual ~CIntObject(); - - bool captureThisKey(EShortcut key) override; - - 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 - 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!) - void enable(); - /// deactivates or activates UI element based on flag - void setEnabled(bool on); - - /// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element - void setInputEnabled(bool on); - - /// Mark this input as one that requires parent redraw on update, - /// for example if current control might have semi-transparent elements and requires redrawing of background - void setRedrawParent(bool on); - - // activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse) - // usually used automatically by parent - void activate() override; - void deactivate() override; - - //called each frame to update screen - void show(Canvas & to) override; - //called on complete redraw only - void showAll(Canvas & to) override; - //request complete redraw of this object - void redraw() override; - - /// returns true if this element is a popup window - /// called only for windows - bool isPopupWindow() const override; - - /// called only for windows whenever screen size changes - /// default behavior is to re-center, can be overriden - void onScreenResize() override; - - /// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...) - /// by default, usedEvents inside UI elements are always handled - bool receiveEvent(const Point & position, int eventType) const override; - - const Rect & getPosition() const override; - - const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position - 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 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) - - void addChild(CIntObject *child, bool adjustPosition = false); - void removeChild(CIntObject *child, bool adjustPosition = false); - -}; - -/// Class for binding keys to left mouse button clicks -/// Classes wanting use it should have it as one of their base classes -class CKeyShortcut : public virtual CIntObject -{ - bool shortcutPressed; -public: - EShortcut assignedKey; - CKeyShortcut(); - CKeyShortcut(EShortcut key); - void keyPressed(EShortcut key) override; - void keyReleased(EShortcut key) override; - -}; - -class WindowBase : public CIntObject -{ -public: - WindowBase(int used_ = 0, Point pos_ = Point()); -protected: - void close(); -}; - -class IGarrisonHolder -{ -public: - virtual void updateGarrisons() = 0; -}; - -class IStatusBar -{ -public: - virtual ~IStatusBar() = default; - - /// set current text for the status bar - virtual void write(const std::string & text) = 0; - - /// remove any current text from the status bar - virtual void clear() = 0; - - /// remove text from status bar if current text matches tested text - virtual void clearIfMatching(const std::string & testedText) = 0; - - /// enables mode for entering text instead of showing hover text - virtual void setEnteringMode(bool on) = 0; - - /// overrides hover text from controls with text entered into in-game console (for chat/cheats) - virtual void setEnteredText(const std::string & text) = 0; - -}; - -class EmptyStatusBar : public IStatusBar -{ - virtual void write(const std::string & text){}; - virtual void clear(){}; - virtual void clearIfMatching(const std::string & testedText){}; - virtual void setEnteringMode(bool on){}; - virtual void setEnteredText(const std::string & text){}; -}; +/* + * CIntObject.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 "EventsReceiver.h" + +#include "../../lib/Rect.h" +#include "../../lib/Color.h" +#include "../../lib/GameConstants.h" + +class CGuiHandler; +class CPicture; +class Canvas; + +VCMI_LIB_NAMESPACE_BEGIN +class CArmedInstance; +VCMI_LIB_NAMESPACE_END + +class IUpdateable +{ +public: + virtual void update()=0; + virtual ~IUpdateable() = default; +}; + +class IShowActivatable +{ +public: + virtual void activate()=0; + virtual void deactivate()=0; + + virtual void redraw()=0; + virtual void show(Canvas & to) = 0; + virtual void showAll(Canvas & to) = 0; + + virtual bool isPopupWindow() const = 0; + virtual void onScreenResize() = 0; + virtual ~IShowActivatable() = default; +}; + +// Base UI element +class CIntObject : public IShowActivatable, public AEventsReceiver //interface object +{ + ui16 used; + + //non-const versions of fields to allow changing them in CIntObject + CIntObject *parent_m; //parent object + + bool inputEnabled; + bool redrawParent; + +public: + std::vector children; + + /// read-only parent access. May not be a "clean" solution but allows some compatibility + CIntObject * const & parent; + + /// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead + /*const*/ Rect pos; + + CIntObject(int used=0, Point offset=Point()); + virtual ~CIntObject(); + + bool captureThisKey(EShortcut key) override; + + 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 + 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!) + void enable(); + /// deactivates or activates UI element based on flag + void setEnabled(bool on); + + /// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element + void setInputEnabled(bool on); + + /// Mark this input as one that requires parent redraw on update, + /// for example if current control might have semi-transparent elements and requires redrawing of background + void setRedrawParent(bool on); + + // activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse) + // usually used automatically by parent + void activate() override; + void deactivate() override; + + //called each frame to update screen + void show(Canvas & to) override; + //called on complete redraw only + void showAll(Canvas & to) override; + //request complete redraw of this object + void redraw() override; + + /// returns true if this element is a popup window + /// called only for windows + bool isPopupWindow() const override; + + /// called only for windows whenever screen size changes + /// default behavior is to re-center, can be overriden + void onScreenResize() override; + + /// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...) + /// by default, usedEvents inside UI elements are always handled + bool receiveEvent(const Point & position, int eventType) const override; + + const Rect & getPosition() const override; + + const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position + 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 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) + + void addChild(CIntObject *child, bool adjustPosition = false); + void removeChild(CIntObject *child, bool adjustPosition = false); + +}; + +/// Class for binding keys to left mouse button clicks +/// Classes wanting use it should have it as one of their base classes +class CKeyShortcut : public virtual CIntObject +{ + bool shortcutPressed; +public: + EShortcut assignedKey; + CKeyShortcut(); + CKeyShortcut(EShortcut key); + void keyPressed(EShortcut key) override; + void keyReleased(EShortcut key) override; + +}; + +class WindowBase : public CIntObject +{ +public: + WindowBase(int used_ = 0, Point pos_ = Point()); +protected: + virtual void close(); +}; + +class IGarrisonHolder +{ +public: + bool holdsGarrisons(std::vector armies) + { + for (auto const * army : armies) + if (holdsGarrison(army)) + return true; + return false; + } + + virtual bool holdsGarrison(const CArmedInstance * army) = 0; + virtual void updateGarrisons() = 0; +}; + +class ITownHolder +{ +public: + virtual void buildChanged() = 0; +}; + +class IStatusBar +{ +public: + virtual ~IStatusBar() = default; + + /// set current text for the status bar + virtual void write(const std::string & text) = 0; + + /// remove any current text from the status bar + virtual void clear() = 0; + + /// remove text from status bar if current text matches tested text + virtual void clearIfMatching(const std::string & testedText) = 0; + + /// enables mode for entering text instead of showing hover text + virtual void setEnteringMode(bool on) = 0; + + /// overrides hover text from controls with text entered into in-game console (for chat/cheats) + virtual void setEnteredText(const std::string & text) = 0; + +}; + +class EmptyStatusBar : public IStatusBar +{ + virtual void write(const std::string & text){}; + virtual void clear(){}; + virtual void clearIfMatching(const std::string & testedText){}; + virtual void setEnteringMode(bool on){}; + virtual void setEnteredText(const std::string & text){}; +}; diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index f51ab4310..a9fa812e2 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -1,291 +1,292 @@ -/* - * CCursorHandler.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 "CursorHandler.h" - -#include "CGuiHandler.h" -#include "FramerateManager.h" -#include "../renderSDL/CursorSoftware.h" -#include "../renderSDL/CursorHardware.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" - -#include "../../lib/CConfigHandler.h" - -std::unique_ptr CursorHandler::createCursor() -{ -#if defined(VCMI_MOBILE) - if (settings["general"]["userRelativePointer"].Bool()) - return std::make_unique(); -#endif - - if (settings["video"]["cursor"].String() == "hardware") - return std::make_unique(); - - assert(settings["video"]["cursor"].String() == "software"); - return std::make_unique(); -} - -CursorHandler::CursorHandler() - : cursor(createCursor()) - , frameTime(0.f) - , showing(false) - , pos(0,0) -{ - - type = Cursor::Type::DEFAULT; - dndObject = nullptr; - - cursors = - { - std::make_unique("CRADVNTR"), - std::make_unique("CRCOMBAT"), - std::make_unique("CRDEFLT"), - std::make_unique("CRSPELL") - }; - - for (auto & cursor : cursors) - cursor->preload(); - - set(Cursor::Map::POINTER); -} - -CursorHandler::~CursorHandler() = default; - -void CursorHandler::changeGraphic(Cursor::Type type, size_t index) -{ - assert(dndObject == nullptr); - - if (type == this->type && index == this->frame) - return; - - this->type = type; - this->frame = index; - - cursor->setImage(getCurrentImage(), getPivotOffset()); -} - -void CursorHandler::set(Cursor::Default index) -{ - changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); -} - -void CursorHandler::set(Cursor::Map index) -{ - changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); -} - -void CursorHandler::set(Cursor::Combat index) -{ - changeGraphic(Cursor::Type::COMBAT, static_cast(index)); -} - -void CursorHandler::set(Cursor::Spellcast index) -{ - //Note: this is animated cursor, ignore specified frame and only change type - changeGraphic(Cursor::Type::SPELLBOOK, frame); -} - -void CursorHandler::dragAndDropCursor(std::shared_ptr image) -{ - dndObject = image; - cursor->setImage(getCurrentImage(), getPivotOffset()); -} - -void CursorHandler::dragAndDropCursor (std::string path, size_t index) -{ - CAnimation anim(path); - anim.load(index); - dragAndDropCursor(anim.getImage(index)); -} - -void CursorHandler::cursorMove(const int & x, const int & y) -{ - pos.x = x; - pos.y = y; - - cursor->setCursorPosition(pos); -} - -Point CursorHandler::getPivotOffsetDefault(size_t index) -{ - return {0, 0}; -} - -Point CursorHandler::getPivotOffsetMap(size_t index) -{ - static const std::array offsets = {{ - { 0, 0}, // POINTER = 0, - { 0, 0}, // HOURGLASS = 1, - { 12, 10}, // HERO = 2, - { 12, 12}, // TOWN = 3, - - { 15, 13}, // T1_MOVE = 4, - { 13, 13}, // T1_ATTACK = 5, - { 16, 32}, // T1_SAIL = 6, - { 13, 20}, // T1_DISEMBARK = 7, - { 8, 9}, // T1_EXCHANGE = 8, - { 14, 16}, // T1_VISIT = 9, - - { 15, 13}, // T2_MOVE = 10, - { 13, 13}, // T2_ATTACK = 11, - { 16, 32}, // T2_SAIL = 12, - { 13, 20}, // T2_DISEMBARK = 13, - { 8, 9}, // T2_EXCHANGE = 14, - { 14, 16}, // T2_VISIT = 15, - - { 15, 13}, // T3_MOVE = 16, - { 13, 13}, // T3_ATTACK = 17, - { 16, 32}, // T3_SAIL = 18, - { 13, 20}, // T3_DISEMBARK = 19, - { 8, 9}, // T3_EXCHANGE = 20, - { 14, 16}, // T3_VISIT = 21, - - { 15, 13}, // T4_MOVE = 22, - { 13, 13}, // T4_ATTACK = 23, - { 16, 32}, // T4_SAIL = 24, - { 13, 20}, // T4_DISEMBARK = 25, - { 8, 9}, // T4_EXCHANGE = 26, - { 14, 16}, // T4_VISIT = 27, - - { 16, 32}, // T1_SAIL_VISIT = 28, - { 16, 32}, // T2_SAIL_VISIT = 29, - { 16, 32}, // T3_SAIL_VISIT = 30, - { 16, 32}, // T4_SAIL_VISIT = 31, - - { 6, 1}, // SCROLL_NORTH = 32, - { 16, 2}, // SCROLL_NORTHEAST = 33, - { 21, 6}, // SCROLL_EAST = 34, - { 16, 16}, // SCROLL_SOUTHEAST = 35, - { 6, 21}, // SCROLL_SOUTH = 36, - { 1, 16}, // SCROLL_SOUTHWEST = 37, - { 1, 5}, // SCROLL_WEST = 38, - { 2, 1}, // SCROLL_NORTHWEST = 39, - - { 0, 0}, // POINTER_COPY = 40, - { 14, 16}, // TELEPORT = 41, - { 20, 20}, // SCUTTLE_BOAT = 42 - }}; - - assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index]; -} - -Point CursorHandler::getPivotOffsetCombat(size_t index) -{ - static const std::array offsets = {{ - { 12, 12 }, // BLOCKED = 0, - { 10, 14 }, // MOVE = 1, - { 14, 14 }, // FLY = 2, - { 12, 12 }, // SHOOT = 3, - { 12, 12 }, // HERO = 4, - { 8, 12 }, // QUERY = 5, - { 0, 0 }, // POINTER = 6, - { 21, 0 }, // HIT_NORTHEAST = 7, - { 31, 5 }, // HIT_EAST = 8, - { 21, 21 }, // HIT_SOUTHEAST = 9, - { 0, 21 }, // HIT_SOUTHWEST = 10, - { 0, 5 }, // HIT_WEST = 11, - { 0, 0 }, // HIT_NORTHWEST = 12, - { 6, 0 }, // HIT_NORTH = 13, - { 6, 31 }, // HIT_SOUTH = 14, - { 14, 0 }, // SHOOT_PENALTY = 15, - { 12, 12 }, // SHOOT_CATAPULT = 16, - { 12, 12 }, // HEAL = 17, - { 12, 12 }, // SACRIFICE = 18, - { 14, 20 }, // TELEPORT = 19 - }}; - - assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index]; -} - -Point CursorHandler::getPivotOffsetSpellcast() -{ - return { 18, 28}; -} - -Point CursorHandler::getPivotOffset() -{ - if (dndObject) - return dndObject->dimensions() / 2; - - switch (type) { - case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame); - case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame); - case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame); - case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast(); - }; - - assert(0); - return {0, 0}; -} - -std::shared_ptr CursorHandler::getCurrentImage() -{ - if (dndObject) - return dndObject; - - return cursors[static_cast(type)]->getImage(frame); -} - -void CursorHandler::updateSpellcastCursor() -{ - static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame - - frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f; - size_t newFrame = frame; - - while (frameTime >= frameDisplayDuration) - { - frameTime -= frameDisplayDuration; - newFrame++; - } - - auto & animation = cursors.at(static_cast(type)); - - while (newFrame >= animation->size()) - newFrame -= animation->size(); - - changeGraphic(Cursor::Type::SPELLBOOK, newFrame); -} - -void CursorHandler::render() -{ - if(!showing) - return; - - if (type == Cursor::Type::SPELLBOOK) - updateSpellcastCursor(); - - cursor->render(); -} - -void CursorHandler::hide() -{ - if (!showing) - return; - - showing = false; - cursor->setVisible(false); -} - -void CursorHandler::show() -{ - if (showing) - return; - - showing = true; - cursor->setVisible(true); -} - +/* + * CCursorHandler.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 "CursorHandler.h" + +#include "CGuiHandler.h" +#include "FramerateManager.h" +#include "../renderSDL/CursorSoftware.h" +#include "../renderSDL/CursorHardware.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../../lib/CConfigHandler.h" + +std::unique_ptr CursorHandler::createCursor() +{ +#if defined(VCMI_MOBILE) + if (settings["general"]["userRelativePointer"].Bool()) + return std::make_unique(); +#endif + + if (settings["video"]["cursor"].String() == "hardware") + return std::make_unique(); + + assert(settings["video"]["cursor"].String() == "software"); + return std::make_unique(); +} + +CursorHandler::CursorHandler() + : cursor(createCursor()) + , frameTime(0.f) + , showing(false) + , pos(0,0) +{ + + type = Cursor::Type::DEFAULT; + dndObject = nullptr; + + 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")) + }; + + for (auto & cursor : cursors) + cursor->preload(); + + set(Cursor::Map::POINTER); +} + +CursorHandler::~CursorHandler() = default; + +void CursorHandler::changeGraphic(Cursor::Type type, size_t index) +{ + assert(dndObject == nullptr); + + if (type == this->type && index == this->frame) + return; + + this->type = type; + this->frame = index; + + cursor->setImage(getCurrentImage(), getPivotOffset()); +} + +void CursorHandler::set(Cursor::Default index) +{ + changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); +} + +void CursorHandler::set(Cursor::Map index) +{ + changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); +} + +void CursorHandler::set(Cursor::Combat index) +{ + changeGraphic(Cursor::Type::COMBAT, static_cast(index)); +} + +void CursorHandler::set(Cursor::Spellcast index) +{ + //Note: this is animated cursor, ignore specified frame and only change type + changeGraphic(Cursor::Type::SPELLBOOK, frame); +} + +void CursorHandler::dragAndDropCursor(std::shared_ptr image) +{ + dndObject = image; + cursor->setImage(getCurrentImage(), getPivotOffset()); +} + +void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) +{ + auto anim = GH.renderHandler().loadAnimation(path); + anim->load(index); + dragAndDropCursor(anim->getImage(index)); +} + +void CursorHandler::cursorMove(const int & x, const int & y) +{ + pos.x = x; + pos.y = y; + + cursor->setCursorPosition(pos); +} + +Point CursorHandler::getPivotOffsetDefault(size_t index) +{ + return {0, 0}; +} + +Point CursorHandler::getPivotOffsetMap(size_t index) +{ + static const std::array offsets = {{ + { 0, 0}, // POINTER = 0, + { 0, 0}, // HOURGLASS = 1, + { 12, 10}, // HERO = 2, + { 12, 12}, // TOWN = 3, + + { 15, 13}, // T1_MOVE = 4, + { 13, 13}, // T1_ATTACK = 5, + { 16, 32}, // T1_SAIL = 6, + { 13, 20}, // T1_DISEMBARK = 7, + { 8, 9}, // T1_EXCHANGE = 8, + { 14, 16}, // T1_VISIT = 9, + + { 15, 13}, // T2_MOVE = 10, + { 13, 13}, // T2_ATTACK = 11, + { 16, 32}, // T2_SAIL = 12, + { 13, 20}, // T2_DISEMBARK = 13, + { 8, 9}, // T2_EXCHANGE = 14, + { 14, 16}, // T2_VISIT = 15, + + { 15, 13}, // T3_MOVE = 16, + { 13, 13}, // T3_ATTACK = 17, + { 16, 32}, // T3_SAIL = 18, + { 13, 20}, // T3_DISEMBARK = 19, + { 8, 9}, // T3_EXCHANGE = 20, + { 14, 16}, // T3_VISIT = 21, + + { 15, 13}, // T4_MOVE = 22, + { 13, 13}, // T4_ATTACK = 23, + { 16, 32}, // T4_SAIL = 24, + { 13, 20}, // T4_DISEMBARK = 25, + { 8, 9}, // T4_EXCHANGE = 26, + { 14, 16}, // T4_VISIT = 27, + + { 16, 32}, // T1_SAIL_VISIT = 28, + { 16, 32}, // T2_SAIL_VISIT = 29, + { 16, 32}, // T3_SAIL_VISIT = 30, + { 16, 32}, // T4_SAIL_VISIT = 31, + + { 6, 1}, // SCROLL_NORTH = 32, + { 16, 2}, // SCROLL_NORTHEAST = 33, + { 21, 6}, // SCROLL_EAST = 34, + { 16, 16}, // SCROLL_SOUTHEAST = 35, + { 6, 21}, // SCROLL_SOUTH = 36, + { 1, 16}, // SCROLL_SOUTHWEST = 37, + { 1, 5}, // SCROLL_WEST = 38, + { 2, 1}, // SCROLL_NORTHWEST = 39, + + { 0, 0}, // POINTER_COPY = 40, + { 14, 16}, // TELEPORT = 41, + { 20, 20}, // SCUTTLE_BOAT = 42 + }}; + + assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor + assert(index < offsets.size()); + return offsets[index]; +} + +Point CursorHandler::getPivotOffsetCombat(size_t index) +{ + static const std::array offsets = {{ + { 12, 12 }, // BLOCKED = 0, + { 10, 14 }, // MOVE = 1, + { 14, 14 }, // FLY = 2, + { 12, 12 }, // SHOOT = 3, + { 12, 12 }, // HERO = 4, + { 8, 12 }, // QUERY = 5, + { 0, 0 }, // POINTER = 6, + { 21, 0 }, // HIT_NORTHEAST = 7, + { 31, 5 }, // HIT_EAST = 8, + { 21, 21 }, // HIT_SOUTHEAST = 9, + { 0, 21 }, // HIT_SOUTHWEST = 10, + { 0, 5 }, // HIT_WEST = 11, + { 0, 0 }, // HIT_NORTHWEST = 12, + { 6, 0 }, // HIT_NORTH = 13, + { 6, 31 }, // HIT_SOUTH = 14, + { 14, 0 }, // SHOOT_PENALTY = 15, + { 12, 12 }, // SHOOT_CATAPULT = 16, + { 12, 12 }, // HEAL = 17, + { 12, 12 }, // SACRIFICE = 18, + { 14, 20 }, // TELEPORT = 19 + }}; + + assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor + assert(index < offsets.size()); + return offsets[index]; +} + +Point CursorHandler::getPivotOffsetSpellcast() +{ + return { 18, 28}; +} + +Point CursorHandler::getPivotOffset() +{ + if (dndObject) + return dndObject->dimensions() / 2; + + switch (type) { + case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame); + case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame); + case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame); + case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast(); + }; + + assert(0); + return {0, 0}; +} + +std::shared_ptr CursorHandler::getCurrentImage() +{ + if (dndObject) + return dndObject; + + return cursors[static_cast(type)]->getImage(frame); +} + +void CursorHandler::updateSpellcastCursor() +{ + static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame + + frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f; + size_t newFrame = frame; + + while (frameTime >= frameDisplayDuration) + { + frameTime -= frameDisplayDuration; + newFrame++; + } + + auto & animation = cursors.at(static_cast(type)); + + while (newFrame >= animation->size()) + newFrame -= animation->size(); + + changeGraphic(Cursor::Type::SPELLBOOK, newFrame); +} + +void CursorHandler::render() +{ + if(!showing) + return; + + if (type == Cursor::Type::SPELLBOOK) + updateSpellcastCursor(); + + cursor->render(); +} + +void CursorHandler::hide() +{ + if (!showing) + return; + + showing = false; + cursor->setVisible(false); +} + +void CursorHandler::show() +{ + if (showing) + return; + + showing = true; + cursor->setVisible(true); +} + diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 20ad2b337..b1f3d4a27 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -1,181 +1,182 @@ -/* - * CCursorHandler.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/Point.h" - -class ICursor; -class IImage; -class CAnimation; - -namespace Cursor -{ - enum class Type { - ADVENTURE, // set of various cursors for adventure map - COMBAT, // set of various cursors for combat - DEFAULT, // default arrow and hourglass cursors - SPELLBOOK // animated cursor for spellcasting - }; - - enum class Default { - POINTER = 0, - //ARROW_COPY = 1, // probably unused - HOURGLASS = 2, - }; - - enum class Combat { - BLOCKED = 0, - MOVE = 1, - FLY = 2, - SHOOT = 3, - HERO = 4, - QUERY = 5, - POINTER = 6, - HIT_NORTHEAST = 7, - HIT_EAST = 8, - HIT_SOUTHEAST = 9, - HIT_SOUTHWEST = 10, - HIT_WEST = 11, - HIT_NORTHWEST = 12, - HIT_NORTH = 13, - HIT_SOUTH = 14, - SHOOT_PENALTY = 15, - SHOOT_CATAPULT = 16, - HEAL = 17, - SACRIFICE = 18, - TELEPORT = 19, - - COUNT - }; - - enum class Map { - POINTER = 0, - HOURGLASS = 1, - HERO = 2, - TOWN = 3, - T1_MOVE = 4, - T1_ATTACK = 5, - T1_SAIL = 6, - T1_DISEMBARK = 7, - T1_EXCHANGE = 8, - T1_VISIT = 9, - T2_MOVE = 10, - T2_ATTACK = 11, - T2_SAIL = 12, - T2_DISEMBARK = 13, - T2_EXCHANGE = 14, - T2_VISIT = 15, - T3_MOVE = 16, - T3_ATTACK = 17, - T3_SAIL = 18, - T3_DISEMBARK = 19, - T3_EXCHANGE = 20, - T3_VISIT = 21, - T4_MOVE = 22, - T4_ATTACK = 23, - T4_SAIL = 24, - T4_DISEMBARK = 25, - T4_EXCHANGE = 26, - T4_VISIT = 27, - T1_SAIL_VISIT = 28, - T2_SAIL_VISIT = 29, - T3_SAIL_VISIT = 30, - T4_SAIL_VISIT = 31, - SCROLL_NORTH = 32, - SCROLL_NORTHEAST = 33, - SCROLL_EAST = 34, - SCROLL_SOUTHEAST = 35, - SCROLL_SOUTH = 36, - SCROLL_SOUTHWEST = 37, - SCROLL_WEST = 38, - SCROLL_NORTHWEST = 39, - //POINTER_COPY = 40, // probably unused - TELEPORT = 41, - SCUTTLE_BOAT = 42, - - COUNT - }; - - enum class Spellcast { - SPELL = 0, - }; -} - -/// handles mouse cursor -class CursorHandler final -{ - std::shared_ptr dndObject; //if set, overrides currentCursor - - std::array, 4> cursors; - - bool showing; - - /// Current cursor - Cursor::Type type; - size_t frame; - float frameTime; - Point pos; - - void changeGraphic(Cursor::Type type, size_t index); - - Point getPivotOffset(); - - void updateSpellcastCursor(); - - std::shared_ptr getCurrentImage(); - - std::unique_ptr cursor; - - static std::unique_ptr createCursor(); -public: - CursorHandler(); - ~CursorHandler(); - - /// Replaces the cursor with a custom image. - /// @param image Image to replace cursor with or nullptr to use the normal cursor. - void dragAndDropCursor(std::shared_ptr image); - - void dragAndDropCursor(std::string path, size_t index); - - /// Changes cursor to specified index - void set(Cursor::Default index); - void set(Cursor::Map index); - void set(Cursor::Combat index); - void set(Cursor::Spellcast index); - - /// Returns current index of cursor - template - std::optional get() - { - bool typeValid = true; - - typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; - typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; - - if (typeValid) - return static_cast(frame); - return std::nullopt; - } - - Point getPivotOffsetSpellcast(); - Point getPivotOffsetDefault(size_t index); - Point getPivotOffsetMap(size_t index); - Point getPivotOffsetCombat(size_t index); - - void render(); - - void hide(); - void show(); - - /// change cursor's positions to (x, y) - void cursorMove(const int & x, const int & y); -}; +/* + * CCursorHandler.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/Point.h" +#include "../../lib/filesystem/ResourcePath.h" + +class ICursor; +class IImage; +class CAnimation; + +namespace Cursor +{ + enum class Type { + ADVENTURE, // set of various cursors for adventure map + COMBAT, // set of various cursors for combat + DEFAULT, // default arrow and hourglass cursors + SPELLBOOK // animated cursor for spellcasting + }; + + enum class Default { + POINTER = 0, + //ARROW_COPY = 1, // probably unused + HOURGLASS = 2, + }; + + enum class Combat { + BLOCKED = 0, + MOVE = 1, + FLY = 2, + SHOOT = 3, + HERO = 4, + QUERY = 5, + POINTER = 6, + HIT_NORTHEAST = 7, + HIT_EAST = 8, + HIT_SOUTHEAST = 9, + HIT_SOUTHWEST = 10, + HIT_WEST = 11, + HIT_NORTHWEST = 12, + HIT_NORTH = 13, + HIT_SOUTH = 14, + SHOOT_PENALTY = 15, + SHOOT_CATAPULT = 16, + HEAL = 17, + SACRIFICE = 18, + TELEPORT = 19, + + COUNT + }; + + enum class Map { + POINTER = 0, + HOURGLASS = 1, + HERO = 2, + TOWN = 3, + T1_MOVE = 4, + T1_ATTACK = 5, + T1_SAIL = 6, + T1_DISEMBARK = 7, + T1_EXCHANGE = 8, + T1_VISIT = 9, + T2_MOVE = 10, + T2_ATTACK = 11, + T2_SAIL = 12, + T2_DISEMBARK = 13, + T2_EXCHANGE = 14, + T2_VISIT = 15, + T3_MOVE = 16, + T3_ATTACK = 17, + T3_SAIL = 18, + T3_DISEMBARK = 19, + T3_EXCHANGE = 20, + T3_VISIT = 21, + T4_MOVE = 22, + T4_ATTACK = 23, + T4_SAIL = 24, + T4_DISEMBARK = 25, + T4_EXCHANGE = 26, + T4_VISIT = 27, + T1_SAIL_VISIT = 28, + T2_SAIL_VISIT = 29, + T3_SAIL_VISIT = 30, + T4_SAIL_VISIT = 31, + SCROLL_NORTH = 32, + SCROLL_NORTHEAST = 33, + SCROLL_EAST = 34, + SCROLL_SOUTHEAST = 35, + SCROLL_SOUTH = 36, + SCROLL_SOUTHWEST = 37, + SCROLL_WEST = 38, + SCROLL_NORTHWEST = 39, + //POINTER_COPY = 40, // probably unused + TELEPORT = 41, + SCUTTLE_BOAT = 42, + + COUNT + }; + + enum class Spellcast { + SPELL = 0, + }; +} + +/// handles mouse cursor +class CursorHandler final +{ + std::shared_ptr dndObject; //if set, overrides currentCursor + + std::array, 4> cursors; + + bool showing; + + /// Current cursor + Cursor::Type type; + size_t frame; + float frameTime; + Point pos; + + void changeGraphic(Cursor::Type type, size_t index); + + Point getPivotOffset(); + + void updateSpellcastCursor(); + + std::shared_ptr getCurrentImage(); + + std::unique_ptr cursor; + + static std::unique_ptr createCursor(); +public: + CursorHandler(); + ~CursorHandler(); + + /// Replaces the cursor with a custom image. + /// @param image Image to replace cursor with or nullptr to use the normal cursor. + void dragAndDropCursor(std::shared_ptr image); + + void dragAndDropCursor(const AnimationPath & path, size_t index); + + /// Changes cursor to specified index + void set(Cursor::Default index); + void set(Cursor::Map index); + void set(Cursor::Combat index); + void set(Cursor::Spellcast index); + + /// Returns current index of cursor + template + std::optional get() + { + bool typeValid = true; + + typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; + typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; + + if (typeValid) + return static_cast(frame); + return std::nullopt; + } + + Point getPivotOffsetSpellcast(); + Point getPivotOffsetDefault(size_t index); + Point getPivotOffsetMap(size_t index); + Point getPivotOffsetCombat(size_t index); + + void render(); + + void hide(); + void show(); + + /// 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 36e5822f2..4c9dc9726 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -116,25 +116,9 @@ void EventDispatcher::dispatchShortcutReleased(const std::vector & sh } } -void EventDispatcher::dispatchMouseDoubleClick(const Point & position) +void EventDispatcher::dispatchMouseDoubleClick(const Point & position, int tolerance) { - bool doubleClicked = false; - auto hlp = doubleClickInterested; - - for(auto & i : hlp) - { - if(!vstd::contains(doubleClickInterested, i)) - continue; - - if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK)) - { - i->clickDouble(position); - doubleClicked = true; - } - } - - if(!doubleClicked) - handleLeftButtonClick(position, 0, true); + handleDoubleButtonClick(position, tolerance); } void EventDispatcher::dispatchMouseLeftButtonPressed(const Point & position, int tolerance) @@ -219,6 +203,7 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc // POSSIBLE SOLUTION: make EventReceivers inherit from create_shared_from this and store weak_ptr's in lists AEventsReceiver * nearestElement = findElementInToleranceRange(lclickable, position, AEventsReceiver::LCLICK, tolerance); auto hlp = lclickable; + bool lastActivated = true; for(auto & i : hlp) { @@ -228,12 +213,13 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement) { if(isPressed) - i->clickPressed(position); + i->clickPressed(position, lastActivated); if (i->mouseClickedState && !isPressed) - i->clickReleased(position); + i->clickReleased(position, lastActivated); i->mouseClickedState = isPressed; + lastActivated = false; } else { @@ -246,6 +232,38 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc } } +void EventDispatcher::handleDoubleButtonClick(const Point & position, int tolerance) +{ + // WARNING: this approach is NOT SAFE + // 1) We allow (un)registering elements when list itself is being processed/iterated + // 2) To avoid iterator invalidation we create a copy of this list for processing + // HOWEVER it is completely possible (as in, actually happen, no just theory) to: + // 1) element gets unregistered and deleted from lclickable + // 2) element is completely deleted, as in - destructor called, memory freed + // 3) new element is created *with exactly same address(!) + // 4) new element is registered and code will incorrectly assume that this element is still registered + // POSSIBLE SOLUTION: make EventReceivers inherit from create_shared_from this and store weak_ptr's in lists + + AEventsReceiver * nearestElement = findElementInToleranceRange(doubleClickInterested, position, AEventsReceiver::DOUBLECLICK, tolerance); + bool doubleClicked = false; + auto hlp = doubleClickInterested; + + for(auto & i : hlp) + { + if(!vstd::contains(doubleClickInterested, i)) + continue; + + if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK) || i == nearestElement) + { + i->clickDouble(position); + doubleClicked = true; + } + } + + if(!doubleClicked) + handleLeftButtonClick(position, tolerance, true); +} + void EventDispatcher::dispatchMouseScrolled(const Point & distance, const Point & position) { EventReceiversList hlp = wheelInterested; diff --git a/client/gui/EventDispatcher.h b/client/gui/EventDispatcher.h index dee3d77cd..0ab25ed5c 100644 --- a/client/gui/EventDispatcher.h +++ b/client/gui/EventDispatcher.h @@ -36,6 +36,7 @@ class EventDispatcher EventReceiversList panningInterested; void handleLeftButtonClick(const Point & position, int tolerance, bool isPressed); + void handleDoubleButtonClick(const Point & position, int tolerance); AEventsReceiver * findElementInToleranceRange(const EventReceiversList & list, const Point & position, int eventToTest, int tolerance); template @@ -59,7 +60,7 @@ public: void dispatchMouseLeftButtonPressed(const Point & position, int tolerance); void dispatchMouseLeftButtonReleased(const Point & position, int tolerance); void dispatchMouseScrolled(const Point & distance, const Point & position); - void dispatchMouseDoubleClick(const Point & position); + void dispatchMouseDoubleClick(const Point & position, int tolerance); void dispatchMouseMoved(const Point & distance, const Point & position); void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance); diff --git a/client/gui/EventsReceiver.cpp b/client/gui/EventsReceiver.cpp index 2dbed3a33..d19447add 100644 --- a/client/gui/EventsReceiver.cpp +++ b/client/gui/EventsReceiver.cpp @@ -68,3 +68,13 @@ void AEventsReceiver::deactivateEvents(ui16 what) // if (!(activeState & HOVER)) // hoveredState = false; } + +void AEventsReceiver::clickPressed(const Point & cursorPosition, bool lastActivated) +{ + clickPressed(cursorPosition); +} + +void AEventsReceiver::clickReleased(const Point & cursorPosition, bool lastActivated) +{ + clickReleased(cursorPosition); +} diff --git a/client/gui/EventsReceiver.h b/client/gui/EventsReceiver.h index 2206c70bf..2d5fedede 100644 --- a/client/gui/EventsReceiver.h +++ b/client/gui/EventsReceiver.h @@ -45,6 +45,8 @@ protected: public: virtual void clickPressed(const Point & cursorPosition) {} virtual void clickReleased(const Point & cursorPosition) {} + virtual void clickPressed(const Point & cursorPosition, bool lastActivated); + virtual void clickReleased(const Point & cursorPosition, bool lastActivated); virtual void clickCancel(const Point & cursorPosition) {} virtual void showPopupWindow(const Point & cursorPosition) {} virtual void clickDouble(const Point & cursorPosition) {} diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 59aa653e7..cc6b7d64c 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -11,11 +11,15 @@ #include "StdInc.h" #include "FramerateManager.h" +#include "../../lib/CConfigHandler.h" +#include + FramerateManager::FramerateManager(int targetFrameRate) : targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate) , lastFrameIndex(0) , lastFrameTimes({}) - , lastTimePoint (Clock::now()) + , lastTimePoint(Clock::now()) + , vsyncEnabled(settings["video"]["vsync"].Bool()) { boost::range::fill(lastFrameTimes, targetFrameTime); } @@ -24,9 +28,11 @@ void FramerateManager::framerateDelay() { Duration timeSpentBusy = Clock::now() - lastTimePoint; - // FPS is higher than it should be, then wait some time - if(timeSpentBusy < targetFrameTime) + if(!vsyncEnabled && timeSpentBusy < targetFrameTime) + { + // if FPS is higher than it should be, then wait some time boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + } // compute actual timeElapsed taking into account actual sleep interval // limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) diff --git a/client/gui/FramerateManager.h b/client/gui/FramerateManager.h index 818e55f94..d653bd667 100644 --- a/client/gui/FramerateManager.h +++ b/client/gui/FramerateManager.h @@ -25,6 +25,8 @@ class FramerateManager /// index of last measured frome in lastFrameTimes array ui32 lastFrameIndex; + bool vsyncEnabled; + public: FramerateManager(int targetFramerate); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index aba2998bc..589b45fe4 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -17,7 +17,10 @@ #include "../gui/CGuiHandler.h" #include "../gui/ShortcutHandler.h" #include "../gui/Shortcut.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" @@ -26,8 +29,9 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../../lib//constants/StringConstants.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset): InterfaceObjectConfigurable(used, offset) @@ -43,12 +47,17 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("texture", &InterfaceObjectConfigurable::buildTexture); REGISTER_BUILDER("animation", &InterfaceObjectConfigurable::buildAnimation); REGISTER_BUILDER("label", &InterfaceObjectConfigurable::buildLabel); + REGISTER_BUILDER("multiLineLabel", &InterfaceObjectConfigurable::buildMultiLineLabel); REGISTER_BUILDER("toggleGroup", &InterfaceObjectConfigurable::buildToggleGroup); REGISTER_BUILDER("toggleButton", &InterfaceObjectConfigurable::buildToggleButton); REGISTER_BUILDER("button", &InterfaceObjectConfigurable::buildButton); REGISTER_BUILDER("labelGroup", &InterfaceObjectConfigurable::buildLabelGroup); REGISTER_BUILDER("slider", &InterfaceObjectConfigurable::buildSlider); REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout); + REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox); + REGISTER_BUILDER("textInput", &InterfaceObjectConfigurable::buildTextInput); + REGISTER_BUILDER("transparentFilledRectangle", &InterfaceObjectConfigurable::buildTransparentFilledRectangle); + REGISTER_BUILDER("textBox", &InterfaceObjectConfigurable::buildTextBox); } void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f) @@ -58,9 +67,15 @@ void InterfaceObjectConfigurable::registerBuilder(const std::string & type, Buil void InterfaceObjectConfigurable::addCallback(const std::string & callbackName, std::function callback) { - callbacks[callbackName] = callback; + callbacks_int[callbackName] = callback; } +void InterfaceObjectConfigurable::addCallback(const std::string & callbackName, std::function callback) +{ + callbacks_string[callbackName] = callback; +} + + void InterfaceObjectConfigurable::deleteWidget(const std::string & name) { auto iter = widgets.find(name); @@ -97,7 +112,7 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) { if (!config["library"].isNull()) { - const JsonNode library(ResourceID(config["library"].String())); + const JsonNode library(JsonPath::fromJson(config["library"])); loadCustomBuilders(library); } @@ -145,6 +160,8 @@ std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const return ""; std::string s = config.String(); + if(s.empty()) + return s; logGlobal->debug("Reading text from translations by key: %s", s); return CGI->generaltexth->translate(s); } @@ -181,32 +198,54 @@ ETextAlignment InterfaceObjectConfigurable::readTextAlignment(const JsonNode & c if(config.String() == "right") return ETextAlignment::BOTTOMRIGHT; } - logGlobal->debug("Uknown text alignment attribute"); + logGlobal->debug("Unknown text alignment attribute"); return ETextAlignment::CENTER; } -SDL_Color InterfaceObjectConfigurable::readColor(const JsonNode & config) const +ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const { logGlobal->debug("Reading color"); if(!config.isNull()) { - if(config.String() == "yellow") - return Colors::YELLOW; - if(config.String() == "white") - return Colors::WHITE; - if(config.String() == "gold") - return Colors::METALLIC_GOLD; - if(config.String() == "green") - return Colors::GREEN; - if(config.String() == "orange") - return Colors::ORANGE; - if(config.String() == "bright-yellow") - return Colors::BRIGHT_YELLOW; + if(config.isString()) + { + if(config.String() == "yellow") + return Colors::YELLOW; + if(config.String() == "white") + return Colors::WHITE; + if(config.String() == "gold") + return Colors::METALLIC_GOLD; + if(config.String() == "green") + return Colors::GREEN; + if(config.String() == "orange") + return Colors::ORANGE; + if(config.String() == "bright-yellow") + return Colors::BRIGHT_YELLOW; + } + if(config.isVector()) + { + const auto & asVector = config.Vector(); + if(asVector.size() == 4) + return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer(), asVector[3].Integer()); + if(asVector.size() == 3) + return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer()); + } } - logGlobal->debug("Uknown color attribute"); + logGlobal->debug("Unknown color attribute"); return Colors::DEFAULT_KEY_COLOR; - + } + +PlayerColor InterfaceObjectConfigurable::readPlayerColor(const JsonNode & config) const +{ + logGlobal->debug("Reading PlayerColor"); + if(!config.isNull() && config.isString()) + return PlayerColor(PlayerColor::decode(config.String())); + + logGlobal->debug("Unknown PlayerColor attribute"); + return PlayerColor::CANNOT_DETERMINE; +} + EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const { logGlobal->debug("Reading font"); @@ -223,7 +262,7 @@ EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const if(config.String() == "calisto") return EFonts::FONT_CALLI; } - logGlobal->debug("Uknown font attribute"); + logGlobal->debug("Unknown font attribute"); return EFonts::FONT_TIMES; } @@ -268,11 +307,9 @@ EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const { logGlobal->debug("Building widget CPicture"); - auto image = config["image"].String(); + auto image = ImagePath::fromJson(config["image"]); auto position = readPosition(config["position"]); auto pic = std::make_shared(image, position.x, position.y); - if(!config["visible"].isNull()) - pic->visible = config["visible"].Bool(); if ( config["playerColored"].Bool() && LOCPLINT) pic->colorize(LOCPLINT->playerID); @@ -290,6 +327,20 @@ std::shared_ptr InterfaceObjectConfigurable::buildLabel(const JsonNode & return std::make_shared(position.x, position.y, font, alignment, color, text); } +std::shared_ptr InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const +{ + logGlobal->debug("Building widget CMultiLineLabel"); + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + auto text = readText(config["text"]); + Rect rect = readRect(config["rect"]); + if(!config["adoptHeight"].isNull() && config["adoptHeight"].Bool()) + rect.h = graphics->fonts[font]->getLineHeight() * 2; + return std::make_shared(rect, font, alignment, color, text); +} + + std::shared_ptr InterfaceObjectConfigurable::buildToggleGroup(const JsonNode & config) const { logGlobal->debug("Building widget CToggleGroup"); @@ -310,7 +361,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleGroup(cons if(!config["selected"].isNull()) group->setSelected(config["selected"].Integer()); if(!config["callback"].isNull()) - group->addCallback(callbacks.at(config["callback"].String())); + group->addCallback(callbacks_int.at(config["callback"].String())); return group; } @@ -318,7 +369,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleButton(co { logGlobal->debug("Building widget CToggleButton"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); auto button = std::make_shared(position, image, help); if(!config["items"].isNull()) @@ -344,7 +395,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildButton(const JsonNode { logGlobal->debug("Building widget CButton"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); auto button = std::make_shared(position, image, help); if(!config["items"].isNull()) @@ -383,8 +434,8 @@ void InterfaceObjectConfigurable::loadToggleButtonCallback(std::shared_ptr 0) - button->addCallback(callbacks.at(callbackName)); + if (callbacks_int.count(callbackName) > 0) + button->addCallback(callbacks_int.at(callbackName)); else logGlobal->error("Invalid callback '%s' in widget", callbackName ); } @@ -396,8 +447,8 @@ void InterfaceObjectConfigurable::loadButtonCallback(std::shared_ptr bu std::string callbackName = config.String(); - if (callbacks.count(callbackName) > 0) - button->addCallback(std::bind(callbacks.at(callbackName), 0)); + if (callbacks_int.count(callbackName) > 0) + button->addCallback(std::bind(callbacks_int.at(callbackName), 0)); else logGlobal->error("Invalid callback '%s' in widget", callbackName ); } @@ -448,18 +499,34 @@ std::shared_ptr InterfaceObjectConfigurable::buildSlider(const JsonNode auto position = readPosition(config["position"]); int length = config["size"].Integer(); auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE; - auto itemsVisible = config["itemsVisible"].Integer(); - auto itemsTotal = config["itemsTotal"].Integer(); auto value = config["selected"].Integer(); bool horizontal = config["orientation"].String() == "horizontal"; - const auto & result = - std::make_shared(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); + auto orientation = horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL; - if (!config["scrollBounds"].isNull()) + std::shared_ptr result; + + if (config["items"].isNull()) + { + auto itemsVisible = config["itemsVisible"].Integer(); + auto itemsTotal = config["itemsTotal"].Integer(); + + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, orientation, style); + } + else + { + auto items = config["items"].convertTo>(); + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), items, value, orientation, style); + } + + + if(!config["scrollBounds"].isNull()) { Rect bounds = readRect(config["scrollBounds"]); result->setScrollBounds(bounds); } + + if(!config["panningStep"].isNull()) + result->setPanningStep(config["panningStep"].Integer()); return result; } @@ -468,7 +535,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildImage(const JsonNo { logGlobal->debug("Building widget CAnimImage"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); int group = config["group"].isNull() ? 0 : config["group"].Integer(); int frame = config["frame"].isNull() ? 0 : config["frame"].Integer(); return std::make_shared(image, frame, group, position.x, position.y); @@ -477,11 +544,68 @@ std::shared_ptr InterfaceObjectConfigurable::buildImage(const JsonNo std::shared_ptr InterfaceObjectConfigurable::buildTexture(const JsonNode & config) const { logGlobal->debug("Building widget CFilledTexture"); - auto image = config["image"].String(); + 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); + return result; + } return std::make_shared(image, rect); } +std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonNode & config) +{ + logGlobal->debug("Building widget ComboBox"); + auto position = readPosition(config["position"]); + auto dropDownPosition = readPosition(config["dropDownPosition"]); + auto image = AnimationPath::fromJson(config["image"]); + auto help = readHintText(config["help"]); + auto result = std::make_shared(position, image, help, config["dropDown"], dropDownPosition); + + if(!config["items"].isNull()) + { + for(const auto & item : config["items"].Vector()) + { + result->addOverlay(buildWidget(item)); + } + } + if(!config["imageOrder"].isNull()) + { + auto imgOrder = config["imageOrder"].Vector(); + assert(imgOrder.size() >= 4); + result->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer()); + } + + loadButtonBorderColor(result, config["borderColor"]); + loadButtonHotkey(result, config["hotkey"]); + return result; +} + +std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const JsonNode & config) const +{ + logGlobal->debug("Building widget CTextInput"); + auto rect = readRect(config["rect"]); + auto offset = readPosition(config["backgroundOffset"]); + auto bgName = ImagePath::fromJson(config["background"]); + auto result = std::make_shared(rect, offset, bgName, 0); + if(!config["alignment"].isNull()) + result->alignment = readTextAlignment(config["alignment"]); + if(!config["font"].isNull()) + result->font = readFont(config["font"]); + if(!config["color"].isNull()) + result->setColor(readColor(config["color"])); + if(!config["text"].isNull() && config["text"].isString()) + result->setText(config["text"].String()); //for input field raw string is taken + if(!config["callback"].isNull()) + result->cb += callbacks_string.at(config["callback"].String()); + if(!config["help"].isNull()) + result->setHelpText(readText(config["help"])); + return result; +} + /// Small helper class that provides ownership for shared_ptr's of child elements class InterfaceLayoutWidget : public CIntObject { @@ -556,7 +680,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const { logGlobal->debug("Building widget CShowableAnim"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); ui8 flags = 0; if(!config["repeat"].Bool()) flags |= CShowableAnim::EFlags::PLAY_ONCE; @@ -566,7 +690,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const if(!config["alpha"].isNull()) anim->setAlpha(config["alpha"].Integer()); if(!config["callback"].isNull()) - anim->callback = std::bind(callbacks.at(config["callback"].String()), 0); + anim->callback = std::bind(callbacks_int.at(config["callback"].String()), 0); if(!config["frames"].isNull()) { auto b = config["frames"]["start"].Integer(); @@ -576,6 +700,33 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const return anim; } +std::shared_ptr InterfaceObjectConfigurable::buildTransparentFilledRectangle(const JsonNode & config) const +{ + logGlobal->debug("Building widget TransparentFilledRectangle"); + + auto rect = readRect(config["rect"]); + auto color = readColor(config["color"]); + if(!config["colorLine"].isNull()) + { + auto colorLine = readColor(config["colorLine"]); + return std::make_shared(rect, color, colorLine); + } + return std::make_shared(rect, color); +} + +std::shared_ptr InterfaceObjectConfigurable::buildTextBox(const JsonNode & config) const +{ + logGlobal->debug("Building widget CTextBox"); + + auto rect = readRect(config["rect"]); + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + auto text = readText(config["text"]); + + return std::make_shared(text, rect, 0, font, alignment, color); +} + std::shared_ptr InterfaceObjectConfigurable::buildWidget(JsonNode config) const { assert(!config.isNull()); @@ -583,7 +734,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildWidget(JsonNode co //overrides from variables for(auto & item : config["overrides"].Struct()) { - logGlobal->debug("Config attribute %s was overriden by variable %s", item.first, item.second.String()); + logGlobal->debug("Config attribute %s was overridden by variable %s", item.first, item.second.String()); config[item.first] = variables[item.second.String()]; } diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 790f1709c..cc812299e 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -12,11 +12,13 @@ #include "CIntObject.h" #include "TextAlignment.h" +#include "../render/EFont.h" #include "../../lib/JsonNode.h" class CPicture; class CLabel; +class CMultiLineLabel; class CToggleGroup; class CToggleButton; class CButton; @@ -25,6 +27,10 @@ class CSlider; class CAnimImage; class CShowableAnim; class CFilledTexture; +class ComboBox; +class CTextInput; +class TransparentFilledRectangle; +class CTextBox; #define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1)) @@ -56,6 +62,7 @@ protected: void addWidget(const std::string & name, std::shared_ptr widget); void addCallback(const std::string & callbackName, std::function callback); + void addCallback(const std::string & callbackName, std::function callback); JsonNode variables; template @@ -73,11 +80,12 @@ protected: Point readPosition(const JsonNode &) const; Rect readRect(const JsonNode &) const; ETextAlignment readTextAlignment(const JsonNode &) const; - SDL_Color readColor(const JsonNode &) const; + ColorRGBA readColor(const JsonNode &) const; EFonts readFont(const JsonNode &) const; std::string readText(const JsonNode &) const; std::pair readHintText(const JsonNode &) const; EShortcut readHotkey(const JsonNode &) const; + PlayerColor readPlayerColor(const JsonNode &) const; void loadToggleButtonCallback(std::shared_ptr button, const JsonNode & config) const; void loadButtonCallback(std::shared_ptr button, const JsonNode & config) const; @@ -87,6 +95,7 @@ protected: //basic widgets std::shared_ptr buildPicture(const JsonNode &) const; std::shared_ptr buildLabel(const JsonNode &) const; + std::shared_ptr buildMultiLineLabel(const JsonNode &) const; std::shared_ptr buildToggleGroup(const JsonNode &) const; std::shared_ptr buildToggleButton(const JsonNode &) const; std::shared_ptr buildButton(const JsonNode &) const; @@ -96,6 +105,10 @@ protected: std::shared_ptr buildAnimation(const JsonNode &) const; std::shared_ptr buildTexture(const JsonNode &) const; std::shared_ptr buildLayout(const JsonNode &); + std::shared_ptr buildComboBox(const JsonNode &); + std::shared_ptr buildTextInput(const JsonNode &) const; + std::shared_ptr buildTransparentFilledRectangle(const JsonNode & config) const; + std::shared_ptr buildTextBox(const JsonNode & config) const; //composite widgets std::shared_ptr buildWidget(JsonNode config) const; @@ -111,7 +124,8 @@ private: int unnamedObjectId = 0; std::map builders; std::map> widgets; - std::map> callbacks; + std::map> callbacks_int; + std::map> callbacks_string; std::map conditionals; std::map shortcuts; }; diff --git a/client/gui/MouseButton.h b/client/gui/MouseButton.h index 74ca41807..84c8ea188 100644 --- a/client/gui/MouseButton.h +++ b/client/gui/MouseButton.h @@ -1,19 +1,19 @@ -/* - * MouseButton.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 - -enum class MouseButton -{ - LEFT = 1, - MIDDLE = 2, - RIGHT = 3, - EXTRA1 = 4, - EXTRA2 = 5 -}; +/* + * MouseButton.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 + +enum class MouseButton +{ + LEFT = 1, + MIDDLE = 2, + RIGHT = 3, + EXTRA1 = 4, + EXTRA2 = 5 +}; diff --git a/client/gui/TextAlignment.h b/client/gui/TextAlignment.h index 5c716ba31..ec74cc5a6 100644 --- a/client/gui/TextAlignment.h +++ b/client/gui/TextAlignment.h @@ -1,12 +1,12 @@ -/* - * TextAlignment.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 - -enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT}; +/* + * TextAlignment.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 + +enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT}; diff --git a/client/gui/WindowHandler.cpp b/client/gui/WindowHandler.cpp index 690372589..522155d19 100644 --- a/client/gui/WindowHandler.cpp +++ b/client/gui/WindowHandler.cpp @@ -105,7 +105,6 @@ void WindowHandler::totalRedraw() void WindowHandler::totalRedrawImpl() { logGlobal->debug("totalRedraw requested!"); - CSDL_Ext::fillSurface(screen2, Colors::BLACK); Canvas target = Canvas::createFromSurface(screen2); diff --git a/client/icons/generate_icns.py b/client/icons/generate_icns.py old mode 100644 new mode 100755 index 25d351afb..4b1defbba --- a/client/icons/generate_icns.py +++ b/client/icons/generate_icns.py @@ -3,7 +3,7 @@ import os, sys, shutil img = Image.open(sys.argv[1]) if img.size != (1024,1024): - print "Input image must be 1024x1024. Provided image is %dx%d" % img.size + print("Input image must be 1024x1024. Provided image is %dx%d" % img.size) os.mkdir("vcmi.iconset") for i in [16, 32, 128, 256, 512]: diff --git a/client/ios/startSDL.mm b/client/ios/startSDL.mm index 24ad9a888..0b3b8028e 100644 --- a/client/ios/startSDL.mm +++ b/client/ios/startSDL.mm @@ -8,6 +8,7 @@ * */ #import "startSDL.h" +#include "StdInc.h" #import "GameChatKeyboardHandler.h" #include "../Global.h" diff --git a/client/ios/utils.mm b/client/ios/utils.mm index 42cca084e..2a04be7b3 100644 --- a/client/ios/utils.mm +++ b/client/ios/utils.mm @@ -8,6 +8,7 @@ * */ +#include "StdInc.h" #include "utils.h" #import diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 18eaa66d0..ed572d72d 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -31,7 +31,9 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/CAnimation.h" +#include "../render/Graphics.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" @@ -64,23 +66,22 @@ CBonusSelection::CBonusSelection() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - std::string bgName = getCampaign()->getRegions().getBackgroundName(); - setBackground(bgName); + setBackground(getCampaign()->getRegions().getBackgroundName()); - panelBackground = std::make_shared("CAMPBRF.BMP", 456, 6); + panelBackground = std::make_shared(ImagePath::builtin("CAMPBRF.BMP"), 456, 6); - buttonStart = std::make_shared(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT); - buttonRestart = std::make_shared(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT); - buttonBack = std::make_shared(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL); + buttonStart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT); + buttonRestart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT); + 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()); - iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 735, 26); + 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()->getDescription(), Rect(480, 86, 286, 117), 1); + 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->getName()); + mapName = std::make_shared(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated()); 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); @@ -96,13 +97,13 @@ CBonusSelection::CBonusSelection() for(size_t b = 0; b < difficultyIcons.size(); ++b) { - difficultyIcons[b] = std::make_shared("GSPBUT" + std::to_string(b + 3) + ".DEF", 0, 0, 709, 455); + difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, 455); } if(getCampaign()->playerSelectedDifficulty()) { - buttonDifficultyLeft = std::make_shared(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); - buttonDifficultyRight = std::make_shared(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); + buttonDifficultyLeft = std::make_shared(Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); + buttonDifficultyRight = std::make_shared(Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); } for(auto scenarioID : getCampaign()->allScenarios()) @@ -114,7 +115,7 @@ CBonusSelection::CBonusSelection() } if (!getCampaign()->getMusic().empty()) - CCS->musich->playMusic( "Music/" + getCampaign()->getMusic(), true, false); + CCS->musich->playMusic( getCampaign()->getMusic(), true, false); } void CBonusSelection::createBonusesIcons() @@ -145,22 +146,22 @@ void CBonusSelection::createBonusesIcons() std::string picName = bonusPics[bonusType]; size_t picNumber = bonDescs[i].info2; - std::string desc; + MetaString desc; switch(bonDescs[i].type) { case CampaignBonusType::SPELL: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); + desc.replaceName(SpellID(bonDescs[i].info2)); break; case CampaignBonusType::MONSTER: picNumber = bonDescs[i].info2 + 2; - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info3)); - boost::algorithm::replace_first(desc, "%s", CGI->creatures()->getByIndex(bonDescs[i].info2)->getNamePluralTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + desc.replaceNumber(bonDescs[i].info3); + desc.replaceNamePlural(bonDescs[i].info2); break; case CampaignBonusType::BUILDING: { - int faction = -1; + FactionID faction; for(auto & elem : CSH->si->playerInfos) { if(elem.second.isControlledByHuman()) @@ -181,17 +182,16 @@ void CBonusSelection::createBonusesIcons() picNumber = -1; if(vstd::contains((*CGI->townh)[faction]->town->buildings, buildID)) - desc = (*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTranslated(); - + desc.appendTextID((*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTextID()); break; } case CampaignBonusType::ARTIFACT: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->artifacts()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::SPELL_SCROLL: - desc = CGI->generaltexth->allTexts[716]; - boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 716); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::PRIMARY_SKILL: { @@ -210,7 +210,7 @@ void CBonusSelection::createBonusesIcons() } } picNumber = leadingSkill; - desc = CGI->generaltexth->allTexts[715]; + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); std::string substitute; //text to be printed instead of %s for(int v = 0; v < toPrint.size(); ++v) @@ -223,52 +223,42 @@ void CBonusSelection::createBonusesIcons() } } - boost::algorithm::replace_first(desc, "%s", substitute); + desc.replaceRawString(substitute); break; } case CampaignBonusType::SECONDARY_SKILL: - desc = CGI->generaltexth->allTexts[718]; - - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level - boost::algorithm::replace_first(desc, "%s", CGI->skillh->getByIndex(bonDescs[i].info2)->getNameTranslated()); //skill name + desc.appendLocalString(EMetaText::GENERAL_TXT, 718); + desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get()); + desc.replaceName(SecondarySkill(bonDescs[i].info2)); picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; break; case CampaignBonusType::RESOURCE: { - int serialResID = 0; + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + switch(bonDescs[i].info1) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - serialResID = bonDescs[i].info1; - break; - case 0xFD: //wood + ore - serialResID = 7; - break; - case 0xFE: //rare resources - serialResID = 8; - break; + case 0xFD: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 721); + picNumber = 7; + break; + } + case 0xFE: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 722); + picNumber = 8; + break; + } + default: + { + desc.replaceName(GameResID(bonDescs[i].info1)); + picNumber = bonDescs[i].info1; + } } - picNumber = serialResID; - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info2)); - std::string replacement; - if(serialResID <= 6) - { - replacement = CGI->generaltexth->restypes[serialResID]; - } - else - { - replacement = CGI->generaltexth->allTexts[714 + serialResID]; - } - boost::algorithm::replace_first(desc, "%s", replacement); + desc.replaceNumber(bonDescs[i].info2); break; } case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO: @@ -276,37 +266,35 @@ void CBonusSelection::createBonusesIcons() auto superhero = getCampaign()->strongestHero(static_cast(bonDescs[i].info2), PlayerColor(bonDescs[i].info1)); if(!superhero) logGlobal->warn("No superhero! How could it be transferred?"); - picNumber = superhero ? superhero->portrait : 0; - desc = CGI->generaltexth->allTexts[719]; - - boost::algorithm::replace_first(desc, "%s", getCampaign()->scenario(static_cast(bonDescs[i].info2)).scenarioName); + picNumber = superhero ? superhero->getIconIndex() : 0; + desc.appendLocalString(EMetaText::GENERAL_TXT, 719); + desc.replaceRawString(getCampaign()->scenario(static_cast(bonDescs[i].info2)).scenarioName.toString()); break; } case CampaignBonusType::HERO: - desc = CGI->generaltexth->allTexts[718]; - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color - + desc.appendLocalString(EMetaText::GENERAL_TXT, 718); + desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get()); if(bonDescs[i].info2 == 0xFFFF) { - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name + desc.replaceLocalString(EMetaText::GENERAL_TXT, 101); picNumber = -1; picName = "CBONN1A3.BMP"; } else { - boost::algorithm::replace_first(desc, "%s", CGI->heroh->objects[bonDescs[i].info2]->getNameTranslated()); + desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID()); } break; } - std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), "", CButton::tooltip(desc, desc)); + std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc.toString(), desc.toString())); if(picNumber != -1) picName += ":" + std::to_string(picNumber); - auto anim = std::make_shared(); + auto anim = GH.renderHandler().createAnimation(); anim->setCustom(picName, 0); bonusButton->setImage(anim); if(CSH->campaignBonus == i) @@ -354,8 +342,8 @@ void CBonusSelection::updateAfterStateChange() if(!CSH->mi) return; iconsMapSizes->setFrame(CSH->mi->getMapSizeIconId()); - mapName->setText(CSH->mi->getName()); - mapDescription->setText(CSH->mi->getDescription()); + mapName->setText(CSH->mi->getNameTranslated()); + mapDescription->setText(CSH->mi->getDescriptionTranslated()); for(size_t i = 0; i < difficultyIcons.size(); i++) { if(i == CSH->si->difficulty) @@ -513,9 +501,9 @@ void CBonusSelection::CRegion::clickReleased(const Point & cursorPosition) 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.size()) + auto & text = CSH->si->campState->scenario(idOfMapAndRegion).regionText; + if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && !text.empty()) { - CRClickPopup::createAndPush(text); + CRClickPopup::createAndPush(text.toString()); } } diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 5c3d5696b..95623b02f 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -8,28 +8,30 @@ * */ #include "StdInc.h" - #include "CLobbyScreen.h" -#include "CBonusSelection.h" -#include "SelectionTab.h" -#include "RandomMapTab.h" -#include "OptionsTab.h" -#include "../CServerHandler.h" +#include "CBonusSelection.h" +#include "TurnOptionsTab.h" +#include "OptionsTab.h" +#include "RandomMapTab.h" +#include "SelectionTab.h" + +#include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../widgets/Buttons.h" #include "../windows/InfoWindows.h" +#include "../render/Colors.h" #include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../../lib/CModHandler.h" -#include "../../lib/NetPacksLobby.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/rmg/CMapGenOptions.h" +#include "../CGameInfo.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) : CSelectionBase(screenType), bonusSel(nullptr) @@ -42,27 +44,30 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) { tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); - buttonSelect = std::make_shared(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, EShortcut::LOBBY_SELECT_SCENARIO); + buttonSelect = std::make_shared(Point(411, 80), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[45], 0, EShortcut::LOBBY_SELECT_SCENARIO); buttonSelect->addCallback([&]() { toggleTab(tabSel); CSH->setMapInfo(tabSel->getSelectedMapInfo()); }); - buttonOptions = std::make_shared(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); + buttonOptions = std::make_shared(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); + if(settings["general"]["enableUiEnhancements"].Bool()) + buttonTurnOptions = std::make_shared(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE); }; - buttonChat = std::make_shared(Point(619, 80), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); + buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); switch(screenType) { case ESelectionScreen::newGame: { tabOpt = std::make_shared(); + tabTurnOptions = std::make_shared(); tabRand = std::make_shared(); tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2); - buttonRMG = std::make_shared(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); + buttonRMG = std::make_shared(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); buttonRMG->addCallback([&]() { toggleTab(tabRand); @@ -71,26 +76,27 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1)); - buttonStart = std::make_shared(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_GAME); initLobby(); break; } case ESelectionScreen::loadGame: { tabOpt = std::make_shared(); - buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); + tabTurnOptions = std::make_shared(); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; } case ESelectionScreen::campaignList: tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); - buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME); break; } buttonStart->block(true); // to be unblocked after map list is ready - buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [&]() + buttonBack = std::make_shared(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [&]() { CSH->sendClientDisconnecting(); close(); @@ -145,6 +151,10 @@ void CLobbyScreen::toggleMode(bool host) auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); + + if (buttonTurnOptions) + buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor); + if(buttonRMG) { buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); @@ -153,23 +163,34 @@ void CLobbyScreen::toggleMode(bool host) buttonSelect->block(!host); buttonOptions->block(!host); + if (buttonTurnOptions) + buttonTurnOptions->block(!host); + if(CSH->mi) + { tabOpt->recreate(); + tabTurnOptions->recreate(); + } } void CLobbyScreen::toggleChat() { card->toggleChat(); if(card->showChat) - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); else - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); } void CLobbyScreen::updateAfterStateChange() { - if(CSH->mi && tabOpt) - tabOpt->recreate(); + if(CSH->mi) + { + if (tabOpt) + tabOpt->recreate(); + if (tabTurnOptions) + tabTurnOptions->recreate(); + } buttonStart->block(CSH->mi == nullptr || CSH->isGuest()); diff --git a/client/lobby/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp index 562643a92..72c18b300 100644 --- a/client/lobby/CSavingScreen.cpp +++ b/client/lobby/CSavingScreen.cpp @@ -40,7 +40,9 @@ CSavingScreen::CSavingScreen() tabSel->toggleMode(); curTab = tabSel; - buttonStart = std::make_shared(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRSAV.DEF"), CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME); + + LOCPLINT->gamePause(true); } const CMapInfo * CSavingScreen::getMapInfo() @@ -65,12 +67,18 @@ void CSavingScreen::changeSelection(std::shared_ptr to) card->redraw(); } +void CSavingScreen::close() +{ + LOCPLINT->gamePause(false); + CSelectionBase::close(); +} + void CSavingScreen::saveGame() { if(!(tabSel && tabSel->inputName && tabSel->inputName->getText().size())) return; - std::string path = "Saves/" + tabSel->inputName->getText(); + std::string path = "Saves/" + tabSel->curFolder + tabSel->inputName->getText(); auto overWrite = [this, path]() -> void { @@ -80,7 +88,7 @@ void CSavingScreen::saveGame() close(); }; - if(CResourceHandler::get("local")->existsResource(ResourceID(path, EResType::SAVEGAME))) + if(CResourceHandler::get("local")->existsResource(ResourcePath(path, EResType::SAVEGAME))) { std::string hlp = CGI->generaltexth->allTexts[493]; //%s exists. Overwrite? boost::algorithm::replace_first(hlp, "%s", tabSel->inputName->getText()); diff --git a/client/lobby/CSavingScreen.h b/client/lobby/CSavingScreen.h index e99809407..17f637b74 100644 --- a/client/lobby/CSavingScreen.h +++ b/client/lobby/CSavingScreen.h @@ -32,4 +32,7 @@ public: const CMapInfo * getMapInfo() override; const StartInfo * getStartInfo() override; + +protected: + void close() override; }; diff --git a/client/lobby/CScenarioInfoScreen.cpp b/client/lobby/CScenarioInfoScreen.cpp index acccf2da4..51a6f242a 100644 --- a/client/lobby/CScenarioInfoScreen.cpp +++ b/client/lobby/CScenarioInfoScreen.cpp @@ -45,7 +45,7 @@ CScenarioInfoScreen::CScenarioInfoScreen() card->changeSelection(); card->iconDifficulty->setSelected(getCurrentDifficulty()); - buttonBack = std::make_shared(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonBack = std::make_shared(Point(584, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } CScenarioInfoScreen::~CScenarioInfoScreen() diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index a80a08ffb..51feeaff3 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -37,8 +37,10 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../render/IRenderHandler.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/CThreadHelper.h" @@ -65,9 +67,9 @@ int ISelectionScreenInfo::getCurrentDifficulty() return getStartInfo()->difficulty; } -PlayerInfo ISelectionScreenInfo::getPlayerInfo(int color) +PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color) { - return getMapInfo()->mapHeader->players[color]; + return getMapInfo()->mapHeader->players[color.getNum()]; } CSelectionBase::CSelectionBase(ESelectionScreen type) @@ -78,16 +80,16 @@ CSelectionBase::CSelectionBase(ESelectionScreen type) pos.h = 584; if(screenType == ESelectionScreen::campaignList) { - setBackground("CamCust.bmp"); + setBackground(ImagePath::builtin("CamCust.bmp")); } else { const JsonVector & bgNames = CMainMenuConfig::get().getConfig()["game-select"].Vector(); - setBackground(RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault())->String()); + setBackground(ImagePath::fromJson(*RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault()))); } pos = background->center(); card = std::make_shared(); - buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonBack = std::make_shared(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } void CSelectionBase::toggleTab(std::shared_ptr tab) @@ -108,6 +110,14 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) { curTab.reset(); } + + if(tabSel->showRandom && tab != tabOpt) + { + tabSel->curFolder = ""; + tabSel->showRandom = false; + tabSel->filter(0, true); + } + GH.windows().totalRedraw(); } @@ -119,11 +129,12 @@ InfoCard::InfoCard() pos.x += 393; pos.y += 6; - labelSaveDate = std::make_shared(158, 19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + 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); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); - playerListBg = std::make_shared("CHATPLUG.bmp", 16, 276); + playerListBg = std::make_shared(ImagePath::builtin("CHATPLUG.bmp"), 16, 276); chat = std::make_shared(Rect(26, 132, 340, 132)); if(SEL->screenType == ESelectionScreen::campaignList) @@ -132,14 +143,14 @@ InfoCard::InfoCard() } else { - background = std::make_shared("GSELPOP1.bmp", 0, 0); + background = std::make_shared(ImagePath::builtin("GSELPOP1.bmp"), 0, 0); parent->addChild(background.get()); auto it = vstd::find(parent->children, this); //our position among parent children parent->children.insert(it, background.get()); //put BG before us parent->children.pop_back(); pos.w = background->pos.w; pos.h = background->pos.h; - iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 318, 22); //let it be custom size (frame 4) by default + iconsMapSizes = std::make_shared(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 318, 22); //let it be custom size (frame 4) by default iconDifficulty = std::make_shared(0); { @@ -147,7 +158,7 @@ InfoCard::InfoCard() for(int i = 0; i < 5; i++) { - auto button = std::make_shared(Point(110 + i * 32, 450), difButns[i], CGI->generaltexth->zelp[24 + i]); + auto button = std::make_shared(Point(110 + i * 32, 450), AnimationPath::builtin(difButns[i]), CGI->generaltexth->zelp[24 + i]); iconDifficulty->addToggle(i, button); if(SEL->screenType != ESelectionScreen::newGame) @@ -163,8 +174,8 @@ InfoCard::InfoCard() labelScenarioDescription = std::make_shared(26, 132, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); labelVictoryCondition = std::make_shared(26, 283, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[497]); labelLossCondition = std::make_shared(26, 339, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[498]); - iconsVictoryCondition = std::make_shared("SCNRVICT", 0, 0, 24, 302); - iconsLossCondition = std::make_shared("SCNRLOSS", 0, 0, 24, 359); + 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); @@ -182,6 +193,7 @@ InfoCard::InfoCard() void InfoCard::disableLabelRedraws() { labelSaveDate->setAutoRedraw(false); + labelMapSize->setAutoRedraw(false); mapName->setAutoRedraw(false); mapDescription->label->setAutoRedraw(false); labelVictoryConditionText->setAutoRedraw(false); @@ -197,8 +209,8 @@ void InfoCard::changeSelection() return; labelSaveDate->setText(mapInfo->date); - mapName->setText(mapInfo->getName()); - mapDescription->setText(mapInfo->getDescription()); + mapName->setText(mapInfo->getNameTranslated()); + mapDescription->setText(mapInfo->getDescriptionTranslated()); mapDescription->label->scrollTextTo(0, false); if(mapDescription->slider) @@ -207,8 +219,11 @@ void InfoCard::changeSelection() if(SEL->screenType == ESelectionScreen::campaignList) return; - iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); const CMapHeader * header = mapInfo->mapHeader.get(); + + labelMapSize->setText(std::to_string(header->width) + "x" + std::to_string(header->height)); + iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); + iconsVictoryCondition->setFrame(header->victoryIconIndex); labelVictoryConditionText->setText(header->victoryMessage.toString()); iconsLossCondition->setFrame(header->defeatIconIndex); @@ -238,7 +253,7 @@ void InfoCard::changeSelection() int pid = p.first; if(pset) { - auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.getStr()); + auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.toString()); labelGroupPlayersAssigned->add(24, 285 + (int)labelGroupPlayersAssigned->currentSize()*(int)graphics->fonts[FONT_SMALL]->getLineHeight(), name); } else @@ -324,6 +339,11 @@ CChatBox::CChatBox(const Rect & rect) chatHistory->label->color = Colors::GREEN; } +bool CChatBox::captureThisKey(EShortcut key) +{ + return !inputBox->getText().empty() && key == EShortcut::GLOBAL_ACCEPT; +} + void CChatBox::keyPressed(EShortcut key) { if(key == EShortcut::GLOBAL_ACCEPT && inputBox->getText().size()) @@ -337,7 +357,7 @@ void CChatBox::keyPressed(EShortcut key) void CChatBox::addNewMessage(const std::string & text) { - CCS->soundh->playSound("CHAT"); + CCS->soundh->playSound(AudioPath::builtin("CHAT")); chatHistory->setText(chatHistory->label->getText() + text + "\n"); if(chatHistory->slider) chatHistory->slider->scrollToMax(); @@ -354,7 +374,7 @@ CFlagBox::CFlagBox(const Rect & rect) 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 = std::make_shared("ITGFLAGS.DEF"); + iconsTeamFlags = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITGFLAGS.DEF")); iconsTeamFlags->preload(); } @@ -388,35 +408,41 @@ void CFlagBox::showPopupWindow(const Point & cursorPosition) } CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr icons) - : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, "DIBOXBCK") + : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("DIBOXBCK")) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.w = 256; - pos.h = 90 + 50 * SEL->getMapInfo()->mapHeader->howManyTeams; 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); - for(int i = 0; i < SEL->getMapInfo()->mapHeader->howManyTeams; i++) + + std::vector> teams(PlayerColor::PLAYER_LIMIT_I); + + for(PlayerColor j(0); j < PlayerColor::PLAYER_LIMIT; j++) { - std::vector flags; - labelGroupTeams->add(128, 65 + 50 * i, boost::str(boost::format(CGI->generaltexth->allTexts[656]) % (i+1))); - - for(int j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) + if(SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) { - if((SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) - && SEL->getPlayerInfo(j).team == TeamID(i)) - { - flags.push_back(j); - } - } - - int curx = 128 - 9 * (int)flags.size(); - for(auto & flag : flags) - { - iconsFlags.push_back(std::make_shared(icons, flag, 0, curx, 75 + 50 * i)); - curx += 18; + teams[SEL->getPlayerInfo(j).team].insert(j); } } + + auto curIdx = 0; + for(const auto & team : teams) + { + if(team.empty()) + continue; + + labelGroupTeams->add(128, 65 + 50 * curIdx, boost::str(boost::format(CGI->generaltexth->allTexts[656]) % (curIdx + 1))); + int curx = 128 - 9 * team.size(); + for(const auto & player : team) + { + iconsFlags.push_back(std::make_shared(icons, player, 0, curx, 75 + 50 * curIdx)); + curx += 18; + } + ++curIdx; + } + pos.w = 256; + pos.h = 90 + 50 * curIdx; + background->scaleTo(Point(pos.w, pos.h)); center(); } diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 76f222a10..e614468cf 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -26,6 +26,7 @@ class CAnimImage; class CToggleGroup; class RandomMapTab; class OptionsTab; +class TurnOptionsTab; class SelectionTab; class InfoCard; class CChatBox; @@ -44,7 +45,7 @@ public: virtual const StartInfo * getStartInfo() = 0; virtual int getCurrentDifficulty(); - virtual PlayerInfo getPlayerInfo(int color); + virtual PlayerInfo getPlayerInfo(PlayerColor color); }; @@ -57,11 +58,14 @@ public: std::shared_ptr buttonSelect; std::shared_ptr buttonRMG; std::shared_ptr buttonOptions; + std::shared_ptr buttonTurnOptions; std::shared_ptr buttonStart; std::shared_ptr buttonBack; + std::shared_ptr buttonSimturns; std::shared_ptr tabSel; std::shared_ptr tabOpt; + std::shared_ptr tabTurnOptions; std::shared_ptr tabRand; std::shared_ptr curTab; @@ -79,6 +83,7 @@ class InfoCard : public CIntObject std::shared_ptr iconsMapSizes; std::shared_ptr labelSaveDate; + std::shared_ptr labelMapSize; std::shared_ptr labelScenarioName; std::shared_ptr labelScenarioDescription; std::shared_ptr labelVictoryCondition; @@ -121,6 +126,7 @@ public: CChatBox(const Rect & rect); void keyPressed(EShortcut key) override; + bool captureThisKey(EShortcut key) override; void addNewMessage(const std::string & text); }; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 2337be6c3..e7698ba48 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -14,18 +14,27 @@ #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 "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" +#include "../widgets/Images.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../windows/CHeroOverview.h" +#include "../eventsSDL/InputHandler.h" -#include "../../lib/NetPacksLobby.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" #include "../../lib/CTownHandler.h" @@ -33,27 +42,18 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" -OptionsTab::OptionsTab() : humanPlayers(0) +static JsonPath optionsTabConfigLocation() { - recActions = 0; - OBJ_CONSTRUCTION; - background = std::make_shared("ADVOPTBK", 0, 6); - pos = background->pos; - labelTitle = std::make_shared(222, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[515]); - labelSubTitle = std::make_shared(Rect(60, 44, 320, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[516]); + if(settings["general"]["enableUiEnhancements"].Bool()) + return JsonPath::builtin("config/widgets/playerOptionsTab.json"); + else + return JsonPath::builtin("config/widgets/advancedOptionsTab.json"); +} - labelPlayerNameAndHandicap = std::make_shared(Rect(58, 86, 100, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[517]); - labelStartingTown = std::make_shared(Rect(163, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[518]); - labelStartingHero = std::make_shared(Rect(239, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[519]); - labelStartingBonus = std::make_shared(Rect(315, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[520]); - if(SEL->screenType == ESelectionScreen::newGame || SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::scenarioInfo) - { - sliderTurnDuration = std::make_shared(Point(55, 551), 194, std::bind(&IServerAPI::setTurnLength, CSH, _1), 1, (int)GameConstants::POSSIBLE_TURNTIME.size(), (int)GameConstants::POSSIBLE_TURNTIME.size(), Orientation::HORIZONTAL, CSlider::BLUE); - sliderTurnDuration->setScrollBounds(Rect(-3, -25, 337, 43)); - sliderTurnDuration->setPanningStep(20); - labelPlayerTurnDuration = std::make_shared(222, 538, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[521]); - labelTurnDurationValue = std::make_shared(319, 559, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } +OptionsTab::OptionsTab() + : OptionsTabBase(optionsTabConfigLocation()) + , humanPlayers(0) +{ } void OptionsTab::recreate() @@ -61,6 +61,11 @@ void OptionsTab::recreate() entries.clear(); humanPlayers = 0; + for (auto selectionWindow : GH.windows().findWindows()) + { + selectionWindow->reopen(); + } + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; for(auto & pInfo : SEL->getStartInfo()->playerInfos) { @@ -70,14 +75,10 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } - if(sliderTurnDuration) - { - sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime)); - labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]); - } + OptionsTabBase::recreate(); } -size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() +size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) { enum EBonusSelection //frames of bonuses file { @@ -87,48 +88,49 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() WOOD = 0, ORE = 0, MITHRIL = 10, // resources unavailable in bonuses file TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA - HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall + HERO_RANDOM = 156, HERO_NONE = 157 // Special frames in PortraitsSmall }; - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = playerSettings.getCastleValidated(); - switch(type) + switch(selectionType) { case TOWN: - switch(settings.castle) - { - case PlayerSettings::NONE: + { + if (playerSettings.castle == FactionID::NONE) return TOWN_NONE; - case PlayerSettings::RANDOM: + + if (playerSettings.castle == FactionID::RANDOM) return TOWN_RANDOM; - default: - return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + 2; - } + + return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + (big ? 0 : 2); + } + case HERO: - switch(settings.hero) - { - case PlayerSettings::NONE: + { + if (playerSettings.hero == HeroTypeID::NONE) return HERO_NONE; - case PlayerSettings::RANDOM: + + if (playerSettings.hero == HeroTypeID::RANDOM) return HERO_RANDOM; - default: - { - if(settings.heroPortrait >= 0) - return settings.heroPortrait; - auto index = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; - return (*CGI->heroh)[index]->imageIndex; - } - } + + if(playerSettings.heroPortrait != HeroTypeID::NONE) + return playerSettings.heroPortrait; + + auto index = playerSettings.getHeroValidated(); + return (*CGI->heroh)[index]->imageIndex; + } + case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return RANDOM; - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return ARTIFACT; - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return GOLD; - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -158,90 +160,79 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() return 0; } -std::string OptionsTab::CPlayerSettingsHelper::getImageName() +AnimationPath OptionsTab::CPlayerSettingsHelper::getImageName(bool big) { - switch(type) + switch(selectionType) { case OptionsTab::TOWN: - return "ITPA"; + return AnimationPath::builtin(big ? "ITPt": "ITPA"); case OptionsTab::HERO: - return "PortraitsSmall"; + return AnimationPath::builtin(big ? "PortraitsLarge": "PortraitsSmall"); case OptionsTab::BONUS: - return "SCNRSTAR"; + return AnimationPath::builtin("SCNRSTAR"); } - return ""; + return {}; } std::string OptionsTab::CPlayerSettingsHelper::getName() { - switch(type) + switch(selectionType) { - case TOWN: - { - switch(settings.castle) + case TOWN: { - case PlayerSettings::NONE: - return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: - { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + if (playerSettings.castle == FactionID::NONE) + return CGI->generaltexth->allTexts[523]; + + if (playerSettings.castle == FactionID::RANDOM) + return CGI->generaltexth->allTexts[522]; + + auto factionIndex = playerSettings.getCastleValidated(); return (*CGI->townh)[factionIndex]->getNameTranslated(); } - } - } - case HERO: - { - switch(settings.hero) + case HERO: { - case PlayerSettings::NONE: - return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: - { - if(!settings.heroName.empty()) - return settings.heroName; - auto index = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + if (playerSettings.hero == HeroTypeID::NONE) + return CGI->generaltexth->allTexts[523]; + + if (playerSettings.hero == HeroTypeID::RANDOM) + return CGI->generaltexth->allTexts[522]; + + if(!playerSettings.heroNameTextId.empty()) + return CGI->generaltexth->translate(playerSettings.heroNameTextId); + auto index = playerSettings.getHeroValidated(); return (*CGI->heroh)[index]->getNameTranslated(); } - } - } - case BONUS: - { - switch(settings.bonus) + case BONUS: { - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: - return CGI->generaltexth->arraytxt[214 + settings.bonus]; + if (playerSettings.bonus == PlayerStartingBonus::RANDOM) + return CGI->generaltexth->allTexts[522]; + + return CGI->generaltexth->arraytxt[214 + static_cast(playerSettings.bonus)]; } } - } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getTitle() { - switch(type) + switch(selectionType) { case OptionsTab::TOWN: - return (settings.castle < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; + return playerSettings.castle.isValid() ? CGI->generaltexth->allTexts[80] : CGI->generaltexth->allTexts[103]; case OptionsTab::HERO: - return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; + return playerSettings.hero.isValid() ? CGI->generaltexth->allTexts[77] : CGI->generaltexth->allTexts[101]; case OptionsTab::BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return CGI->generaltexth->allTexts[86]; //{Random Bonus} - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return CGI->generaltexth->allTexts[83]; //{Artifact Bonus} - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[84]; //{Gold Bonus} - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: return CGI->generaltexth->allTexts[85]; //{Resource Bonus} } } @@ -250,27 +241,27 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() } std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; - auto heroIndex = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + auto factionIndex = playerSettings.getCastleValidated(); + auto heroIndex = playerSettings.getHeroValidated(); - switch(type) + switch(selectionType) { case TOWN: return getName(); case HERO: { - if(settings.hero >= 0) + if(playerSettings.hero.isValid()) return getName() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated(); return getName(); } case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[87]; //500-1000 - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -294,9 +285,9 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() std::string OptionsTab::CPlayerSettingsHelper::getDescription() { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = playerSettings.getCastleValidated(); - switch(type) + switch(selectionType) { case TOWN: return CGI->generaltexth->allTexts[104]; @@ -304,15 +295,15 @@ std::string OptionsTab::CPlayerSettingsHelper::getDescription() return CGI->generaltexth->allTexts[102]; case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return CGI->generaltexth->allTexts[90]; //An artifact is randomly chosen and equipped to your starting hero - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[92]; //At the start of the game, 500-1000 gold is added to your Kingdom's resource pool - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -339,36 +330,25 @@ OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelp { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - int value = PlayerSettings::NONE; - - switch(CPlayerSettingsHelper::type) + switch(selectionType) { - break; - case TOWN: - value = settings.castle; - break; - case HERO: - value = settings.hero; - break; - case BONUS: - value = settings.bonus; + case TOWN: + genTownWindow(); + break; + case HERO: + genHeroWindow(); + break; + case BONUS: + genBonusWindow(); + break; } - if(value == PlayerSettings::RANDOM) - genBonusWindow(); - else if(CPlayerSettingsHelper::type == BONUS) - genBonusWindow(); - else if(CPlayerSettingsHelper::type == HERO) - genHeroWindow(); - else if(CPlayerSettingsHelper::type == TOWN) - genTownWindow(); - center(); } void OptionsTab::CPlayerOptionTooltipBox::genHeader() { - backgroundTexture = std::make_shared("DIBOXBCK", pos); + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, getTitle()); @@ -378,29 +358,37 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeader() void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() { + auto factionIndex = playerSettings.getCastleValidated(); + + if (playerSettings.castle == FactionID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; for(auto & elem : town->creatures) { if(!elem.empty()) - components.push_back(std::make_shared(CComponent::creature, elem.front(), 0, CComponent::tiny)); + components.push_back(std::make_shared(ComponentType::CREATURE, elem.front(), std::nullopt, CComponent::tiny)); } boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140)); } void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() { + auto heroIndex = playerSettings.getHeroValidated(); + + if (playerSettings.hero == HeroTypeID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; - imageSpeciality = std::make_shared("UN44", (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); } @@ -412,9 +400,376 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) - : Scrollable(SHOW_POPUP, position, Orientation::HORIZONTAL) - , CPlayerSettingsHelper(settings, type) +OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) + : CWindowObject(BORDERED) +{ + addUsedEvents(LCLICK | SHOW_POPUP); + + color = _color; + type = _type; + + initialFaction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; + initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; + initialBonus = SEL->getStartInfo()->playerInfos.find(color)->second.bonus; + selectedFaction = initialFaction; + selectedHero = initialHero; + selectedBonus = initialBonus; + allowedFactions = SEL->getPlayerInfo(color).allowedFactions; + allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; + + for(auto & player : SEL->getStartInfo()->playerInfos) + { + if(player.first != color && (int)player.second.hero > HeroTypeID::RANDOM) + unusableHeroes.insert(player.second.hero); + } + + allowedBonus.push_back(PlayerStartingBonus::RANDOM); + + if(initialHero != HeroTypeID::NONE|| SEL->getPlayerInfo(color).heroesNames.size() > 0) + allowedBonus.push_back(PlayerStartingBonus::ARTIFACT); + + allowedBonus.push_back(PlayerStartingBonus::GOLD); + + if(initialFaction.isValid()) + allowedBonus.push_back(PlayerStartingBonus::RESOURCE); + + recreate(); +} + +int OptionsTab::SelectionWindow::calcLines(FactionID faction) +{ + double additionalItems = 1; // random + + if(!faction.isValid()) + return std::ceil(((double)allowedFactions.size() + additionalItems) / elementsPerLine); + + int count = 0; + for(auto & elemh : allowedHeroes) + { + CHero * type = VLC->heroh->objects[elemh]; + if(type->heroClass->faction == faction) + count++; + } + + return std::ceil(std::max((double)count + additionalItems, (double)allowedFactions.size() + additionalItems) / (double)elementsPerLine); +} + +void OptionsTab::SelectionWindow::apply() +{ + if(GH.windows().isTopWindow(this)) + { + GH.input().hapticFeedback(); + CCS->soundh->playSound(soundBase::button); + + close(); + + setSelection(); + } +} + +void OptionsTab::SelectionWindow::setSelection() +{ + if(selectedFaction != initialFaction) + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN_ID, selectedFaction, color); + + if(selectedHero != initialHero) + CSH->setPlayerOption(LobbyChangePlayerOption::HERO_ID, selectedHero, color); + + if(selectedBonus != initialBonus) + CSH->setPlayerOption(LobbyChangePlayerOption::BONUS_ID, static_cast(selectedBonus), color); +} + +void OptionsTab::SelectionWindow::reopen() +{ + std::shared_ptr window = std::shared_ptr(new SelectionWindow(color, type)); + close(); + GH.windows().pushWindow(window); +} + +void OptionsTab::SelectionWindow::recreate() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + 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) + { + CHero * type = VLC->heroh->objects[elem]; + if(type->heroClass->faction == selectedFaction) + { + count++; + } + } + elementsPerLine = floor(sqrt(count)); + } + + amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : FactionID::RANDOM); + } + + int x = (elementsPerLine) * (ICON_BIG_WIDTH-1); + int y = (amountLines) * (ICON_BIG_HEIGHT-1); + + pos = Rect(0, 0, x, y); + + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); + backgroundTexture->playerColored(PlayerColor(1)); + updateShadow(); + + if(type == SelType::TOWN) + genContentFactions(); + if(type == SelType::HERO) + genContentHeroes(); + if(type == SelType::BONUS) + genContentBonus(); + genContentGrid(amountLines); + + center(); +} + +void OptionsTab::SelectionWindow::drawOutlinedText(int x, int y, ColorRGBA color, std::string text) +{ + components.push_back(std::make_shared(x-1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x+1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y-1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y+1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y, FONT_TINY, ETextAlignment::CENTER, color, text)); +} + +void OptionsTab::SelectionWindow::genContentGrid(int lines) +{ + for(int y = 0; y < lines; y++) + { + for(int x = 0; x < elementsPerLine; x++) + { + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderBig"), x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + } + } +} + +void OptionsTab::SelectionWindow::genContentFactions() +{ + int i = 1; + + // random + PlayerSettings set = PlayerSettings(); + set.castle = FactionID::RANDOM; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction == FactionID::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedFaction == FactionID::RANDOM) + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); + + for(auto & elem : allowedFactions) + { + int x = i % elementsPerLine; + int y = i / elementsPerLine; + + PlayerSettings set = PlayerSettings(); + set.castle = elem; + + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + + 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++; + } +} + +void OptionsTab::SelectionWindow::genContentHeroes() +{ + int i = 1; + + // random + PlayerSettings set = PlayerSettings(); + set.hero = HeroTypeID::RANDOM; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero == HeroTypeID::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedHero == HeroTypeID::RANDOM) + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); + + for(auto & elem : allowedHeroes) + { + CHero * type = VLC->heroh->objects[elem]; + + if(type->heroClass->faction == selectedFaction) + { + + int x = i % elementsPerLine; + int y = i / elementsPerLine; + + PlayerSettings set = PlayerSettings(); + set.hero = elem; + + 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); + + i++; + } + } +} + +void OptionsTab::SelectionWindow::genContentBonus() +{ + PlayerSettings set = PlayerSettings(); + + int i = 0; + for(auto elem : allowedBonus) + { + int x = i; + int y = 0; + + set.bonus = elem; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::WHITE , helper.getName()); + if(selectedBonus == elem) + { + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::YELLOW , helper.getName()); + } + + i++; + } +} + +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); + + return x + y * elementsPerLine; +} + +void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) +{ + PlayerSettings set = PlayerSettings(); + if(type == SelType::TOWN) + { + if(elem > 0) + { + elem--; + if(elem >= factions.size()) + return; + set.castle = factions[elem]; + } + else + { + set.castle = FactionID::RANDOM; + } + if(set.castle != FactionID::NONE) + { + if(!doApply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + GH.windows().createAndPushWindow(helper); + } + else + selectedFaction = set.castle; + } + } + if(type == SelType::HERO) + { + if(elem > 0) + { + elem--; + if(elem >= heroes.size()) + return; + set.hero = heroes[elem]; + } + else + { + set.hero = HeroTypeID::RANDOM; + } + + if(doApply && unusableHeroes.count(heroes[elem])) + return; + + if(set.hero != HeroTypeID::NONE) + { + if(!doApply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.isValid() && helper.playerSettings.heroNameTextId.empty()) + GH.windows().createAndPushWindow(helper.playerSettings.hero); + else + GH.windows().createAndPushWindow(helper); + } + else + selectedHero = set.hero; + } + } + if(type == SelType::BONUS) + { + if(elem >= 4) + return; + set.bonus = static_cast(allowedBonus[elem]); + + if(!doApply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + GH.windows().createAndPushWindow(helper); + } + else + selectedBonus = set.bonus; + } + + if(doApply) + apply(); +} + +bool OptionsTab::SelectionWindow::receiveEvent(const Point & position, int eventType) const +{ + return true; // capture click also outside of window +} + +void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) +{ + if(!pos.isInside(cursorPosition)) + { + close(); + return; + } + + int elem = getElement(cursorPosition); + + setElement(elem, true); +} + +void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) +{ + if(!pos.isInside(cursorPosition)) + return; + + int elem = getElement(cursorPosition); + + setElement(elem, false); +} + +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; @@ -435,12 +790,36 @@ void OptionsTab::SelectedBox::update() void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) { // cases when we do not need to display a message - if(settings.castle == -2 && CPlayerSettingsHelper::type == TOWN) + if(playerSettings.castle == FactionID::NONE && CPlayerSettingsHelper::selectionType == TOWN) return; - if(settings.hero == -2 && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + if(playerSettings.hero == HeroTypeID::NONE && !SEL->getPlayerInfo(playerSettings.color).hasCustomMainHero() && CPlayerSettingsHelper::selectionType == HERO) return; - GH.windows().createAndPushWindow(*this); + if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::selectionType == HERO && playerSettings.hero.isValid() && playerSettings.heroNameTextId.empty()) + GH.windows().createAndPushWindow(playerSettings.hero); + else + GH.windows().createAndPushWindow(*this); +} + +void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) +{ + if(SEL->screenType != ESelectionScreen::newGame) + return; + + PlayerInfo pi = SEL->getPlayerInfo(playerSettings.color); + const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(playerSettings.color); + + if(selectionType == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) + return; + + if(selectionType == SelType::HERO && ((pi.defaultHero() == HeroTypeID::NONE || !playerSettings.castle.isValid() || foreignPlayer))) + return; + + if(selectionType == SelType::BONUS && foreignPlayer) + return; + + GH.input().hapticFeedback(); + GH.windows().createAndPushWindow(playerSettings.color, selectionType); } void OptionsTab::SelectedBox::scrollBy(int distance) @@ -450,16 +829,16 @@ void OptionsTab::SelectedBox::scrollBy(int distance) // so, currently, gesture will always move selection only by 1, and then wait for recreation from server info distance = std::clamp(distance, -1, 1); - switch(CPlayerSettingsHelper::type) + switch(CPlayerSettingsHelper::selectionType) { case TOWN: - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, playerSettings.color); break; case HERO: - CSH->setPlayerOption(LobbyChangePlayerOption::HERO, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::HERO, distance, playerSettings.color); break; case BONUS: - CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, distance, playerSettings.color); break; } @@ -467,15 +846,17 @@ void OptionsTab::SelectedBox::scrollBy(int distance) } OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parent) - : pi(std::make_unique(SEL->getPlayerInfo(S.color.getNum()))) + : CIntObject(LCLICK | KEYBOARD | TEXTINPUT) + , pi(std::make_unique(SEL->getPlayerInfo(S.color))) , s(std::make_unique(S)) , parentTab(parent) + , name(S.name) { OBJ_CONSTRUCTION; defActions |= SHARE_POS; int serial = 0; - for(int g = 0; g < s->color.getNum(); ++g) + for(PlayerColor g = PlayerColor(0); g < s->color; ++g) { auto itred = SEL->getPlayerInfo(g); if(itred.canComputerPlay || itred.canHumanPlay) @@ -483,10 +864,10 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con } pos.x += 54; - pos.y += 122 + serial * 50; + pos.y += 128 + serial * 50; assert(CSH->mi && CSH->mi->mapHeader); - const PlayerInfo & p = SEL->getPlayerInfo(s->color.getNum()); + const PlayerInfo & p = SEL->getPlayerInfo(s->color); assert(p.canComputerPlay || p.canHumanPlay); //someone must be able to control this player if(p.canHumanPlay && p.canComputerPlay) whoCanPlay = HUMAN_OR_CPU; @@ -506,27 +887,33 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp" }}; - background = std::make_shared(bgs[s->color.getNum()], 0, 0); - labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); + background = std::make_shared(ImagePath::builtin(bgs[s->color]), 0, 0); + if(s->isControlledByAI() || CSH->isGuest()) + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name); + else + { + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, 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]); if(SEL->screenType == ESelectionScreen::newGame) { - buttonTownLeft = std::make_shared(Point(107, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color)); - buttonTownRight = std::make_shared(Point(168, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[133], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, +1, s->color)); - buttonHeroLeft = std::make_shared(Point(183, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[148], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, -1, s->color)); - buttonHeroRight = std::make_shared(Point(244, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[149], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, +1, s->color)); - buttonBonusLeft = std::make_shared(Point(259, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[164], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, -1, s->color)); - buttonBonusRight = std::make_shared(Point(320, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[165], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, +1, s->color)); + buttonTownLeft = std::make_shared(Point(107, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color)); + buttonTownRight = std::make_shared(Point(168, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[133], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, +1, s->color)); + buttonHeroLeft = std::make_shared(Point(183, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[148], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, -1, s->color)); + buttonHeroRight = std::make_shared(Point(244, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[149], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, +1, s->color)); + buttonBonusLeft = std::make_shared(Point(259, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[164], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, -1, s->color)); + buttonBonusRight = std::make_shared(Point(320, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[165], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, +1, s->color)); } hideUnavailableButtons(); - if(SEL->screenType != ESelectionScreen::scenarioInfo && SEL->getPlayerInfo(s->color.getNum()).canHumanPlay) + if(SEL->screenType != ESelectionScreen::scenarioInfo && SEL->getPlayerInfo(s->color).canHumanPlay) { flag = std::make_shared( Point(-43, 2), - flags[s->color.getNum()], + AnimationPath::builtin(flags[s->color.getNum()]), CGI->generaltexth->zelp[180], std::bind(&OptionsTab::onSetPlayerClicked, &parentTab, *s) ); @@ -541,9 +928,46 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con bonus = std::make_shared(Point(271, 2), *s, BONUS); } +bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) +{ + return labelPlayerNameEdit && labelPlayerNameEdit->hasFocus() && key == EShortcut::GLOBAL_ACCEPT; +} + +void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) +{ + if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT) + updateName(); +} + +bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const +{ + return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) +} + +void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) +{ + if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition)) + updateName(); +} + +void OptionsTab::PlayerOptionsEntry::updateName() { + if(labelPlayerNameEdit->getText() != name) + { + CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); + if(CSH->isMyColor(s->color)) + { + Settings set = settings.write["general"]["playerName"]; + set->String() = labelPlayerNameEdit->getText(); + } + } + + labelPlayerNameEdit->removeFocus(); + name = labelPlayerNameEdit->getText(); +} + void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const { - if(ps.isControlledByAI() || humanPlayers > 0) + if(ps.isControlledByAI() || humanPlayers > 1) CSH->setPlayer(ps.color); } @@ -565,7 +989,7 @@ void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() buttonTownRight->enable(); } - if((pi->defaultHero() != -1 || s->castle < 0) //fixed hero + if((pi->defaultHero() != HeroTypeID::RANDOM || !s->castle.isValid()) //fixed hero || foreignPlayer) //or not our player { buttonHeroLeft->disable(); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index b27e1dd50..691a162bb 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -9,16 +9,16 @@ */ #pragma once +#include "OptionsTabBase.h" #include "../windows/CWindowObject.h" +#include "../widgets/Scrollable.h" VCMI_LIB_NAMESPACE_BEGIN struct PlayerSettings; struct PlayerInfo; +enum class PlayerStartingBonus : int8_t; VCMI_LIB_NAMESPACE_END -#include "../widgets/Scrollable.h" - -class CSlider; class CLabel; class CMultiLineLabel; class CFilledTexture; @@ -27,22 +27,22 @@ class CComponentBox; class CTextBox; class CButton; +class FilledTexturePlayerColored; + /// The options tab which is shown at the map selection phase. -class OptionsTab : public CIntObject +class OptionsTab : public OptionsTabBase { - std::shared_ptr background; - std::shared_ptr labelTitle; - std::shared_ptr labelSubTitle; - std::shared_ptr labelPlayerNameAndHandicap; - std::shared_ptr labelStartingTown; - std::shared_ptr labelStartingHero; - std::shared_ptr labelStartingBonus; - - std::shared_ptr labelPlayerTurnDuration; - std::shared_ptr labelTurnDurationValue; + struct PlayerOptionsEntry; + ui8 humanPlayers; - + std::map> entries; + public: + + OptionsTab(); + void recreate(); + void onSetPlayerClicked(const PlayerSettings & ps) const; + enum SelType { TOWN, @@ -50,18 +50,20 @@ public: BONUS }; +private: + struct CPlayerSettingsHelper { - const PlayerSettings & settings; - const SelType type; + const PlayerSettings & playerSettings; + const SelType selectionType; - CPlayerSettingsHelper(const PlayerSettings & settings, SelType type) - : settings(settings), type(type) + CPlayerSettingsHelper(const PlayerSettings & playerSettings, SelType type) + : playerSettings(playerSettings), selectionType(type) {} /// visible image settings - size_t getImageIndex(); - std::string getImageName(); + size_t getImageIndex(bool big = false); + AnimationPath getImageName(bool big = false); std::string getName(); /// name visible in options dialog std::string getTitle(); /// title in popup box @@ -94,14 +96,70 @@ public: CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper); }; + class SelectionWindow : public CWindowObject + { + //const int ICON_SMALL_WIDTH = 48; + const int ICON_SMALL_HEIGHT = 32; + const int ICON_BIG_WIDTH = 58; + const int ICON_BIG_HEIGHT = 64; + const int TEXT_POS_X = 29; + const int TEXT_POS_Y = 56; + + int elementsPerLine; + + PlayerColor color; + SelType type; + + std::shared_ptr backgroundTexture; + std::vector> components; + + std::vector factions; + std::vector heroes; + std::set unusableHeroes; + + FactionID initialFaction; + HeroTypeID initialHero; + PlayerStartingBonus initialBonus; + FactionID selectedFaction; + HeroTypeID selectedHero; + PlayerStartingBonus selectedBonus; + + std::set allowedFactions; + std::set allowedHeroes; + std::vector allowedBonus; + + void genContentGrid(int lines); + void genContentFactions(); + void genContentHeroes(); + void genContentBonus(); + + void drawOutlinedText(int x, int y, ColorRGBA color, std::string text); + int calcLines(FactionID faction); + void apply(); + void recreate(); + void setSelection(); + int getElement(const Point & cursorPosition); + void setElement(int element, bool doApply); + + bool receiveEvent(const Point & position, int eventType) const override; + void clickReleased(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + + public: + void reopen(); + + SelectionWindow(PlayerColor _color, SelType _type); + }; + /// Image with current town/hero/bonus struct SelectedBox : public Scrollable, public CPlayerSettingsHelper { std::shared_ptr image; std::shared_ptr subtitle; - SelectedBox(Point position, PlayerSettings & settings, SelType type); + SelectedBox(Point position, PlayerSettings & playerSettings, SelType type); void showPopupWindow(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; void scrollBy(int distance) override; void update(); @@ -109,9 +167,11 @@ public: struct PlayerOptionsEntry : public CIntObject { + std::string name; std::unique_ptr pi; std::unique_ptr s; std::shared_ptr labelPlayerName; + std::shared_ptr labelPlayerNameEdit; std::shared_ptr labelWhoCanPlay; std::shared_ptr background; std::shared_ptr buttonTownLeft; @@ -128,15 +188,14 @@ public: PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab); void hideUnavailableButtons(); + bool captureThisKey(EShortcut key) override; + void keyPressed(EShortcut key) override; + void clickReleased(const Point & cursorPosition) override; + bool receiveEvent(const Point & position, int eventType) const override; private: const OptionsTab & parentTab; + + void updateName(); }; - - std::shared_ptr sliderTurnDuration; - std::map> entries; - - OptionsTab(); - void recreate(); - void onSetPlayerClicked(const PlayerSettings & ps) const; }; diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp new file mode 100644 index 000000000..3dc78819b --- /dev/null +++ b/client/lobby/OptionsTabBase.cpp @@ -0,0 +1,388 @@ +/* + * OptionsTabBase.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 "OptionsTabBase.h" +#include "CSelectionBase.h" + +#include "../widgets/ComboBox.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../CServerHandler.h" +#include "../CGameInfo.h" + +#include "../../lib/StartInfo.h" +#include "../../lib/Languages.h" +#include "../../lib/MetaString.h" +#include "../../lib/CGeneralTextHandler.h" + +std::vector OptionsTabBase::getTimerPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["timerPresets"].Vector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = tpreset[0].Integer() * 1000; + tinfo.turnTimer = tpreset[1].Integer() * 1000; + tinfo.battleTimer = tpreset[2].Integer() * 1000; + tinfo.unitTimer = tpreset[3].Integer() * 1000; + tinfo.accumulatingTurnTimer = tpreset[4].Bool(); + tinfo.accumulatingUnitTimer = tpreset[5].Bool(); + result.push_back(tinfo); + } + return result; +} + +std::vector OptionsTabBase::getSimturnsPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["simturnsPresets"].Vector()) + { + SimturnsInfo tinfo; + tinfo.optionalTurns = tpreset[0].Integer(); + tinfo.requiredTurns = tpreset[1].Integer(); + tinfo.allowHumanWithAI = tpreset[2].Bool(); + result.push_back(tinfo); + } + return result; +} + +OptionsTabBase::OptionsTabBase(const JsonPath & configPath) +{ + recActions = 0; + + auto setTimerPresetCallback = [this](int index){ + CSH->setTurnTimerInfo(getTimerPresets().at(index)); + }; + + auto setSimturnsPresetCallback = [this](int index){ + CSH->setSimturnsInfo(getSimturnsPresets().at(index)); + }; + + addCallback("setTimerPreset", setTimerPresetCallback); + addCallback("setSimturnPreset", setSimturnsPresetCallback); + + addCallback("setSimturnDurationMin", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.requiredTurns = index; + info.optionalTurns = std::max(info.optionalTurns, index); + CSH->setSimturnsInfo(info); + }); + + addCallback("setSimturnDurationMax", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.optionalTurns = index; + info.requiredTurns = std::min(info.requiredTurns, index); + CSH->setSimturnsInfo(info); + }); + + addCallback("setSimturnAI", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.allowHumanWithAI = index; + CSH->setSimturnsInfo(info); + }); + + addCallback("setTurnTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingTurnTimer = index; + CSH->setTurnTimerInfo(info); + }); + + addCallback("setUnitTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingUnitTimer = index; + CSH->setTurnTimerInfo(info); + }); + + //helper function to parse string containing time to integer reflecting time in seconds + //assumed that input string can be modified by user, function shall support user's intention + // normal: 2:00, 12:30 + // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, + // adding symbol (>60 seconds): 12:095 -> 129:05 + // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 + auto parseTimerString = [](const std::string & str) -> int + { + auto sc = str.find(":"); + if(sc == std::string::npos) + return str.empty() ? 0 : std::stoi(str); + + auto l = str.substr(0, sc); + auto r = str.substr(sc + 1, std::string::npos); + if(r.length() == 3) //symbol added + { + l.push_back(r.front()); + r.erase(r.begin()); + } + else if(r.length() == 1) //symbol removed + { + r.insert(r.begin(), l.back()); + l.pop_back(); + } + else if(r.empty()) + r = "0"; + + int sec = std::stoi(r); + if(sec >= 60) + { + if(l.empty()) //9:00 -> 0:09 + return sec / 10; + + l.push_back(r.front()); //0:090 -> 9:00 + r.erase(r.begin()); + } + else if(l.empty()) + return sec; + + return std::stoi(l) * 60 + std::stoi(r); + }; + + addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.baseTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.turnTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.battleTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_unit", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.unitTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + + const JsonNode config(configPath); + build(config); + + //set timers combo box callbacks + if(auto w = widget("timerModeSwitch")) + { + w->onConstructItems = [&](std::vector & curItems){ + if(variables["timers"].isNull()) + return; + + for(auto & p : variables["timers"].Vector()) + { + curItems.push_back(&p); + } + }; + + w->onSetItem = [&](const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + { + for(auto wname : (*tObj)["hideWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(false); + } + for(auto wname : (*tObj)["showWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(true); + } + if((*tObj)["default"].isVector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; + tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; + tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; + tinfo.unitTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } + } + redraw(); + } + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + return readText((*tObj)["text"]); + } + return std::string(""); + }; + + w->setItem(0); + } + + if(auto w = widget("simturnsPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["simturnsPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setSimturnsPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setSimturnsPresetCallback(itemIndex); + }; + } + + if(auto w = widget("timerPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["timerPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setTimerPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setTimerPresetCallback(itemIndex); + }; + } +} + +void OptionsTabBase::recreate() +{ + auto const & generateSimturnsDurationText = [](int days) -> std::string + { + if (days == 0) + return CGI->generaltexth->translate("core.genrltxt.523"); + + if (days >= 1000000) // Not "unlimited" but close enough + return CGI->generaltexth->translate("core.turndur.10"); + + bool canUseMonth = days % 28 == 0 && days >= 28*2; + bool canUseWeek = days % 7 == 0 && days >= 7*2; + + int value = days; + std::string text = "vcmi.optionsTab.simturns.days"; + + if (canUseWeek && !canUseMonth) + { + value = days / 7; + text = "vcmi.optionsTab.simturns.weeks"; + } + + if (canUseMonth) + { + value = days / 28; + text = "vcmi.optionsTab.simturns.months"; + } + + MetaString message; + message.appendTextID(Languages::getPluralFormTextID( CGI->generaltexth->getPreferredLanguage(), value, text)); + message.replaceNumber(value); + return message.toString(); + }; + + //Simultaneous turns + if(auto turnSlider = widget("simturnsDurationMin")) + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns); + + if(auto turnSlider = widget("simturnsDurationMax")) + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns); + + if(auto w = widget("labelSimturnsDurationValueMin")) + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); + + if(auto w = widget("labelSimturnsDurationValueMax")) + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.optionalTurns)); + + if(auto buttonSimturnsAI = widget("buttonSimturnsAI")) + buttonSimturnsAI->setSelectedSilent(SEL->getStartInfo()->simturnsInfo.allowHumanWithAI); + + if(auto buttonTurnTimerAccumulate = widget("buttonTurnTimerAccumulate")) + buttonTurnTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer); + + if(auto chessFieldTurnLabel = widget("chessFieldTurnLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer) + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnAccumulate.help")); + else + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnDiscard.help")); + } + + if(auto chessFieldUnitLabel = widget("chessFieldUnitLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer) + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitAccumulate.help")); + else + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitDiscard.help")); + } + + if(auto buttonUnitTimerAccumulate = widget("buttonUnitTimerAccumulate")) + buttonUnitTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer); + + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; + + //classic timer + if(auto turnSlider = widget("sliderTurnDuration")) + { + if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.unitTimer && !turnTimerRemote.baseTimer) + { + for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) + { + auto & tpreset = variables["timerPresets"].Vector()[idx]; + if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) + { + turnSlider->scrollTo(idx); + if(auto w = widget("labelTurnDurationValue")) + w->setText(CGI->generaltexth->turnDurations[idx]); + } + } + } + } + + //chess timer + auto timeToString = [](int time) -> std::string + { + std::stringstream ss; + ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; + return ss.str(); + }; + + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(turnTimerRemote.baseTimer), false); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(turnTimerRemote.turnTimer), false); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(turnTimerRemote.battleTimer), false); + if(auto ww = widget("chessFieldUnit")) + ww->setText(timeToString(turnTimerRemote.unitTimer), false); + + if(auto w = widget("timerModeSwitch")) + { + if(turnTimerRemote.battleTimer || turnTimerRemote.unitTimer || turnTimerRemote.baseTimer) + { + if(auto turnSlider = widget("sliderTurnDuration")) + if(turnSlider->isActive()) + w->setItem(1); + } + } +} diff --git a/client/lobby/OptionsTabBase.h b/client/lobby/OptionsTabBase.h new file mode 100644 index 000000000..dc7bf63f2 --- /dev/null +++ b/client/lobby/OptionsTabBase.h @@ -0,0 +1,32 @@ +/* + * OptionsTabBase.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/InterfaceObjectConfigurable.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct TurnTimerInfo; +struct SimturnsInfo; + +VCMI_LIB_NAMESPACE_END + +/// The options tab which is shown at the map selection phase. +class OptionsTabBase : public InterfaceObjectConfigurable +{ + std::vector getTimerPresets() const; + std::vector getSimturnsPresets() const; + +public: + OptionsTabBase(const JsonPath & configPath); + + void recreate(); +}; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 2a0ca914f..1deff1ece 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -11,6 +11,8 @@ #include "RandomMapTab.h" #include "CSelectionBase.h" +#include "CLobbyScreen.h" +#include "SelectionTab.h" #include "../CGameInfo.h" #include "../CServerHandler.h" @@ -18,6 +20,7 @@ #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" @@ -31,7 +34,6 @@ #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/rmg/CMapGenOptions.h" -#include "../../lib/CModHandler.h" #include "../../lib/rmg/CRmgTemplateStorage.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/RoadHandler.h" @@ -42,7 +44,6 @@ RandomMapTab::RandomMapTab(): recActions = 0; mapGenOptions = std::make_shared(); - const JsonNode config(ResourceID("config/widgets/randomMapTab.json")); addCallback("toggleMapSize", [&](int btnId) { auto mapSizeVal = getPossibleMapSizes(); @@ -64,7 +65,7 @@ RandomMapTab::RandomMapTab(): addCallback("setPlayersCount", [&](int btnId) { - mapGenOptions->setPlayerCount(btnId); + mapGenOptions->setHumanOrCpuPlayerCount(btnId); setMapGenOptions(mapGenOptions); updateMapInfoByHost(); }); @@ -104,14 +105,9 @@ RandomMapTab::RandomMapTab(): }); //new callbacks available only from mod - addCallback("templateSelection", [&](int) - { - GH.windows().createAndPushWindow(*this, int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), 1 + mapGenOptions->getHasTwoLevels()}); - }); - addCallback("teamAlignments", [&](int) { - GH.windows().createAndPushWindow(*this); + GH.windows().createAndPushWindow(*this); }); for(auto road : VLC->roadTypeHandler->objects) @@ -124,8 +120,48 @@ RandomMapTab::RandomMapTab(): }); } + const JsonNode config(JsonPath::builtin("config/widgets/randomMapTab.json")); build(config); + if(auto w = widget("buttonShowRandomMaps")) + { + w->addCallback([&]() + { + (static_cast(parent))->toggleTab((static_cast(parent))->tabSel); + (static_cast(parent))->tabSel->showRandom = true; + (static_cast(parent))->tabSel->filter(0, true); + }); + } + + //set combo box callbacks + if(auto w = widget("templateList")) + { + w->onConstructItems = [](std::vector & curItems){ + auto templates = VLC->tplh->getTemplates(); + + boost::range::sort(templates, [](const CRmgTemplate * a, const CRmgTemplate * b){ + return a->getName() < b->getName(); + }); + + curItems.push_back(nullptr); //default template + + for(auto & t : templates) + curItems.push_back(t); + }; + + w->onSetItem = [&](const void * item){ + this->setTemplate(reinterpret_cast(item)); + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + return reinterpret_cast(item)->getName(); + if(idx == 0) + return readText(variables["randomTemplate"]); + return std::string(""); + }; + } + updateMapInfoByHost(); } @@ -139,63 +175,46 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->isRandomMap = true; mapInfo->mapHeader = std::make_unique(); mapInfo->mapHeader->version = EMapFormat::VCMI; - mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740]; - mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741]; + mapInfo->mapHeader->name.appendLocalString(EMetaText::GENERAL_TXT, 740); + mapInfo->mapHeader->description.appendLocalString(EMetaText::GENERAL_TXT, 741); mapInfo->mapHeader->difficulty = 1; // Normal mapInfo->mapHeader->height = mapGenOptions->getHeight(); mapInfo->mapHeader->width = mapGenOptions->getWidth(); mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); // Generate player information - int playersToGen = PlayerColor::PLAYER_LIMIT_I; - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) - { - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) - playersToGen = mapGenOptions->getPlayerCount() + mapGenOptions->getCompOnlyPlayerCount(); - else - playersToGen = mapGenOptions->getPlayerCount(); - } + int playersToGen = mapGenOptions->getMaxPlayersCount(); mapInfo->mapHeader->howManyTeams = playersToGen; - std::set occupiedTeams; + //TODO: Assign all human-controlled colors in first place + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { mapInfo->mapHeader->players[i].canComputerPlay = false; mapInfo->mapHeader->players[i].canHumanPlay = false; } - for(int i = 0; i < playersToGen; ++i) + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) { - PlayerInfo player; - player.isFactionRandom = true; - player.canComputerPlay = true; - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE && i >= mapGenOptions->getPlayerCount()) - { - player.canHumanPlay = false; - } - else - { - player.canHumanPlay = true; - } - auto team = mapGenOptions->getPlayersSettings().at(PlayerColor(i)).getTeam(); - player.team = team; - occupiedTeams.insert(team); - player.hasMainTown = true; - player.generateHeroAtMainTown = true; - mapInfo->mapHeader->players[i] = player; + availableColors.push_back(PlayerColor(color)); } - for(auto & player : mapInfo->mapHeader->players) + + //First restore known players + for (auto& player : mapGenOptions->getPlayersSettings()) { - for(int i = 0; player.team == TeamID::NO_TEAM; ++i) - { - TeamID team(i); - if(!occupiedTeams.count(team)) - { - player.team = team; - occupiedTeams.insert(team); - } - } + PlayerInfo playerInfo; + playerInfo.isFactionRandom = (player.second.getStartingTown() == FactionID::RANDOM); + playerInfo.canComputerPlay = (player.second.getPlayerType() != EPlayerType::HUMAN); + playerInfo.canHumanPlay = (player.second.getPlayerType() != EPlayerType::COMP_ONLY); + + auto team = player.second.getTeam(); + playerInfo.team = team; + playerInfo.hasMainTown = true; + playerInfo.generateHeroAtMainTown = true; + mapInfo->mapHeader->players[player.first] = playerInfo; + vstd::erase(availableColors, player.first); } mapInfoChanged(mapInfo, mapGenOptions); @@ -205,47 +224,72 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { mapGenOptions = opts; - //prepare allowed options + //Prepare allowed options - add all, then erase the ones above the limit for(int i = 0; i <= PlayerColor::PLAYER_LIMIT_I; ++i) { playerCountAllowed.insert(i); compCountAllowed.insert(i); - playerTeamsAllowed.insert(i); - compTeamsAllowed.insert(i); + if (i >= 2) + { + playerTeamsAllowed.insert(i); + } + if (i >= 1) + { + compTeamsAllowed.insert(i); + } } + std::set humanCountAllowed; + auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) { playerCountAllowed = tmpl->getPlayers().getNumbers(); - compCountAllowed = tmpl->getCpuPlayers().getNumbers(); + humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now? } - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) + + si8 playerLimit = opts->getMaxPlayersCount(); + si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); + si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); + + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) { - vstd::erase_if(compCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getPlayerCount() < el; + vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) + { + return (playerLimit - humanOrCpuPlayerCount) < el; }); - vstd::erase_if(playerTeamsAllowed, - [opts](int el){ - return opts->getPlayerCount() <= el; + vstd::erase_if(playerTeamsAllowed, [humanOrCpuPlayerCount](int el) + { + return humanOrCpuPlayerCount <= el; }); - - if(!playerTeamsAllowed.count(opts->getTeamCount())) - opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); } - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) + else // Random { - vstd::erase_if(playerCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getCompOnlyPlayerCount() < el; + vstd::erase_if(compCountAllowed, [playerLimit](int el) + { + return (playerLimit - 1) < el; // Must leave at least 1 human player }); - vstd::erase_if(compTeamsAllowed, - [opts](int el){ - return opts->getCompOnlyPlayerCount() <= el; + vstd::erase_if(playerTeamsAllowed, [playerLimit](int el) + { + return playerLimit <= el; + }); + } + if(!playerTeamsAllowed.count(opts->getTeamCount())) + { + opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); + } + + if(compOnlyPlayersCount != CMapGenOptions::RANDOM_SIZE) + { + // This setting doesn't impact total number of players + vstd::erase_if(compTeamsAllowed, [compOnlyPlayersCount](int el) + { + return compOnlyPlayersCount<= el; }); if(!compTeamsAllowed.count(opts->getCompOnlyTeamCount())) + { opts->setCompOnlyTeamCount(CMapGenOptions::RANDOM_SIZE); + } } if(auto w = widget("groupMapSize")) @@ -274,7 +318,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) } if(auto w = widget("groupMaxPlayers")) { - w->setSelected(opts->getPlayerCount()); + w->setSelected(opts->getHumanOrCpuPlayerCount()); deactivateButtonsFrom(*w, playerCountAllowed); } if(auto w = widget("groupMaxTeams")) @@ -308,9 +352,9 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); + w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } for(auto r : VLC->roadTypeHandler->objects) { @@ -328,9 +372,9 @@ void RandomMapTab::setTemplate(const CRmgTemplate * tmpl) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); + w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } updateMapInfoByHost(); } @@ -361,181 +405,53 @@ std::vector RandomMapTab::getPossibleMapSizes() return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_GIANT}; } -TemplatesDropBox::ListItem::ListItem(const JsonNode & config, TemplatesDropBox & _dropBox, Point position) - : InterfaceObjectConfigurable(LCLICK | HOVER, position), - dropBox(_dropBox) +void TeamAlignmentsWidget::checkTeamCount() { - OBJ_CONSTRUCTION; - - build(config); - - if(auto w = widget("hoverImage")) + //Do not allow to select one team only + std::set teams; + for (int plId = 0; plId < players.size(); ++plId) { - pos.w = w->pos.w; - pos.h = w->pos.h; + teams.insert(TeamID(players[plId]->getSelected())); } - setRedrawParent(true); -} - -void TemplatesDropBox::ListItem::updateItem(int idx, const CRmgTemplate * _item) -{ - if(auto w = widget("labelName")) + if (teams.size() < 2) { - item = _item; - if(item) - { - w->setText(item->getName()); - } - else - { - if(idx) - w->setText(""); - else - w->setText(readText(dropBox.variables["randomTemplate"])); - } + //Do not let player close the window + buttonOk->block(true); + } + else + { + buttonOk->block(false); } } -void TemplatesDropBox::ListItem::hover(bool on) +TeamAlignments::TeamAlignments(RandomMapTab & randomMapTab) + : CWindowObject(BORDERED) { - auto h = widget("hoverImage"); - auto w = widget("labelName"); - if(h && w) - { - if(w->getText().empty()) - h->visible = false; - else - h->visible = on; - } - redraw(); -} + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; -void TemplatesDropBox::ListItem::clickPressed(const Point & cursorPosition) -{ - if(isHovered()) - dropBox.setTemplate(item); -} + widget = std::make_shared(randomMapTab); + pos = widget->pos; -void TemplatesDropBox::ListItem::clickReleased(const Point & cursorPosition) -{ - dropBox.clickPressed(cursorPosition); - dropBox.clickReleased(cursorPosition); -} - -TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size): - InterfaceObjectConfigurable(LCLICK | HOVER), - randomMapTab(randomMapTab) -{ - REGISTER_BUILDER("templateListItem", &TemplatesDropBox::buildListItem); - - curItems = VLC->tplh->getTemplates(); - - boost::range::sort(curItems, [](const CRmgTemplate * a, const CRmgTemplate * b){ - return a->getName() < b->getName(); - }); - - curItems.insert(curItems.begin(), nullptr); //default template - - const JsonNode config(ResourceID("config/widgets/randomMapTemplateWidget.json")); - - addCallback("sliderMove", std::bind(&TemplatesDropBox::sliderMove, this, std::placeholders::_1)); - - OBJ_CONSTRUCTION; - pos = randomMapTab.pos; - - build(config); - - if(auto w = widget("slider")) - { - w->setAmount(curItems.size()); - } - - //FIXME: this should be done by InterfaceObjectConfigurable, but might have side-effects - pos = children.front()->pos; - for (auto const & child : children) - pos = pos.include(child->pos); - - updateListItems(); -} - -std::shared_ptr TemplatesDropBox::buildListItem(const JsonNode & config) -{ - auto position = readPosition(config["position"]); - listItems.push_back(std::make_shared(config, *this, position)); - return listItems.back(); -} - -void TemplatesDropBox::sliderMove(int slidPos) -{ - auto w = widget("slider"); - if(!w) - return; // ignore spurious call when slider is being created - updateListItems(); - redraw(); -} - -bool TemplatesDropBox::receiveEvent(const Point & position, int eventType) const -{ - if (eventType == LCLICK) - return true; // we want drop box to close when clicking outside drop box borders - - return CIntObject::receiveEvent(position, eventType); -} - -void TemplatesDropBox::clickPressed(const Point & cursorPosition) -{ - if (!pos.isInside(cursorPosition)) - { - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); - } -} - -void TemplatesDropBox::updateListItems() -{ - if(auto w = widget("slider")) - { - int elemIdx = w->getValue(); - for(auto item : listItems) - { - if(elemIdx < curItems.size()) - { - item->updateItem(elemIdx, curItems[elemIdx]); - elemIdx++; - } - else - { - item->updateItem(elemIdx); - } - } - } -} - -void TemplatesDropBox::setTemplate(const CRmgTemplate * tmpl) -{ - randomMapTab.setTemplate(tmpl); - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); + updateShadow(); + center(); } TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { - const JsonNode config(ResourceID("config/widgets/randomMapTeamsWidget.json")); + const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; - int humanPlayers = randomMapTab.obtainMapGenOptions().getPlayerCount(); - int cpuPlayers = randomMapTab.obtainMapGenOptions().getCompOnlyPlayerCount(); - int totalPlayers = humanPlayers == CMapGenOptions::RANDOM_SIZE || cpuPlayers == CMapGenOptions::RANDOM_SIZE - ? PlayerColor::PLAYER_LIMIT_I : humanPlayers + cpuPlayers; + //int totalPlayers = randomMapTab.obtainMapGenOptions().getPlayerLimit(); + int totalPlayers = randomMapTab.obtainMapGenOptions().getMaxPlayersCount(); assert(totalPlayers <= PlayerColor::PLAYER_LIMIT_I); auto settings = randomMapTab.obtainMapGenOptions().getPlayersSettings(); variables["totalPlayers"].Integer() = totalPlayers; pos.w = variables["windowSize"]["x"].Integer() + totalPlayers * variables["cellMargin"]["x"].Integer(); pos.h = variables["windowSize"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); - variables["backgroundRect"]["x"].Integer() = pos.x; - variables["backgroundRect"]["y"].Integer() = pos.y; + variables["backgroundRect"]["x"].Integer() = 0; + variables["backgroundRect"]["y"].Integer() = 0; variables["backgroundRect"]["w"].Integer() = pos.w; variables["backgroundRect"]["h"].Integer() = pos.h; variables["okButtonPosition"]["x"].Integer() = variables["buttonsOffset"]["ok"]["x"].Integer(); @@ -550,14 +466,15 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): randomMapTab.obtainMapGenOptions().setPlayerTeam(PlayerColor(plId), TeamID(players[plId]->getSelected())); } randomMapTab.updateMapInfoByHost(); - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); + + for(auto & window : GH.windows().findWindows()) + GH.windows().popWindow(window); }); addCallback("cancel", [&](int) { - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); + for(auto & window : GH.windows().findWindows()) + GH.windows().popWindow(window); }); build(config); @@ -566,6 +483,27 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): OBJ_CONSTRUCTION; + // Window should have X * X columns, where X is max players allowed for current settings + // For random player count, X is 8 + + if (totalPlayers > settings.size()) + { + auto savedPlayers = randomMapTab.obtainMapGenOptions().getSavedPlayersMap(); + for (const auto & player : savedPlayers) + { + if (!vstd::contains(settings, player.first)) + { + settings[player.first] = player.second; + } + } + } + + std::vector settingsVec; + for (const auto & player : settings) + { + settingsVec.push_back(player.second); + } + for(int plId = 0; plId < totalPlayers; ++plId) { players.push_back(std::make_shared([&, totalPlayers, plId](int sel) @@ -584,10 +522,15 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): { button->addOverlay(nullptr); } + button->addCallback([this](bool) + { + checkTeamCount(); + }); } })); OBJ_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(); @@ -596,10 +539,17 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): players.back()->addToggle(teamId, std::dynamic_pointer_cast(button)); } - auto team = settings.at(PlayerColor(plId)).getTeam(); + // plId is not neccessarily player color, just an index + auto team = settingsVec.at(plId).getTeam(); if(team == TeamID::NO_TEAM) + { + logGlobal->warn("Player %d (id %d) has uninitialized team", settingsVec.at(plId).getColor(), plId); players.back()->setSelected(plId); + } else players.back()->setSelected(team.getNum()); } + + buttonOk = widget("buttonOK"); + buttonCancel = widget("buttonCancel"); } diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index 356085ab5..e46b0ba4b 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -51,52 +51,24 @@ private: std::set playerCountAllowed, playerTeamsAllowed, compCountAllowed, compTeamsAllowed; }; -class TemplatesDropBox : public InterfaceObjectConfigurable -{ - struct ListItem : public InterfaceObjectConfigurable - { - TemplatesDropBox & dropBox; - const CRmgTemplate * item = nullptr; - - ListItem(const JsonNode &, TemplatesDropBox &, Point position); - void updateItem(int index, const CRmgTemplate * item = nullptr); - - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void clickReleased(const Point & cursorPosition) override; - }; - - friend struct ListItem; - -public: - TemplatesDropBox(RandomMapTab & randomMapTab, int3 size); - - bool receiveEvent(const Point & position, int eventType) const override; - void clickPressed(const Point & cursorPosition) override; - void setTemplate(const CRmgTemplate *); - -private: - std::shared_ptr buildListItem(const JsonNode & config); - - void sliderMove(int slidPos); - void updateListItems(); - - RandomMapTab & randomMapTab; - std::vector> listItems; - - std::vector curItems; - -}; - class TeamAlignmentsWidget: public InterfaceObjectConfigurable { public: TeamAlignmentsWidget(RandomMapTab & randomMapTab); private: + void checkTeamCount(); + std::shared_ptr background; std::shared_ptr labels; std::shared_ptr buttonOk, buttonCancel; std::vector> players; std::vector> placeholders; }; + +class TeamAlignments: public CWindowObject +{ + std::shared_ptr widget; +public: + TeamAlignments(RandomMapTab & randomMapTab); +}; diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 09c4c6149..8098715f2 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -18,6 +18,7 @@ #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -26,24 +27,38 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../windows/CMapOverview.h" #include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.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/TerrainHandler.h" #include "../../lib/serializer/Connection.h" -bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) +bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { + if(aaa->isFolder || bbb->isFolder) + { + if(aaa->isFolder != bbb->isFolder) + return (aaa->isFolder > bbb->isFolder); + else + { + if(boost::algorithm::starts_with(aaa->folderName, "..") || boost::algorithm::starts_with(bbb->folderName, "..")) + return boost::algorithm::starts_with(aaa->folderName, ".."); + return boost::ilexicographical_compare(aaa->folderName, bbb->folderName); + } + } + auto a = aaa->mapHeader.get(); auto b = bbb->mapHeader.get(); if(a && b) //if we are sorting scenarios @@ -92,11 +107,11 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::share return (a->victoryIconIndex < b->victoryIconIndex); break; case _name: //by name - return boost::ilexicographical_compare(a->name, b->name); + return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); case _fileName: //by filename return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); default: - return boost::ilexicographical_compare(a->name, b->name); + return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); } } else //if we are sorting campaigns @@ -106,9 +121,9 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::share case _numOfMaps: //by number of maps in campaign return aaa->campaign->scenariosCount() < bbb->campaign->scenariosCount(); case _name: //by name - return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName()); + return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated()); default: - return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName()); + return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated()); } } } @@ -131,7 +146,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20} + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false) { OBJ_CONSTRUCTION; @@ -140,16 +155,16 @@ SelectionTab::SelectionTab(ESelectionScreen Type) if(tabType != ESelectionScreen::campaignList) { sortingBy = _format; - background = std::make_shared("SCSELBCK.bmp", 0, 6); + background = std::make_shared(ImagePath::builtin("SCSELBCK.bmp"), 0, 6); pos = background->pos; - inputName = std::make_shared(inputNameRect, Point(-32, -25), "GSSTRIP.bmp", 0); + inputName = std::make_shared(inputNameRect, Point(-32, -25), ImagePath::builtin("GSSTRIP.bmp"), 0); inputName->filters += CTextInput::filenameFilter; labelMapSizes = std::make_shared(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]); int sizes[] = {36, 72, 108, 144, 0}; const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; for(int i = 0; i < 5; i++) - buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); + buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); int xpos[] = {23, 55, 88, 121, 306, 339}; const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; @@ -159,7 +174,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) if(criteria == _name) criteria = generalSortingBy; - buttonsSortBy.push_back(std::make_shared(Point(xpos[i], 86), sortIconNames[i], CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria))); + buttonsSortBy.push_back(std::make_shared(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria))); } } @@ -185,17 +200,17 @@ SelectionTab::SelectionTab(ESelectionScreen Type) pos.x += 3; pos.y += 6; - buttonsSortBy.push_back(std::make_shared(Point(23, 86), "CamCusM.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps))); - buttonsSortBy.push_back(std::make_shared(Point(55, 86), "CamCusL.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name))); + buttonsSortBy.push_back(std::make_shared(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps))); + buttonsSortBy.push_back(std::make_shared(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name))); break; default: assert(0); break; } - iconsMapFormats = std::make_shared("SCSELC.DEF"); - iconsVictoryCondition = std::make_shared("SCNRVICT.DEF"); - iconsLossCondition = std::make_shared("SCNRLOSS.DEF"); + 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)); @@ -223,9 +238,13 @@ void SelectionTab::toggleMode() switch(tabType) { case ESelectionScreen::newGame: - inputName->disable(); - parseMaps(getFiles("Maps/", EResType::MAP)); - break; + { + inputName->disable(); + auto files = getFiles("Maps/", EResType::MAP); + files.erase(ResourcePath("Maps/Tutorial.tut", EResType::MAP)); + parseMaps(files); + break; + } case ESelectionScreen::loadGame: inputName->disable(); @@ -235,6 +254,7 @@ void SelectionTab::toggleMode() case ESelectionScreen::saveGame: parseSaves(getFiles("Saves/", EResType::SAVEGAME)); inputName->enable(); + inputName->activate(); restoreLastSelection(); break; @@ -314,6 +334,18 @@ void SelectionTab::keyPressed(EShortcut key) void SelectionTab::clickDouble(const Point & cursorPosition) { + int position = getLine(); + int itemIndex = position + slider->getValue(); + + if(itemIndex >= curItems.size()) + return; + + if(itemIndex >= 0 && curItems[itemIndex]->isFolder) + { + select(position); + return; + } + if(getLine() != -1) //double clicked scenarios list { (static_cast(parent))->buttonStart->clickPressed(cursorPosition); @@ -329,29 +361,99 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(py >= curItems.size()) return; - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); - if(curItems[py]->date != "") - text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); + if(!curItems[py]->isFolder) + GH.windows().createAndPushWindow(curItems[py]->getNameTranslated(), curItems[py]->fullFileURI, curItems[py]->date, ResourcePath(curItems[py]->fileURI), tabType); + else + CRClickPopup::createAndPush(curItems[py]->folderName); +} - CRClickPopup::createAndPush(text); +auto SelectionTab::checkSubfolder(std::string path) +{ + struct Ret + { + std::string folderName; + std::string baseFolder; + bool parentExists; + bool fileInFolder; + } ret; + + ret.parentExists = (curFolder != ""); + ret.fileInFolder = false; + + std::vector filetree; + // delete first element (e.g. 'MAPS') + boost::split(filetree, path, boost::is_any_of("/")); + filetree.erase(filetree.begin()); + std::string pathWithoutPrefix = boost::algorithm::join(filetree, "/"); + + if(!filetree.empty()) + { + filetree.pop_back(); + ret.baseFolder = boost::algorithm::join(filetree, "/"); + } + else + ret.baseFolder = ""; + + if(boost::algorithm::starts_with(ret.baseFolder, curFolder)) + { + std::string folder = ret.baseFolder.substr(curFolder.size()); + + if(folder != "") + { + boost::split(filetree, folder, boost::is_any_of("/")); + ret.folderName = filetree[0]; + } + } + + if(boost::algorithm::starts_with(pathWithoutPrefix, curFolder)) + if(boost::count(pathWithoutPrefix.substr(curFolder.size()), '/') == 0) + ret.fileInFolder = true; + + return ret; } // A new size filter (Small, Medium, ...) has been selected. Populate // selMaps with the relevant data. void SelectionTab::filter(int size, bool selectFirst) { + if(size == -1) + size = currentMapSizeFilter; + currentMapSizeFilter = size; + curItems.clear(); - if(tabType == ESelectionScreen::campaignList) + for(auto elem : allItems) { - for(auto elem : allItems) - curItems.push_back(elem); - } - else - { - for(auto elem : allItems) + if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { - if(elem->mapHeader && (!size || elem->mapHeader->width == size)) + if(showRandom) + curFolder = "RANDOMMAPS/"; + + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->originalFileURI); + + if((showRandom && baseFolder != "RANDOMMAPS") || (!showRandom && baseFolder == "RANDOMMAPS")) + continue; + + if(parentExists && !showRandom) + { + auto folder = std::make_shared(); + folder->isFolder = true; + folder->folderName = ".. (" + curFolder + ")"; + auto itemIt = boost::range::find_if(curItems, [](std::shared_ptr e) { return boost::starts_with(e->folderName, ".."); }); + if (itemIt == curItems.end()) { + curItems.push_back(folder); + } + } + + std::shared_ptr folder = std::make_shared(); + folder->isFolder = true; + folder->folderName = folderName; + 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); + } + + if(fileInFolder) curItems.push_back(elem); } } @@ -363,13 +465,19 @@ void SelectionTab::filter(int size, bool selectFirst) sort(); if(selectFirst) { - slider->scrollTo(0); - callOnSelect(curItems[0]); - selectAbs(0); + int firstPos = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + if(firstPos < curItems.size()) + { + slider->scrollTo(firstPos); + callOnSelect(curItems[firstPos]); + selectAbs(firstPos); + } } } else { + updateListItems(); + redraw(); slider->block(true); if(callOnSelect) callOnSelect(nullptr); @@ -389,7 +497,7 @@ void SelectionTab::sortBy(int criteria) } sort(); - selectAbs(0); + selectAbs(-1); } void SelectionTab::sort() @@ -398,8 +506,9 @@ void SelectionTab::sort() std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); + int firstMapIndex = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); if(!sortModeAscending) - std::reverse(curItems.begin(), curItems.end()); + std::reverse(std::next(curItems.begin(), firstMapIndex), curItems.end()); updateListItems(); redraw(); @@ -422,11 +531,34 @@ void SelectionTab::select(int position) else if(position >= listItems.size()) slider->scrollBy(position - (int)listItems.size() + 1); + if(curItems[py]->isFolder) { + if(boost::starts_with(curItems[py]->folderName, "..")) + { + std::vector filetree; + boost::split(filetree, curFolder, boost::is_any_of("/")); + filetree.pop_back(); + filetree.pop_back(); + curFolder = filetree.size() > 0 ? boost::algorithm::join(filetree, "/") + "/" : ""; + } + else + curFolder += curItems[py]->folderName + "/"; + filter(-1); + slider->scrollTo(0); + + int firstPos = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + if(firstPos < curItems.size()) + { + selectAbs(firstPos); + } + + return; + } + rememberCurrentSelection(); if(inputName && inputName->isActive()) { - auto filename = *CResourceHandler::get()->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); + auto filename = *CResourceHandler::get()->getResourceName(ResourcePath(curItems[py]->fileURI, EResType::SAVEGAME)); inputName->setText(filename.stem().string()); } @@ -438,6 +570,8 @@ void SelectionTab::select(int position) void SelectionTab::selectAbs(int position) { + if(position == -1) + position = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); select(position - slider->getValue()); } @@ -503,6 +637,16 @@ int SelectionTab::getLine(const Point & clickPos) const void SelectionTab::selectFileName(std::string fname) { boost::to_upper(fname); + + for(int i = (int)allItems.size() - 1; i >= 0; i--) + { + if(allItems[i]->fileURI == fname) + { + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(allItems[i]->originalFileURI); + curFolder = baseFolder != "" ? baseFolder + "/" : ""; + } + } + for(int i = (int)curItems.size() - 1; i >= 0; i--) { if(curItems[i]->fileURI == fname) @@ -513,16 +657,20 @@ void SelectionTab::selectFileName(std::string fname) } } - selectAbs(0); + filter(-1); + selectAbs(-1); } -std::shared_ptr SelectionTab::getSelectedMapInfo() const +std::shared_ptr SelectionTab::getSelectedMapInfo() const { - return curItems.empty() ? nullptr : curItems[selectionPos]; + return curItems.empty() || curItems[selectionPos]->isFolder ? nullptr : curItems[selectionPos]; } void SelectionTab::rememberCurrentSelection() { + if(getSelectedMapInfo()->isFolder) + return; + // TODO: this can be more elegant if(tabType == ESelectionScreen::newGame) { @@ -577,7 +725,7 @@ bool SelectionTab::isMapSupported(const CMapInfo & info) return false; } -void SelectionTab::parseMaps(const std::unordered_set & files) +void SelectionTab::parseMaps(const std::unordered_set & files) { logGlobal->debug("Parsing %d maps", files.size()); allItems.clear(); @@ -585,7 +733,7 @@ void SelectionTab::parseMaps(const std::unordered_set & files) { try { - auto mapInfo = std::make_shared(); + auto mapInfo = std::make_shared(); mapInfo->mapInit(file.getName()); if (isMapSupported(*mapInfo)) @@ -598,28 +746,33 @@ void SelectionTab::parseMaps(const std::unordered_set & files) } } -void SelectionTab::parseSaves(const std::unordered_set & files) +void SelectionTab::parseSaves(const std::unordered_set & files) { for(auto & file : files) { try { - auto mapInfo = std::make_shared(); + auto mapInfo = std::make_shared(); mapInfo->saveInit(file); // Filter out other game modes bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == StartInfo::CAMPAIGN; bool isMultiplayer = mapInfo->amountOfHumanPlayersInSave > 1; + bool isTutorial = boost::to_upper_copy(mapInfo->scenarioOptionsOfSave->mapname) == "MAPS/TUTORIAL"; switch(CSH->getLoadMode()) { case ELoadMode::SINGLE: - if(isMultiplayer || isCampaign) + if(isMultiplayer || isCampaign || isTutorial) mapInfo->mapHeader.reset(); break; case ELoadMode::CAMPAIGN: if(!isCampaign) mapInfo->mapHeader.reset(); break; + case ELoadMode::TUTORIAL: + if(!isTutorial) + mapInfo->mapHeader.reset(); + break; default: if(!isMultiplayer) mapInfo->mapHeader.reset(); @@ -635,12 +788,12 @@ void SelectionTab::parseSaves(const std::unordered_set & files) } } -void SelectionTab::parseCampaigns(const std::unordered_set & files) +void SelectionTab::parseCampaigns(const std::unordered_set & files) { allItems.reserve(files.size()); for(auto & file : files) { - auto info = std::make_shared(); + auto info = std::make_shared(); //allItems[i].date = std::asctime(std::localtime(&files[i].date)); info->fileURI = file.getName(); info->campaignInit(); @@ -649,7 +802,7 @@ void SelectionTab::parseCampaigns(const std::unordered_set & files) } } -std::unordered_set SelectionTab::getFiles(std::string dirURI, int resType) +std::unordered_set SelectionTab::getFiles(std::string dirURI, EResType resType) { boost::to_upper(dirURI); CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) @@ -657,7 +810,7 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re return boost::algorithm::starts_with(mount, dirURI); }); - std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); }); @@ -669,6 +822,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico : 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); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -678,17 +832,20 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico labelMapSizeLetter = std::make_shared(41, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); 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); } -void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) +void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) { if(!info) { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->disable(); iconVictoryCondition->disable(); iconLossCondition->disable(); @@ -698,10 +855,28 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool sel } auto color = selected ? Colors::YELLOW : Colors::WHITE; + if(info->isFolder) + { + labelAmountOfPlayers->disable(); + labelMapSizeLetter->disable(); + iconFolder->enable(); + pictureEmptyLine->enable(); + iconFormat->disable(); + iconVictoryCondition->disable(); + iconLossCondition->disable(); + labelNumberOfCampaignMaps->disable(); + labelName->enable(); + labelName->setText(info->folderName); + labelName->setColor(color); + return; + } + if(info->campaign) { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->disable(); iconVictoryCondition->disable(); iconLossCondition->disable(); @@ -722,6 +897,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool sel labelMapSizeLetter->enable(); labelMapSizeLetter->setText(info->getMapSizeName()); labelMapSizeLetter->setColor(color); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->enable(); iconFormat->setFrame(info->getMapSizeFormatIconId()); iconVictoryCondition->enable(); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 74bc41233..b457bcef8 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -10,21 +10,37 @@ #pragma once #include "CSelectionBase.h" +VCMI_LIB_NAMESPACE_BEGIN +class CMap; +VCMI_LIB_NAMESPACE_END +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/filesystem/ResourcePath.h" class CSlider; class CLabel; +class CPicture; +class IImage; enum ESortBy { _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName }; //_numOfMaps is for campaigns +class ElementInfo : public CMapInfo +{ +public: + ElementInfo() : CMapInfo() { } + ~ElementInfo() { } + std::string folderName = ""; + bool isFolder = false; +}; + /// Class which handles map sorting by different criteria class mapSorter { public: ESortBy sortBy; - bool operator()(const std::shared_ptr aaa, const std::shared_ptr bbb); + bool operator()(const std::shared_ptr aaa, const std::shared_ptr bbb); mapSorter(ESortBy es) : sortBy(es){}; }; @@ -35,13 +51,15 @@ class SelectionTab : public CIntObject std::shared_ptr labelAmountOfPlayers; std::shared_ptr labelNumberOfCampaignMaps; std::shared_ptr labelMapSizeLetter; + std::shared_ptr iconFolder; std::shared_ptr iconFormat; std::shared_ptr iconVictoryCondition; std::shared_ptr iconLossCondition; + std::shared_ptr pictureEmptyLine; std::shared_ptr labelName; ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); - void updateItem(std::shared_ptr info = {}, bool selected = false); + void updateItem(std::shared_ptr info = {}, bool selected = false); }; std::vector> listItems; @@ -49,16 +67,18 @@ class SelectionTab : public CIntObject // FIXME: CSelectionBase use them too! std::shared_ptr iconsVictoryCondition; std::shared_ptr iconsLossCondition; - public: - std::vector> allItems; - std::vector> curItems; + std::vector> allItems; + std::vector> curItems; + std::string curFolder; size_t selectionPos; - std::function)> callOnSelect; + std::function)> callOnSelect; ESortBy sortingBy; ESortBy generalSortingBy; bool sortModeAscending; + int currentMapSizeFilter = 0; + bool showRandom; std::shared_ptr inputName; @@ -81,12 +101,11 @@ public: int getLine() const; int getLine(const Point & position) const; void selectFileName(std::string fname); - std::shared_ptr getSelectedMapInfo() const; + std::shared_ptr getSelectedMapInfo() const; void rememberCurrentSelection(); void restoreLastSelection(); private: - std::shared_ptr background; std::shared_ptr slider; std::vector> buttonsSortBy; @@ -95,9 +114,11 @@ private: ESelectionScreen tabType; Rect inputNameRect; + auto checkSubfolder(std::string path); + bool isMapSupported(const CMapInfo & info); - void parseMaps(const std::unordered_set & files); - void parseSaves(const std::unordered_set & files); - void parseCampaigns(const std::unordered_set & files); - std::unordered_set getFiles(std::string dirURI, int resType); + void parseMaps(const std::unordered_set & files); + void parseSaves(const std::unordered_set & files); + void parseCampaigns(const std::unordered_set & files); + std::unordered_set getFiles(std::string dirURI, EResType resType); }; diff --git a/client/lobby/TurnOptionsTab.cpp b/client/lobby/TurnOptionsTab.cpp new file mode 100644 index 000000000..6bf3523a8 --- /dev/null +++ b/client/lobby/TurnOptionsTab.cpp @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.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 "TurnOptionsTab.h" + +TurnOptionsTab::TurnOptionsTab() + : OptionsTabBase(JsonPath::builtin("config/widgets/turnOptionsTab.json")) +{ + +} diff --git a/client/lobby/TurnOptionsTab.h b/client/lobby/TurnOptionsTab.h new file mode 100644 index 000000000..ad9ad1450 --- /dev/null +++ b/client/lobby/TurnOptionsTab.h @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.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 "OptionsTabBase.h" + +class TurnOptionsTab : public OptionsTabBase +{ +public: + TurnOptionsTab(); +}; diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index d1c9fd328..4078908e1 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -47,12 +47,12 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CCampaignScreen::CCampaignScreen(const JsonNode & config) - : CWindowObject(BORDERED) +CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) + : CWindowObject(BORDERED), campaignSet(name) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - for(const JsonNode & node : config["images"].Vector()) + for(const JsonNode & node : config[name]["images"].Vector()) images.push_back(CMainMenu::createPicture(node)); if(!images.empty()) @@ -63,19 +63,19 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config) pos = images[0]->pos; // fix height\width of this window } - if(!config["exitbutton"].isNull()) + if(!config[name]["exitbutton"].isNull()) { - buttonBack = createExitButton(config["exitbutton"]); + buttonBack = createExitButton(config[name]["exitbutton"]); buttonBack->hoverable = true; } - for(const JsonNode & node : config["items"].Vector()) - campButtons.push_back(std::make_shared(node)); + for(const JsonNode & node : config[name]["items"].Vector()) + campButtons.push_back(std::make_shared(node, config, campaignSet)); } void CCampaignScreen::activate() { - CCS->musich->playMusic("Music/MainMenu", true, false); + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, false); CWindowObject::activate(); } @@ -86,10 +86,11 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt if(!button["help"].isNull() && button["help"].Float() > 0) help = CGI->generaltexth->zelp[(size_t)button["help"].Float()]; - return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), button["name"].String(), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); + return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), AnimationPath::fromJson(button["name"]), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); } -CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) +CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet) + : campaignSet(campaignSet) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -99,24 +100,40 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) pos.h = 116; campFile = config["file"].String(); - video = config["video"].String(); + video = VideoPath::fromJson(config["video"]); - status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; + status = CCampaignScreen::ENABLED; auto header = CampaignHandler::getHeader(campFile); - hoverText = header->getName(); + hoverText = header->getNameTranslated(); + + if(persistentStorage["completedCampaigns"][header->getFilename()].Bool()) + status = CCampaignScreen::COMPLETED; + + for(const JsonNode & node : parentConfig[campaignSet]["items"].Vector()) + { + for(const JsonNode & requirement : config["requires"].Vector()) + { + if(node["id"].Integer() == requirement.Integer()) + if(!persistentStorage["completedCampaigns"][node["file"].String()].Bool()) + status = CCampaignScreen::DISABLED; + } + } + + if(persistentStorage["unlockAllCampaigns"].Bool()) + status = CCampaignScreen::ENABLED; if(status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); - graphicsImage = std::make_shared(config["image"].String()); + 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()); } if(status == CCampaignScreen::COMPLETED) - graphicsCompleted = std::make_shared("CAMPCHK"); + graphicsCompleted = std::make_shared(ImagePath::builtin("CAMPCHK")); } void CCampaignScreen::CCampaignButton::show(Canvas & to) @@ -128,27 +145,22 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to) // Play the campaign button video when the mouse cursor is placed over the button if(isHovered()) - { - if(CCS->videoh->fname != video) - CCS->videoh->open(video); - 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 - } - else if(CCS->videoh->fname == video) // When you got out of the bounds of the button then close the video - { - CCS->videoh->close(); - redraw(); - } } void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) { CCS->videoh->close(); - CMainMenu::openCampaignLobby(campFile); + CMainMenu::openCampaignLobby(campFile, campaignSet); } void CCampaignScreen::CCampaignButton::hover(bool on) { + if (on) + CCS->videoh->open(video); + else + CCS->videoh->close(); + if(hoverLabel) { if(on) diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index 0189980e6..e19395974 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -37,17 +37,21 @@ private: CampaignStatus status; std::string campFile; // the filename/resourcename of the campaign - std::string video; // the resource name of the video + VideoPath video; // the resource name of the video std::string hoverText; + std::string campaignSet; + void clickReleased(const Point & cursorPosition) override; void hover(bool on) override; public: - CCampaignButton(const JsonNode & config); + CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet); void show(Canvas & to) override; }; + std::string campaignSet; + std::vector> campButtons; std::vector> images; std::shared_ptr buttonBack; @@ -55,9 +59,7 @@ private: std::shared_ptr createExitButton(const JsonNode & button); public: - enum CampaignSet {ROE, AB, SOD, WOG}; - - CCampaignScreen(const JsonNode & config); + CCampaignScreen(const JsonNode & config, std::string campaignSet); void activate() override; }; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp new file mode 100644 index 000000000..b7e7508a2 --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -0,0 +1,387 @@ +/* + * CHighScoreScreen.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 "CHighScoreScreen.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/MiscWidgets.h" +#include "../windows/InfoWindows.h" +#include "../render/Canvas.h" + +#include "../CGameInfo.h" +#include "../CVideoHandler.h" +#include "../CMusicHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/TextOperations.h" + +#include "vstd/DateUtils.h" + +auto HighScoreCalculation::calculate() +{ + struct Result + { + int basic = 0; + int total = 0; + int sumDays = 0; + bool cheater = false; + }; + + Result firstResult, 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; +} + +CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) +{ + static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + auto creatures = configCreatures["creatures"].Vector(); + int divide = campaign ? 5 : 1; + + for(auto & creature : creatures) + if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer()) + return CreatureID::decode(creature["creature"].String()); + + return -1; +} + +CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) + : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) +{ + addUsedEvents(SHOW_POPUP); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + addHighScores(); + addButtons(); +} + +void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) +{ + for (int i = 0; i < screenRows; i++) + { + bool currentGameNotInListEntry = i == (screenRows - 1) && highlighted > (screenRows - 1); + + Rect r = Rect(80, 40 + i * 50, 635, 50); + if(r.isInside(cursorPosition - pos)) + { + std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][currentGameNotInListEntry ? highlighted : i]["datetime"].String(); + if(!tmp.empty()) + CRClickPopup::createAndPush(tmp); + } + } +} + +void CHighScoreScreen::addButtons() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + buttons.clear(); + + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); })); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); })); + buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); + buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); +} + +void CHighScoreScreen::addHighScores() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); + + texts.clear(); + images.clear(); + + // Header + texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); // rank + texts.push_back(std::make_shared(225, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); // player + + if(highscorepage == HighScorePage::SCENARIO) + { + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); // land + texts.push_back(std::make_shared(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); // days + texts.push_back(std::make_shared(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score + } + else + { + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); // campaign + texts.push_back(std::make_shared(592, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score + } + + // Content + int y = 66; + auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; + for (int i = 0; i < screenRows; i++) + { + bool currentGameNotInListEntry = (i == (screenRows - 1) && highlighted > (screenRows - 1)); + auto & curData = data[currentGameNotInListEntry ? highlighted : i]; + + ColorRGBA color = (i == highlighted || currentGameNotInListEntry) ? Colors::YELLOW : Colors::WHITE; + + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string((currentGameNotInListEntry ? highlighted : i) + 1))); + std::string tmp = curData["player"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); + texts.push_back(std::make_shared(225, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + + if(highscorepage == HighScorePage::SCENARIO) + { + std::string tmp = curData["scenarioName"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(557, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(627, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + else + { + std::string tmp = curData["campaignName"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(592, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + + if(curData["points"].Integer() > 0 && curData["points"].Integer() <= ((highscorepage == HighScorePage::CAMPAIGN) ? 2500 : 500)) + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); + } +} + +void CHighScoreScreen::buttonCampaignClick() +{ + highscorepage = HighScorePage::CAMPAIGN; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonScenarioClick() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + highscorepage = HighScorePage::SCENARIO; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonResetClick() +{ + CInfoWindow::showYesNoDialog( + CGI->generaltexth->allTexts[666], + {}, + [this]() + { + Settings entry = persistentStorage.write["highscore"]; + entry->clear(); + addHighScores(); + addButtons(); + redraw(); + }, + 0 + ); +} + +void CHighScoreScreen::buttonExitClick() +{ + close(); +} + +CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) + : CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1) +{ + addUsedEvents(LCLICK | KEYBOARD); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + background = std::make_shared(Rect(0, 0, pos.w, pos.h), Colors::BLACK); + + if(won) + { + int border = 100; + int textareaW = ((pos.w - 2 * border) / 4); + std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + + std::string creatureName = (calc.calculate().cheater) ? CGI->generaltexth->translate("core.genrltxt.260") : (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(calc.calculate().total, calc.isCampaign)]->getNameSingularTranslated(); + t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), creatureName }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); + + CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); + } + else + CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); + + video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; +} + +int CHighScoreInputScreen::addEntry(std::string text) { + std::vector baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"].Vector(); + + auto sortFunctor = [](const JsonNode & left, const JsonNode & right) + { + if(left["points"].Integer() == right["points"].Integer()) + return left["posFlag"].Bool() > right["posFlag"].Bool(); + return left["points"].Integer() > right["points"].Integer(); + }; + + JsonNode newNode = JsonNode(); + newNode["player"].String() = text; + if(calc.isCampaign) + newNode["campaignName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaignName; + else + newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName; + newNode["days"].Integer() = calc.calculate().sumDays; + newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; + newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(nullptr)); + newNode["posFlag"].Bool() = true; + + baseNode.push_back(newNode); + boost::range::sort(baseNode, sortFunctor); + + int pos = -1; + for (int i = 0; i < baseNode.size(); i++) + { + if(!baseNode[i]["posFlag"].isNull()) + { + baseNode[i]["posFlag"].clear(); + pos = i; + } + } + + Settings s = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"]; + s->Vector() = baseNode; + + return pos; +} + +void CHighScoreInputScreen::show(Canvas & 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(); + }); + redraw(); + + 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); +} + +void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(!won) + { + close(); + return; + } + + if(!input) + { + input = std::make_shared(calc.parameters[0].playerName, + [&] (std::string text) + { + if(!text.empty()) + { + int pos = addEntry(text); + close(); + GH.windows().createAndPushWindow(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO, pos); + } + else + close(); + }); + } +} + +void CHighScoreInputScreen::keyPressed(EShortcut key) +{ + 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; + + pos = center(Rect(0, 0, 232, 212)); + updateShadow(); + + text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); + + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput->setText(playerName); +} + +void CHighScoreInput::okay() +{ + ready(textInput->getText()); +} + +void CHighScoreInput::abort() +{ + ready(""); +} \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h new file mode 100644 index 000000000..2261dbe60 --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.h @@ -0,0 +1,111 @@ +/* + * CHighScoreScreen.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" + +class CButton; +class CLabel; +class CMultiLineLabel; +class CAnimImage; +class CTextInput; + +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: + enum HighScorePage { SCENARIO, CAMPAIGN }; + +private: + void addButtons(); + void addHighScores(); + + void buttonCampaignClick(); + void buttonScenarioClick(); + void buttonResetClick(); + void buttonExitClick(); + + void showPopupWindow(const Point & cursorPosition) override; + + HighScorePage highscorepage; + + std::shared_ptr background; + std::vector> buttons; + std::vector> texts; + std::vector> images; + + const int screenRows = 11; + + int highlighted; +public: + CHighScoreScreen(HighScorePage highscorepage, int highlighted = -1); +}; + +class CHighScoreInput : public CWindowObject +{ + std::shared_ptr text; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + std::shared_ptr textInput; + + std::function ready; + + void okay(); + void abort(); +public: + CHighScoreInput(std::string playerName, std::function readyCB); +}; + +class CHighScoreInputScreen : public CWindowObject +{ + std::vector> texts; + std::shared_ptr input; + std::shared_ptr background; + + std::string video; + int videoSoundHandle; + bool won; + HighScoreCalculation calc; +public: + CHighScoreInputScreen(bool won, HighScoreCalculation calc); + + 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 diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 6b72fb1e7..6e9cfdada 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -12,6 +12,7 @@ #include "CCampaignScreen.h" #include "CreditsScreen.h" +#include "CHighScoreScreen.h" #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" @@ -47,9 +48,10 @@ #include "../../lib/serializer/CTypeList.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/VCMIDirs.h" #include "../../lib/CStopWatch.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CThreadHelper.h" #include "../../lib/CConfigHandler.h" #include "../../lib/GameConstants.h" @@ -61,8 +63,6 @@ #include #endif -namespace fs = boost::filesystem; - std::shared_ptr CMM; ISelectionScreenInfo * SEL; @@ -79,7 +79,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared(config["background"].String()); + background = std::make_shared(ImagePath::fromJson(config["background"])); if(config["scalable"].Bool()) background->scaleTo(GH.screenDimensions()); @@ -95,7 +95,8 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) menuNameToEntry.push_back("credits"); tabs = std::make_shared(std::bind(&CMenuScreen::createTab, this, _1)); - tabs->setRedrawParent(true); + if(config["video"].isNull()) + tabs->setRedrawParent(true); } std::shared_ptr CMenuScreen::createTab(size_t index) @@ -109,15 +110,20 @@ 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->redraw(); CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false); + tabs->redraw(); + } CIntObject::show(to); } void CMenuScreen::activate() { - CCS->musich->playMusic("Music/MainMenu", true, true); + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); if(!config["video"].isNull()) - CCS->videoh->open(config["video"]["name"].String()); + CCS->videoh->open(VideoPath::fromJson(config["video"]["name"])); CIntObject::activate(); } @@ -185,7 +191,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.tutorialNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::startTutorial); } break; } @@ -200,7 +206,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.tutorialNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::openLobby, ESelectionScreen::loadGame, true, nullptr, ELoadMode::TUTORIAL); } } break; @@ -211,7 +217,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.highscoresNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::openHighScoreScreen); } } } @@ -238,7 +244,7 @@ std::shared_ptr CMenuEntry::createButton(CMenuScreen * parent, const Js EShortcut shortcut = GH.shortcuts().findShortcut(button["shortcut"].String()); - auto result = std::make_shared(Point(posx, posy), button["name"].String(), help, command, shortcut); + auto result = std::make_shared(Point(posx, posy), AnimationPath::fromJson(button["name"]), help, command, shortcut); if (button["center"].Bool()) result->moveBy(Point(-result->pos.w/2, -result->pos.h/2)); @@ -263,7 +269,8 @@ CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) } CMainMenuConfig::CMainMenuConfig() - : campaignSets(JsonNode(ResourceID("config/campaignSets.json"))), config(JsonNode(ResourceID("config/mainmenu.json"))) + : campaignSets(JsonPath::builtin("config/campaignSets.json")) + , config(JsonPath::builtin("config/mainmenu.json")) { } @@ -292,13 +299,11 @@ CMainMenu::CMainMenu() GH.defActionsDef = 63; menu = std::make_shared(CMainMenuConfig::get().getConfig()["window"]); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - backgroundAroundMenu = std::make_shared("DIBOXBCK", pos); + backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); } CMainMenu::~CMainMenu() { - boost::unique_lock lock(*CPlayerInterface::pim); - if(GH.curInt == this) GH.curInt = nullptr; } @@ -335,6 +340,17 @@ 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(); @@ -349,9 +365,10 @@ void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vec GH.windows().createAndPushWindow(host); } -void CMainMenu::openCampaignLobby(const std::string & campaignFileName) +void CMainMenu::openCampaignLobby(const std::string & campaignFileName, std::string campaignSet) { auto ourCampaign = CampaignHandler::getCampaign(campaignFileName); + ourCampaign->campaignSet = campaignSet; openCampaignLobby(ourCampaign); } @@ -367,24 +384,44 @@ void CMainMenu::openCampaignScreen(std::string name) { if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name)) { - GH.windows().createAndPushWindow(CMainMenuConfig::get().getCampaigns()[name]); + GH.windows().createAndPushWindow(CMainMenuConfig::get().getCampaigns(), name); return; } logGlobal->error("Unknown campaign set: %s", name); } +void CMainMenu::startTutorial() +{ + ResourcePath tutorialMap("Maps/Tutorial.tut", EResType::MAP); + if(!CResourceHandler::get()->existsResource(tutorialMap)) + { + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("core.genrltxt.742"), std::vector>(), PlayerColor(1)); + return; + } + + auto mapInfo = std::make_shared(); + mapInfo->mapInit(tutorialMap.getName()); + CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + CSH->startMapAfterConnection(mapInfo); +} + +void CMainMenu::openHighScoreScreen() +{ + GH.windows().createAndPushWindow(CHighScoreScreen::HighScorePage::SCENARIO); + return; +} + std::shared_ptr CMainMenu::create() { if(!CMM) CMM = std::shared_ptr(new CMainMenu()); - GH.terminate_cond->setn(false); return CMM; } std::shared_ptr CMainMenu::createPicture(const JsonNode & config) { - return std::make_shared(config["name"].String(), (int)config["x"].Float(), (int)config["y"].Float()); + return std::make_shared(ImagePath::fromJson(config["name"]), (int)config["x"].Float(), (int)config["y"].Float()); } CMultiMode::CMultiMode(ESelectionScreen ScreenType) @@ -392,20 +429,20 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUPOPUP.bmp"); + background = std::make_shared(ImagePath::builtin("MUPOPUP.bmp")); pos = background->center(); //center, window has size of bg graphic - picture = std::make_shared("MUMAP.bmp", 16, 77); + picture = std::make_shared(ImagePath::builtin("MUMAP.bmp"), 16, 77); 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->cb += std::bind(&CMultiMode::onNameChange, this, _1); - buttonHotseat = std::make_shared(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); - buttonHost = std::make_shared(Point(373, 78 + 57 * 1), "MUBHOST.DEF", CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); - buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), "MUBJOIN.DEF", CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); - buttonCancel = std::make_shared(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonHotseat = std::make_shared(Point(373, 78), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); + buttonHost = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); + buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonCancel = std::make_shared(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } void CMultiMode::hostTCP() @@ -440,7 +477,7 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S : loadMode(LoadMode), screenType(ScreenType), host(Host) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUHOTSEA.bmp"); + 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]; @@ -453,8 +490,8 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S inputNames[i]->cb += std::bind(&CMultiPlayers::onChange, this, _1); } - buttonOk = std::make_shared(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT); - buttonCancel = std::make_shared(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonOk = std::make_shared(Point(95, 338), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT); + 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, true); @@ -485,15 +522,17 @@ void CMultiPlayers::enterSelectionScreen() CSimpleJoinScreen::CSimpleJoinScreen(bool host) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUDIALOG.bmp"); // address background + 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) textTitle = std::make_shared("", Rect(20, 20, 205, 50), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); inputAddress = std::make_shared(Rect(25, 68, 175, 16), background->getSurface()); inputPort = std::make_shared(Rect(25, 115, 175, 16), background->getSurface()); + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); if(host && !settings["session"]["donotstartserver"].Bool()) { textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverConnecting")); + buttonOk->block(true); startConnectThread(); } else @@ -502,14 +541,12 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); - buttonOk = std::make_shared(Point(26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); - inputAddress->giveFocus(); } inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); inputPort->setText(std::to_string(CSH->getHostPort()), true); - buttonCancel = std::make_shared(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); } @@ -548,12 +585,14 @@ void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) // https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md#threads-and-the-java-vm CVCMIServer::reuseClientJNIEnv(SDL_AndroidGetJNIEnv()); #endif - boost::thread(&CSimpleJoinScreen::connectThread, this, addr, port); + boost::thread connector(&CSimpleJoinScreen::connectThread, this, addr, port); + + connector.detach(); } void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) { - setThreadName("CSimpleJoinScreen::connectThread"); + setThreadName("connectThread"); if(!addr.length()) CSH->startLocalServerAndConnect(); else @@ -561,6 +600,15 @@ void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) // async call to prevent thread race GH.dispatchMainThread([this](){ + if(CSH->state == EClientState::CONNECTION_FAILED) + { + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); + + textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverAddressEnter")); + GH.startTextInput(inputAddress->pos); + buttonOk->block(false); + } + if(GH.windows().isTopWindow(this)) { close(); @@ -568,36 +616,67 @@ void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) }); } -CLoadingScreen::CLoadingScreen(std::function loader) - : CWindowObject(BORDERED, getBackground()), loadingThread(loader) +CLoadingScreen::CLoadingScreen() + : CWindowObject(BORDERED, getBackground()) { + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + addUsedEvents(TIME); + CCS->musich->stopMusic(5000); + + const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; + if(conf.isStruct()) + { + const int posx = conf["x"].Integer(), posy = conf["y"].Integer(); + const int blockSize = conf["size"].Integer(); + const int blocksAmount = conf["amount"].Integer(); + + for(int i = 0; i < blocksAmount; ++i) + { + progressBlocks.push_back(std::make_shared(AnimationPath::fromJson(conf["name"]), i, 0, posx + i * blockSize, posy)); + progressBlocks.back()->deactivate(); + progressBlocks.back()->visible = false; + } + } } CLoadingScreen::~CLoadingScreen() { - loadingThread.join(); } -void CLoadingScreen::showAll(Canvas & to) +void CLoadingScreen::tick(uint32_t msPassed) { - //FIXME: filling screen with transparency? BLACK intended? - //Rect rect(0, 0, to->w, to->h); - //CSDL_Ext::fillRect(to, rect, Colors::TRANSPARENCY); - - CWindowObject::showAll(to); -} - -std::string CLoadingScreen::getBackground() -{ - const auto & conf = CMainMenuConfig::get().getConfig()["loading"].Vector(); - - if(conf.empty()) + if(!progressBlocks.empty()) { - return "loadbar"; - } - else - { - return RandomGeneratorUtil::nextItem(conf, CRandomGenerator::getDefault())->String(); + int status = float(get()) / 255.f * progressBlocks.size(); + + for(int i = 0; i < status; ++i) + { + progressBlocks.at(i)->activate(); + progressBlocks.at(i)->visible = true; + } } } + +ImagePath CLoadingScreen::getBackground() +{ + ImagePath fname = ImagePath::builtin("loadbar"); + const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; + + if(conf.isStruct()) + { + if(conf["background"].isVector()) + return ImagePath::fromJson(*RandomGeneratorUtil::nextItem(conf["background"].Vector(), CRandomGenerator::getDefault())); + + if(conf["background"].isString()) + return ImagePath::fromJson(conf["background"]); + + return fname; + } + + if(conf.isVector() && !conf.Vector().empty()) + return ImagePath::fromJson(*RandomGeneratorUtil::nextItem(conf.Vector(), CRandomGenerator::getDefault())); + + return fname; +} diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 3d65cf9f5..ea9010797 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -11,6 +11,7 @@ #include "../windows/CWindowObject.h" #include "../../lib/JsonNode.h" +#include "../../lib/LoadProgress.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,9 +23,11 @@ class CTextInput; class CGStatusBar; class CTextBox; class CTabbedInt; +class CAnimImage; class CAnimation; class CButton; class CFilledTexture; +class CLabel; // TODO: Find new location for these enums @@ -34,7 +37,7 @@ enum ESelectionScreen : ui8 { enum ELoadMode : ui8 { - NONE = 0, SINGLE, MULTI, CAMPAIGN + NONE = 0, SINGLE, MULTI, CAMPAIGN, TUTORIAL }; /// The main menu screens listed in the EState enum @@ -146,8 +149,10 @@ public: void onScreenResize() override; void update() override; static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); - static void openCampaignLobby(const std::string & campaignFileName); + static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); + static void startTutorial(); + static void openHighScoreScreen(); void openCampaignScreen(std::string name); static std::shared_ptr create(); @@ -177,17 +182,17 @@ public: CSimpleJoinScreen(bool host = true); }; -class CLoadingScreen : public CWindowObject +class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progress { - boost::thread loadingThread; + std::vector> progressBlocks; + + ImagePath getBackground(); - std::string getBackground(); - -public: - CLoadingScreen(std::function loader); +public: + CLoadingScreen(); ~CLoadingScreen(); - void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; }; extern std::shared_ptr CMM; diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index e4db4fc94..6446d6d1b 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -20,19 +20,25 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) - : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), exitCb(callback) + : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; addUsedEvents(LCLICK); pos = center(Rect(0, 0, 800, 600)); updateShadow(); + auto audioData = CCS->videoh->getAudio(spe.prologVideo); + videoSoundHandle = CCS->soundh->playSound(audioData); CCS->videoh->open(spe.prologVideo); - CCS->musich->playMusic("Music/" + spe.prologMusic, true, true); - // MPTODO: Custom campaign crashing on this? -// voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); + CCS->musich->playMusic(spe.prologMusic, true, true); + voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); + auto onVoiceStop = [this]() + { + voiceStopped = true; + }; + CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop); - text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText); + text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString()); text->scrollTextTo(-100); } @@ -51,7 +57,7 @@ void CPrologEpilogVideo::show(Canvas & to) else text->showAll(to); // blit text over video, if needed - if(text->textSize.y + 100 < positionCounter / 5) + if(text->textSize.y + 100 < positionCounter / 5 && voiceStopped) clickPressed(GH.getCursorPosition()); } @@ -59,5 +65,6 @@ void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) { close(); CCS->soundh->stopSound(voiceSoundHandle); + CCS->soundh->stopSound(videoSoundHandle); exitCb(); } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index ec6de9055..1666a87c1 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -19,10 +19,13 @@ class CPrologEpilogVideo : public CWindowObject CampaignScenarioPrologEpilog spe; int positionCounter; int voiceSoundHandle; + int videoSoundHandle; std::function exitCb; std::shared_ptr text; + bool voiceStopped = false; + public: CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback); diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp index dd8aae62c..bd36ce98b 100644 --- a/client/mainmenu/CreditsScreen.cpp +++ b/client/mainmenu/CreditsScreen.cpp @@ -19,6 +19,8 @@ #include "../../lib/filesystem/Filesystem.h" +#include "../../AUTHORS.h" + CreditsScreen::CreditsScreen(Rect rect) : CIntObject(LCLICK), positionCounter(0) { @@ -26,10 +28,22 @@ CreditsScreen::CreditsScreen(Rect rect) pos.h = rect.h; setRedrawParent(true); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/CREDITS.TXT"))->readAll(); + + std::string contributorsText = ""; + std::string contributorsTask = ""; + for (auto & element : contributors) + { + if(element[0] != contributorsTask) + contributorsText += "\r\n{" + element[0] + ":}\r\n"; + contributorsText += (element[2] != "" ? element[2] : element[1]) + "\r\n"; + contributorsTask = element[0]; + } + + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/CREDITS.TXT"))->readAll(); std::string text((char *)textFile.first.get(), textFile.second); size_t firstQuote = text.find('\"') + 1; text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote); + text = "{- VCMI -}\r\n\r\n" + contributorsText + "\r\n\r\n{Website:}\r\nhttps://vcmi.eu\r\n\r\n\r\n\r\n\r\n{- Heroes of Might and Magic III -}\r\n\r\n" + text; credits = std::make_shared(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, ETextAlignment::CENTER, Colors::WHITE, text); credits->scrollTextTo(-600); // move all text below the screen } diff --git a/client/mapView/IMapRendererContext.h b/client/mapView/IMapRendererContext.h index 049204c08..1fab9394d 100644 --- a/client/mapView/IMapRendererContext.h +++ b/client/mapView/IMapRendererContext.h @@ -1,93 +1,93 @@ -/* - * IMapRendererContext.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 int3; -class Point; -class CGObjectInstance; -class ObjectInstanceID; -struct TerrainTile; -struct CGPath; - -VCMI_LIB_NAMESPACE_END - -class IMapRendererContext -{ -public: - using MapObject = ObjectInstanceID; - using MapObjectsList = std::vector; - - virtual ~IMapRendererContext() = default; - - /// returns dimensions of current map - virtual int3 getMapSize() const = 0; - - /// returns true if chosen coordinates exist on map - virtual bool isInMap(const int3 & coordinates) const = 0; - - /// returns true if selected tile has animation and should not be cached - virtual bool tileAnimated(const int3 & coordinates) const = 0; - - /// returns tile by selected coordinates. Coordinates MUST be valid - virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0; - - /// returns all objects visible on specified tile - virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0; - - /// returns specific object by ID, or nullptr if not found - virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0; - - /// returns path of currently active hero, or nullptr if none - virtual const CGPath * currentPath() const = 0; - - /// returns true if specified tile is visible in current context - virtual bool isVisible(const int3 & coordinates) const = 0; - - /// returns true if specified object is the currently active hero - virtual bool isActiveHero(const CGObjectInstance* obj) const = 0; - - virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0; - virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0; - - /// returns object animation transparency. IF set to 0, object will not be visible - virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0; - - /// returns animation frame for selected object - virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0; - - /// 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 animation frame for terrain - virtual size_t terrainImageIndex(size_t groupSize) const = 0; - - virtual double viewTransitionProgress() const = 0; - - /// if true, rendered images will be converted to grayscale - virtual bool filterGrayscale() const = 0; - - virtual bool showRoads() const = 0; - virtual bool showRivers() const = 0; - virtual bool showBorder() const = 0; - - /// if true, world view overlay will be shown - virtual bool showOverlay() const = 0; - - /// if true, map grid should be visible on map - virtual bool showGrid() const = 0; - virtual bool showVisitable() const = 0; - virtual bool showBlocked() const = 0; - - /// if true, spell range for teleport / scuttle boat will be visible - virtual bool showSpellRange(const int3 & position) const = 0; - -}; +/* + * IMapRendererContext.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 int3; +class Point; +class CGObjectInstance; +class ObjectInstanceID; +struct TerrainTile; +struct CGPath; + +VCMI_LIB_NAMESPACE_END + +class IMapRendererContext +{ +public: + using MapObject = ObjectInstanceID; + using MapObjectsList = std::vector; + + virtual ~IMapRendererContext() = default; + + /// returns dimensions of current map + virtual int3 getMapSize() const = 0; + + /// returns true if chosen coordinates exist on map + virtual bool isInMap(const int3 & coordinates) const = 0; + + /// returns true if selected tile has animation and should not be cached + virtual bool tileAnimated(const int3 & coordinates) const = 0; + + /// returns tile by selected coordinates. Coordinates MUST be valid + virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0; + + /// returns all objects visible on specified tile + virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0; + + /// returns specific object by ID, or nullptr if not found + virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0; + + /// returns path of currently active hero, or nullptr if none + virtual const CGPath * currentPath() const = 0; + + /// returns true if specified tile is visible in current context + virtual bool isVisible(const int3 & coordinates) const = 0; + + /// returns true if specified object is the currently active hero + virtual bool isActiveHero(const CGObjectInstance* obj) const = 0; + + virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0; + virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0; + + /// returns object animation transparency. IF set to 0, object will not be visible + virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0; + + /// returns animation frame for selected object + virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0; + + /// 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 animation frame for terrain + virtual size_t terrainImageIndex(size_t groupSize) const = 0; + + virtual double viewTransitionProgress() const = 0; + + /// if true, rendered images will be converted to grayscale + virtual bool filterGrayscale() const = 0; + + virtual bool showRoads() const = 0; + virtual bool showRivers() const = 0; + virtual bool showBorder() const = 0; + + /// if true, world view overlay will be shown + virtual bool showOverlay() const = 0; + + /// if true, map grid should be visible on map + virtual bool showGrid() const = 0; + virtual bool showVisitable() const = 0; + virtual bool showBlocked() const = 0; + + /// if true, spell range for teleport / scuttle boat will be visible + virtual bool showSpellRange(const int3 & position) const = 0; + +}; diff --git a/client/mapView/IMapRendererObserver.h b/client/mapView/IMapRendererObserver.h index 10e36807b..d1df5d9f1 100644 --- a/client/mapView/IMapRendererObserver.h +++ b/client/mapView/IMapRendererObserver.h @@ -1,52 +1,53 @@ -/* - * IMapRendererObserver.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 int3; -class CGObjectInstance; -class CGHeroInstance; - -VCMI_LIB_NAMESPACE_END - -class IMapObjectObserver -{ -public: - IMapObjectObserver(); - virtual ~IMapObjectObserver(); - - virtual bool hasOngoingAnimations() = 0; - - /// Plays fade-in animation and adds object to map - virtual void onObjectFadeIn(const CGObjectInstance * obj) = 0; - - /// Plays fade-out animation and removed object from map - virtual void onObjectFadeOut(const CGObjectInstance * obj) = 0; - - /// Adds object to map instantly, with no animation - virtual void onObjectInstantAdd(const CGObjectInstance * obj) = 0; - - /// Removes object from map instantly, with no animation - virtual void onObjectInstantRemove(const CGObjectInstance * obj) = 0; - - /// Perform hero movement animation, moving hero across terrain - virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - /// Perform initialization of hero teleportation animation with terrain fade animation - virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; -}; +/* + * IMapRendererObserver.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 int3; +class CGObjectInstance; +class CGHeroInstance; +class PlayerColor; + +VCMI_LIB_NAMESPACE_END + +class IMapObjectObserver +{ +public: + IMapObjectObserver(); + virtual ~IMapObjectObserver(); + + virtual bool hasOngoingAnimations() = 0; + + /// Plays fade-in animation and adds object to map + virtual void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Plays fade-out animation and removed object from map + virtual void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Adds object to map instantly, with no animation + virtual void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Removes object from map instantly, with no animation + virtual void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Perform hero movement animation, moving hero across terrain + virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + /// Perform initialization of hero teleportation animation with terrain fade animation + virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; +}; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 65a374f97..ebe2dbf14 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -1,787 +1,795 @@ -/* - * MapRenderer.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 "MapRenderer.h" - -#include "IMapRendererContext.h" -#include "mapHandler.h" - -#include "../CGameInfo.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/Colors.h" - -#include "../../CCallback.h" - -#include "../../lib/RiverHandler.h" -#include "../../lib/RoadHandler.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/mapping/CMapDefines.h" -#include "../../lib/pathfinder/CGPathNode.h" - -struct NeighborTilesInfo -{ - //567 - //3 4 - //012 - std::bitset<8> d; - - NeighborTilesInfo(IMapRendererContext & context, const int3 & pos) - { - auto checkTile = [&](int dx, int dy) - { - return context.isVisible(pos + int3(dx, dy, 0)); - }; - - // sides - d[1] = checkTile(0, +1); - d[3] = checkTile(-1, 0); - d[4] = checkTile(+1, 0); - d[6] = checkTile(0, -1); - - // corners - select visible image if either corner or adjacent sides are visible - d[0] = d[1] || d[3] || checkTile(-1, +1); - d[2] = d[1] || d[4] || checkTile(+1, +1); - d[5] = d[3] || d[6] || checkTile(-1, -1); - d[7] = d[4] || d[6] || checkTile(+1, -1); - } - - bool areAllHidden() const - { - return d.none(); - } - - int getBitmapID() const - { - //NOTE: some images have unused in VCMI pair (same blockmap but a bit different look) - // 0-1, 2-3, 4-5, 11-13, 12-14 - static const int visBitmaps[256] = { - -1, 34, 4, 4, 22, 23, 4, 4, 36, 36, 38, 38, 47, 47, 38, 38, //16 - 3, 25, 12, 12, 3, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //32 - 35, 39, 48, 48, 41, 43, 48, 48, 36, 36, 38, 38, 47, 47, 38, 38, //48 - 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //64 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //80 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //96 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //112 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //128 - 15, 17, 30, 30, 16, 19, 30, 30, 46, 46, 40, 40, 32, 32, 40, 40, //144 - 2, 25, 12, 12, 2, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //160 - 18, 42, 31, 31, 20, 21, 31, 31, 46, 46, 40, 40, 32, 32, 40, 40, //176 - 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //192 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //208 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //224 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //240 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10 //256 - }; - - return visBitmaps[d.to_ulong()]; // >=0 -> partial hide, <0 - full hide - } -}; - -MapTileStorage::MapTileStorage(size_t capacity) - : animations(capacity) -{ -} - -void MapTileStorage::load(size_t index, const std::string & filename, EImageBlitMode blitMode) -{ - auto & terrainAnimations = animations[index]; - - for(auto & entry : terrainAnimations) - { - if (!filename.empty()) - { - entry = std::make_unique(filename); - entry->preload(); - } - else - entry = std::make_unique(); - - for(size_t i = 0; i < entry->size(); ++i) - entry->getImage(i)->setBlitMode(blitMode); - } - - for(size_t i = 0; i < terrainAnimations[0]->size(); ++i) - { - terrainAnimations[1]->getImage(i)->verticalFlip(); - terrainAnimations[3]->getImage(i)->verticalFlip(); - - terrainAnimations[2]->getImage(i)->horizontalFlip(); - terrainAnimations[3]->getImage(i)->horizontalFlip(); - } -} - -std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) -{ - const auto & animation = animations[fileIndex][rotationIndex]; - return animation->getImage(imageIndex); -} - -MapRendererTerrain::MapRendererTerrain() - : storage(VLC->terrainTypeHandler->objects.size()) -{ - for(const auto & terrain : VLC->terrainTypeHandler->objects) - storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); -} - -void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - int32_t terrainIndex = mapTile.terType->getIndex(); - int32_t imageIndex = mapTile.terView; - int32_t rotationIndex = mapTile.extTileFlags % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - - assert(image); - if (!image) - { - logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.terType->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)); -} - -uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(!mapTile.terType->paletteAnimation.empty()) - return context.terrainImageIndex(250); - return 0xff - 1; -} - -MapRendererRiver::MapRendererRiver() - : storage(VLC->riverTypeHandler->objects.size()) -{ - for(const auto & river : VLC->riverTypeHandler->objects) - storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); -} - -void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(mapTile.riverType->getId() == River::NO_RIVER) - return; - - int32_t terrainIndex = mapTile.riverType->getIndex(); - 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)); - - target.draw(image, Point(0, 0)); -} - -uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(!mapTile.riverType->paletteAnimation.empty()) - return context.terrainImageIndex(250); - return 0xff-1; -} - -MapRendererRoad::MapRendererRoad() - : storage(VLC->roadTypeHandler->objects.size()) -{ - for(const auto & road : VLC->roadTypeHandler->objects) - storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); -} - -void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const int3 coordinatesAbove = coordinates - int3(0, 1, 0); - - if(context.isInMap(coordinatesAbove)) - { - const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove); - if(mapTileAbove.roadType->getId() != Road::NO_ROAD) - { - int32_t terrainIndex = mapTileAbove.roadType->getIndex(); - int32_t imageIndex = mapTileAbove.roadDir; - int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - target.draw(image, Point(0, 0), Rect(0, 16, 32, 16)); - } - } - - const TerrainTile & mapTile = context.getMapTile(coordinates); - if(mapTile.roadType->getId() != Road::NO_ROAD) - { - int32_t terrainIndex = mapTile.roadType->getIndex(); - int32_t imageIndex = mapTile.roadDir; - int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - target.draw(image, Point(0, 16), Rect(0, 0, 32, 16)); - } -} - -uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return 0; -} - -MapRendererBorder::MapRendererBorder() -{ - animation = std::make_unique("EDG"); - animation->preload(); -} - -size_t MapRendererBorder::getIndexForTile(IMapRendererContext & context, const int3 & tile) -{ - assert(!context.isInMap(tile)); - - int3 size = context.getMapSize(); - - if(tile.x < -1 || tile.x > size.x || tile.y < -1 || tile.y > size.y) - return std::abs(tile.x) % 4 + 4 * (std::abs(tile.y) % 4); - - if(tile.x == -1 && tile.y == -1) - return 16; - - if(tile.x == size.x && tile.y == -1) - return 17; - - if(tile.x == size.x && tile.y == size.y) - return 18; - - if(tile.x == -1 && tile.y == size.y) - return 19; - - if(tile.y == -1) - return 20 + (tile.x % 4); - - if(tile.x == size.x) - return 24 + (tile.y % 4); - - if(tile.y == size.y) - return 28 + (tile.x % 4); - - if(tile.x == -1) - return 32 + (tile.y % 4); - - //else - visible area, no renderable border - assert(0); - return 0; -} - -void MapRendererBorder::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if (context.showBorder()) - { - const auto & image = animation->getImage(getIndexForTile(context, coordinates)); - target.draw(image, Point(0, 0)); - } - else - { - target.drawColor(Rect(0,0,32,32), Colors::BLACK); - } -} - -uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return 0; -} - -MapRendererFow::MapRendererFow() -{ - fogOfWarFullHide = std::make_unique("TSHRC"); - fogOfWarFullHide->preload(); - fogOfWarPartialHide = std::make_unique("TSHRE"); - fogOfWarPartialHide->preload(); - - for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) - fogOfWarFullHide->getImage(i)->setBlitMode(EImageBlitMode::OPAQUE); - - static const std::vector rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; - - size_t size = fogOfWarPartialHide->size(0); //group size after next rotation - - for(const int rotation : rotations) - { - fogOfWarPartialHide->duplicateImage(0, rotation, 0); - auto image = fogOfWarPartialHide->getImage(size, 0); - image->verticalFlip(); - size++; - } -} - -void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - assert(!context.isVisible(coordinates)); - - const NeighborTilesInfo neighborInfo(context, coordinates); - - int retBitmapID = neighborInfo.getBitmapID(); // >=0 -> partial hide, <0 - full hide - if(retBitmapID < 0) - { - // generate a number that is predefined for each tile, - // but appears random to break visible pattern in large areas of fow - // current approach (use primes as magic numbers for formula) looks to be suitable - size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101; - size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size(); - - target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0)); - } - else - { - target.draw(fogOfWarPartialHide->getImage(retBitmapID), Point(0, 0)); - } -} - -uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const NeighborTilesInfo neighborInfo(context, coordinates); - int retBitmapID = neighborInfo.getBitmapID(); - if(retBitmapID < 0) - return 0xff - 1; - return retBitmapID; -} - -std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectInstance* obj) -{ - const auto & info = obj->appearance; - - //the only(?) invisible object - if(info->id == Obj::EVENT) - return std::shared_ptr(); - - if(info->animationFile.empty()) - { - logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid); - return std::shared_ptr(); - } - - bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO); - - // 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(info->animationFile, generateMovementGroups); -} - -std::shared_ptr MapRendererObjects::getAnimation(const std::string & filename, bool generateMovementGroups) -{ - auto it = animations.find(filename); - - if(it != animations.end()) - return it->second; - - auto ret = std::make_shared(filename); - animations[filename] = ret; - ret->preload(); - - if(generateMovementGroups) - { - ret->createFlippedGroup(1, 13); - ret->createFlippedGroup(2, 14); - ret->createFlippedGroup(3, 15); - - ret->createFlippedGroup(6, 10); - ret->createFlippedGroup(7, 11); - ret->createFlippedGroup(8, 12); - } - return ret; -} - -std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectInstance* obj) -{ - //TODO: relocate to config file? - static const std::vector heroFlags = { - "AF00", "AF01", "AF02", "AF03", "AF04", "AF05", "AF06", "AF07" - }; - - if(obj->ID == Obj::HERO) - { - assert(dynamic_cast(obj) != nullptr); - assert(obj->tempOwner.isValidPlayer()); - return getAnimation(heroFlags[obj->tempOwner.getNum()], true); - } - - 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 nullptr; -} - -std::shared_ptr MapRendererObjects::getOverlayAnimation(const CGObjectInstance * obj) -{ - if(obj->ID == Obj::BOAT) - { - // 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 nullptr; -} - -std::shared_ptr MapRendererObjects::getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr& animation) const -{ - if(!animation) - return nullptr; - - size_t groupIndex = context.objectGroupIndex(obj->id); - - if(animation->size(groupIndex) == 0) - return nullptr; - - size_t frameIndex = context.objectImageIndex(obj->id, animation->size(groupIndex)); - - return animation->getImage(frameIndex, groupIndex); -} - -void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr& image) -{ - if(!image) - return; - - auto transparency = static_cast(std::round(255 * context.objectTransparency(object->id, coordinates))); - - if (transparency == 0) - return; - - image->setAlpha(transparency); - image->setFlagColor(object->tempOwner); - - 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))); - } -} - -void MapRendererObjects::renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * instance) -{ - renderImage(context, target, coordinates, instance, getImage(context, instance, getBaseAnimation(instance))); - renderImage(context, target, coordinates, instance, getImage(context, instance, getFlagAnimation(instance))); - renderImage(context, target, coordinates, instance, getImage(context, instance, getOverlayAnimation(instance))); -} - -void MapRendererObjects::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * objectInstance = context.getObject(objectID); - - assert(objectInstance); - if(!objectInstance) - { - logGlobal->error("Stray map object that isn't fading"); - continue; - } - - renderObject(context, target, coordinates, objectInstance); - } -} - -uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * objectInstance = context.getObject(objectID); - - assert(objectInstance); - if(!objectInstance) - { - logGlobal->error("Stray map object that isn't fading"); - continue; - } - - size_t groupIndex = context.objectGroupIndex(objectInstance->id); - Point offsetPixels = context.objectImageOffset(objectInstance->id, coordinates); - - auto base = getBaseAnimation(objectInstance); - auto flag = getFlagAnimation(objectInstance); - - if (base && base->size(groupIndex) > 1) - { - auto imageIndex = context.objectImageIndex(objectID, base->size(groupIndex)); - auto image = base->getImage(imageIndex, groupIndex); - if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) - return context.objectImageIndex(objectID, 250); - } - - if (flag && flag->size(groupIndex) > 1) - { - auto imageIndex = context.objectImageIndex(objectID, flag->size(groupIndex)); - auto image = flag->getImage(imageIndex, groupIndex); - if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) - return context.objectImageIndex(objectID, 250); - } - } - return 0xff-1; -} - -MapRendererOverlay::MapRendererOverlay() - : imageGrid(IImage::createFromFile("debug/grid", EImageBlitMode::ALPHA)) - , imageBlocked(IImage::createFromFile("debug/blocked", EImageBlitMode::ALPHA)) - , imageVisitable(IImage::createFromFile("debug/visitable", EImageBlitMode::ALPHA)) - , imageSpellRange(IImage::createFromFile("debug/spellRange", EImageBlitMode::ALPHA)) -{ - -} - -void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if(context.showGrid()) - target.draw(imageGrid, Point(0,0)); - - if(context.showVisitable() || context.showBlocked()) - { - bool blocking = false; - bool visitable = false; - - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * object = context.getObject(objectID); - - if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) - { - visitable |= object->visitableAt(coordinates.x, coordinates.y); - blocking |= object->blockingAt(coordinates.x, coordinates.y); - } - } - - if (context.showBlocked() && blocking) - target.draw(imageBlocked, Point(0,0)); - if (context.showVisitable() && visitable) - target.draw(imageVisitable, Point(0,0)); - } - - if (context.showSpellRange(coordinates)) - target.draw(imageSpellRange, Point(0,0)); -} - -uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - uint8_t result = 0; - - if (context.showVisitable()) - result += 1; - - if (context.showBlocked()) - result += 2; - - if (context.showGrid()) - result += 4; - - if (context.showSpellRange(coordinates)) - result += 8; - - return result; -} - -MapRendererPath::MapRendererPath() - : pathNodes(new CAnimation("ADAG")) -{ - pathNodes->preload(); -} - -size_t MapRendererPath::selectImageReachability(bool reachableToday, size_t imageIndex) -{ - const static size_t unreachableTodayOffset = 25; - - if(!reachableToday) - return unreachableTodayOffset + imageIndex; - - return imageIndex; -} - -size_t MapRendererPath::selectImageCross(bool reachableToday, const int3 & curr) -{ - return selectImageReachability(reachableToday, 0); -} - -size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next) -{ - // Vector directions - // 0 1 2 - // | - // 3 - 4 - 5 - // | - // 6 7 8 - //For example: - // | - // +-> - // is (directionToArrowIndex[7][5]) - // - const static size_t directionToArrowIndex[9][9] = { - {16, 17, 18, 7, 0, 19, 6, 5, 0 }, - {8, 9, 18, 7, 0, 19, 6, 0, 20}, - {8, 1, 10, 7, 0, 19, 0, 21, 20}, - {24, 17, 18, 15, 0, 0, 6, 5, 4 }, - {0, 0, 0, 0, 0, 0, 0, 0, 0 }, - {8, 1, 2, 0, 0, 11, 22, 21, 20}, - {24, 17, 0, 23, 0, 3, 14, 5, 4 }, - {24, 0, 2, 23, 0, 3, 22, 13, 4 }, - {0, 1, 2, 23, 0, 3, 22, 21, 12} - }; - - size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1); - size_t leaveDirection = (prev.x - curr.x + 1) + 3 * (prev.y - curr.y + 1); - size_t imageIndex = directionToArrowIndex[enterDirection][leaveDirection]; - - return selectImageReachability(reachableToday, imageIndex); -} - -void MapRendererPath::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - size_t imageID = selectImage(context, coordinates); - - if (imageID < pathNodes->size()) - target.draw(pathNodes->getImage(imageID), Point(0,0)); -} - -size_t MapRendererPath::selectImage(IMapRendererContext & context, const int3 & coordinates) -{ - const auto & functor = [&](const CGPathNode & node) - { - return node.coord == coordinates; - }; - - const auto * path = context.currentPath(); - if(!path) - return std::numeric_limits::max(); - - const auto & iter = boost::range::find_if(path->nodes, functor); - - if(iter == path->nodes.end()) - return std::numeric_limits::max(); - - bool reachableToday = iter->turns == 0; - if(iter == path->nodes.begin()) - return selectImageCross(reachableToday, iter->coord); - - auto next = iter + 1; - auto prev = iter - 1; - - // start of path - current hero location - if(next == path->nodes.end()) - return std::numeric_limits::max(); - - bool pathContinuous = iter->coord.areNeighbours(next->coord) && iter->coord.areNeighbours(prev->coord); - bool embarking = iter->action == EPathNodeAction::EMBARK || iter->action == EPathNodeAction::DISEMBARK; - - if(pathContinuous && !embarking) - return selectImageArrow(reachableToday, iter->coord, prev->coord, next->coord); - - return selectImageCross(reachableToday, iter->coord); -} - -uint8_t MapRendererPath::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return selectImage(context, coordinates) & 0xff; -} - -MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & context, const int3 & coordinates) -{ - // computes basic checksum to determine whether tile needs an update - // if any component gives different value, tile will be updated - TileChecksum result; - boost::range::fill(result, std::numeric_limits::max()); - - if(!context.isInMap(coordinates)) - { - result[0] = rendererBorder.checksum(context, coordinates); - return result; - } - - const NeighborTilesInfo neighborInfo(context, coordinates); - - if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) - { - result[7] = rendererFow.checksum(context, coordinates); - } - else - { - result[1] = rendererTerrain.checksum(context, coordinates); - if (context.showRivers()) - result[2] = rendererRiver.checksum(context, coordinates); - if (context.showRoads()) - result[3] = rendererRoad.checksum(context, coordinates); - result[4] = rendererObjects.checksum(context, coordinates); - result[5] = rendererPath.checksum(context, coordinates); - result[6] = rendererOverlay.checksum(context, coordinates); - - if(!context.isVisible(coordinates)) - result[7] = rendererFow.checksum(context, coordinates); - } - return result; -} - -void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if(!context.isInMap(coordinates)) - { - rendererBorder.renderTile(context, target, coordinates); - return; - } - - const NeighborTilesInfo neighborInfo(context, coordinates); - - if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) - { - rendererFow.renderTile(context, target, coordinates); - } - else - { - rendererTerrain.renderTile(context, target, coordinates); - - if (context.showRivers()) - rendererRiver.renderTile(context, target, coordinates); - - if (context.showRoads()) - rendererRoad.renderTile(context, target, coordinates); - - rendererObjects.renderTile(context, target, coordinates); - rendererPath.renderTile(context, target, coordinates); - rendererOverlay.renderTile(context, target, coordinates); - - if(!context.isVisible(coordinates)) - rendererFow.renderTile(context, target, coordinates); - } -} +/* + * MapRenderer.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 "MapRenderer.h" + +#include "IMapRendererContext.h" +#include "mapHandler.h" + +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/Colors.h" + +#include "../../CCallback.h" + +#include "../../lib/RiverHandler.h" +#include "../../lib/RoadHandler.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/pathfinder/CGPathNode.h" + +struct NeighborTilesInfo +{ + //567 + //3 4 + //012 + std::bitset<8> d; + + NeighborTilesInfo(IMapRendererContext & context, const int3 & pos) + { + auto checkTile = [&](int dx, int dy) + { + return context.isVisible(pos + int3(dx, dy, 0)); + }; + + // sides + d[1] = checkTile(0, +1); + d[3] = checkTile(-1, 0); + d[4] = checkTile(+1, 0); + d[6] = checkTile(0, -1); + + // corners - select visible image if either corner or adjacent sides are visible + d[0] = d[1] || d[3] || checkTile(-1, +1); + d[2] = d[1] || d[4] || checkTile(+1, +1); + d[5] = d[3] || d[6] || checkTile(-1, -1); + d[7] = d[4] || d[6] || checkTile(+1, -1); + } + + bool areAllHidden() const + { + return d.none(); + } + + int getBitmapID() const + { + //NOTE: some images have unused in VCMI pair (same blockmap but a bit different look) + // 0-1, 2-3, 4-5, 11-13, 12-14 + static const int visBitmaps[256] = { + -1, 34, 4, 4, 22, 23, 4, 4, 36, 36, 38, 38, 47, 47, 38, 38, //16 + 3, 25, 12, 12, 3, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //32 + 35, 39, 48, 48, 41, 43, 48, 48, 36, 36, 38, 38, 47, 47, 38, 38, //48 + 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //64 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //80 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //96 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //112 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //128 + 15, 17, 30, 30, 16, 19, 30, 30, 46, 46, 40, 40, 32, 32, 40, 40, //144 + 2, 25, 12, 12, 2, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //160 + 18, 42, 31, 31, 20, 21, 31, 31, 46, 46, 40, 40, 32, 32, 40, 40, //176 + 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //192 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //208 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //224 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //240 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10 //256 + }; + + return visBitmaps[d.to_ulong()]; // >=0 -> partial hide, <0 - full hide + } +}; + +MapTileStorage::MapTileStorage(size_t capacity) + : animations(capacity) +{ +} + +void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode) +{ + auto & terrainAnimations = animations[index]; + + 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); + } + + for(size_t i = 0; i < terrainAnimations[0]->size(); ++i) + { + terrainAnimations[1]->getImage(i)->verticalFlip(); + terrainAnimations[3]->getImage(i)->verticalFlip(); + + terrainAnimations[2]->getImage(i)->horizontalFlip(); + terrainAnimations[3]->getImage(i)->horizontalFlip(); + } +} + +std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +{ + const auto & animation = animations[fileIndex][rotationIndex]; + return animation->getImage(imageIndex); +} + +MapRendererTerrain::MapRendererTerrain() + : storage(VLC->terrainTypeHandler->objects.size()) +{ + logGlobal->debug("Loading map terrains"); + for(const auto & terrain : VLC->terrainTypeHandler->objects) + storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); + logGlobal->debug("Done loading map terrains"); +} + +void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + int32_t terrainIndex = mapTile.terType->getIndex(); + int32_t imageIndex = mapTile.terView; + int32_t rotationIndex = mapTile.extTileFlags % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + + assert(image); + if (!image) + { + logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.terType->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)); +} + +uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(!mapTile.terType->paletteAnimation.empty()) + return context.terrainImageIndex(250); + return 0xff - 1; +} + +MapRendererRiver::MapRendererRiver() + : storage(VLC->riverTypeHandler->objects.size()) +{ + logGlobal->debug("Loading map rivers"); + for(const auto & river : VLC->riverTypeHandler->objects) + storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map rivers"); +} + +void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(mapTile.riverType->getId() == River::NO_RIVER) + return; + + int32_t terrainIndex = mapTile.riverType->getIndex(); + 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)); + + target.draw(image, Point(0, 0)); +} + +uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(!mapTile.riverType->paletteAnimation.empty()) + return context.terrainImageIndex(250); + return 0xff-1; +} + +MapRendererRoad::MapRendererRoad() + : storage(VLC->roadTypeHandler->objects.size()) +{ + logGlobal->debug("Loading map roads"); + for(const auto & road : VLC->roadTypeHandler->objects) + storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map roads"); +} + +void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const int3 coordinatesAbove = coordinates - int3(0, 1, 0); + + if(context.isInMap(coordinatesAbove)) + { + const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove); + if(mapTileAbove.roadType->getId() != Road::NO_ROAD) + { + int32_t terrainIndex = mapTileAbove.roadType->getIndex(); + int32_t imageIndex = mapTileAbove.roadDir; + int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + target.draw(image, Point(0, 0), Rect(0, 16, 32, 16)); + } + } + + const TerrainTile & mapTile = context.getMapTile(coordinates); + if(mapTile.roadType->getId() != Road::NO_ROAD) + { + int32_t terrainIndex = mapTile.roadType->getIndex(); + int32_t imageIndex = mapTile.roadDir; + int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + target.draw(image, Point(0, 16), Rect(0, 0, 32, 16)); + } +} + +uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return 0; +} + +MapRendererBorder::MapRendererBorder() +{ + animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG")); + animation->preload(); +} + +size_t MapRendererBorder::getIndexForTile(IMapRendererContext & context, const int3 & tile) +{ + assert(!context.isInMap(tile)); + + int3 size = context.getMapSize(); + + if(tile.x < -1 || tile.x > size.x || tile.y < -1 || tile.y > size.y) + return std::abs(tile.x) % 4 + 4 * (std::abs(tile.y) % 4); + + if(tile.x == -1 && tile.y == -1) + return 16; + + if(tile.x == size.x && tile.y == -1) + return 17; + + if(tile.x == size.x && tile.y == size.y) + return 18; + + if(tile.x == -1 && tile.y == size.y) + return 19; + + if(tile.y == -1) + return 20 + (tile.x % 4); + + if(tile.x == size.x) + return 24 + (tile.y % 4); + + if(tile.y == size.y) + return 28 + (tile.x % 4); + + if(tile.x == -1) + return 32 + (tile.y % 4); + + //else - visible area, no renderable border + assert(0); + return 0; +} + +void MapRendererBorder::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if (context.showBorder()) + { + const auto & image = animation->getImage(getIndexForTile(context, coordinates)); + target.draw(image, Point(0, 0)); + } + else + { + target.drawColor(Rect(0,0,32,32), Colors::BLACK); + } +} + +uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return 0; +} + +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); + + static const std::vector rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; + + size_t size = fogOfWarPartialHide->size(0); //group size after next rotation + + for(const int rotation : rotations) + { + fogOfWarPartialHide->duplicateImage(0, rotation, 0); + auto image = fogOfWarPartialHide->getImage(size, 0); + image->verticalFlip(); + size++; + } +} + +void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + assert(!context.isVisible(coordinates)); + + const NeighborTilesInfo neighborInfo(context, coordinates); + + int retBitmapID = neighborInfo.getBitmapID(); // >=0 -> partial hide, <0 - full hide + if(retBitmapID < 0) + { + // generate a number that is predefined for each tile, + // but appears random to break visible pattern in large areas of fow + // current approach (use primes as magic numbers for formula) looks to be suitable + size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101; + size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size(); + + target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0)); + } + else + { + target.draw(fogOfWarPartialHide->getImage(retBitmapID), Point(0, 0)); + } +} + +uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const NeighborTilesInfo neighborInfo(context, coordinates); + int retBitmapID = neighborInfo.getBitmapID(); + if(retBitmapID < 0) + return 0xff - 1; + return retBitmapID; +} + +std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectInstance* obj) +{ + const auto & info = obj->appearance; + + //the only(?) invisible object + if(info->id == Obj::EVENT) + return std::shared_ptr(); + + if(info->animationFile.empty()) + { + logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid); + return std::shared_ptr(); + } + + bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO); + + // 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(info->animationFile, generateMovementGroups); +} + +std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups) +{ + auto it = animations.find(filename); + + if(it != animations.end()) + return it->second; + + auto ret = GH.renderHandler().loadAnimation(filename); + animations[filename] = ret; + ret->preload(); + + if(generateMovementGroups) + { + ret->createFlippedGroup(1, 13); + ret->createFlippedGroup(2, 14); + ret->createFlippedGroup(3, 15); + + ret->createFlippedGroup(6, 10); + ret->createFlippedGroup(7, 11); + ret->createFlippedGroup(8, 12); + } + return ret; +} + +std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectInstance* obj) +{ + //TODO: relocate to config file? + static const std::vector heroFlags = { + "AF00", "AF01", "AF02", "AF03", "AF04", "AF05", "AF06", "AF07" + }; + + if(obj->ID == Obj::HERO) + { + assert(dynamic_cast(obj) != nullptr); + assert(obj->tempOwner.isValidPlayer()); + return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true); + } + + 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 nullptr; +} + +std::shared_ptr MapRendererObjects::getOverlayAnimation(const CGObjectInstance * obj) +{ + if(obj->ID == Obj::BOAT) + { + // 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 nullptr; +} + +std::shared_ptr MapRendererObjects::getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr& animation) const +{ + if(!animation) + return nullptr; + + size_t groupIndex = context.objectGroupIndex(obj->id); + + if(animation->size(groupIndex) == 0) + return nullptr; + + size_t frameIndex = context.objectImageIndex(obj->id, animation->size(groupIndex)); + + return animation->getImage(frameIndex, groupIndex); +} + +void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr& image) +{ + if(!image) + return; + + auto transparency = static_cast(std::round(255 * context.objectTransparency(object->id, coordinates))); + + if (transparency == 0) + return; + + image->setAlpha(transparency); + image->setFlagColor(object->tempOwner); + + 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))); + } +} + +void MapRendererObjects::renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * instance) +{ + renderImage(context, target, coordinates, instance, getImage(context, instance, getBaseAnimation(instance))); + renderImage(context, target, coordinates, instance, getImage(context, instance, getFlagAnimation(instance))); + renderImage(context, target, coordinates, instance, getImage(context, instance, getOverlayAnimation(instance))); +} + +void MapRendererObjects::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * objectInstance = context.getObject(objectID); + + assert(objectInstance); + if(!objectInstance) + { + logGlobal->error("Stray map object that isn't fading"); + continue; + } + + renderObject(context, target, coordinates, objectInstance); + } +} + +uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * objectInstance = context.getObject(objectID); + + assert(objectInstance); + if(!objectInstance) + { + logGlobal->error("Stray map object that isn't fading"); + continue; + } + + size_t groupIndex = context.objectGroupIndex(objectInstance->id); + Point offsetPixels = context.objectImageOffset(objectInstance->id, coordinates); + + auto base = getBaseAnimation(objectInstance); + auto flag = getFlagAnimation(objectInstance); + + if (base && base->size(groupIndex) > 1) + { + auto imageIndex = context.objectImageIndex(objectID, base->size(groupIndex)); + auto image = base->getImage(imageIndex, groupIndex); + if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) + return context.objectImageIndex(objectID, 250); + } + + if (flag && flag->size(groupIndex) > 1) + { + auto imageIndex = context.objectImageIndex(objectID, flag->size(groupIndex)); + auto image = flag->getImage(imageIndex, groupIndex); + if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) + return context.objectImageIndex(objectID, 250); + } + } + return 0xff-1; +} + +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)) +{ + +} + +void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if(context.showGrid()) + target.draw(imageGrid, Point(0,0)); + + if(context.showVisitable() || context.showBlocked()) + { + bool blocking = false; + bool visitable = false; + + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * object = context.getObject(objectID); + + if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) + { + visitable |= object->visitableAt(coordinates.x, coordinates.y); + blocking |= object->blockingAt(coordinates.x, coordinates.y); + } + } + + if (context.showBlocked() && blocking) + target.draw(imageBlocked, Point(0,0)); + if (context.showVisitable() && visitable) + target.draw(imageVisitable, Point(0,0)); + } + + if (context.showSpellRange(coordinates)) + target.draw(imageSpellRange, Point(0,0)); +} + +uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + uint8_t result = 0; + + if (context.showVisitable()) + result += 1; + + if (context.showBlocked()) + result += 2; + + if (context.showGrid()) + result += 4; + + if (context.showSpellRange(coordinates)) + result += 8; + + return result; +} + +MapRendererPath::MapRendererPath() + : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"))) +{ + pathNodes->preload(); +} + +size_t MapRendererPath::selectImageReachability(bool reachableToday, size_t imageIndex) +{ + const static size_t unreachableTodayOffset = 25; + + if(!reachableToday) + return unreachableTodayOffset + imageIndex; + + return imageIndex; +} + +size_t MapRendererPath::selectImageCross(bool reachableToday, const int3 & curr) +{ + return selectImageReachability(reachableToday, 0); +} + +size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next) +{ + // Vector directions + // 0 1 2 + // | + // 3 - 4 - 5 + // | + // 6 7 8 + //For example: + // | + // +-> + // is (directionToArrowIndex[7][5]) + // + const static size_t directionToArrowIndex[9][9] = { + {16, 17, 18, 7, 0, 19, 6, 5, 0 }, + {8, 9, 18, 7, 0, 19, 6, 0, 20}, + {8, 1, 10, 7, 0, 19, 0, 21, 20}, + {24, 17, 18, 15, 0, 0, 6, 5, 4 }, + {0, 0, 0, 0, 0, 0, 0, 0, 0 }, + {8, 1, 2, 0, 0, 11, 22, 21, 20}, + {24, 17, 0, 23, 0, 3, 14, 5, 4 }, + {24, 0, 2, 23, 0, 3, 22, 13, 4 }, + {0, 1, 2, 23, 0, 3, 22, 21, 12} + }; + + size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1); + size_t leaveDirection = (prev.x - curr.x + 1) + 3 * (prev.y - curr.y + 1); + size_t imageIndex = directionToArrowIndex[enterDirection][leaveDirection]; + + return selectImageReachability(reachableToday, imageIndex); +} + +void MapRendererPath::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + size_t imageID = selectImage(context, coordinates); + + if (imageID < pathNodes->size()) + target.draw(pathNodes->getImage(imageID), Point(0,0)); +} + +size_t MapRendererPath::selectImage(IMapRendererContext & context, const int3 & coordinates) +{ + const auto & functor = [&](const CGPathNode & node) + { + return node.coord == coordinates; + }; + + const auto * path = context.currentPath(); + if(!path) + return std::numeric_limits::max(); + + const auto & iter = boost::range::find_if(path->nodes, functor); + + if(iter == path->nodes.end()) + return std::numeric_limits::max(); + + bool reachableToday = iter->turns == 0; + if(iter == path->nodes.begin()) + return selectImageCross(reachableToday, iter->coord); + + auto next = iter + 1; + auto prev = iter - 1; + + // start of path - current hero location + if(next == path->nodes.end()) + return std::numeric_limits::max(); + + bool pathContinuous = iter->coord.areNeighbours(next->coord) && iter->coord.areNeighbours(prev->coord); + bool embarking = iter->action == EPathNodeAction::EMBARK || iter->action == EPathNodeAction::DISEMBARK; + + if(pathContinuous && !embarking) + return selectImageArrow(reachableToday, iter->coord, prev->coord, next->coord); + + return selectImageCross(reachableToday, iter->coord); +} + +uint8_t MapRendererPath::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return selectImage(context, coordinates) & 0xff; +} + +MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & context, const int3 & coordinates) +{ + // computes basic checksum to determine whether tile needs an update + // if any component gives different value, tile will be updated + TileChecksum result; + boost::range::fill(result, std::numeric_limits::max()); + + if(!context.isInMap(coordinates)) + { + result[0] = rendererBorder.checksum(context, coordinates); + return result; + } + + const NeighborTilesInfo neighborInfo(context, coordinates); + + if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) + { + result[7] = rendererFow.checksum(context, coordinates); + } + else + { + result[1] = rendererTerrain.checksum(context, coordinates); + if (context.showRivers()) + result[2] = rendererRiver.checksum(context, coordinates); + if (context.showRoads()) + result[3] = rendererRoad.checksum(context, coordinates); + result[4] = rendererObjects.checksum(context, coordinates); + result[5] = rendererPath.checksum(context, coordinates); + result[6] = rendererOverlay.checksum(context, coordinates); + + if(!context.isVisible(coordinates)) + result[7] = rendererFow.checksum(context, coordinates); + } + return result; +} + +void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if(!context.isInMap(coordinates)) + { + rendererBorder.renderTile(context, target, coordinates); + return; + } + + const NeighborTilesInfo neighborInfo(context, coordinates); + + if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) + { + rendererFow.renderTile(context, target, coordinates); + } + else + { + rendererTerrain.renderTile(context, target, coordinates); + + if (context.showRivers()) + rendererRiver.renderTile(context, target, coordinates); + + if (context.showRoads()) + rendererRoad.renderTile(context, target, coordinates); + + rendererObjects.renderTile(context, target, coordinates); + rendererPath.renderTile(context, target, coordinates); + rendererOverlay.renderTile(context, target, coordinates); + + if(!context.isVisible(coordinates)) + rendererFow.renderTile(context, target, coordinates); + } +} diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index d56a37c74..46bbca888 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -1,161 +1,163 @@ -/* - * MapRenderer.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 int3; -class ObjectInstanceID; -class CGObjectInstance; - -VCMI_LIB_NAMESPACE_END - -class CAnimation; -class IImage; -class Canvas; -class IMapRendererContext; -enum class EImageBlitMode : uint8_t; - -class MapTileStorage -{ - using TerrainAnimation = std::array, 4>; - std::vector animations; - -public: - explicit MapTileStorage(size_t capacity); - void load(size_t index, const std::string & filename, EImageBlitMode blitMode); - std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); -}; - -class MapRendererTerrain -{ - MapTileStorage storage; - -public: - MapRendererTerrain(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererRiver -{ - MapTileStorage storage; - -public: - MapRendererRiver(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererRoad -{ - MapTileStorage storage; - -public: - MapRendererRoad(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererObjects -{ - std::unordered_map> animations; - - std::shared_ptr getBaseAnimation(const CGObjectInstance * obj); - std::shared_ptr getFlagAnimation(const CGObjectInstance * obj); - std::shared_ptr getOverlayAnimation(const CGObjectInstance * obj); - - std::shared_ptr getAnimation(const std::string & filename, bool generateMovementGroups); - - std::shared_ptr getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; - - void renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr & image); - void renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * obj); - -public: - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererBorder -{ - std::unique_ptr animation; - - size_t getIndexForTile(IMapRendererContext & context, const int3 & coordinates); - -public: - MapRendererBorder(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererFow -{ - std::unique_ptr fogOfWarFullHide; - std::unique_ptr fogOfWarPartialHide; - -public: - MapRendererFow(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererPath -{ - std::unique_ptr pathNodes; - - size_t selectImageReachability(bool reachableToday, size_t imageIndex); - size_t selectImageCross(bool reachableToday, const int3 & curr); - size_t selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next); - size_t selectImage(IMapRendererContext & context, const int3 & coordinates); - -public: - MapRendererPath(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererOverlay -{ - std::shared_ptr imageGrid; - std::shared_ptr imageVisitable; - std::shared_ptr imageBlocked; - std::shared_ptr imageSpellRange; -public: - MapRendererOverlay(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRenderer -{ - MapRendererTerrain rendererTerrain; - MapRendererRiver rendererRiver; - MapRendererRoad rendererRoad; - MapRendererBorder rendererBorder; - MapRendererFow rendererFow; - MapRendererObjects rendererObjects; - MapRendererPath rendererPath; - MapRendererOverlay rendererOverlay; - -public: - using TileChecksum = std::array; - - TileChecksum getTileChecksum(IMapRendererContext & context, const int3 & coordinates); - - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; +/* + * MapRenderer.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" + +VCMI_LIB_NAMESPACE_BEGIN + +class int3; +class ObjectInstanceID; +class CGObjectInstance; + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class IImage; +class Canvas; +class IMapRendererContext; +enum class EImageBlitMode; + +class MapTileStorage +{ + using TerrainAnimation = std::array, 4>; + std::vector animations; + +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); +}; + +class MapRendererTerrain +{ + MapTileStorage storage; + +public: + MapRendererTerrain(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererRiver +{ + MapTileStorage storage; + +public: + MapRendererRiver(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererRoad +{ + MapTileStorage storage; + +public: + MapRendererRoad(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererObjects +{ + std::map> animations; + + std::shared_ptr getBaseAnimation(const CGObjectInstance * obj); + 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 getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; + + void renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr & image); + void renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * obj); + +public: + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererBorder +{ + std::shared_ptr animation; + + size_t getIndexForTile(IMapRendererContext & context, const int3 & coordinates); + +public: + MapRendererBorder(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererFow +{ + std::shared_ptr fogOfWarFullHide; + std::shared_ptr fogOfWarPartialHide; + +public: + MapRendererFow(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererPath +{ + std::shared_ptr pathNodes; + + size_t selectImageReachability(bool reachableToday, size_t imageIndex); + size_t selectImageCross(bool reachableToday, const int3 & curr); + size_t selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next); + size_t selectImage(IMapRendererContext & context, const int3 & coordinates); + +public: + MapRendererPath(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererOverlay +{ + std::shared_ptr imageGrid; + std::shared_ptr imageVisitable; + std::shared_ptr imageBlocked; + std::shared_ptr imageSpellRange; +public: + MapRendererOverlay(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRenderer +{ + MapRendererTerrain rendererTerrain; + MapRendererRiver rendererRiver; + MapRendererRoad rendererRoad; + MapRendererBorder rendererBorder; + MapRendererFow rendererFow; + MapRendererObjects rendererObjects; + MapRendererPath rendererPath; + MapRendererOverlay rendererOverlay; + +public: + using TileChecksum = std::array; + + TileChecksum getTileChecksum(IMapRendererContext & context, const int3 & coordinates); + + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index caa355673..07b5575eb 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -1,530 +1,526 @@ -/* - * MapRendererContextState.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 "MapRendererContext.h" - -#include "MapRendererContextState.h" -#include "mapHandler.h" - -#include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" - -#include "../../lib/Point.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/pathfinder/CGPathNode.h" - -MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState) - : viewState(viewState) -{ -} - -uint32_t MapRendererBaseContext::getObjectRotation(ObjectInstanceID objectID) const -{ - const CGObjectInstance * obj = getObject(objectID); - - if(obj->ID == Obj::HERO) - { - const auto * hero = dynamic_cast(obj); - return hero->moveDir; - } - - if(obj->ID == Obj::BOAT) - { - const auto * boat = dynamic_cast(obj); - - if(boat->hero) - return boat->hero->moveDir; - return boat->direction; - } - return 0; -} - -int3 MapRendererBaseContext::getMapSize() const -{ - return LOCPLINT->cb->getMapSize(); -} - -bool MapRendererBaseContext::isInMap(const int3 & coordinates) const -{ - return LOCPLINT->cb->isInTheMap(coordinates); -} - -bool MapRendererBaseContext::isVisible(const int3 & coordinates) const -{ - if(settingsSessionSpectate) - return LOCPLINT->cb->isInTheMap(coordinates); - else - return LOCPLINT->cb->isVisible(coordinates); -} - -bool MapRendererBaseContext::isActiveHero(const CGObjectInstance * obj) const -{ - if(obj->ID == Obj::HERO) - { - assert(dynamic_cast(obj) != nullptr); - if(LOCPLINT->localState->getCurrentHero() != nullptr) - { - if(obj->id == LOCPLINT->localState->getCurrentHero()->id) - return true; - } - } - - return false; -} - -bool MapRendererBaseContext::tileAnimated(const int3 & coordinates) const -{ - return false; -} - -const TerrainTile & MapRendererBaseContext::getMapTile(const int3 & coordinates) const -{ - return CGI->mh->getMap()->getTile(coordinates); -} - -const MapRendererBaseContext::MapObjectsList & MapRendererBaseContext::getObjects(const int3 & coordinates) const -{ - assert(isInMap(coordinates)); - return viewState.objects[coordinates.z][coordinates.x][coordinates.y]; -} - -const CGObjectInstance * MapRendererBaseContext::getObject(ObjectInstanceID objectID) const -{ - return CGI->mh->getMap()->objects.at(objectID.getNum()); -} - -const CGPath * MapRendererBaseContext::currentPath() const -{ - return nullptr; -} - -size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const -{ - static const std::vector 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); - return Point(offsetTiles) * Point(32, 32); -} - -double MapRendererBaseContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - const CGObjectInstance * object = getObject(objectID); - - if(object->ID == Obj::HERO) - { - const auto * hero = dynamic_cast(object); - - if(hero->inTownGarrison) - return 0; - - if(hero->boat) - return 0; - } - return 1; -} - -size_t MapRendererBaseContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - return 0; -} - -size_t MapRendererBaseContext::terrainImageIndex(size_t groupSize) const -{ - return 0; -} - -size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const -{ - return std::numeric_limits::max(); -} - -double MapRendererBaseContext::viewTransitionProgress() const -{ - return 0; -} - -bool MapRendererBaseContext::filterGrayscale() const -{ - return false; -} - -bool MapRendererBaseContext::showRoads() const -{ - return true; -} - -bool MapRendererBaseContext::showRivers() const -{ - return true; -} - -bool MapRendererBaseContext::showBorder() const -{ - return false; -} - -bool MapRendererBaseContext::showOverlay() const -{ - return false; -} - -bool MapRendererBaseContext::showGrid() const -{ - return false; -} - -bool MapRendererBaseContext::showVisitable() const -{ - return false; -} - -bool MapRendererBaseContext::showBlocked() const -{ - return false; -} - -bool MapRendererBaseContext::showSpellRange(const int3 & position) const -{ - return false; -} - -MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -const CGPath * MapRendererAdventureContext::currentPath() const -{ - const auto * hero = LOCPLINT->localState->getCurrentHero(); - - if(!hero) - return nullptr; - - if(!LOCPLINT->localState->hasPath(hero)) - return nullptr; - - return &LOCPLINT->localState->getPath(hero); -} - -size_t MapRendererAdventureContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - assert(groupSize > 0); - - if(!settingsAdventureObjectAnimation) - return 0; - - if(groupSize == 0) - return 0; - - // usign objectID for frameCounter to add pseudo-random element per-object. - // Without it, animation of multiple visible objects of the same type will always be in sync - size_t baseFrameTime = 180; - size_t frameCounter = animationTime / baseFrameTime + objectID.getNum(); - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const -{ - if(!settingsAdventureTerrainAnimation) - return 0; - - size_t baseFrameTime = 180; - size_t frameCounter = animationTime / baseFrameTime; - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -bool MapRendererAdventureContext::showBorder() const -{ - return true; -} - -bool MapRendererAdventureContext::showGrid() const -{ - return settingShowGrid; -} - -bool MapRendererAdventureContext::showVisitable() const -{ - return settingShowVisitable; -} - -bool MapRendererAdventureContext::showBlocked() const -{ - return settingShowBlocked; -} - -bool MapRendererAdventureContext::showSpellRange(const int3 & position) const -{ - if (!settingSpellRange) - return false; - - auto hero = LOCPLINT->localState->getCurrentHero(); - - if (!hero) - return false; - - return !isInScreenRange(hero->getSightCenter(), position); -} - -MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -double MapRendererAdventureTransitionContext::viewTransitionProgress() const -{ - return progress; -} - -MapRendererAdventureFadingContext::MapRendererAdventureFadingContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -bool MapRendererAdventureFadingContext::tileAnimated(const int3 & coordinates) const -{ - if(!isInMap(coordinates)) - return false; - - auto objects = getObjects(coordinates); - if(vstd::contains(objects, target)) - return true; - - return false; -} - -double MapRendererAdventureFadingContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(objectID == target) - return progress; - - return MapRendererAdventureContext::objectTransparency(objectID, coordinates); -} - -MapRendererAdventureMovingContext::MapRendererAdventureMovingContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID objectID) const -{ - if(target == objectID) - { - static const std::vector moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; - return moveGroups[getObjectRotation(objectID)]; - } - return MapRendererAdventureContext::objectGroupIndex(objectID); -} - -bool MapRendererAdventureMovingContext::tileAnimated(const int3 & coordinates) const -{ - if(!isInMap(coordinates)) - return false; - - auto objects = getObjects(coordinates); - if(vstd::contains(objects, target)) - return true; - - return false; -} - -Point MapRendererAdventureMovingContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(target == objectID) - { - int3 offsetTilesFrom = tileFrom - coordinates; - int3 offsetTilesDest = tileDest - coordinates; - - Point offsetPixelsFrom = Point(offsetTilesFrom) * Point(32, 32); - Point offsetPixelsDest = Point(offsetTilesDest) * Point(32, 32); - - Point result = vstd::lerp(offsetPixelsFrom, offsetPixelsDest, progress); - - return result; - } - - return MapRendererAdventureContext::objectImageOffset(objectID, coordinates); -} - -size_t MapRendererAdventureMovingContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - if(target != objectID) - return MapRendererAdventureContext::objectImageIndex(objectID, groupSize); - - int32_t baseFrameTime = 50; - size_t frameCounter = animationTime / baseFrameTime; - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -size_t MapRendererWorldViewContext::selectOverlayImageForObject(const ObjectPosInfo & object) const -{ - size_t ownerIndex = PlayerColor::PLAYER_LIMIT.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); - - if(object.owner.isValidPlayer()) - ownerIndex = object.owner.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); - - switch(object.id) - { - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_ONE_WAY_EXIT: - case Obj::MONOLITH_TWO_WAY: - return ownerIndex + static_cast(EWorldViewIcon::TELEPORT); - case Obj::SUBTERRANEAN_GATE: - return ownerIndex + static_cast(EWorldViewIcon::GATE); - case Obj::ARTIFACT: - return ownerIndex + static_cast(EWorldViewIcon::ARTIFACT); - case Obj::TOWN: - return ownerIndex + static_cast(EWorldViewIcon::TOWN); - case Obj::HERO: - return ownerIndex + static_cast(EWorldViewIcon::HERO); - case Obj::MINE: - return ownerIndex + static_cast(EWorldViewIcon::MINE_WOOD) + object.subId; - case Obj::RESOURCE: - return ownerIndex + static_cast(EWorldViewIcon::RES_WOOD) + object.subId; - } - return std::numeric_limits::max(); -} - -MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -bool MapRendererWorldViewContext::showOverlay() const -{ - return true; -} - -size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) const -{ - if(!isVisible(coordinates)) - return std::numeric_limits::max(); - - for(const auto & objectID : getObjects(coordinates)) - { - const auto * object = getObject(objectID); - - if(!object->visitableAt(coordinates.x, coordinates.y)) - continue; - - ObjectPosInfo info; - info.pos = coordinates; - info.id = object->ID; - info.subId = object->subID; - info.owner = object->tempOwner; - - size_t iconIndex = selectOverlayImageForObject(info); - - if(iconIndex != std::numeric_limits::max()) - return iconIndex; - } - - return std::numeric_limits::max(); -} - -MapRendererSpellViewContext::MapRendererSpellViewContext(const MapRendererContextState & viewState) - : MapRendererWorldViewContext(viewState) -{ -} - -double MapRendererSpellViewContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(showAllTerrain) - { - if(getObject(objectID)->isVisitable() && !MapRendererWorldViewContext::isVisible(coordinates)) - return 0; - } - - return MapRendererWorldViewContext::objectTransparency(objectID, coordinates); -} - -bool MapRendererSpellViewContext::isVisible(const int3 & coordinates) const -{ - if(showAllTerrain) - return isInMap(coordinates); - return MapRendererBaseContext::isVisible(coordinates); -} - -size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) const -{ - for(const auto & entry : additionalOverlayIcons) - { - if(entry.pos != coordinates) - continue; - - size_t iconIndex = selectOverlayImageForObject(entry); - - if(iconIndex != std::numeric_limits::max()) - return iconIndex; - } - - return MapRendererWorldViewContext::overlayImageIndex(coordinates); -} - -MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -MapRendererPuzzleMapContext::~MapRendererPuzzleMapContext() = default; - -const CGPath * MapRendererPuzzleMapContext::currentPath() const -{ - return grailPos.get(); -} - -double MapRendererPuzzleMapContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - const auto * object = getObject(objectID); - - if(!object) - return 0; - - if(object->isVisitable()) - return 0; - - if(object->ID == Obj::HOLE) - return 0; - - return MapRendererBaseContext::objectTransparency(objectID, coordinates); -} - -bool MapRendererPuzzleMapContext::isVisible(const int3 & coordinates) const -{ - return LOCPLINT->cb->isInTheMap(coordinates); -} - -bool MapRendererPuzzleMapContext::filterGrayscale() const -{ - return true; -} - -bool MapRendererPuzzleMapContext::showRoads() const -{ - return false; -} - -bool MapRendererPuzzleMapContext::showRivers() const -{ - return false; -} +/* + * MapRendererContextState.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 "MapRendererContext.h" + +#include "MapRendererContextState.h" +#include "mapHandler.h" + +#include "../../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" + +#include "../../lib/Point.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/pathfinder/CGPathNode.h" + +MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState) + : viewState(viewState) +{ +} + +uint32_t MapRendererBaseContext::getObjectRotation(ObjectInstanceID objectID) const +{ + const CGObjectInstance * obj = getObject(objectID); + + if(obj->ID == Obj::HERO) + { + const auto * hero = dynamic_cast(obj); + return hero->moveDir; + } + + if(obj->ID == Obj::BOAT) + { + const auto * boat = dynamic_cast(obj); + + if(boat->hero) + return boat->hero->moveDir; + return boat->direction; + } + return 0; +} + +int3 MapRendererBaseContext::getMapSize() const +{ + return LOCPLINT->cb->getMapSize(); +} + +bool MapRendererBaseContext::isInMap(const int3 & coordinates) const +{ + return LOCPLINT->cb->isInTheMap(coordinates); +} + +bool MapRendererBaseContext::isVisible(const int3 & coordinates) const +{ + if(settingsSessionSpectate) + return LOCPLINT->cb->isInTheMap(coordinates); + else + return LOCPLINT->cb->isVisible(coordinates); +} + +bool MapRendererBaseContext::isActiveHero(const CGObjectInstance * obj) const +{ + if(obj->ID == Obj::HERO) + { + assert(dynamic_cast(obj) != nullptr); + if(LOCPLINT->localState->getCurrentHero() != nullptr) + { + if(obj->id == LOCPLINT->localState->getCurrentHero()->id) + return true; + } + } + + return false; +} + +bool MapRendererBaseContext::tileAnimated(const int3 & coordinates) const +{ + return false; +} + +const TerrainTile & MapRendererBaseContext::getMapTile(const int3 & coordinates) const +{ + return CGI->mh->getMap()->getTile(coordinates); +} + +const MapRendererBaseContext::MapObjectsList & MapRendererBaseContext::getObjects(const int3 & coordinates) const +{ + assert(isInMap(coordinates)); + return viewState.objects[coordinates.z][coordinates.x][coordinates.y]; +} + +const CGObjectInstance * MapRendererBaseContext::getObject(ObjectInstanceID objectID) const +{ + return CGI->mh->getMap()->objects.at(objectID.getNum()); +} + +const CGPath * MapRendererBaseContext::currentPath() const +{ + return nullptr; +} + +size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const +{ + static const std::vector 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); + return Point(offsetTiles) * Point(32, 32); +} + +double MapRendererBaseContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + const CGObjectInstance * object = getObject(objectID); + + if(object->ID == Obj::HERO) + { + const auto * hero = dynamic_cast(object); + + if(hero->inTownGarrison) + return 0; + + if(hero->boat) + return 0; + } + return 1; +} + +size_t MapRendererBaseContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + return 0; +} + +size_t MapRendererBaseContext::terrainImageIndex(size_t groupSize) const +{ + return 0; +} + +size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const +{ + return std::numeric_limits::max(); +} + +double MapRendererBaseContext::viewTransitionProgress() const +{ + return 0; +} + +bool MapRendererBaseContext::filterGrayscale() const +{ + return false; +} + +bool MapRendererBaseContext::showRoads() const +{ + return true; +} + +bool MapRendererBaseContext::showRivers() const +{ + return true; +} + +bool MapRendererBaseContext::showBorder() const +{ + return false; +} + +bool MapRendererBaseContext::showOverlay() const +{ + return false; +} + +bool MapRendererBaseContext::showGrid() const +{ + return false; +} + +bool MapRendererBaseContext::showVisitable() const +{ + return false; +} + +bool MapRendererBaseContext::showBlocked() const +{ + return false; +} + +bool MapRendererBaseContext::showSpellRange(const int3 & position) const +{ + return false; +} + +MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +const CGPath * MapRendererAdventureContext::currentPath() const +{ + const auto * hero = LOCPLINT->localState->getCurrentHero(); + + if(!hero) + return nullptr; + + if(!LOCPLINT->localState->hasPath(hero)) + return nullptr; + + return &LOCPLINT->localState->getPath(hero); +} + +size_t MapRendererAdventureContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + assert(groupSize > 0); + + if(!settingsAdventureObjectAnimation) + return 0; + + if(groupSize == 0) + return 0; + + // usign objectID for frameCounter to add pseudo-random element per-object. + // Without it, animation of multiple visible objects of the same type will always be in sync + size_t baseFrameTime = 180; + size_t frameCounter = animationTime / baseFrameTime + objectID.getNum(); + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const +{ + if(!settingsAdventureTerrainAnimation) + return 0; + + size_t baseFrameTime = 180; + size_t frameCounter = animationTime / baseFrameTime; + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +bool MapRendererAdventureContext::showBorder() const +{ + return true; +} + +bool MapRendererAdventureContext::showGrid() const +{ + return settingShowGrid; +} + +bool MapRendererAdventureContext::showVisitable() const +{ + return settingShowVisitable; +} + +bool MapRendererAdventureContext::showBlocked() const +{ + return settingShowBlocked; +} + +bool MapRendererAdventureContext::showSpellRange(const int3 & position) const +{ + if (!settingSpellRange) + return false; + + auto hero = LOCPLINT->localState->getCurrentHero(); + + if (!hero) + return false; + + return !isInScreenRange(hero->getSightCenter(), position); +} + +MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +double MapRendererAdventureTransitionContext::viewTransitionProgress() const +{ + return progress; +} + +MapRendererAdventureFadingContext::MapRendererAdventureFadingContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +bool MapRendererAdventureFadingContext::tileAnimated(const int3 & coordinates) const +{ + if(!isInMap(coordinates)) + return false; + + auto objects = getObjects(coordinates); + if(vstd::contains(objects, target)) + return true; + + return false; +} + +double MapRendererAdventureFadingContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(objectID == target) + return progress; + + return MapRendererAdventureContext::objectTransparency(objectID, coordinates); +} + +MapRendererAdventureMovingContext::MapRendererAdventureMovingContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID objectID) const +{ + if(target == objectID) + { + static const std::vector moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; + return moveGroups[getObjectRotation(objectID)]; + } + return MapRendererAdventureContext::objectGroupIndex(objectID); +} + +bool MapRendererAdventureMovingContext::tileAnimated(const int3 & coordinates) const +{ + if(!isInMap(coordinates)) + return false; + + auto objects = getObjects(coordinates); + if(vstd::contains(objects, target)) + return true; + + return false; +} + +Point MapRendererAdventureMovingContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(target == objectID) + { + int3 offsetTilesFrom = tileFrom - coordinates; + int3 offsetTilesDest = tileDest - coordinates; + + Point offsetPixelsFrom = Point(offsetTilesFrom) * Point(32, 32); + Point offsetPixelsDest = Point(offsetTilesDest) * Point(32, 32); + + Point result = vstd::lerp(offsetPixelsFrom, offsetPixelsDest, progress); + + return result; + } + + return MapRendererAdventureContext::objectImageOffset(objectID, coordinates); +} + +size_t MapRendererAdventureMovingContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + if(target != objectID) + return MapRendererAdventureContext::objectImageIndex(objectID, groupSize); + + int32_t baseFrameTime = 50; + size_t frameCounter = animationTime / baseFrameTime; + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +size_t MapRendererWorldViewContext::selectOverlayImageForObject(const ObjectPosInfo & object) const +{ + size_t ownerIndex = PlayerColor::PLAYER_LIMIT.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); + + if(object.owner.isValidPlayer()) + ownerIndex = object.owner.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); + + switch(object.id) + { + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_ONE_WAY_EXIT: + case Obj::MONOLITH_TWO_WAY: + return ownerIndex + static_cast(EWorldViewIcon::TELEPORT); + case Obj::SUBTERRANEAN_GATE: + return ownerIndex + static_cast(EWorldViewIcon::GATE); + case Obj::ARTIFACT: + return ownerIndex + static_cast(EWorldViewIcon::ARTIFACT); + case Obj::TOWN: + return ownerIndex + static_cast(EWorldViewIcon::TOWN); + case Obj::HERO: + return ownerIndex + static_cast(EWorldViewIcon::HERO); + case Obj::MINE: + return ownerIndex + static_cast(EWorldViewIcon::MINE_WOOD) + object.subId; + case Obj::RESOURCE: + return ownerIndex + static_cast(EWorldViewIcon::RES_WOOD) + object.subId; + } + return std::numeric_limits::max(); +} + +MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +bool MapRendererWorldViewContext::showOverlay() const +{ + return true; +} + +size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) const +{ + if(!isVisible(coordinates)) + return std::numeric_limits::max(); + + for(const auto & objectID : getObjects(coordinates)) + { + const auto * object = getObject(objectID); + + if(!object->visitableAt(coordinates.x, coordinates.y)) + continue; + + ObjectPosInfo info(object); + + size_t iconIndex = selectOverlayImageForObject(info); + + if(iconIndex != std::numeric_limits::max()) + return iconIndex; + } + + return std::numeric_limits::max(); +} + +MapRendererSpellViewContext::MapRendererSpellViewContext(const MapRendererContextState & viewState) + : MapRendererWorldViewContext(viewState) +{ +} + +double MapRendererSpellViewContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(showAllTerrain) + { + if(getObject(objectID)->isVisitable() && !MapRendererWorldViewContext::isVisible(coordinates)) + return 0; + } + + return MapRendererWorldViewContext::objectTransparency(objectID, coordinates); +} + +bool MapRendererSpellViewContext::isVisible(const int3 & coordinates) const +{ + if(showAllTerrain) + return isInMap(coordinates); + return MapRendererBaseContext::isVisible(coordinates); +} + +size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) const +{ + for(const auto & entry : additionalOverlayIcons) + { + if(entry.pos != coordinates) + continue; + + size_t iconIndex = selectOverlayImageForObject(entry); + + if(iconIndex != std::numeric_limits::max()) + return iconIndex; + } + + return MapRendererWorldViewContext::overlayImageIndex(coordinates); +} + +MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +MapRendererPuzzleMapContext::~MapRendererPuzzleMapContext() = default; + +const CGPath * MapRendererPuzzleMapContext::currentPath() const +{ + return grailPos.get(); +} + +double MapRendererPuzzleMapContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + const auto * object = getObject(objectID); + + if(!object) + return 0; + + if(object->isVisitable()) + return 0; + + if(object->ID == Obj::HOLE) + return 0; + + return MapRendererBaseContext::objectTransparency(objectID, coordinates); +} + +bool MapRendererPuzzleMapContext::isVisible(const int3 & coordinates) const +{ + return LOCPLINT->cb->isInTheMap(coordinates); +} + +bool MapRendererPuzzleMapContext::filterGrayscale() const +{ + return true; +} + +bool MapRendererPuzzleMapContext::showRoads() const +{ + return false; +} + +bool MapRendererPuzzleMapContext::showRivers() const +{ + return false; +} diff --git a/client/mapView/MapRendererContext.h b/client/mapView/MapRendererContext.h index ee8061535..742053fd1 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -1,166 +1,166 @@ -/* - * MapRendererContext.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 "IMapRendererContext.h" - -#include "../lib/GameConstants.h" -#include "../lib/int3.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -VCMI_LIB_NAMESPACE_END - -struct MapRendererContextState; - -class MapRendererBaseContext : public IMapRendererContext -{ -public: - const MapRendererContextState & viewState; - bool settingsSessionSpectate = false; - - explicit MapRendererBaseContext(const MapRendererContextState & viewState); - - uint32_t getObjectRotation(ObjectInstanceID objectID) const; - - int3 getMapSize() const override; - bool isInMap(const int3 & coordinates) const override; - bool isVisible(const int3 & coordinates) const override; - bool tileAnimated(const int3 & coordinates) const override; - - bool isActiveHero(const CGObjectInstance* obj) const override; - - const TerrainTile & getMapTile(const int3 & coordinates) const override; - const MapObjectsList & getObjects(const int3 & coordinates) const override; - const CGObjectInstance * getObject(ObjectInstanceID objectID) const override; - const CGPath * currentPath() const override; - - size_t objectGroupIndex(ObjectInstanceID objectID) const override; - Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - 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; - - 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 showGrid() const override; - bool showVisitable() const override; - bool showBlocked() const override; - bool showSpellRange(const int3 & position) const override; -}; - -class MapRendererAdventureContext : public MapRendererBaseContext -{ -public: - uint32_t animationTime = 0; - bool settingShowGrid = false; - bool settingShowVisitable = false; - bool settingShowBlocked = false; - bool settingSpellRange= false; - bool settingsAdventureObjectAnimation = true; - bool settingsAdventureTerrainAnimation = true; - - explicit MapRendererAdventureContext(const MapRendererContextState & viewState); - - const CGPath * currentPath() const override; - size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; - size_t terrainImageIndex(size_t groupSize) const override; - - bool showBorder() const override; - bool showGrid() const override; - bool showVisitable() const override; - bool showBlocked() const override; - - bool showSpellRange(const int3 & position) const override; -}; - -class MapRendererAdventureTransitionContext : public MapRendererAdventureContext -{ -public: - double progress = 0; - - explicit MapRendererAdventureTransitionContext(const MapRendererContextState & viewState); - - double viewTransitionProgress() const override; -}; - -class MapRendererAdventureFadingContext : public MapRendererAdventureContext -{ -public: - ObjectInstanceID target; - double progress; - - explicit MapRendererAdventureFadingContext(const MapRendererContextState & viewState); - - bool tileAnimated(const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; -}; - -class MapRendererAdventureMovingContext : public MapRendererAdventureContext -{ -public: - ObjectInstanceID target; - int3 tileFrom; - int3 tileDest; - double progress; - - explicit MapRendererAdventureMovingContext(const MapRendererContextState & viewState); - - bool tileAnimated(const int3 & coordinates) const override; - size_t objectGroupIndex(ObjectInstanceID objectID) const override; - Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; - size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; -}; - -class MapRendererWorldViewContext : public MapRendererBaseContext -{ -protected: - size_t selectOverlayImageForObject(const ObjectPosInfo & object) const; - -public: - explicit MapRendererWorldViewContext(const MapRendererContextState & viewState); - - size_t overlayImageIndex(const int3 & coordinates) const override; - bool showOverlay() const override; -}; - -class MapRendererSpellViewContext : public MapRendererWorldViewContext -{ -public: - std::vector additionalOverlayIcons; - bool showAllTerrain = false; - - explicit MapRendererSpellViewContext(const MapRendererContextState & viewState); - - bool isVisible(const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - size_t overlayImageIndex(const int3 & coordinates) const override; -}; - -class MapRendererPuzzleMapContext : public MapRendererBaseContext -{ -public: - std::unique_ptr grailPos; - - explicit MapRendererPuzzleMapContext(const MapRendererContextState & viewState); - ~MapRendererPuzzleMapContext(); - - const CGPath * currentPath() const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - bool isVisible(const int3 & coordinates) const override; - bool filterGrayscale() const override; - bool showRoads() const override; - bool showRivers() const override; -}; +/* + * MapRendererContext.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 "IMapRendererContext.h" + +#include "../lib/GameConstants.h" +#include "../lib/int3.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +VCMI_LIB_NAMESPACE_END + +struct MapRendererContextState; + +class MapRendererBaseContext : public IMapRendererContext +{ +public: + const MapRendererContextState & viewState; + bool settingsSessionSpectate = false; + + explicit MapRendererBaseContext(const MapRendererContextState & viewState); + + uint32_t getObjectRotation(ObjectInstanceID objectID) const; + + int3 getMapSize() const override; + bool isInMap(const int3 & coordinates) const override; + bool isVisible(const int3 & coordinates) const override; + bool tileAnimated(const int3 & coordinates) const override; + + bool isActiveHero(const CGObjectInstance* obj) const override; + + const TerrainTile & getMapTile(const int3 & coordinates) const override; + const MapObjectsList & getObjects(const int3 & coordinates) const override; + const CGObjectInstance * getObject(ObjectInstanceID objectID) const override; + const CGPath * currentPath() const override; + + size_t objectGroupIndex(ObjectInstanceID objectID) const override; + Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + 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; + + 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 showGrid() const override; + bool showVisitable() const override; + bool showBlocked() const override; + bool showSpellRange(const int3 & position) const override; +}; + +class MapRendererAdventureContext : public MapRendererBaseContext +{ +public: + uint32_t animationTime = 0; + bool settingShowGrid = false; + bool settingShowVisitable = false; + bool settingShowBlocked = false; + bool settingSpellRange= false; + bool settingsAdventureObjectAnimation = true; + bool settingsAdventureTerrainAnimation = true; + + explicit MapRendererAdventureContext(const MapRendererContextState & viewState); + + const CGPath * currentPath() const override; + size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; + size_t terrainImageIndex(size_t groupSize) const override; + + bool showBorder() const override; + bool showGrid() const override; + bool showVisitable() const override; + bool showBlocked() const override; + + bool showSpellRange(const int3 & position) const override; +}; + +class MapRendererAdventureTransitionContext : public MapRendererAdventureContext +{ +public: + double progress = 0; + + explicit MapRendererAdventureTransitionContext(const MapRendererContextState & viewState); + + double viewTransitionProgress() const override; +}; + +class MapRendererAdventureFadingContext : public MapRendererAdventureContext +{ +public: + ObjectInstanceID target; + double progress; + + explicit MapRendererAdventureFadingContext(const MapRendererContextState & viewState); + + bool tileAnimated(const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; +}; + +class MapRendererAdventureMovingContext : public MapRendererAdventureContext +{ +public: + ObjectInstanceID target; + int3 tileFrom; + int3 tileDest; + double progress; + + explicit MapRendererAdventureMovingContext(const MapRendererContextState & viewState); + + bool tileAnimated(const int3 & coordinates) const override; + size_t objectGroupIndex(ObjectInstanceID objectID) const override; + Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; + size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; +}; + +class MapRendererWorldViewContext : public MapRendererBaseContext +{ +protected: + size_t selectOverlayImageForObject(const ObjectPosInfo & object) const; + +public: + explicit MapRendererWorldViewContext(const MapRendererContextState & viewState); + + size_t overlayImageIndex(const int3 & coordinates) const override; + bool showOverlay() const override; +}; + +class MapRendererSpellViewContext : public MapRendererWorldViewContext +{ +public: + std::vector additionalOverlayIcons; + bool showAllTerrain = false; + + explicit MapRendererSpellViewContext(const MapRendererContextState & viewState); + + bool isVisible(const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + size_t overlayImageIndex(const int3 & coordinates) const override; +}; + +class MapRendererPuzzleMapContext : public MapRendererBaseContext +{ +public: + std::unique_ptr grailPos; + + explicit MapRendererPuzzleMapContext(const MapRendererContextState & viewState); + ~MapRendererPuzzleMapContext(); + + const CGPath * currentPath() const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + bool isVisible(const int3 & coordinates) const override; + bool filterGrayscale() const override; + bool showRoads() const override; + bool showRivers() const override; +}; diff --git a/client/mapView/MapRendererContextState.cpp b/client/mapView/MapRendererContextState.cpp index 1571cc459..aa1a6ab0a 100644 --- a/client/mapView/MapRendererContextState.cpp +++ b/client/mapView/MapRendererContextState.cpp @@ -1,93 +1,95 @@ -/* - * MapRendererContext.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 "MapRendererContextState.h" - -#include "IMapRendererContext.h" -#include "mapHandler.h" - -#include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" - -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapping/CMap.h" - -static bool compareObjectBlitOrder(ObjectInstanceID left, ObjectInstanceID right) -{ - //FIXME: remove mh access - return CGI->mh->compareObjectBlitOrder(CGI->mh->getMap()->objects[left.getNum()], CGI->mh->getMap()->objects[right.getNum()]); -} - -MapRendererContextState::MapRendererContextState() -{ - auto mapSize = LOCPLINT->cb->getMapSize(); - - objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); - - for(const auto & obj : CGI->mh->getMap()->objects) - addObject(obj); -} - -void MapRendererContextState::addObject(const CGObjectInstance * obj) -{ - if(!obj) - return; - - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) - { - auto & container = objects[currTile.z][currTile.x][currTile.y]; - - container.push_back(obj->id); - boost::range::sort(container, compareObjectBlitOrder); - } - } - } -} - -void MapRendererContextState::addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest) -{ - int xFrom = std::min(tileFrom.x, tileDest.x) - object->getWidth(); - int xDest = std::max(tileFrom.x, tileDest.x); - int yFrom = std::min(tileFrom.y, tileDest.y) - object->getHeight(); - int yDest = std::max(tileFrom.y, tileDest.y); - - for(int x = xFrom; x <= xDest; ++x) - { - for(int y = yFrom; y <= yDest; ++y) - { - int3 currTile(x, y, object->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile)) - { - auto & container = objects[currTile.z][currTile.x][currTile.y]; - - container.push_back(object->id); - boost::range::sort(container, compareObjectBlitOrder); - } - } - } -} - -void MapRendererContextState::removeObject(const CGObjectInstance * object) -{ - for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) - for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) - for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) - vstd::erase(objects[z][x][y], object->id); -} +/* + * MapRendererContext.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 "MapRendererContextState.h" + +#include "IMapRendererContext.h" +#include "mapHandler.h" + +#include "../../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapping/CMap.h" + +static bool compareObjectBlitOrder(ObjectInstanceID left, ObjectInstanceID right) +{ + //FIXME: remove mh access + return CGI->mh->compareObjectBlitOrder(CGI->mh->getMap()->objects[left.getNum()], CGI->mh->getMap()->objects[right.getNum()]); +} + +MapRendererContextState::MapRendererContextState() +{ + auto mapSize = LOCPLINT->cb->getMapSize(); + + objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + logGlobal->debug("Loading map objects"); + for(const auto & obj : CGI->mh->getMap()->objects) + addObject(obj); + logGlobal->debug("Done loading map objects"); +} + +void MapRendererContextState::addObject(const CGObjectInstance * obj) +{ + if(!obj) + return; + + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + { + auto & container = objects[currTile.z][currTile.x][currTile.y]; + + container.push_back(obj->id); + boost::range::sort(container, compareObjectBlitOrder); + } + } + } +} + +void MapRendererContextState::addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest) +{ + int xFrom = std::min(tileFrom.x, tileDest.x) - object->getWidth(); + int xDest = std::max(tileFrom.x, tileDest.x); + int yFrom = std::min(tileFrom.y, tileDest.y) - object->getHeight(); + int yDest = std::max(tileFrom.y, tileDest.y); + + for(int x = xFrom; x <= xDest; ++x) + { + for(int y = yFrom; y <= yDest; ++y) + { + int3 currTile(x, y, object->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile)) + { + auto & container = objects[currTile.z][currTile.x][currTile.y]; + + container.push_back(object->id); + boost::range::sort(container, compareObjectBlitOrder); + } + } + } +} + +void MapRendererContextState::removeObject(const CGObjectInstance * object) +{ + for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) + for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) + for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) + vstd::erase(objects[z][x][y], object->id); +} diff --git a/client/mapView/MapRendererContextState.h b/client/mapView/MapRendererContextState.h index 9adee457f..cf0442b72 100644 --- a/client/mapView/MapRendererContextState.h +++ b/client/mapView/MapRendererContextState.h @@ -1,62 +1,62 @@ -/* - * MapRendererContext.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/GameConstants.h" -#include "../lib/int3.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -class CGObjectInstance; -VCMI_LIB_NAMESPACE_END - -class IMapRendererContext; - -// from VwSymbol.def -enum class EWorldViewIcon -{ - TOWN = 0, - HERO = 1, - ARTIFACT = 2, - TELEPORT = 3, - GATE = 4, - MINE_WOOD = 5, - MINE_MERCURY = 6, - MINE_STONE = 7, - MINE_SULFUR = 8, - MINE_CRYSTAL = 9, - MINE_GEM = 10, - MINE_GOLD = 11, - RES_WOOD = 12, - RES_MERCURY = 13, - RES_STONE = 14, - RES_SULFUR = 15, - RES_CRYSTAL = 16, - RES_GEM = 17, - RES_GOLD = 18, - - ICONS_PER_PLAYER = 19, - ICONS_TOTAL = 19 * 9 // 8 players + neutral set at the end -}; - -struct MapRendererContextState -{ -public: - MapRendererContextState(); - - using MapObject = ObjectInstanceID; - using MapObjectsList = std::vector; - - boost::multi_array objects; - - void addObject(const CGObjectInstance * object); - void addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest); - void removeObject(const CGObjectInstance * object); -}; +/* + * MapRendererContext.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/GameConstants.h" +#include "../lib/int3.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class IMapRendererContext; + +// from VwSymbol.def +enum class EWorldViewIcon +{ + TOWN = 0, + HERO = 1, + ARTIFACT = 2, + TELEPORT = 3, + GATE = 4, + MINE_WOOD = 5, + MINE_MERCURY = 6, + MINE_STONE = 7, + MINE_SULFUR = 8, + MINE_CRYSTAL = 9, + MINE_GEM = 10, + MINE_GOLD = 11, + RES_WOOD = 12, + RES_MERCURY = 13, + RES_STONE = 14, + RES_SULFUR = 15, + RES_CRYSTAL = 16, + RES_GEM = 17, + RES_GOLD = 18, + + ICONS_PER_PLAYER = 19, + ICONS_TOTAL = 19 * 9 // 8 players + neutral set at the end +}; + +struct MapRendererContextState +{ +public: + MapRendererContextState(); + + using MapObject = ObjectInstanceID; + using MapObjectsList = std::vector; + + boost::multi_array objects; + + void addObject(const CGObjectInstance * object); + void addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest); + void removeObject(const CGObjectInstance * object); +}; diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 76f981337..b39c8a6f5 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -1,162 +1,215 @@ -/* - * MapView.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 "MapView.h" - -#include "MapViewActions.h" -#include "MapViewCache.h" -#include "MapViewController.h" -#include "MapViewModel.h" -#include "mapHandler.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" - -#include "../../CCallback.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" - -BasicMapView::~BasicMapView() = default; - -std::shared_ptr BasicMapView::createModel(const Point & dimensions) const -{ - auto result = std::make_shared(); - - result->setLevel(0); - result->setTileSize(Point(32, 32)); - result->setViewCenter(Point(0, 0)); - result->setViewDimensions(dimensions); - - return result; -} - -BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) - : model(createModel(dimensions)) - , tilesCache(new MapViewCache(model)) - , controller(new MapViewController(model, tilesCache)) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += offset; - pos.w = dimensions.x; - pos.h = dimensions.y; -} - -void BasicMapView::render(Canvas & target, bool fullUpdate) -{ - Canvas targetClipped(target, pos); - tilesCache->update(controller->getContext()); - tilesCache->render(controller->getContext(), targetClipped, fullUpdate); -} - -void BasicMapView::tick(uint32_t msPassed) -{ - controller->tick(msPassed); -} - -void BasicMapView::show(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - render(to, false); - - controller->afterRender(); -} - -void BasicMapView::showAll(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - render(to, true); -} - -void MapView::show(Canvas & to) -{ - actions->setContext(controller->getContext()); - BasicMapView::show(to); -} - -MapView::MapView(const Point & offset, const Point & dimensions) - : BasicMapView(offset, dimensions) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - actions = std::make_shared(*this, model); - actions->setContext(controller->getContext()); -} - -void MapView::onMapLevelSwitched() -{ - if(LOCPLINT->cb->getMapSize().z > 1) - { - if(model->getLevel() == 0) - controller->setViewCenter(model->getMapViewCenter(), 1); - else - controller->setViewCenter(model->getMapViewCenter(), 0); - } -} - -void MapView::onMapScrolled(const Point & distance) -{ - if(!isGesturing()) - controller->setViewCenter(model->getMapViewCenter() + distance, model->getLevel()); -} - -void MapView::onMapSwiped(const Point & viewPosition) -{ - controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); -} - -void MapView::onCenteredTile(const int3 & tile) -{ - controller->setViewCenter(tile); -} - -void MapView::onCenteredObject(const CGObjectInstance * target) -{ - controller->setViewCenter(target->getSightCenter()); -} - -void MapView::onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain) -{ - controller->activateSpellViewContext(); - controller->setTileSize(Point(tileSize, tileSize)); - controller->setOverlayVisibility(objectPositions); - controller->setTerrainVisibility(showTerrain); -} - -void MapView::onViewWorldActivated(uint32_t tileSize) -{ - controller->activateWorldViewContext(); - controller->setTileSize(Point(tileSize, tileSize)); -} - -void MapView::onMapZoomLevelChanged(int stepsChange) -{ - controller->modifyTileSize(stepsChange); -} - -void MapView::onViewMapActivated() -{ - controller->activateAdventureContext(); - controller->setTileSize(Point(32, 32)); -} - -PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter) - : BasicMapView(offset, dimensions) -{ - controller->activatePuzzleMapContext(tileToCenter); - controller->setViewCenter(tileToCenter); - -} +/* + * MapView.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 "MapView.h" + +#include "MapViewActions.h" +#include "MapViewCache.h" +#include "MapViewController.h" +#include "MapViewModel.h" +#include "mapHandler.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../renderSDL/SDL_Extensions.h" +#include "../eventsSDL/InputHandler.h" + +#include "../../CCallback.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +BasicMapView::~BasicMapView() = default; + +std::shared_ptr BasicMapView::createModel(const Point & dimensions) const +{ + auto result = std::make_shared(); + + result->setLevel(0); + result->setTileSize(Point(32, 32)); + result->setViewCenter(Point(0, 0)); + result->setViewDimensions(dimensions); + + return result; +} + +BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) + : model(createModel(dimensions)) + , tilesCache(new MapViewCache(model)) + , controller(new MapViewController(model, tilesCache)) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += offset; + pos.w = dimensions.x; + pos.h = dimensions.y; +} + +void BasicMapView::render(Canvas & target, bool fullUpdate) +{ + Canvas targetClipped(target, pos); + tilesCache->update(controller->getContext()); + tilesCache->render(controller->getContext(), targetClipped, fullUpdate); +} + +void BasicMapView::tick(uint32_t msPassed) +{ + controller->tick(msPassed); +} + +void BasicMapView::show(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + render(to, false); + + controller->afterRender(); +} + +void BasicMapView::showAll(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + render(to, true); +} + +void MapView::tick(uint32_t msPassed) +{ + if(settings["adventure"]["smoothDragging"].Bool()) + postSwipe(msPassed); + + BasicMapView::tick(msPassed); +} + +void MapView::show(Canvas & to) +{ + actions->setContext(controller->getContext()); + BasicMapView::show(to); +} + +MapView::MapView(const Point & offset, const Point & dimensions) + : BasicMapView(offset, dimensions) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + actions = std::make_shared(*this, model); + actions->setContext(controller->getContext()); + + // catch min 6 frames + postSwipeCatchIntervalMs = std::max(100, static_cast(6.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float()))); +} + +void MapView::onMapLevelSwitched() +{ + if(LOCPLINT->cb->getMapSize().z > 1) + { + if(model->getLevel() == 0) + controller->setViewCenter(model->getMapViewCenter(), 1); + else + controller->setViewCenter(model->getMapViewCenter(), 0); + } +} + +void MapView::onMapScrolled(const Point & distance) +{ + if(!isGesturing()) + controller->setViewCenter(model->getMapViewCenter() + distance, model->getLevel()); +} + +void MapView::onMapSwiped(const Point & viewPosition) +{ + if(settings["adventure"]["smoothDragging"].Bool()) + swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); + + controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); +} + +void MapView::postSwipe(uint32_t msPassed) +{ + if(!actions->dragActive) + { + if(swipeHistory.size() > 1) + { + Point diff = Point(0, 0); + 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(firstAccepted.first == 0) + firstAccepted = x; + diff += x.second; + } + } + + uint32_t timediff = swipeHistory.back().first - firstAccepted.first; + + if(diff.length() > 0 && timediff > 0) + { + postSwipeAngle = diff.angle(); + postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + } + } + swipeHistory.clear(); + } else + postSwipeSpeed = 0.0; + if(postSwipeSpeed > postSwipeMinimalSpeed) { + double len = postSwipeSpeed * static_cast(msPassed); + Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); + + controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); + + postSwipeSpeed /= 1 + msPassed * postSwipeSlowdownSpeed; + } +} + +void MapView::onCenteredTile(const int3 & tile) +{ + controller->setViewCenter(tile); +} + +void MapView::onCenteredObject(const CGObjectInstance * target) +{ + controller->setViewCenter(target->getSightCenter()); +} + +void MapView::onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain) +{ + controller->activateSpellViewContext(); + controller->setTileSize(Point(tileSize, tileSize)); + controller->setOverlayVisibility(objectPositions); + controller->setTerrainVisibility(showTerrain); +} + +void MapView::onViewWorldActivated(uint32_t tileSize) +{ + controller->activateWorldViewContext(); + controller->setTileSize(Point(tileSize, tileSize)); +} + +void MapView::onMapZoomLevelChanged(int stepsChange) +{ + controller->modifyTileSize(stepsChange); +} + +void MapView::onViewMapActivated() +{ + controller->activateAdventureContext(); + controller->setTileSize(Point(32, 32)); +} + +PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter) + : BasicMapView(offset, dimensions) +{ + controller->activatePuzzleMapContext(tileToCenter); + controller->setViewCenter(tileToCenter); + +} diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index a3434dc1a..f83b9d319 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -1,89 +1,101 @@ -/* - * MapView.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" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -VCMI_LIB_NAMESPACE_END - -class Canvas; -class MapViewActions; -class MapViewController; -class MapViewModel; -class MapViewCache; - -/// Internal class that contains logic shared between all map views -class BasicMapView : public CIntObject -{ -protected: - std::shared_ptr model; - std::shared_ptr tilesCache; - std::shared_ptr controller; - - std::shared_ptr createModel(const Point & dimensions) const; - - void render(Canvas & target, bool fullUpdate); - -public: - BasicMapView(const Point & offset, const Point & dimensions); - ~BasicMapView() override; - - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; -}; - -/// Main class that represents visible section of adventure map -/// Contains all public interface of view and translates calls to internal model -class MapView : public BasicMapView -{ - std::shared_ptr actions; - -public: - void show(Canvas & to) override; - - MapView(const Point & offset, const Point & dimensions); - - /// Moves current view to another level, preserving position - void onMapLevelSwitched(); - - /// Moves current view by specified distance in pixels - void onMapScrolled(const Point & distance); - - /// Moves current view to specified position, in pixels - void onMapSwiped(const Point & viewPosition); - - /// Moves current view to specified tile - void onCenteredTile(const int3 & tile); - - /// Moves current view to specified object - void onCenteredObject(const CGObjectInstance * target); - - /// Switches view to "View Earth" / "View Air" mode, displaying downscaled map with overlay - void onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain); - - /// Switches view to downscaled View World - void onViewWorldActivated(uint32_t tileSize); - - /// Changes zoom level / tile size of current view by specified factor - void onMapZoomLevelChanged(int stepsChange); - - /// Switches view from View World mode back to standard view - void onViewMapActivated(); -}; - -/// Main class that represents map view for puzzle map -class PuzzleMapView : public BasicMapView -{ -public: - PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter); -}; +/* + * MapView.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" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class Canvas; +class MapViewActions; +class MapViewController; +class MapViewModel; +class MapViewCache; + +/// Internal class that contains logic shared between all map views +class BasicMapView : public CIntObject +{ +protected: + std::shared_ptr model; + std::shared_ptr tilesCache; + std::shared_ptr controller; + + std::shared_ptr createModel(const Point & dimensions) const; + + void render(Canvas & target, bool fullUpdate); + +public: + BasicMapView(const Point & offset, const Point & dimensions); + ~BasicMapView() override; + + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; +}; + +/// Main class that represents visible section of adventure map +/// Contains all public interface of view and translates calls to internal model +class MapView : public BasicMapView +{ + std::shared_ptr actions; + + std::vector> swipeHistory; + double postSwipeAngle = 0.0; + double postSwipeSpeed = 0.0; + + int postSwipeCatchIntervalMs; + const double postSwipeSlowdownSpeed = 0.006; + const double postSwipeMinimalSpeed = 0.1; + + void postSwipe(uint32_t msPassed); + +public: + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + + MapView(const Point & offset, const Point & dimensions); + + /// Moves current view to another level, preserving position + void onMapLevelSwitched(); + + /// Moves current view by specified distance in pixels + void onMapScrolled(const Point & distance); + + /// Moves current view to specified position, in pixels + void onMapSwiped(const Point & viewPosition); + + /// Moves current view to specified tile + void onCenteredTile(const int3 & tile); + + /// Moves current view to specified object + void onCenteredObject(const CGObjectInstance * target); + + /// Switches view to "View Earth" / "View Air" mode, displaying downscaled map with overlay + void onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain); + + /// Switches view to downscaled View World + void onViewWorldActivated(uint32_t tileSize); + + /// Changes zoom level / tile size of current view by specified factor + void onMapZoomLevelChanged(int stepsChange); + + /// Switches view from View World mode back to standard view + void onViewMapActivated(); +}; + +/// Main class that represents map view for puzzle map +class PuzzleMapView : public BasicMapView +{ +public: + PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter); +}; diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index b1cd6c240..68cb692fd 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -1,147 +1,149 @@ -/* - * MapViewActions.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 "MapViewActions.h" - -#include "IMapRendererContext.h" -#include "MapView.h" -#include "MapViewModel.h" - -#include "../CGameInfo.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/MouseButton.h" - -#include "../CPlayerInterface.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../lib/CConfigHandler.h" - -MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr & model) - : model(model) - , owner(owner) - , pinchZoomFactor(1.0) - , dragActive(false) -{ - pos.w = model->getPixelsVisibleDimensions().x; - pos.h = model->getPixelsVisibleDimensions().y; - - addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); -} - -void MapViewActions::setContext(const std::shared_ptr & context) -{ - this->context = context; -} - -void MapViewActions::clickPressed(const Point & cursorPosition) -{ - if (!settings["adventure"]["leftButtonDrag"].Bool()) - { - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileLeftClicked(tile); - } -} - -void MapViewActions::clickReleased(const Point & cursorPosition) -{ - if (!dragActive && settings["adventure"]["leftButtonDrag"].Bool()) - { - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileLeftClicked(tile); - } - dragActive = false; - dragDistance = Point(0,0); -} - -void MapViewActions::clickCancel(const Point & cursorPosition) -{ - dragActive = false; - dragDistance = Point(0,0); -} - -void MapViewActions::showPopupWindow(const Point & cursorPosition) -{ - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileRightClicked(tile); -} - -void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - handleHover(cursorPosition); -} - -void MapViewActions::wheelScrolled(int distance) -{ - adventureInt->hotkeyZoom(distance * 4); -} - -void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - dragDistance += lastUpdateDistance; - - if (dragDistance.length() > 16) - dragActive = true; - - if (dragActive && settings["adventure"]["leftButtonDrag"].Bool()) - owner.onMapSwiped(lastUpdateDistance); -} - -void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) -{ - owner.onMapSwiped(lastUpdateDistance); -} - -void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdateFactor) -{ - double newZoom = pinchZoomFactor * lastUpdateFactor; - - int newZoomSteps = std::round(std::log(newZoom) / std::log(1.01)); - int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01)); - - if (newZoomSteps != oldZoomSteps) - adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps); - - pinchZoomFactor = newZoom; -} - -void MapViewActions::gesture(bool on, const Point & initialPosition, const Point & finalPosition) -{ - pinchZoomFactor = 1.0; -} - -void MapViewActions::handleHover(const Point & cursorPosition) -{ - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(!context->isInMap(tile)) - { - CCS->curh->set(Cursor::Map::POINTER); - return; - } - - adventureInt->onTileHovered(tile); -} - -void MapViewActions::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - CCS->curh->set(Cursor::Map::POINTER); - } -} +/* + * MapViewActions.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 "MapViewActions.h" + +#include "IMapRendererContext.h" +#include "MapView.h" +#include "MapViewModel.h" + +#include "../CGameInfo.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/MouseButton.h" + +#include "../CPlayerInterface.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../lib/CConfigHandler.h" + +MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr & model) + : model(model) + , owner(owner) + , pinchZoomFactor(1.0) + , dragActive(false) +{ + pos.w = model->getPixelsVisibleDimensions().x; + pos.h = model->getPixelsVisibleDimensions().y; + + addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); +} + +void MapViewActions::setContext(const std::shared_ptr & context) +{ + this->context = context; +} + +void MapViewActions::clickPressed(const Point & cursorPosition) +{ + if (!settings["adventure"]["leftButtonDrag"].Bool()) + { + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileLeftClicked(tile); + } +} + +void MapViewActions::clickReleased(const Point & cursorPosition) +{ + if (!dragActive && settings["adventure"]["leftButtonDrag"].Bool()) + { + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileLeftClicked(tile); + } + dragActive = false; + dragDistance = Point(0,0); +} + +void MapViewActions::clickCancel(const Point & cursorPosition) +{ + dragActive = false; + dragDistance = Point(0,0); +} + +void MapViewActions::showPopupWindow(const Point & cursorPosition) +{ + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileRightClicked(tile); +} + +void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + handleHover(cursorPosition); +} + +void MapViewActions::wheelScrolled(int distance) +{ + adventureInt->hotkeyZoom(distance * 4); +} + +void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + dragDistance += lastUpdateDistance; + + if (dragDistance.length() > 16) + dragActive = true; + + if (dragActive && settings["adventure"]["leftButtonDrag"].Bool()) + owner.onMapSwiped(lastUpdateDistance); +} + +void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + owner.onMapSwiped(lastUpdateDistance); +} + +void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdateFactor) +{ + double newZoom = pinchZoomFactor * lastUpdateFactor; + + int newZoomSteps = std::round(std::log(newZoom) / std::log(1.01)); + int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01)); + + if (newZoomSteps != oldZoomSteps) + adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps); + + pinchZoomFactor = newZoom; +} + +void MapViewActions::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + dragActive = on; + + pinchZoomFactor = 1.0; +} + +void MapViewActions::handleHover(const Point & cursorPosition) +{ + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(!context->isInMap(tile)) + { + CCS->curh->set(Cursor::Map::POINTER); + return; + } + + adventureInt->onTileHovered(tile); +} + +void MapViewActions::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + CCS->curh->set(Cursor::Map::POINTER); + } +} diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index de6158f43..02dd877aa 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -1,47 +1,48 @@ -/* - * MapViewActions.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/int3.h" -#include "../gui/CIntObject.h" - -class IMapRendererContext; -class MapViewModel; -class MapView; - -class MapViewActions : public CIntObject -{ - MapView & owner; - std::shared_ptr model; - std::shared_ptr context; - - Point dragDistance; - double pinchZoomFactor; - bool dragActive; - - void handleHover(const Point & cursorPosition); - -public: - MapViewActions(MapView & owner, const std::shared_ptr & model); - - void setContext(const std::shared_ptr & context); - - void clickPressed(const Point & cursorPosition) override; - void clickReleased(const Point & cursorPosition) override; - void clickCancel(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; - void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override; - void hover(bool on) override; - 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 wheelScrolled(int distance) override; -}; +/* + * MapViewActions.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/int3.h" +#include "../gui/CIntObject.h" + +class IMapRendererContext; +class MapViewModel; +class MapView; + +class MapViewActions : public CIntObject +{ + MapView & owner; + std::shared_ptr model; + std::shared_ptr context; + + Point dragDistance; + double pinchZoomFactor; + + void handleHover(const Point & cursorPosition); + +public: + MapViewActions(MapView & owner, const std::shared_ptr & model); + + void setContext(const std::shared_ptr & context); + + void clickPressed(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; + void clickCancel(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override; + void hover(bool on) override; + 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 wheelScrolled(int distance) override; + + bool dragActive; +}; diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index 18ed65208..ad851ef11 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -1,194 +1,197 @@ -/* - * MapViewCache.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 "MapViewCache.h" - -#include "IMapRendererContext.h" -#include "MapRenderer.h" -#include "MapViewModel.h" - -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" - -#include "../../lib/mapObjects/CObjectHandler.h" -#include "../../lib/int3.h" - -MapViewCache::~MapViewCache() = default; - -MapViewCache::MapViewCache(const std::shared_ptr & model) - : model(model) - , cachedLevel(0) - , mapRenderer(new MapRenderer()) - , iconsStorage(new CAnimation("VwSymbol")) - , intermediate(new Canvas(Point(32, 32))) - , terrain(new Canvas(model->getCacheDimensionsPixels())) - , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) -{ - 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]); -} - -Canvas MapViewCache::getTile(const int3 & coordinates) -{ - return Canvas(*terrain, model->getCacheTileArea(coordinates)); -} - -std::shared_ptr MapViewCache::getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates) -{ - size_t imageIndex = context->overlayImageIndex(coordinates); - - if(imageIndex < iconsStorage->size()) - return iconsStorage->getImage(imageIndex); - return nullptr; -} - -void MapViewCache::invalidate(const std::shared_ptr & context, const ObjectInstanceID & object) -{ - for(size_t cacheY = 0; cacheY < terrainChecksum.shape()[1]; ++cacheY) - { - for(size_t cacheX = 0; cacheX < terrainChecksum.shape()[0]; ++cacheX) - { - auto & entry = terrainChecksum[cacheX][cacheY]; - - int3 tile(entry.tileX, entry.tileY, cachedLevel); - - if(context->isInMap(tile) && vstd::contains(context->getObjects(tile), object)) - entry = TileChecksum{}; - } - } -} - -void MapViewCache::updateTile(const std::shared_ptr & context, const int3 & coordinates) -{ - int cacheX = (terrainChecksum.shape()[0] + coordinates.x) % terrainChecksum.shape()[0]; - int cacheY = (terrainChecksum.shape()[1] + coordinates.y) % terrainChecksum.shape()[1]; - - auto & oldCacheEntry = terrainChecksum[cacheX][cacheY]; - TileChecksum newCacheEntry; - - newCacheEntry.tileX = coordinates.x; - newCacheEntry.tileY = coordinates.y; - newCacheEntry.checksum = mapRenderer->getTileChecksum(*context, coordinates); - - if(cachedLevel == coordinates.z && oldCacheEntry == newCacheEntry && !context->tileAnimated(coordinates)) - return; - - Canvas target = getTile(coordinates); - - if(model->getSingleTileSize() == Point(32, 32)) - { - mapRenderer->renderTile(*context, target, coordinates); - } - else - { - mapRenderer->renderTile(*context, *intermediate, coordinates); - target.drawScaled(*intermediate, Point(0, 0), model->getSingleTileSize()); - } - - if(context->filterGrayscale()) - target.applyGrayscale(); - - oldCacheEntry = newCacheEntry; - tilesUpToDate[cacheX][cacheY] = false; -} - -void MapViewCache::update(const std::shared_ptr & context) -{ - Rect dimensions = model->getTilesTotalRect(); - bool mapResized = cachedSize != model->getSingleTileSize(); - - if(mapResized || dimensions.w != terrainChecksum.shape()[0] || dimensions.h != terrainChecksum.shape()[1]) - { - boost::multi_array newCache; - newCache.resize(boost::extents[dimensions.w][dimensions.h]); - terrainChecksum.resize(boost::extents[dimensions.w][dimensions.h]); - terrainChecksum = newCache; - } - - if(mapResized || dimensions.w != tilesUpToDate.shape()[0] || dimensions.h != tilesUpToDate.shape()[1]) - { - boost::multi_array newCache; - newCache.resize(boost::extents[dimensions.w][dimensions.h]); - tilesUpToDate.resize(boost::extents[dimensions.w][dimensions.h]); - tilesUpToDate = newCache; - } - - for(int y = dimensions.top(); y < dimensions.bottom(); ++y) - for(int x = dimensions.left(); x < dimensions.right(); ++x) - updateTile(context, {x, y, model->getLevel()}); - - cachedSize = model->getSingleTileSize(); - cachedLevel = model->getLevel(); -} - -void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) -{ - bool mapMoved = (cachedPosition != model->getMapViewCenter()); - bool lazyUpdate = !mapMoved && !fullRedraw && context->viewTransitionProgress() == 0; - - Rect dimensions = model->getTilesTotalRect(); - - for(int y = dimensions.top(); y < dimensions.bottom(); ++y) - { - for(int x = dimensions.left(); x < dimensions.right(); ++x) - { - int cacheX = (terrainChecksum.shape()[0] + x) % terrainChecksum.shape()[0]; - int cacheY = (terrainChecksum.shape()[1] + y) % terrainChecksum.shape()[1]; - int3 tile(x, y, model->getLevel()); - - if(lazyUpdate && tilesUpToDate[cacheX][cacheY]) - continue; - - Canvas source = getTile(tile); - Rect targetRect = model->getTargetTileArea(tile); - target.draw(source, targetRect.topLeft()); - - if (!fullRedraw) - tilesUpToDate[cacheX][cacheY] = true; - } - } - - if(context->showOverlay()) - { - 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) - { - Point position = targetRect.center() - overlay->dimensions() / 2; - target.draw(overlay, position); - } - } - } - } - - if(context->viewTransitionProgress() != 0) - target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); - - cachedPosition = model->getMapViewCenter(); -} - -void MapViewCache::createTransitionSnapshot(const std::shared_ptr & context) -{ - update(context); - render(context, *terrainTransition, true); -} +/* + * MapViewCache.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 "MapViewCache.h" + +#include "IMapRendererContext.h" +#include "MapRenderer.h" +#include "MapViewModel.h" + +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../gui/CGuiHandler.h" + +#include "../../lib/mapObjects/CObjectHandler.h" +#include "../../lib/int3.h" + +MapViewCache::~MapViewCache() = default; + +MapViewCache::MapViewCache(const std::shared_ptr & model) + : model(model) + , cachedLevel(0) + , 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->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]); +} + +Canvas MapViewCache::getTile(const int3 & coordinates) +{ + return Canvas(*terrain, model->getCacheTileArea(coordinates)); +} + +std::shared_ptr MapViewCache::getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates) +{ + size_t imageIndex = context->overlayImageIndex(coordinates); + + if(imageIndex < iconsStorage->size()) + return iconsStorage->getImage(imageIndex); + return nullptr; +} + +void MapViewCache::invalidate(const std::shared_ptr & context, const ObjectInstanceID & object) +{ + for(size_t cacheY = 0; cacheY < terrainChecksum.shape()[1]; ++cacheY) + { + for(size_t cacheX = 0; cacheX < terrainChecksum.shape()[0]; ++cacheX) + { + auto & entry = terrainChecksum[cacheX][cacheY]; + + int3 tile(entry.tileX, entry.tileY, cachedLevel); + + if(context->isInMap(tile) && vstd::contains(context->getObjects(tile), object)) + entry = TileChecksum{}; + } + } +} + +void MapViewCache::updateTile(const std::shared_ptr & context, const int3 & coordinates) +{ + int cacheX = (terrainChecksum.shape()[0] + coordinates.x) % terrainChecksum.shape()[0]; + int cacheY = (terrainChecksum.shape()[1] + coordinates.y) % terrainChecksum.shape()[1]; + + auto & oldCacheEntry = terrainChecksum[cacheX][cacheY]; + TileChecksum newCacheEntry; + + newCacheEntry.tileX = coordinates.x; + newCacheEntry.tileY = coordinates.y; + newCacheEntry.checksum = mapRenderer->getTileChecksum(*context, coordinates); + + if(cachedLevel == coordinates.z && oldCacheEntry == newCacheEntry && !context->tileAnimated(coordinates)) + return; + + Canvas target = getTile(coordinates); + + if(model->getSingleTileSize() == Point(32, 32)) + { + mapRenderer->renderTile(*context, target, coordinates); + } + else + { + mapRenderer->renderTile(*context, *intermediate, coordinates); + target.drawScaled(*intermediate, Point(0, 0), model->getSingleTileSize()); + } + + if(context->filterGrayscale()) + target.applyGrayscale(); + + oldCacheEntry = newCacheEntry; + tilesUpToDate[cacheX][cacheY] = false; +} + +void MapViewCache::update(const std::shared_ptr & context) +{ + Rect dimensions = model->getTilesTotalRect(); + bool mapResized = cachedSize != model->getSingleTileSize(); + + if(mapResized || dimensions.w != terrainChecksum.shape()[0] || dimensions.h != terrainChecksum.shape()[1]) + { + boost::multi_array newCache; + newCache.resize(boost::extents[dimensions.w][dimensions.h]); + terrainChecksum.resize(boost::extents[dimensions.w][dimensions.h]); + terrainChecksum = newCache; + } + + if(mapResized || dimensions.w != tilesUpToDate.shape()[0] || dimensions.h != tilesUpToDate.shape()[1]) + { + boost::multi_array newCache; + newCache.resize(boost::extents[dimensions.w][dimensions.h]); + tilesUpToDate.resize(boost::extents[dimensions.w][dimensions.h]); + tilesUpToDate = newCache; + } + + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + for(int x = dimensions.left(); x < dimensions.right(); ++x) + updateTile(context, {x, y, model->getLevel()}); + + cachedSize = model->getSingleTileSize(); + cachedLevel = model->getLevel(); +} + +void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) +{ + bool mapMoved = (cachedPosition != model->getMapViewCenter()); + bool lazyUpdate = !mapMoved && !fullRedraw && context->viewTransitionProgress() == 0; + + Rect dimensions = model->getTilesTotalRect(); + + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + { + for(int x = dimensions.left(); x < dimensions.right(); ++x) + { + int cacheX = (terrainChecksum.shape()[0] + x) % terrainChecksum.shape()[0]; + int cacheY = (terrainChecksum.shape()[1] + y) % terrainChecksum.shape()[1]; + int3 tile(x, y, model->getLevel()); + + if(lazyUpdate && tilesUpToDate[cacheX][cacheY]) + continue; + + Canvas source = getTile(tile); + Rect targetRect = model->getTargetTileArea(tile); + target.draw(source, targetRect.topLeft()); + + if (!fullRedraw) + tilesUpToDate[cacheX][cacheY] = true; + } + } + + if(context->showOverlay()) + { + 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) + { + Point position = targetRect.center() - overlay->dimensions() / 2; + target.draw(overlay, position); + } + } + } + } + + if(context->viewTransitionProgress() != 0) + target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); + + cachedPosition = model->getMapViewCenter(); +} + +void MapViewCache::createTransitionSnapshot(const std::shared_ptr & context) +{ + update(context); + render(context, *terrainTransition, true); +} diff --git a/client/mapView/MapViewCache.h b/client/mapView/MapViewCache.h index d2795ccc0..0435d3584 100644 --- a/client/mapView/MapViewCache.h +++ b/client/mapView/MapViewCache.h @@ -1,78 +1,78 @@ -/* - * MapViewCache.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/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN -class ObjectInstanceID; -VCMI_LIB_NAMESPACE_END - -class IImage; -class CAnimation; -class Canvas; -class MapRenderer; -class IMapRendererContext; -class MapViewModel; - -/// Class responsible for rendering of entire map view -/// uses rendering parameters provided by owner class -class MapViewCache -{ - struct TileChecksum - { - int tileX = std::numeric_limits::min(); - int tileY = std::numeric_limits::min(); - std::array checksum{}; - - bool operator==(const TileChecksum & other) const - { - return tileX == other.tileX && tileY == other.tileY && checksum == other.checksum; - } - }; - - boost::multi_array terrainChecksum; - boost::multi_array tilesUpToDate; - - Point cachedSize; - Point cachedPosition; - int cachedLevel; - - std::shared_ptr model; - - std::unique_ptr terrain; - std::unique_ptr terrainTransition; - std::unique_ptr intermediate; - std::unique_ptr mapRenderer; - - std::unique_ptr iconsStorage; - - Canvas getTile(const int3 & coordinates); - void updateTile(const std::shared_ptr & context, const int3 & coordinates); - - std::shared_ptr getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates); - -public: - explicit MapViewCache(const std::shared_ptr & model); - ~MapViewCache(); - - /// invalidates cache of specified object - void invalidate(const std::shared_ptr & context, const ObjectInstanceID & object); - - /// updates internal terrain cache according to provided time delta - void update(const std::shared_ptr & context); - - /// renders updated terrain cache onto provided canvas - void render(const std::shared_ptr & context, Canvas & target, bool fullRedraw); - - /// creates snapshot of current view and stores it into internal canvas - /// used for view transition, e.g. Dimension Door spell or teleporters (Subterra gates / Monolith) - void createTransitionSnapshot(const std::shared_ptr & context); -}; +/* + * MapViewCache.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/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +class ObjectInstanceID; +VCMI_LIB_NAMESPACE_END + +class IImage; +class CAnimation; +class Canvas; +class MapRenderer; +class IMapRendererContext; +class MapViewModel; + +/// Class responsible for rendering of entire map view +/// uses rendering parameters provided by owner class +class MapViewCache +{ + struct TileChecksum + { + int tileX = std::numeric_limits::min(); + int tileY = std::numeric_limits::min(); + std::array checksum{}; + + bool operator==(const TileChecksum & other) const + { + return tileX == other.tileX && tileY == other.tileY && checksum == other.checksum; + } + }; + + boost::multi_array terrainChecksum; + boost::multi_array tilesUpToDate; + + Point cachedSize; + Point cachedPosition; + int cachedLevel; + + std::shared_ptr model; + + std::unique_ptr terrain; + std::unique_ptr terrainTransition; + std::unique_ptr intermediate; + std::unique_ptr mapRenderer; + + std::shared_ptr iconsStorage; + + Canvas getTile(const int3 & coordinates); + void updateTile(const std::shared_ptr & context, const int3 & coordinates); + + std::shared_ptr getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates); + +public: + explicit MapViewCache(const std::shared_ptr & model); + ~MapViewCache(); + + /// invalidates cache of specified object + void invalidate(const std::shared_ptr & context, const ObjectInstanceID & object); + + /// updates internal terrain cache according to provided time delta + void update(const std::shared_ptr & context); + + /// renders updated terrain cache onto provided canvas + void render(const std::shared_ptr & context, Canvas & target, bool fullRedraw); + + /// creates snapshot of current view and stores it into internal canvas + /// used for view transition, e.g. Dimension Door spell or teleporters (Subterra gates / Monolith) + void createTransitionSnapshot(const std::shared_ptr & context); +}; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 7f37cfeb4..d710dc6d2 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -1,615 +1,639 @@ -/* - * MapViewController.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 "MapViewController.h" - -#include "MapRendererContext.h" -#include "MapRendererContextState.h" -#include "MapViewCache.h" -#include "MapViewModel.h" - -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/pathfinder/CGPathNode.h" -#include "../../lib/spells/ViewSpellInt.h" - -void MapViewController::setViewCenter(const int3 & position) -{ - setViewCenter(Point(position) * model->getSingleTileSize() + model->getSingleTileSize() / 2, position.z); -} - -void MapViewController::setViewCenter(const Point & position, int level) -{ - Point upperLimit = Point(context->getMapSize()) * model->getSingleTileSize(); - Point lowerLimit = Point(0, 0); - - if(worldViewContext) - { - Point area = model->getPixelsVisibleDimensions(); - Point mapCenter = upperLimit / 2; - - Point desiredLowerLimit = lowerLimit + area / 2; - Point desiredUpperLimit = upperLimit - area / 2; - - Point actualLowerLimit{ - std::min(desiredLowerLimit.x, mapCenter.x), - std::min(desiredLowerLimit.y, mapCenter.y) - }; - - Point actualUpperLimit{ - std::max(desiredUpperLimit.x, mapCenter.x), - std::max(desiredUpperLimit.y, mapCenter.y) - }; - - upperLimit = actualUpperLimit; - lowerLimit = actualLowerLimit; - } - - Point betterPosition = {std::clamp(position.x, lowerLimit.x, upperLimit.x), std::clamp(position.y, lowerLimit.y, upperLimit.y)}; - - model->setViewCenter(betterPosition); - model->setLevel(std::clamp(level, 0, context->getMapSize().z)); - - if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized - adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel()); -} - -void MapViewController::setTileSize(const Point & tileSize) -{ - Point oldSize = model->getSingleTileSize(); - model->setTileSize(tileSize); - - double scaleChangeX = 1.0 * tileSize.x / oldSize.x; - double scaleChangeY = 1.0 * tileSize.y / oldSize.y; - - Point newViewCenter { - static_cast(std::round(model->getMapViewCenter().x * scaleChangeX)), - static_cast(std::round(model->getMapViewCenter().y * scaleChangeY)) - }; - - // force update of view center since changing tile size may invalidated it - setViewCenter(newViewCenter, model->getLevel()); -} - -void MapViewController::modifyTileSize(int stepsChange) -{ - // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling - // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale - // try to determine current zooming level and change it by requested number of steps - double currentZoomFactor = model->getSingleTileSize().x / 32.0; - double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); - double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; - double newZoomFactor = std::pow(1.01, newZoomSteps); - - Point currentZoom = model->getSingleTileSize(); - Point desiredZoom = Point(32,32) * newZoomFactor; - - if (desiredZoom == currentZoom && stepsChange < 0) - desiredZoom -= Point(1,1); - - if (desiredZoom == currentZoom && stepsChange > 0) - desiredZoom += Point(1,1); - - Point minimal = model->getSingleTileSizeLowerLimit(); - Point maximal = model->getSingleTileSizeUpperLimit(); - Point actualZoom = { - std::clamp(desiredZoom.x, minimal.x, maximal.x), - std::clamp(desiredZoom.y, minimal.y, maximal.y) - }; - - if (actualZoom != currentZoom) - setTileSize(actualZoom); -} - -MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) - : state(new MapRendererContextState()) - , model(std::move(model)) - , view(view) -{ - adventureContext = std::make_shared(*state); - context = adventureContext; -} - -std::shared_ptr MapViewController::getContext() const -{ - return context; -} - -void MapViewController::tick(uint32_t timeDelta) -{ - // confirmed to match H3 for - // - hero embarking on boat (500 ms) - // - hero disembarking from boat (500 ms) - // - TODO: picking up resources - // - TODO: killing mosters - // - teleporting ( 250 ms) - static const double fadeOutDuration = 500; - static const double fadeInDuration = 500; - static const double heroTeleportDuration = 250; - - if(movementContext) - { - const auto * object = context->getObject(movementContext->target); - const auto * hero = dynamic_cast(object); - const auto * boat = dynamic_cast(object); - - assert(boat || hero); - - if(!hero) - hero = boat->hero; - - double heroMoveTime = LOCPLINT->makingTurn ? - settings["adventure"]["heroMoveTime"].Float() : - settings["adventure"]["enemyMoveTime"].Float(); - - movementContext->progress += timeDelta / heroMoveTime; - movementContext->progress = std::min( 1.0, movementContext->progress); - - Point positionFrom = Point(hero->convertToVisitablePos(movementContext->tileFrom)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; - Point positionDest = Point(hero->convertToVisitablePos(movementContext->tileDest)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; - - Point positionCurr = vstd::lerp(positionFrom, positionDest, movementContext->progress); - - setViewCenter(positionCurr, movementContext->tileDest.z); - } - - if(teleportContext) - { - teleportContext->progress += timeDelta / heroTeleportDuration; - teleportContext->progress = std::min( 1.0, teleportContext->progress); - } - - if(fadingOutContext) - { - fadingOutContext->progress -= timeDelta / fadeOutDuration; - fadingOutContext->progress = std::max( 0.0, fadingOutContext->progress); - } - - if(fadingInContext) - { - fadingInContext->progress += timeDelta / fadeInDuration; - fadingInContext->progress = std::min( 1.0, fadingInContext->progress); - } - - if (adventureContext) - adventureContext->animationTime += timeDelta; - - updateState(); -} - -void MapViewController::updateState() -{ - if(adventureContext) - { - adventureContext->settingsSessionSpectate = settings["session"]["spectate"].Bool(); - adventureContext->settingsAdventureObjectAnimation = settings["adventure"]["objectAnimation"].Bool(); - adventureContext->settingsAdventureTerrainAnimation = settings["adventure"]["terrainAnimation"].Bool(); - adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); - adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); - adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); - adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); - } -} - -void MapViewController::afterRender() -{ - if(movementContext) - { - const auto * object = context->getObject(movementContext->target); - const auto * hero = dynamic_cast(object); - const auto * boat = dynamic_cast(object); - - assert(boat || hero); - - if(!hero) - hero = boat->hero; - - if(movementContext->progress >= 0.999) - { - logGlobal->debug("Ending movement animation"); - setViewCenter(hero->getSightCenter()); - - removeObject(context->getObject(movementContext->target)); - addObject(context->getObject(movementContext->target)); - - activateAdventureContext(movementContext->animationTime); - } - } - - if(teleportContext && teleportContext->progress >= 0.999) - { - logGlobal->debug("Ending teleport animation"); - activateAdventureContext(teleportContext->animationTime); - } - - if(fadingOutContext && fadingOutContext->progress <= 0.001) - { - logGlobal->debug("Ending fade out animation"); - removeObject(context->getObject(fadingOutContext->target)); - - activateAdventureContext(fadingOutContext->animationTime); - } - - if(fadingInContext && fadingInContext->progress >= 0.999) - { - logGlobal->debug("Ending fade in animation"); - activateAdventureContext(fadingInContext->animationTime); - } -} - -bool MapViewController::isEventInstant(const CGObjectInstance * obj) -{ - if (!isEventVisible(obj)) - return true; - - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() <= 0) - return true; // instant movement speed - - if(LOCPLINT->makingTurn && settings["adventure"]["heroMoveTime"].Float() <= 0) - return true; // instant movement speed - - return false; -} - -bool MapViewController::isEventVisible(const CGObjectInstance * obj) -{ - if(adventureContext == nullptr) - return false; - - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() < 0) - return false; // enemy move speed set to "hidden/none" - - if(!GH.windows().isTopWindow(adventureInt)) - return false; - - if(obj->isVisitable()) - return context->isVisible(obj->visitablePos()); - else - return context->isVisible(obj->pos); -} - -bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(adventureContext == nullptr) - return false; - - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() < 0) - return false; // enemy move speed set to "hidden/none" - - if(!GH.windows().isTopWindow(adventureInt)) - return false; - - if(context->isVisible(obj->convertToVisitablePos(from))) - return true; - - if(context->isVisible(obj->convertToVisitablePos(dest))) - return true; - - return false; -} - -void MapViewController::fadeOutObject(const CGObjectInstance * obj) -{ - logGlobal->debug("Starting fade out animation"); - fadingOutContext = std::make_shared(*state); - fadingOutContext->animationTime = adventureContext->animationTime; - adventureContext = fadingOutContext; - context = fadingOutContext; - - const CGObjectInstance * movingObject = obj; - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - movingObject = hero->boat; - } - - fadingOutContext->target = movingObject->id; - fadingOutContext->progress = 1.0; -} - -void MapViewController::fadeInObject(const CGObjectInstance * obj) -{ - logGlobal->debug("Starting fade in animation"); - fadingInContext = std::make_shared(*state); - fadingInContext->animationTime = adventureContext->animationTime; - adventureContext = fadingInContext; - context = fadingInContext; - - const CGObjectInstance * movingObject = obj; - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - movingObject = hero->boat; - } - - fadingInContext->target = movingObject->id; - fadingInContext->progress = 0.0; -} - -void MapViewController::removeObject(const CGObjectInstance * obj) -{ - if (obj->ID == Obj::BOAT) - { - auto * boat = dynamic_cast(obj); - if (boat->hero) - { - view->invalidate(context, boat->hero->id); - state->removeObject(boat->hero); - } - } - - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - { - view->invalidate(context, hero->boat->id); - state->removeObject(hero->boat); - } - } - - view->invalidate(context, obj->id); - state->removeObject(obj); -} - -void MapViewController::addObject(const CGObjectInstance * obj) -{ - state->addObject(obj); - view->invalidate(context, obj->id); -} - -void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - { - if (!isEventInstant(obj)) - fadeOutObject(obj); - setViewCenter(obj->getSightCenter()); - } - else - removeObject(obj); -} - -void MapViewController::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - setViewCenter(obj->getSightCenter()); -} - -void MapViewController::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - setViewCenter(obj->getSightCenter()); -} - -void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - { - if (!isEventInstant(obj)) - fadeInObject(obj); - setViewCenter(obj->getSightCenter()); - } - addObject(obj); -} - -void MapViewController::onObjectFadeIn(const CGObjectInstance * obj) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj) && !isEventInstant(obj) ) - fadeInObject(obj); - - addObject(obj); -} - -void MapViewController::onObjectFadeOut(const CGObjectInstance * obj) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj) && !isEventInstant(obj) ) - fadeOutObject(obj); - else - removeObject(obj); -} - -void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj) -{ - addObject(obj); -}; - -void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj) -{ - removeObject(obj); -}; - -void MapViewController::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj, from, dest)) - { - setViewCenter(obj->getSightCenter()); - view->createTransitionSnapshot(context); - } -} - -void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - const CGObjectInstance * movingObject = obj; - if(obj->boat) - movingObject = obj->boat; - - removeObject(movingObject); - addObject(movingObject); - - if(isEventVisible(obj, from, dest)) - { - logGlobal->debug("Starting teleport animation"); - teleportContext = std::make_shared(*state); - teleportContext->animationTime = adventureContext->animationTime; - adventureContext = teleportContext; - context = teleportContext; - setViewCenter(movingObject->getSightCenter()); - } -} - -void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - // revisiting via spacebar, no need to animate - if(from == dest) - return; - - const CGObjectInstance * movingObject = obj; - if(obj->boat) - movingObject = obj->boat; - - removeObject(movingObject); - - if(!isEventVisible(obj, from, dest)) - { - addObject(movingObject); - return; - } - - double movementTime = LOCPLINT->playerID == obj->tempOwner ? - settings["adventure"]["heroMoveTime"].Float() : - settings["adventure"]["enemyMoveTime"].Float(); - - if(movementTime > 1) - { - logGlobal->debug("Starting movement animation"); - movementContext = std::make_shared(*state); - movementContext->animationTime = adventureContext->animationTime; - adventureContext = movementContext; - context = movementContext; - - state->addMovingObject(movingObject, from, dest); - - movementContext->target = movingObject->id; - movementContext->tileFrom = from; - movementContext->tileDest = dest; - movementContext->progress = 0.0; - } - else // instant movement - { - addObject(movingObject); - setViewCenter(movingObject->visitablePos()); - } -} - -bool MapViewController::hasOngoingAnimations() -{ - if(movementContext) - return true; - - if(fadingOutContext) - return true; - - if(fadingInContext) - return true; - - if(teleportContext) - return true; - - return false; -} - -void MapViewController::activateAdventureContext(uint32_t animationTime) -{ - resetContext(); - - adventureContext = std::make_shared(*state); - adventureContext->animationTime = animationTime; - context = adventureContext; - updateState(); -} - -void MapViewController::activateAdventureContext() -{ - activateAdventureContext(0); -} - -void MapViewController::activateWorldViewContext() -{ - if(worldViewContext) - return; - - resetContext(); - - worldViewContext = std::make_shared(*state); - context = worldViewContext; -} - -void MapViewController::activateSpellViewContext() -{ - if(spellViewContext) - return; - - resetContext(); - - spellViewContext = std::make_shared(*state); - worldViewContext = spellViewContext; - context = spellViewContext; -} - -void MapViewController::activatePuzzleMapContext(const int3 & grailPosition) -{ - resetContext(); - - puzzleMapContext = std::make_shared(*state); - context = puzzleMapContext; - - CGPathNode fakeNode; - fakeNode.coord = grailPosition; - - puzzleMapContext->grailPos = std::make_unique(); - - // create two nodes since 1st one is normally not visible - puzzleMapContext->grailPos->nodes.push_back(fakeNode); - puzzleMapContext->grailPos->nodes.push_back(fakeNode); -} - -void MapViewController::resetContext() -{ - adventureContext.reset(); - movementContext.reset(); - fadingOutContext.reset(); - fadingInContext.reset(); - teleportContext.reset(); - worldViewContext.reset(); - spellViewContext.reset(); - puzzleMapContext.reset(); -} - -void MapViewController::setTerrainVisibility(bool showAllTerrain) -{ - assert(spellViewContext); - spellViewContext->showAllTerrain = showAllTerrain; -} - -void MapViewController::setOverlayVisibility(const std::vector & objectPositions) -{ - assert(spellViewContext); - spellViewContext->additionalOverlayIcons = objectPositions; -} +/* + * MapViewController.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 "MapViewController.h" + +#include "MapRendererContext.h" +#include "MapRendererContextState.h" +#include "MapViewCache.h" +#include "MapViewModel.h" + +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/pathfinder/CGPathNode.h" +#include "../../lib/spells/ViewSpellInt.h" + +void MapViewController::setViewCenter(const int3 & position) +{ + setViewCenter(Point(position) * model->getSingleTileSize() + model->getSingleTileSize() / 2, position.z); +} + +void MapViewController::setViewCenter(const Point & position, int level) +{ + Point upperLimit = Point(context->getMapSize()) * model->getSingleTileSize(); + Point lowerLimit = Point(0, 0); + + if(worldViewContext) + { + Point area = model->getPixelsVisibleDimensions(); + Point mapCenter = upperLimit / 2; + + Point desiredLowerLimit = lowerLimit + area / 2; + Point desiredUpperLimit = upperLimit - area / 2; + + Point actualLowerLimit{ + std::min(desiredLowerLimit.x, mapCenter.x), + std::min(desiredLowerLimit.y, mapCenter.y) + }; + + Point actualUpperLimit{ + std::max(desiredUpperLimit.x, mapCenter.x), + std::max(desiredUpperLimit.y, mapCenter.y) + }; + + upperLimit = actualUpperLimit; + lowerLimit = actualLowerLimit; + } + + Point betterPosition = {std::clamp(position.x, lowerLimit.x, upperLimit.x), std::clamp(position.y, lowerLimit.y, upperLimit.y)}; + + model->setViewCenter(betterPosition); + model->setLevel(std::clamp(level, 0, context->getMapSize().z)); + + if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized + adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel()); +} + +void MapViewController::setTileSize(const Point & tileSize) +{ + Point oldSize = model->getSingleTileSize(); + model->setTileSize(tileSize); + + double scaleChangeX = 1.0 * tileSize.x / oldSize.x; + double scaleChangeY = 1.0 * tileSize.y / oldSize.y; + + Point newViewCenter { + static_cast(std::round(model->getMapViewCenter().x * scaleChangeX)), + static_cast(std::round(model->getMapViewCenter().y * scaleChangeY)) + }; + + // force update of view center since changing tile size may invalidated it + setViewCenter(newViewCenter, model->getLevel()); +} + +void MapViewController::modifyTileSize(int stepsChange) +{ + // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling + // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale + // try to determine current zooming level and change it by requested number of steps + double currentZoomFactor = targetTileSize.x / static_cast(defaultTileSize); + double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); + double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; + double newZoomFactor = std::pow(1.01, newZoomSteps); + + Point currentZoom = targetTileSize; + Point desiredZoom = Point(defaultTileSize,defaultTileSize) * newZoomFactor; + + if (desiredZoom == currentZoom && stepsChange < 0) + desiredZoom -= Point(1,1); + + if (desiredZoom == currentZoom && stepsChange > 0) + desiredZoom += Point(1,1); + + Point minimal = model->getSingleTileSizeLowerLimit(); + Point maximal = model->getSingleTileSizeUpperLimit(); + Point actualZoom = { + std::clamp(desiredZoom.x, minimal.x, maximal.x), + std::clamp(desiredZoom.y, minimal.y, maximal.y) + }; + + if (actualZoom != currentZoom) + { + targetTileSize = actualZoom; + if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea) + actualZoom.x = defaultTileSize; + if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) + actualZoom.y = defaultTileSize; + + bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize); + + if(!wasInDeadZone && isInDeadZone) + GH.input().hapticFeedback(); + + wasInDeadZone = isInDeadZone; + + setTileSize(actualZoom); + } +} + +MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) + : state(new MapRendererContextState()) + , model(std::move(model)) + , view(view) +{ + adventureContext = std::make_shared(*state); + context = adventureContext; +} + +std::shared_ptr MapViewController::getContext() const +{ + return context; +} + +void MapViewController::tick(uint32_t timeDelta) +{ + // confirmed to match H3 for + // - hero embarking on boat (500 ms) + // - hero disembarking from boat (500 ms) + // - TODO: picking up resources + // - TODO: killing mosters + // - teleporting ( 250 ms) + static const double fadeOutDuration = 500; + static const double fadeInDuration = 500; + static const double heroTeleportDuration = 250; + + if(movementContext) + { + const auto * object = context->getObject(movementContext->target); + const auto * hero = dynamic_cast(object); + const auto * boat = dynamic_cast(object); + + assert(boat || hero); + + if(!hero) + hero = boat->hero; + + double heroMoveTime = LOCPLINT->playerID == hero->getOwner() ? + settings["adventure"]["heroMoveTime"].Float() : + settings["adventure"]["enemyMoveTime"].Float(); + + movementContext->progress += timeDelta / heroMoveTime; + movementContext->progress = std::min( 1.0, movementContext->progress); + + Point positionFrom = Point(hero->convertToVisitablePos(movementContext->tileFrom)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; + Point positionDest = Point(hero->convertToVisitablePos(movementContext->tileDest)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; + + Point positionCurr = vstd::lerp(positionFrom, positionDest, movementContext->progress); + + setViewCenter(positionCurr, movementContext->tileDest.z); + } + + if(teleportContext) + { + teleportContext->progress += timeDelta / heroTeleportDuration; + teleportContext->progress = std::min( 1.0, teleportContext->progress); + } + + if(fadingOutContext) + { + fadingOutContext->progress -= timeDelta / fadeOutDuration; + fadingOutContext->progress = std::max( 0.0, fadingOutContext->progress); + } + + if(fadingInContext) + { + fadingInContext->progress += timeDelta / fadeInDuration; + fadingInContext->progress = std::min( 1.0, fadingInContext->progress); + } + + if (adventureContext) + adventureContext->animationTime += timeDelta; + + updateState(); +} + +void MapViewController::updateState() +{ + if(adventureContext) + { + adventureContext->settingsSessionSpectate = settings["session"]["spectate"].Bool(); + adventureContext->settingsAdventureObjectAnimation = settings["adventure"]["objectAnimation"].Bool(); + adventureContext->settingsAdventureTerrainAnimation = settings["adventure"]["terrainAnimation"].Bool(); + adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); + adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); + adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); + adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); + } +} + +void MapViewController::afterRender() +{ + if(movementContext) + { + const auto * object = context->getObject(movementContext->target); + const auto * hero = dynamic_cast(object); + const auto * boat = dynamic_cast(object); + + assert(boat || hero); + + if(!hero) + hero = boat->hero; + + if(movementContext->progress >= 0.999) + { + logGlobal->debug("Ending movement animation"); + setViewCenter(hero->getSightCenter()); + + removeObject(context->getObject(movementContext->target)); + addObject(context->getObject(movementContext->target)); + + activateAdventureContext(movementContext->animationTime); + } + } + + if(teleportContext && teleportContext->progress >= 0.999) + { + logGlobal->debug("Ending teleport animation"); + activateAdventureContext(teleportContext->animationTime); + } + + if(fadingOutContext && fadingOutContext->progress <= 0.001) + { + logGlobal->debug("Ending fade out animation"); + removeObject(context->getObject(fadingOutContext->target)); + + activateAdventureContext(fadingOutContext->animationTime); + } + + if(fadingInContext && fadingInContext->progress >= 0.999) + { + logGlobal->debug("Ending fade in animation"); + activateAdventureContext(fadingInContext->animationTime); + } +} + +bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + if (!isEventVisible(obj, initiator)) + return true; + + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() <= 0) + return true; // instant movement speed + + if(initiator == LOCPLINT->playerID && settings["adventure"]["heroMoveTime"].Float() <= 0) + return true; // instant movement speed + + return false; +} + +bool MapViewController::isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + if(adventureContext == nullptr) + return false; + + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) + return false; // enemy move speed set to "hidden/none" + + if(!GH.windows().isTopWindow(adventureInt)) + return false; + + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && initiator != LOCPLINT->playerID) + return false; + + if(obj->isVisitable()) + return context->isVisible(obj->visitablePos()); + else + return context->isVisible(obj->pos); +} + +bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(adventureContext == nullptr) + return false; + + if(obj->getOwner() != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) + return false; // enemy move speed set to "hidden/none" + + if(!GH.windows().isTopWindow(adventureInt)) + return false; + + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && obj->getOwner() != LOCPLINT->playerID) + return false; + + if(context->isVisible(obj->convertToVisitablePos(from))) + return true; + + if(context->isVisible(obj->convertToVisitablePos(dest))) + return true; + + return false; +} + +void MapViewController::fadeOutObject(const CGObjectInstance * obj) +{ + logGlobal->debug("Starting fade out animation"); + fadingOutContext = std::make_shared(*state); + fadingOutContext->animationTime = adventureContext->animationTime; + adventureContext = fadingOutContext; + context = fadingOutContext; + + const CGObjectInstance * movingObject = obj; + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + movingObject = hero->boat; + } + + fadingOutContext->target = movingObject->id; + fadingOutContext->progress = 1.0; +} + +void MapViewController::fadeInObject(const CGObjectInstance * obj) +{ + logGlobal->debug("Starting fade in animation"); + fadingInContext = std::make_shared(*state); + fadingInContext->animationTime = adventureContext->animationTime; + adventureContext = fadingInContext; + context = fadingInContext; + + const CGObjectInstance * movingObject = obj; + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + movingObject = hero->boat; + } + + fadingInContext->target = movingObject->id; + fadingInContext->progress = 0.0; +} + +void MapViewController::removeObject(const CGObjectInstance * obj) +{ + if (obj->ID == Obj::BOAT) + { + auto * boat = dynamic_cast(obj); + if (boat->hero) + { + view->invalidate(context, boat->hero->id); + state->removeObject(boat->hero); + } + } + + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + { + view->invalidate(context, hero->boat->id); + state->removeObject(hero->boat); + } + } + + view->invalidate(context, obj->id); + state->removeObject(obj); +} + +void MapViewController::addObject(const CGObjectInstance * obj) +{ + state->addObject(obj); + view->invalidate(context, obj->id); +} + +void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + { + if (!isEventInstant(obj, obj->getOwner())) + fadeOutObject(obj); + setViewCenter(obj->getSightCenter()); + } + else + removeObject(obj); +} + +void MapViewController::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + setViewCenter(obj->getSightCenter()); +} + +void MapViewController::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + setViewCenter(obj->getSightCenter()); +} + +void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + { + if (!isEventInstant(obj, obj->getOwner())) + fadeInObject(obj); + setViewCenter(obj->getSightCenter()); + } + addObject(obj); +} + +void MapViewController::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) + fadeInObject(obj); + + addObject(obj); +} + +void MapViewController::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) + fadeOutObject(obj); + else + removeObject(obj); +} + +void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +}; + +void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +}; + +void MapViewController::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, from, dest)) + { + setViewCenter(obj->getSightCenter()); + view->createTransitionSnapshot(context); + } +} + +void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + const CGObjectInstance * movingObject = obj; + if(obj->boat) + movingObject = obj->boat; + + removeObject(movingObject); + addObject(movingObject); + + if(isEventVisible(obj, from, dest)) + { + logGlobal->debug("Starting teleport animation"); + teleportContext = std::make_shared(*state); + teleportContext->animationTime = adventureContext->animationTime; + adventureContext = teleportContext; + context = teleportContext; + setViewCenter(movingObject->getSightCenter()); + } +} + +void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + // revisiting via spacebar, no need to animate + if(from == dest) + return; + + const CGObjectInstance * movingObject = obj; + if(obj->boat) + movingObject = obj->boat; + + removeObject(movingObject); + + if(!isEventVisible(obj, from, dest)) + { + addObject(movingObject); + return; + } + + double movementTime = LOCPLINT->playerID == obj->tempOwner ? + settings["adventure"]["heroMoveTime"].Float() : + settings["adventure"]["enemyMoveTime"].Float(); + + if(movementTime > 1) + { + logGlobal->debug("Starting movement animation"); + movementContext = std::make_shared(*state); + movementContext->animationTime = adventureContext->animationTime; + adventureContext = movementContext; + context = movementContext; + + state->addMovingObject(movingObject, from, dest); + + movementContext->target = movingObject->id; + movementContext->tileFrom = from; + movementContext->tileDest = dest; + movementContext->progress = 0.0; + } + else // instant movement + { + addObject(movingObject); + setViewCenter(movingObject->visitablePos()); + } +} + +bool MapViewController::hasOngoingAnimations() +{ + if(movementContext) + return true; + + if(fadingOutContext) + return true; + + if(fadingInContext) + return true; + + if(teleportContext) + return true; + + return false; +} + +void MapViewController::activateAdventureContext(uint32_t animationTime) +{ + resetContext(); + + adventureContext = std::make_shared(*state); + adventureContext->animationTime = animationTime; + context = adventureContext; + updateState(); +} + +void MapViewController::activateAdventureContext() +{ + activateAdventureContext(0); +} + +void MapViewController::activateWorldViewContext() +{ + if(worldViewContext) + return; + + resetContext(); + + worldViewContext = std::make_shared(*state); + context = worldViewContext; +} + +void MapViewController::activateSpellViewContext() +{ + if(spellViewContext) + return; + + resetContext(); + + spellViewContext = std::make_shared(*state); + worldViewContext = spellViewContext; + context = spellViewContext; +} + +void MapViewController::activatePuzzleMapContext(const int3 & grailPosition) +{ + resetContext(); + + puzzleMapContext = std::make_shared(*state); + context = puzzleMapContext; + + CGPathNode fakeNode; + fakeNode.coord = grailPosition; + + puzzleMapContext->grailPos = std::make_unique(); + + // create two nodes since 1st one is normally not visible + puzzleMapContext->grailPos->nodes.push_back(fakeNode); + puzzleMapContext->grailPos->nodes.push_back(fakeNode); +} + +void MapViewController::resetContext() +{ + adventureContext.reset(); + movementContext.reset(); + fadingOutContext.reset(); + fadingInContext.reset(); + teleportContext.reset(); + worldViewContext.reset(); + spellViewContext.reset(); + puzzleMapContext.reset(); +} + +void MapViewController::setTerrainVisibility(bool showAllTerrain) +{ + assert(spellViewContext); + spellViewContext->showAllTerrain = showAllTerrain; +} + +void MapViewController::setOverlayVisibility(const std::vector & objectPositions) +{ + assert(spellViewContext); + spellViewContext->additionalOverlayIcons = objectPositions; +} diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index bc67e90de..151ef6a93 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -1,100 +1,106 @@ -/* - * MapViewController.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 "IMapRendererObserver.h" - -VCMI_LIB_NAMESPACE_BEGIN -class Point; -struct ObjectPosInfo; -VCMI_LIB_NAMESPACE_END - -struct MapRendererContextState; - -class MapViewCache; -class MapViewModel; -class IMapRendererContext; -class MapRendererAdventureContext; -class MapRendererAdventureFadingContext; -class MapRendererAdventureMovingContext; -class MapRendererAdventureTransitionContext; -class MapRendererWorldViewContext; -class MapRendererSpellViewContext; -class MapRendererPuzzleMapContext; - -/// Class responsible for updating view state, -/// such as its position and any animations -class MapViewController : public IMapObjectObserver -{ - std::shared_ptr context; - std::shared_ptr state; - std::shared_ptr model; - std::shared_ptr view; - - // all potential contexts for view - // only some are present at any given moment, rest are nullptr's - std::shared_ptr adventureContext; - std::shared_ptr movementContext; - std::shared_ptr teleportContext; - std::shared_ptr fadingOutContext; - std::shared_ptr fadingInContext; - std::shared_ptr worldViewContext; - std::shared_ptr spellViewContext; - std::shared_ptr puzzleMapContext; - -private: - bool isEventInstant(const CGObjectInstance * obj); - bool isEventVisible(const CGObjectInstance * obj); - bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - - void fadeOutObject(const CGObjectInstance * obj); - void fadeInObject(const CGObjectInstance * obj); - - void removeObject(const CGObjectInstance * obj); - void addObject(const CGObjectInstance * obj); - - // IMapObjectObserver impl - bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj) override; - void onObjectFadeOut(const CGObjectInstance * obj) override; - void onObjectInstantAdd(const CGObjectInstance * obj) override; - void onObjectInstantRemove(const CGObjectInstance * obj) override; - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - - void resetContext(); - void updateState(); - -public: - MapViewController(std::shared_ptr model, std::shared_ptr view); - - std::shared_ptr getContext() const; - - void setViewCenter(const int3 & position); - void setViewCenter(const Point & position, int level); - void setTileSize(const Point & tileSize); - void modifyTileSize(int stepsChange); - void tick(uint32_t timePassed); - void afterRender(); - - void activateAdventureContext(uint32_t animationTime); - void activateAdventureContext(); - void activateWorldViewContext(); - void activateSpellViewContext(); - void activatePuzzleMapContext(const int3 & grailPosition); - - void setTerrainVisibility(bool showAllTerrain); - void setOverlayVisibility(const std::vector & objectPositions); -}; +/* + * MapViewController.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 "IMapRendererObserver.h" +#include "../../lib/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class PlayerColor; +VCMI_LIB_NAMESPACE_END + +struct MapRendererContextState; + +class MapViewCache; +class MapViewModel; +class IMapRendererContext; +class MapRendererAdventureContext; +class MapRendererAdventureFadingContext; +class MapRendererAdventureMovingContext; +class MapRendererAdventureTransitionContext; +class MapRendererWorldViewContext; +class MapRendererSpellViewContext; +class MapRendererPuzzleMapContext; + +/// Class responsible for updating view state, +/// such as its position and any animations +class MapViewController : public IMapObjectObserver +{ + std::shared_ptr context; + std::shared_ptr state; + std::shared_ptr model; + std::shared_ptr view; + + // all potential contexts for view + // only some are present at any given moment, rest are nullptr's + std::shared_ptr adventureContext; + std::shared_ptr movementContext; + std::shared_ptr teleportContext; + std::shared_ptr fadingOutContext; + std::shared_ptr fadingInContext; + std::shared_ptr worldViewContext; + std::shared_ptr spellViewContext; + std::shared_ptr puzzleMapContext; + +private: + const int defaultTileSize = 32; + const int zoomTileDeadArea = 5; + Point targetTileSize = Point(32, 32); + bool wasInDeadZone = true; + + bool isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator); + bool isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator); + bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + + void fadeOutObject(const CGObjectInstance * obj); + void fadeInObject(const CGObjectInstance * obj); + + void removeObject(const CGObjectInstance * obj); + void addObject(const CGObjectInstance * obj); + + // IMapObjectObserver impl + bool hasOngoingAnimations() override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + + void resetContext(); + void updateState(); + +public: + MapViewController(std::shared_ptr model, std::shared_ptr view); + + std::shared_ptr getContext() const; + + void setViewCenter(const int3 & position); + void setViewCenter(const Point & position, int level); + void setTileSize(const Point & tileSize); + void modifyTileSize(int stepsChange); + void tick(uint32_t timePassed); + void afterRender(); + + void activateAdventureContext(uint32_t animationTime); + void activateAdventureContext(); + void activateWorldViewContext(); + void activateSpellViewContext(); + void activatePuzzleMapContext(const int3 & grailPosition); + + void setTerrainVisibility(bool showAllTerrain); + void setOverlayVisibility(const std::vector & objectPositions); +}; diff --git a/client/mapView/MapViewModel.cpp b/client/mapView/MapViewModel.cpp index 9651c92c9..38b1271a7 100644 --- a/client/mapView/MapViewModel.cpp +++ b/client/mapView/MapViewModel.cpp @@ -1,129 +1,129 @@ -/* - * MapViewModel.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 "MapViewModel.h" - -#include "../../lib/int3.h" - -void MapViewModel::setTileSize(const Point & newValue) -{ - tileSize = newValue; -} - -void MapViewModel::setViewCenter(const Point & newValue) -{ - viewCenter = newValue; -} - -void MapViewModel::setViewDimensions(const Point & newValue) -{ - viewDimensions = newValue; -} - -void MapViewModel::setLevel(int newLevel) -{ - mapLevel = newLevel; -} - -Point MapViewModel::getSingleTileSizeUpperLimit() const -{ - // arbitrary-seleted upscaling limit - return Point(256, 256); -} - -Point MapViewModel::getSingleTileSizeLowerLimit() const -{ - // arbitrary-seleted downscaling limit - return Point(4, 4); -} - -Point MapViewModel::getSingleTileSize() const -{ - return tileSize; -} - -Point MapViewModel::getMapViewCenter() const -{ - return viewCenter; -} - -Point MapViewModel::getPixelsVisibleDimensions() const -{ - return viewDimensions; -} - -int MapViewModel::getLevel() const -{ - return mapLevel; -} - -Point MapViewModel::getTilesVisibleDimensions() const -{ - // total number of potentially visible tiles is: - // 1) number of completely visible tiles - // 2) additional tile that might be partially visible from left/top size - // 3) additional tile that might be partially visible from right/bottom size - return { - getPixelsVisibleDimensions().x / getSingleTileSize().x + 2, - getPixelsVisibleDimensions().y / getSingleTileSize().y + 2, - }; -} - -Rect MapViewModel::getTilesTotalRect() const -{ - return Rect( - Point(getTileAtPoint(Point(0,0))), - getTilesVisibleDimensions() - ); -} - -int3 MapViewModel::getTileAtPoint(const Point & position) const -{ - Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; - - Point absolutePosition = position + topLeftOffset; - - // NOTE: using division via double in order to use std::floor - // which rounds to negative infinity and not towards zero (like integer division) - return { - static_cast(std::floor(static_cast(absolutePosition.x) / getSingleTileSize().x)), - static_cast(std::floor(static_cast(absolutePosition.y) / getSingleTileSize().y)), - getLevel() - }; -} - -Point MapViewModel::getCacheDimensionsPixels() const -{ - return getTilesVisibleDimensions() * getSingleTileSizeUpperLimit(); -} - -Rect MapViewModel::getCacheTileArea(const int3 & coordinates) const -{ - assert(mapLevel == coordinates.z); - assert(getTilesVisibleDimensions().x + coordinates.x >= 0); - assert(getTilesVisibleDimensions().y + coordinates.y >= 0); - - Point tileIndex{ - (getTilesVisibleDimensions().x + coordinates.x) % getTilesVisibleDimensions().x, - (getTilesVisibleDimensions().y + coordinates.y) % getTilesVisibleDimensions().y - }; - - return Rect(tileIndex * getSingleTileSize(), getSingleTileSize()); -} - -Rect MapViewModel::getTargetTileArea(const int3 & coordinates) const -{ - Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; - Point tilePosAbsolute = Point(coordinates) * getSingleTileSize(); - Point tilePosRelative = tilePosAbsolute - topLeftOffset; - - return Rect(tilePosRelative, getSingleTileSize()); -} +/* + * MapViewModel.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 "MapViewModel.h" + +#include "../../lib/int3.h" + +void MapViewModel::setTileSize(const Point & newValue) +{ + tileSize = newValue; +} + +void MapViewModel::setViewCenter(const Point & newValue) +{ + viewCenter = newValue; +} + +void MapViewModel::setViewDimensions(const Point & newValue) +{ + viewDimensions = newValue; +} + +void MapViewModel::setLevel(int newLevel) +{ + mapLevel = newLevel; +} + +Point MapViewModel::getSingleTileSizeUpperLimit() const +{ + // arbitrary-seleted upscaling limit + return Point(256, 256); +} + +Point MapViewModel::getSingleTileSizeLowerLimit() const +{ + // arbitrary-seleted downscaling limit + return Point(4, 4); +} + +Point MapViewModel::getSingleTileSize() const +{ + return tileSize; +} + +Point MapViewModel::getMapViewCenter() const +{ + return viewCenter; +} + +Point MapViewModel::getPixelsVisibleDimensions() const +{ + return viewDimensions; +} + +int MapViewModel::getLevel() const +{ + return mapLevel; +} + +Point MapViewModel::getTilesVisibleDimensions() const +{ + // total number of potentially visible tiles is: + // 1) number of completely visible tiles + // 2) additional tile that might be partially visible from left/top size + // 3) additional tile that might be partially visible from right/bottom size + return { + getPixelsVisibleDimensions().x / getSingleTileSize().x + 2, + getPixelsVisibleDimensions().y / getSingleTileSize().y + 2, + }; +} + +Rect MapViewModel::getTilesTotalRect() const +{ + return Rect( + Point(getTileAtPoint(Point(0,0))), + getTilesVisibleDimensions() + ); +} + +int3 MapViewModel::getTileAtPoint(const Point & position) const +{ + Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; + + Point absolutePosition = position + topLeftOffset; + + // NOTE: using division via double in order to use std::floor + // which rounds to negative infinity and not towards zero (like integer division) + return { + static_cast(std::floor(static_cast(absolutePosition.x) / getSingleTileSize().x)), + static_cast(std::floor(static_cast(absolutePosition.y) / getSingleTileSize().y)), + getLevel() + }; +} + +Point MapViewModel::getCacheDimensionsPixels() const +{ + return getTilesVisibleDimensions() * getSingleTileSizeUpperLimit(); +} + +Rect MapViewModel::getCacheTileArea(const int3 & coordinates) const +{ + assert(mapLevel == coordinates.z); + assert(getTilesVisibleDimensions().x + coordinates.x >= 0); + assert(getTilesVisibleDimensions().y + coordinates.y >= 0); + + Point tileIndex{ + (getTilesVisibleDimensions().x + coordinates.x) % getTilesVisibleDimensions().x, + (getTilesVisibleDimensions().y + coordinates.y) % getTilesVisibleDimensions().y + }; + + return Rect(tileIndex * getSingleTileSize(), getSingleTileSize()); +} + +Rect MapViewModel::getTargetTileArea(const int3 & coordinates) const +{ + Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; + Point tilePosAbsolute = Point(coordinates) * getSingleTileSize(); + Point tilePosRelative = tilePosAbsolute - topLeftOffset; + + return Rect(tilePosRelative, getSingleTileSize()); +} diff --git a/client/mapView/MapViewModel.h b/client/mapView/MapViewModel.h index c5902f16d..b8e3a9236 100644 --- a/client/mapView/MapViewModel.h +++ b/client/mapView/MapViewModel.h @@ -1,63 +1,63 @@ -/* - * MapViewModel.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" - -class MapViewModel -{ - Point tileSize; - Point viewCenter; - Point viewDimensions; - - int mapLevel = 0; - -public: - void setTileSize(const Point & newValue); - void setViewCenter(const Point & newValue); - void setViewDimensions(const Point & newValue); - void setLevel(int newLevel); - - /// returns maximal possible size for a single tile - Point getSingleTileSizeUpperLimit() const; - - /// returns minimal possible size for a single tile - Point getSingleTileSizeLowerLimit() const; - - /// returns current size of map tile in pixels - Point getSingleTileSize() const; - - /// returns center point of map view, in Map coordinates - Point getMapViewCenter() const; - - /// returns total number of visible tiles - Point getTilesVisibleDimensions() const; - - /// returns rect encompassing all visible tiles - Rect getTilesTotalRect() const; - - /// returns required area in pixels of cache canvas - Point getCacheDimensionsPixels() const; - - /// returns actual player-visible area - Point getPixelsVisibleDimensions() const; - - /// returns area covered by specified tile in map cache - Rect getCacheTileArea(const int3 & coordinates) const; - - /// returns area covered by specified tile in target view - Rect getTargetTileArea(const int3 & coordinates) const; - - /// returns tile under specified position in target view - int3 getTileAtPoint(const Point & position) const; - - /// returns currently visible map level - int getLevel() const; -}; +/* + * MapViewModel.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" + +class MapViewModel +{ + Point tileSize; + Point viewCenter; + Point viewDimensions; + + int mapLevel = 0; + +public: + void setTileSize(const Point & newValue); + void setViewCenter(const Point & newValue); + void setViewDimensions(const Point & newValue); + void setLevel(int newLevel); + + /// returns maximal possible size for a single tile + Point getSingleTileSizeUpperLimit() const; + + /// returns minimal possible size for a single tile + Point getSingleTileSizeLowerLimit() const; + + /// returns current size of map tile in pixels + Point getSingleTileSize() const; + + /// returns center point of map view, in Map coordinates + Point getMapViewCenter() const; + + /// returns total number of visible tiles + Point getTilesVisibleDimensions() const; + + /// returns rect encompassing all visible tiles + Rect getTilesTotalRect() const; + + /// returns required area in pixels of cache canvas + Point getCacheDimensionsPixels() const; + + /// returns actual player-visible area + Point getPixelsVisibleDimensions() const; + + /// returns area covered by specified tile in map cache + Rect getCacheTileArea(const int3 & coordinates) const; + + /// returns area covered by specified tile in target view + Rect getTargetTileArea(const int3 & coordinates) const; + + /// returns tile under specified position in target view + int3 getTileAtPoint(const Point & position) const; + + /// returns currently visible map level + int getLevel() const; +}; diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index fda8cb883..aac3232e1 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -1,235 +1,240 @@ -/* - * mapHandler.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 "IMapRendererObserver.h" -#include "mapHandler.h" - -#include "../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" - -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" -#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/mapping/CMap.h" - -bool CMapHandler::hasOngoingAnimations() -{ - for(auto * observer : observers) - if(observer->hasOngoingAnimations()) - return true; - - return false; -} - -void CMapHandler::waitForOngoingAnimations() -{ - while(CGI->mh->hasOngoingAnimations()) - { - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); - boost::this_thread::sleep(boost::posix_time::milliseconds(1)); - } -} - -std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) const -{ - const TerrainTile & t = map->getTile(pos); - - if(t.hasFavorableWinds()) - return CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0); - - std::string result = t.terType->getNameTranslated(); - - for(const auto & object : map->objects) - { - if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain()) - { - result = object->getObjectName(); - break; - } - } - - if(LOCPLINT->cb->getTileDigStatus(pos, false) == EDiggingStatus::CAN_DIG) - { - return boost::str( - boost::format(rightClick ? "%s\r\n%s" : "%s %s") // New line for the Message Box, space for the Status Bar - % result % CGI->generaltexth->allTexts[330] - ); // 'digging ok' - } - - return result; -} - -bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) -{ - if(!a) - return true; - if(!b) - return false; - - // Background objects will always be placed below foreground objects - if(a->appearance->printPriority != 0 || b->appearance->printPriority != 0) - { - if(a->appearance->printPriority != b->appearance->printPriority) - return a->appearance->printPriority > b->appearance->printPriority; - - //Two background objects will be placed based on their placement order on map - return a->id < b->id; - } - - int aBlocksB = 0; - int bBlocksA = 0; - - for(const auto & aOffset : a->getBlockedOffsets()) - { - int3 testTarget = a->pos + aOffset + int3(0, 1, 0); - if(b->blockingAt(testTarget.x, testTarget.y)) - bBlocksA += 1; - } - - for(const auto & bOffset : b->getBlockedOffsets()) - { - int3 testTarget = b->pos + bOffset + int3(0, 1, 0); - if(a->blockingAt(testTarget.x, testTarget.y)) - aBlocksB += 1; - } - - // Discovered by experimenting with H3 maps - object priority depends on how many tiles of object A are "blocked" by object B - // For example if blockmap of two objects looks like this: - // ABB - // AAB - // Here, in middle column object A has blocked tile that is immediately below tile blocked by object B - // Meaning, object A blocks 1 tile of object B and object B blocks 0 tiles of object A - // In this scenario in H3 object A will always appear above object B, irregardless of H3M order - if(aBlocksB != bBlocksA) - 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; - - // heroes should appear on top of objects on the same tile - if(b->ID==Obj::HERO && a->ID!=Obj::HERO) - return true; - if(b->ID!=Obj::HERO && a->ID==Obj::HERO) - return false; - - // or, if all other tests fail to determine priority - simply based on H3M order - return a->id < b->id; -} - -CMapHandler::CMapHandler(const CMap * map) - : map(map) -{ -} - -const CMap * CMapHandler::getMap() -{ - return map; -} - -bool CMapHandler::isInMap(const int3 & tile) -{ - return map->isInTheMap(tile); -} - -void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj) -{ - for(auto * observer : observers) - observer->onObjectFadeIn(obj); -} - -void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj) -{ - for(auto * observer : observers) - observer->onObjectFadeOut(obj); -} - -void CMapHandler::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onBeforeHeroEmbark(obj, from, dest); -} - -void CMapHandler::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onAfterHeroEmbark(obj, from, dest); -} - -void CMapHandler::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onBeforeHeroDisembark(obj, from, dest); -} - -void CMapHandler::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onAfterHeroDisembark(obj, from, dest); -} - -void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj) -{ - for(auto * observer : observers) - observer->onObjectInstantAdd(obj); -} - -void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj) -{ - for(auto * observer : observers) - observer->onObjectInstantRemove(obj); -} - -void CMapHandler::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == dest); - for(auto * observer : observers) - observer->onAfterHeroTeleported(obj, from, dest); -} - -void CMapHandler::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == from); - for(auto * observer : observers) - observer->onBeforeHeroTeleported(obj, from, dest); -} - -void CMapHandler::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == dest); - for(auto * observer : observers) - observer->onHeroMoved(obj, from, dest); -} - -void CMapHandler::addMapObserver(IMapObjectObserver * object) -{ - observers.push_back(object); -} - -void CMapHandler::removeMapObserver(IMapObjectObserver * object) -{ - vstd::erase(observers, object); -} - -IMapObjectObserver::IMapObjectObserver() -{ - CGI->mh->addMapObserver(this); -} - -IMapObjectObserver::~IMapObjectObserver() -{ - if (CGI && CGI->mh) - CGI->mh->removeMapObserver(this); -} +/* + * mapHandler.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 "IMapRendererObserver.h" +#include "mapHandler.h" + +#include "../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMap.h" + +bool CMapHandler::hasOngoingAnimations() +{ + for(auto * observer : observers) + if(observer->hasOngoingAnimations()) + return true; + + return false; +} + +void CMapHandler::waitForOngoingAnimations() +{ + while(CGI->mh->hasOngoingAnimations()) + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); + } +} + +std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) const +{ + const TerrainTile & t = map->getTile(pos); + + if(t.hasFavorableWinds()) + return CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0); + + std::string result = t.terType->getNameTranslated(); + + for(const auto & object : map->objects) + { + if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain()) + { + result = object->getObjectName(); + break; + } + } + + if(LOCPLINT->cb->getTileDigStatus(pos, false) == EDiggingStatus::CAN_DIG) + { + return boost::str( + boost::format(rightClick ? "%s\r\n%s" : "%s %s") // New line for the Message Box, space for the Status Bar + % result % CGI->generaltexth->allTexts[330] + ); // 'digging ok' + } + + return result; +} + +bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) +{ + //FIXME: Optimize + // this method is called A LOT on game start and some parts, e.g. for loops are too slow for that + + assert(a && b); + if(!a) + return true; + if(!b) + return false; + + // Background objects will always be placed below foreground objects + if(a->appearance->printPriority != 0 || b->appearance->printPriority != 0) + { + if(a->appearance->printPriority != b->appearance->printPriority) + return a->appearance->printPriority > b->appearance->printPriority; + + //Two background objects will be placed based on their placement order on map + return a->id < b->id; + } + + int aBlocksB = 0; + int bBlocksA = 0; + + for(const auto & aOffset : a->getBlockedOffsets()) + { + int3 testTarget = a->pos + aOffset + int3(0, 1, 0); + if(b->blockingAt(testTarget.x, testTarget.y)) + bBlocksA += 1; + } + + for(const auto & bOffset : b->getBlockedOffsets()) + { + int3 testTarget = b->pos + bOffset + int3(0, 1, 0); + if(a->blockingAt(testTarget.x, testTarget.y)) + aBlocksB += 1; + } + + // Discovered by experimenting with H3 maps - object priority depends on how many tiles of object A are "blocked" by object B + // For example if blockmap of two objects looks like this: + // ABB + // AAB + // Here, in middle column object A has blocked tile that is immediately below tile blocked by object B + // Meaning, object A blocks 1 tile of object B and object B blocks 0 tiles of object A + // In this scenario in H3 object A will always appear above object B, irregardless of H3M order + if(aBlocksB != bBlocksA) + 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; + + // heroes should appear on top of objects on the same tile + if(b->ID==Obj::HERO && a->ID!=Obj::HERO) + return true; + if(b->ID!=Obj::HERO && a->ID==Obj::HERO) + return false; + + // or, if all other tests fail to determine priority - simply based on H3M order + return a->id < b->id; +} + +CMapHandler::CMapHandler(const CMap * map) + : map(map) +{ +} + +const CMap * CMapHandler::getMap() +{ + return map; +} + +bool CMapHandler::isInMap(const int3 & tile) +{ + return map->isInTheMap(tile); +} + +void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectFadeIn(obj, initiator); +} + +void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectFadeOut(obj, initiator); +} + +void CMapHandler::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onBeforeHeroEmbark(obj, from, dest); +} + +void CMapHandler::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onAfterHeroEmbark(obj, from, dest); +} + +void CMapHandler::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onBeforeHeroDisembark(obj, from, dest); +} + +void CMapHandler::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onAfterHeroDisembark(obj, from, dest); +} + +void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectInstantAdd(obj, initiator); +} + +void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectInstantRemove(obj, initiator); +} + +void CMapHandler::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == dest); + for(auto * observer : observers) + observer->onAfterHeroTeleported(obj, from, dest); +} + +void CMapHandler::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == from); + for(auto * observer : observers) + observer->onBeforeHeroTeleported(obj, from, dest); +} + +void CMapHandler::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == dest); + for(auto * observer : observers) + observer->onHeroMoved(obj, from, dest); +} + +void CMapHandler::addMapObserver(IMapObjectObserver * object) +{ + observers.push_back(object); +} + +void CMapHandler::removeMapObserver(IMapObjectObserver * object) +{ + vstd::erase(observers, object); +} + +IMapObjectObserver::IMapObjectObserver() +{ + CGI->mh->addMapObserver(this); +} + +IMapObjectObserver::~IMapObjectObserver() +{ + if (CGI && CGI->mh) + CGI->mh->removeMapObserver(this); +} diff --git a/client/mapView/mapHandler.h b/client/mapView/mapHandler.h index 179998eab..4ca5e6cc7 100644 --- a/client/mapView/mapHandler.h +++ b/client/mapView/mapHandler.h @@ -1,76 +1,76 @@ -/* - * mapHandler.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/Rect.h" -#include "../../lib/int3.h" -#include "../../lib/spells/ViewSpellInt.h" - -#ifdef IN -# undef IN -#endif - -#ifdef OUT -# undef OUT -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CMap; - -VCMI_LIB_NAMESPACE_END - -class IMapObjectObserver; - -class CMapHandler -{ - const CMap * map; - std::vector observers; - -public: - explicit CMapHandler(const CMap * map); - - const CMap * getMap(); - - /// returns true if tile is within map bounds - bool isInMap(const int3 & tile); - - /// see MapObjectObserver interface - void onObjectFadeIn(const CGObjectInstance * obj); - void onObjectFadeOut(const CGObjectInstance * obj); - void onObjectInstantAdd(const CGObjectInstance * obj); - void onObjectInstantRemove(const CGObjectInstance * obj); - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - - /// Add object to receive notifications on any changes in visible map state - void addMapObserver(IMapObjectObserver * observer); - void removeMapObserver(IMapObjectObserver * observer); - - /// returns string description for terrain interaction - std::string getTerrainDescr(const int3 & pos, bool rightClick) const; - - /// determines if the map is ready to handle new hero movement (not available during fading animations) - bool hasOngoingAnimations(); - - /// blocking wait until all ongoing animatins are over - void waitForOngoingAnimations(); - - static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b); -}; +/* + * mapHandler.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/Rect.h" +#include "../../lib/int3.h" +#include "../../lib/spells/ViewSpellInt.h" + +#ifdef IN +# undef IN +#endif + +#ifdef OUT +# undef OUT +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGHeroInstance; +class CMap; + +VCMI_LIB_NAMESPACE_END + +class IMapObjectObserver; + +class CMapHandler +{ + const CMap * map; + std::vector observers; + +public: + explicit CMapHandler(const CMap * map); + + const CMap * getMap(); + + /// returns true if tile is within map bounds + bool isInMap(const int3 & tile); + + /// see MapObjectObserver interface + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator); + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + + /// Add object to receive notifications on any changes in visible map state + void addMapObserver(IMapObjectObserver * observer); + void removeMapObserver(IMapObjectObserver * observer); + + /// returns string description for terrain interaction + std::string getTerrainDescr(const int3 & pos, bool rightClick) const; + + /// determines if the map is ready to handle new hero movement (not available during fading animations) + bool hasOngoingAnimations(); + + /// blocking wait until all ongoing animatins are over + void waitForOngoingAnimations(); + + static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b); +}; diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 40cf03681..662f41ccc 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -1,377 +1,375 @@ -/* - * CAnimation.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 "CAnimation.h" - -#include "CDefFile.h" - -#include "Graphics.h" -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/JsonNode.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(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) -{ - if(size(group) <= frame) - { - printError(frame, group, "LoadFrame"); - return false; - } - - auto image = getImage(frame, group, false); - if(image) - { - return true; - } - - //try to get image from def - if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) - { - 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 - - printError(frame, group, "LoadFrame"); - images[group][frame] = std::make_shared("DEFAULT", EImageBlitMode::ALPHA); - } - 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) -{ - auto image = getImage(frame, group, false); - if(image) - { - images[group].erase(frame); - - if(images[group].empty()) - images.erase(group); - return true; - } - return false; -} - -void CAnimation::initFromJson(const JsonNode & config) -{ - std::string basepath; - basepath = config["basepath"].String(); - - JsonNode base(JsonNode::JsonType::DATA_STRUCT); - 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(JsonNode::JsonType::DATA_STRUCT); - 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(JsonNode::JsonType::DATA_STRUCT); - 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()) - { - logGlobal->error("Nothing to export, animation is empty"); - return; - } - - boost::filesystem::path actualPath = path / "SPRITES" / name; - boost::filesystem::create_directories(actualPath); - - size_t counter = 0; - - for(const auto & groupPair : images) - { - size_t group = groupPair.first; - - for(const auto & imagePair : groupPair.second) - { - size_t frame = imagePair.first; - const auto img = imagePair.second; - - boost::format fmt("%d_%d.bmp"); - fmt % group % frame; - - img->exportBitmap(actualPath / fmt.str()); - counter++; - } - } - - 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); - } - - ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); - - if (vstd::contains(graphics->imageLists, resID.getName())) - initFromJson(graphics->imageLists[resID.getName()]); - - auto configList = CResourceHandler::get()->getResourcesWithName(resID); - - for(auto & loader : configList) - { - auto stream = loader->load(resID); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - const JsonNode config((char*)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, group, frame); -} - -CAnimation::CAnimation(std::string Name): - name(Name), - preloaded(false), - defFile() -{ - size_t dotPos = name.find_last_of('.'); - if ( dotPos!=-1 ) - name.erase(dotPos); - std::transform(name.begin(), name.end(), name.begin(), toupper); - - ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); - - if(CResourceHandler::get()->existsResource(resource)) - defFile = std::make_shared(name); - - init(); - - if(source.empty()) - logAnim->error("Animation %s failed to load", Name); -} - -CAnimation::CAnimation(): - name(""), - preloaded(false), - defFile() -{ - 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); - return; - } - - if(source[sourceGroup].size() <= sourceFrame) - { - logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name); - return; - } - - //todo: clone actual loaded Image object - JsonNode clone(source[sourceGroup][sourceFrame]); - - if(clone.getType() == JsonNode::JsonType::DATA_NULL) - { - std::string temp = name+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); - clone["file"].String() = temp; - } - - 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) -{ - 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 CAnimation::getImage(size_t frame, size_t group, bool verbose) const -{ - auto groupIter = images.find(group); - if (groupIter != images.end()) - { - auto imageIter = groupIter->second.find(frame); - if (imageIter != groupIter->second.end()) - return imageIter->second; - } - if (verbose) - printError(frame, group, "GetImage"); - 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); - if (iter != source.end()) - return iter->second.size(); - return 0; -} - -void CAnimation::horizontalFlip() -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->horizontalFlip(); -} - -void CAnimation::verticalFlip() -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->verticalFlip(); -} - -void CAnimation::playerColored(PlayerColor player) -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->playerColored(player); -} - -void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) -{ - for(size_t frame = 0; frame < size(sourceGroup); ++frame) - { - duplicateImage(sourceGroup, frame, targetGroup); - - auto image = getImage(frame, targetGroup); - image->verticalFlip(); - } -} - +/* + * CAnimation.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 "CAnimation.h" + +#include "CDefFile.h" + +#include "Graphics.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/JsonNode.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) +{ + if(size(group) <= frame) + { + printError(frame, group, "LoadFrame"); + return false; + } + + auto image = getImage(frame, group, false); + if(image) + { + return true; + } + + //try to get image from def + if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) + { + 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 + + printError(frame, group, "LoadFrame"); + images[group][frame] = std::make_shared(ImagePath::builtin("DEFAULT"), EImageBlitMode::ALPHA); + } + 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) +{ + auto image = getImage(frame, group, false); + if(image) + { + images[group].erase(frame); + + if(images[group].empty()) + images.erase(group); + return true; + } + return false; +} + +void CAnimation::initFromJson(const JsonNode & config) +{ + std::string basepath; + basepath = config["basepath"].String(); + + JsonNode base(JsonNode::JsonType::DATA_STRUCT); + 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(JsonNode::JsonType::DATA_STRUCT); + 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(JsonNode::JsonType::DATA_STRUCT); + 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()) + { + logGlobal->error("Nothing to export, animation is empty"); + return; + } + + boost::filesystem::path actualPath = path / "SPRITES" / name.getName(); + boost::filesystem::create_directories(actualPath); + + size_t counter = 0; + + for(const auto & groupPair : images) + { + size_t group = groupPair.first; + + for(const auto & imagePair : groupPair.second) + { + size_t frame = imagePair.first; + const auto img = imagePair.second; + + boost::format fmt("%d_%d.bmp"); + fmt % group % frame; + + img->exportBitmap(actualPath / fmt.str()); + counter++; + } + } + + 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((char*)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): + name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), + preloaded(false) +{ + 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; + } + + 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) +{ + 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 CAnimation::getImage(size_t frame, size_t group, bool verbose) const +{ + auto groupIter = images.find(group); + if (groupIter != images.end()) + { + auto imageIter = groupIter->second.find(frame); + if (imageIter != groupIter->second.end()) + return imageIter->second; + } + if (verbose) + printError(frame, group, "GetImage"); + 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); + if (iter != source.end()) + return iter->second.size(); + return 0; +} + +void CAnimation::horizontalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->horizontalFlip(); +} + +void CAnimation::verticalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->verticalFlip(); +} + +void CAnimation::playerColored(PlayerColor player) +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->playerColored(player); +} + +void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) +{ + for(size_t frame = 0; frame < size(sourceGroup); ++frame) + { + duplicateImage(sourceGroup, frame, targetGroup); + + auto image = getImage(frame, targetGroup); + image->verticalFlip(); + } +} + diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index 7e26d13cd..efb66f602 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -1,93 +1,95 @@ -/* - * CAnimation.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/GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -class CDefFile; -class IImage; - -/// 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; - - //bitmap[group][position], store objects with loaded bitmaps - std::map > > images; - - //animation file name - std::string name; - - bool preloaded; - - std::shared_ptr defFile; - - //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); - - //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); - -public: - CAnimation(std::string Name); - CAnimation(); - ~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; - - 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(); - void verticalFlip(); - void playerColored(PlayerColor player); - - void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); -}; - +/* + * CAnimation.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/GameConstants.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN +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; + + //bitmap[group][position], store objects with loaded bitmaps + std::map > > images; + + //animation file name + AnimationPath name; + + bool preloaded; + + std::shared_ptr defFile; + + //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); + + //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); + +public: + CAnimation(const AnimationPath & Name); + CAnimation(); + ~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; + + 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(); + void verticalFlip(); + void playerColored(PlayerColor player); + + void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); +}; + diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index c18612e8f..79b597480 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -1,205 +1,210 @@ -/* - * CBitmapHandler.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 "CBitmapHandler.h" - -#include "../renderSDL/SDL_Extensions.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/vcmi_endian.h" - -#include - -namespace BitmapHandler -{ - SDL_Surface * loadH3PCX(ui8 * data, size_t size); - - SDL_Surface * loadBitmapFromDir(std::string path, std::string fname); -} - -bool isPCX(const ui8 *header)//check whether file can be PCX according to header -{ - ui32 fSize = read_le_u32(header + 0); - ui32 width = read_le_u32(header + 4); - ui32 height = read_le_u32(header + 8); - return fSize == width*height || fSize == width*height*3; -} - -enum Epcxformat -{ - PCX8B, - PCX24B -}; - -SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) -{ - SDL_Surface * ret; - - Epcxformat format; - int it=0; - - ui32 fSize = read_le_u32(pcx + it); it+=4; - ui32 width = read_le_u32(pcx + it); it+=4; - ui32 height = read_le_u32(pcx + it); it+=4; - - if (fSize==width*height*3) - format=PCX24B; - else if (fSize==width*height) - format=PCX8B; - else - return nullptr; - - if (format==PCX8B) - { - ret = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); - - it = 0xC; - for (int i=0; i<(int)height; i++) - { - memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width); - it+= width; - } - - //palette - last 256*3 bytes - it = (int)size-256*3; - for (int i=0;i<256;i++) - { - SDL_Color tp; - tp.r = pcx[it++]; - tp.g = pcx[it++]; - tp.b = pcx[it++]; - tp.a = SDL_ALPHA_OPAQUE; - ret->format->palette->colors[i] = tp; - } - } - else - { -#ifdef VCMI_ENDIAN_BIG - int bmask = 0xff0000; - int gmask = 0x00ff00; - int rmask = 0x0000ff; -#else - int bmask = 0x0000ff; - int gmask = 0x00ff00; - int rmask = 0xff0000; -#endif - ret = SDL_CreateRGBSurface(0, width, height, 24, rmask, gmask, bmask, 0); - - //it == 0xC; - for (int i=0; i<(int)height; i++) - { - memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width*3); - it+= width*3; - } - - } - return ret; -} - -SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname) -{ - if(!fname.size()) - { - logGlobal->warn("Call to loadBitmap with void fname!"); - return nullptr; - } - if (!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) - { - return nullptr; - } - - SDL_Surface * ret=nullptr; - - auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->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!", fname); - 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) - { - // 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", fname); - logGlobal->error("Reason: %s", IMG_GetError()); - return nullptr; - } - } - - // When modifying anything here please check two use cases: - // 1) Vampire mansion in Necropolis (not 1st color is transparent) - // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) - // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) - // 4) special case - there are 2 .bmp images that have semi-transparency (CCELLGRD.BMP & CCELLSHD.BMP) - if (ret->format->palette && - ret->format->palette->colors[0].r == 255 && - ret->format->palette->colors[0].g == 0 && - ret->format->palette->colors[0].b == 255 ) - { - static SDL_Color shadow[3] = - { - { 0, 0, 0, 0},// 100% - transparency - { 0, 0, 0, 32},// 75% - shadow border, - { 0, 0, 0, 128},// 50% - shadow body - }; - - CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]); - - ret->format->palette->colors[0] = shadow[0]; - ret->format->palette->colors[1] = shadow[1]; - ret->format->palette->colors[4] = shadow[2]; - } - else if (ret->format->palette) - { - CSDL_Ext::setDefaultColorKeyPresize(ret); - } - else if (ret->format->Amask) - { - SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); - } - else // always set - { - CSDL_Ext::setDefaultColorKey(ret); - } - - return ret; -} - -SDL_Surface * BitmapHandler::loadBitmap(std::string fname) -{ - SDL_Surface * bitmap = nullptr; - - if (!(bitmap = loadBitmapFromDir("DATA/", fname)) && - !(bitmap = loadBitmapFromDir("SPRITES/", fname))) - { - logGlobal->error("Error: Failed to find file %s", fname); - } - - return bitmap; -} +/* + * CBitmapHandler.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 "CBitmapHandler.h" + +#include "../renderSDL/SDL_Extensions.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/vcmi_endian.h" + +#include + +namespace BitmapHandler +{ + SDL_Surface * loadH3PCX(ui8 * data, size_t size); + + SDL_Surface * loadBitmapFromDir(const ImagePath & path); +} + +bool isPCX(const ui8 *header)//check whether file can be PCX according to header +{ + ui32 fSize = read_le_u32(header + 0); + ui32 width = read_le_u32(header + 4); + ui32 height = read_le_u32(header + 8); + return fSize == width*height || fSize == width*height*3; +} + +enum Epcxformat +{ + PCX8B, + PCX24B +}; + +SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) +{ + SDL_Surface * ret; + + Epcxformat format; + int it=0; + + ui32 fSize = read_le_u32(pcx + it); it+=4; + ui32 width = read_le_u32(pcx + it); it+=4; + ui32 height = read_le_u32(pcx + it); it+=4; + + if (fSize==width*height*3) + format=PCX24B; + else if (fSize==width*height) + format=PCX8B; + else + return nullptr; + + if (format==PCX8B) + { + ret = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); + + it = 0xC; + for (int i=0; i<(int)height; i++) + { + memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width); + it+= width; + } + + //palette - last 256*3 bytes + it = (int)size-256*3; + for (int i=0;i<256;i++) + { + SDL_Color tp; + tp.r = pcx[it++]; + tp.g = pcx[it++]; + tp.b = pcx[it++]; + tp.a = SDL_ALPHA_OPAQUE; + ret->format->palette->colors[i] = tp; + } + } + else + { +#ifdef VCMI_ENDIAN_BIG + int bmask = 0xff0000; + int gmask = 0x00ff00; + int rmask = 0x0000ff; +#else + int bmask = 0x0000ff; + int gmask = 0x00ff00; + int rmask = 0xff0000; +#endif + ret = SDL_CreateRGBSurface(0, width, height, 24, rmask, gmask, bmask, 0); + + //it == 0xC; + for (int i=0; i<(int)height; i++) + { + memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width*3); + it+= width*3; + } + + } + return ret; +} + +SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) +{ + if (!CResourceHandler::get()->existsResource(path)) + { + return nullptr; + } + + SDL_Surface * ret=nullptr; + + 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) + { + // 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; + } + } + + // When modifying anything here please check two use cases: + // 1) Vampire mansion in Necropolis (not 1st color is transparent) + // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) + // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) + // 4) special case - there are 2 .bmp images that have semi-transparency (CCELLGRD.BMP & CCELLSHD.BMP) + if (ret->format->palette && + ret->format->palette->colors[0].r == 255 && + ret->format->palette->colors[0].g == 0 && + ret->format->palette->colors[0].b == 255 ) + { + static SDL_Color shadow[3] = + { + { 0, 0, 0, 0},// 100% - transparency + { 0, 0, 0, 32},// 75% - shadow border, + { 0, 0, 0, 128},// 50% - shadow body + }; + + CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]); + + ret->format->palette->colors[0] = shadow[0]; + ret->format->palette->colors[1] = shadow[1]; + ret->format->palette->colors[4] = shadow[2]; + } + else if (ret->format->palette) + { + CSDL_Ext::setDefaultColorKeyPresize(ret); + } + else if (ret->format->Amask) + { + SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); + } + else // always set + { + CSDL_Ext::setDefaultColorKey(ret); + } + return ret; +} + +SDL_Surface * BitmapHandler::loadBitmap(const ImagePath & fname) +{ + if(fname.empty()) + { + logGlobal->warn("Call to loadBitmap with void fname!"); + return nullptr; + } + + SDL_Surface * bitmap = loadBitmapFromDir(fname); + if (bitmap != nullptr) + return bitmap; + + SDL_Surface * bitmapData = loadBitmapFromDir(fname.addPrefix("DATA/")); + if (bitmapData != nullptr) + return bitmapData; + + SDL_Surface * bitmapSprites = loadBitmapFromDir(fname.addPrefix("SPRITES/")); + if (bitmapSprites != nullptr) + return bitmapSprites; + + logGlobal->error("Error: Failed to find file %s", fname.getOriginalName()); + return nullptr; +} diff --git a/client/render/CBitmapHandler.h b/client/render/CBitmapHandler.h index b36156211..ac3ae5f35 100644 --- a/client/render/CBitmapHandler.h +++ b/client/render/CBitmapHandler.h @@ -1,18 +1,20 @@ -/* - * CBitmapHandler.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 - -struct SDL_Surface; - -namespace BitmapHandler -{ - //Load file from /DATA or /SPRITES - SDL_Surface * loadBitmap(std::string fname); -} +/* + * CBitmapHandler.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" + +struct SDL_Surface; + +namespace BitmapHandler +{ + //Load file from /DATA or /SPRITES + SDL_Surface * loadBitmap(const ImagePath & fname); +} diff --git a/client/render/CDefFile.cpp b/client/render/CDefFile.cpp index a19acc5be..23377c4e5 100644 --- a/client/render/CDefFile.cpp +++ b/client/render/CDefFile.cpp @@ -1,338 +1,338 @@ -/* - * CDefFile.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 "CDefFile.h" - -#include "IImageLoader.h" - -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/Point.h" - -#include - -// Extremely simple file cache. TODO: smarter, more general solution -class CFileCache -{ - static const int cacheSize = 50; //Max number of cached files - struct FileData - { - ResourceID 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(ResourceID 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(ResourceID 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, - SPRITE = 0x41, - CREATURE = 0x42, - MAP = 0x43, - MAP_HERO = 0x44, - TERRAIN = 0x45, - CURSOR = 0x46, - INTERFACE = 0x47, - SPRITE_FRAME = 0x48, - 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(std::string 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(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); - - palette = std::unique_ptr(new SDL_Color[256]); - int it = 0; - - //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; - it+=8; - ui32 totalBlocks = read_le_u32(data.get() + it); - it+=4; - - for (ui32 i= 0; i<256; i++) - { - palette[i].r = data[it++]; - palette[i].g = data[it++]; - palette[i].b = data[it++]; - 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 >::const_iterator it; - it = offset.find(group); - assert (it != offset.end()); - - const ui8 * FDef = data.get()+it->second[frame]; - - const SSpriteDef sd = * reinterpret_cast(FDef); - SSpriteDef sprite; - - sprite.format = read_le_u32(&sd.format); - sprite.fullWidth = read_le_u32(&sd.fullWidth); - sprite.fullHeight = read_le_u32(&sd.fullHeight); - sprite.width = read_le_u32(&sd.width); - sprite.height = read_le_u32(&sd.height); - sprite.leftMargin = read_le_u32(&sd.leftMargin); - sprite.topMargin = read_le_u32(&sd.topMargin); - - ui32 currentOffset = sizeof(SSpriteDef); - - //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) - - if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) - { - sprite.leftMargin = 0; - sprite.topMargin = 0; - sprite.width = sprite.fullWidth; - sprite.height = sprite.fullHeight; - - currentOffset -= 16; - } - - const ui32 BaseOffset = currentOffset; - - loader.init(Point(sprite.width, sprite.height), - Point(sprite.leftMargin, sprite.topMargin), - Point(sprite.fullWidth, sprite.fullHeight), palette.get()); - - switch(sprite.format) - { - case 0: - { - //pixel data is not compressed, copy data to surface - for(ui32 i=0; i(FDef+currentOffset); - currentOffset += sizeof(ui32) * sprite.height; - - for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); - break; - } -} - -CDefFile::~CDefFile() = default; - -const std::map CDefFile::getEntries() const -{ - std::map ret; - - for (auto & elem : offset) - ret[elem.first] = elem.second.size(); - return ret; -} - +/* + * CDefFile.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 "CDefFile.h" + +#include "IImageLoader.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/Point.h" + +#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, + SPRITE = 0x41, + CREATURE = 0x42, + MAP = 0x43, + MAP_HERO = 0x44, + TERRAIN = 0x45, + CURSOR = 0x46, + INTERFACE = 0x47, + SPRITE_FRAME = 0x48, + 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); + + palette = std::unique_ptr(new SDL_Color[256]); + int it = 0; + + //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; + it+=8; + ui32 totalBlocks = read_le_u32(data.get() + it); + it+=4; + + for (ui32 i= 0; i<256; i++) + { + palette[i].r = data[it++]; + palette[i].g = data[it++]; + palette[i].b = data[it++]; + 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 >::const_iterator it; + it = offset.find(group); + assert (it != offset.end()); + + const ui8 * FDef = data.get()+it->second[frame]; + + const SSpriteDef sd = * reinterpret_cast(FDef); + SSpriteDef sprite; + + sprite.format = read_le_u32(&sd.format); + sprite.fullWidth = read_le_u32(&sd.fullWidth); + sprite.fullHeight = read_le_u32(&sd.fullHeight); + sprite.width = read_le_u32(&sd.width); + sprite.height = read_le_u32(&sd.height); + sprite.leftMargin = read_le_u32(&sd.leftMargin); + sprite.topMargin = read_le_u32(&sd.topMargin); + + ui32 currentOffset = sizeof(SSpriteDef); + + //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) + + if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) + { + sprite.leftMargin = 0; + sprite.topMargin = 0; + sprite.width = sprite.fullWidth; + sprite.height = sprite.fullHeight; + + currentOffset -= 16; + } + + const ui32 BaseOffset = currentOffset; + + loader.init(Point(sprite.width, sprite.height), + Point(sprite.leftMargin, sprite.topMargin), + Point(sprite.fullWidth, sprite.fullHeight), palette.get()); + + switch(sprite.format) + { + case 0: + { + //pixel data is not compressed, copy data to surface + for(ui32 i=0; i(FDef+currentOffset); + currentOffset += sizeof(ui32) * sprite.height; + + for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); + break; + } +} + +CDefFile::~CDefFile() = default; + +const std::map CDefFile::getEntries() const +{ + std::map ret; + + for (auto & elem : offset) + ret[elem.first] = elem.second.size(); + return ret; +} + diff --git a/client/render/CDefFile.h b/client/render/CDefFile.h index 3aec57f4c..72f3996db 100644 --- a/client/render/CDefFile.h +++ b/client/render/CDefFile.h @@ -1,51 +1,52 @@ -/* - * CDefFile.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/vcmi_endian.h" - -class IImageLoader; -struct SDL_Color; - -/// Class for def loading -/// After loading will store general info (palette and frame offsets) and pointer to file itself -class CDefFile -{ -private: - - PACKED_STRUCT_BEGIN - struct SSpriteDef - { - ui32 size; - ui32 format; /// format in which pixel data is stored - ui32 fullWidth; /// full width and height of frame, including borders - ui32 fullHeight; - ui32 width; /// width and height of pixel data, borders excluded - ui32 height; - si32 leftMargin; - si32 topMargin; - } PACKED_STRUCT_END; - //offset[group][frame] - offset of frame data in file - std::map > offset; - - std::unique_ptr data; - std::unique_ptr palette; - -public: - CDefFile(std::string Name); - ~CDefFile(); - - //load frame as SDL_Surface - void loadFrame(size_t frame, size_t group, IImageLoader &loader) const; - - const std::map getEntries() const; -}; - - +/* + * CDefFile.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/vcmi_endian.h" +#include "../../lib/filesystem/ResourcePath.h" + +class IImageLoader; +struct SDL_Color; + +/// Class for def loading +/// After loading will store general info (palette and frame offsets) and pointer to file itself +class CDefFile +{ +private: + + PACKED_STRUCT_BEGIN + struct SSpriteDef + { + ui32 size; + ui32 format; /// format in which pixel data is stored + ui32 fullWidth; /// full width and height of frame, including borders + ui32 fullHeight; + ui32 width; /// width and height of pixel data, borders excluded + ui32 height; + si32 leftMargin; + si32 topMargin; + } PACKED_STRUCT_END; + //offset[group][frame] - offset of frame data in file + std::map > offset; + + std::unique_ptr data; + std::unique_ptr palette; + +public: + CDefFile(const AnimationPath & Name); + ~CDefFile(); + + //load frame as SDL_Surface + void loadFrame(size_t frame, size_t group, IImageLoader &loader) const; + + const std::map getEntries() const; +}; + + diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index 8a20e3d3d..261c7b556 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -1,179 +1,193 @@ -/* - * Canvas.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 "Canvas.h" - -#include "../renderSDL/SDL_Extensions.h" -#include "Colors.h" -#include "IImage.h" -#include "Graphics.h" - -#include - -Canvas::Canvas(SDL_Surface * surface): - surface(surface), - renderArea(0,0, surface->w, surface->h) -{ - surface->refcount++; -} - -Canvas::Canvas(const Canvas & other): - surface(other.surface), - renderArea(other.renderArea) -{ - surface->refcount++; -} - -Canvas::Canvas(Canvas && other): - surface(other.surface), - renderArea(other.renderArea) -{ - surface->refcount++; -} - -Canvas::Canvas(const Canvas & other, const Rect & newClipRect): - Canvas(other) -{ - renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft()); -} - -Canvas::Canvas(const Point & size): - renderArea(Point(0,0), size), - surface(CSDL_Ext::newSurface(size.x, size.y)) -{ - CSDL_Ext::fillSurface(surface, Colors::TRANSPARENCY ); - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); -} - -Canvas Canvas::createFromSurface(SDL_Surface * surface) -{ - return Canvas(surface); -} - -void Canvas::applyTransparency(bool on) -{ - if (on) - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND); - else - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); -} - -void Canvas::applyGrayscale() -{ - CSDL_Ext::convertToGrayscale(surface, renderArea); -} - -Canvas::~Canvas() -{ - SDL_FreeSurface(surface); -} - -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); -} - -void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) -{ - assert(image); - if (image) - image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect); -} - -void Canvas::draw(const Canvas & image, const Point & pos) -{ - CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); -} - -void Canvas::drawTransparent(const Canvas & image, const Point & pos, double transparency) -{ - SDL_BlendMode oldMode; - - 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); - 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_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); -} - -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)); -} - -void Canvas::drawBorder(const Rect & target, const SDL_Color & color, int width) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, color, width); -} - -void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) -{ - Rect realTarget = target + 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)); - CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.bottomLeft(), CSDL_Ext::toSDL(color)); - CSDL_Ext::drawLineDashed(surface, realTarget.topRight(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); -} - -void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text ) -{ - 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); - } -} - -void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector & text ) -{ - 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); - } -} - -void Canvas::drawColor(const Rect & target, const SDL_Color & color) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::fillRect(surface, realTarget, color); -} - -SDL_Surface * Canvas::getInternalSurface() -{ - return surface; -} +/* + * Canvas.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 "Canvas.h" + +#include "../renderSDL/SDL_Extensions.h" +#include "Colors.h" +#include "IImage.h" +#include "Graphics.h" +#include "IFont.h" + +#include +#include + +Canvas::Canvas(SDL_Surface * surface): + surface(surface), + renderArea(0,0, surface->w, surface->h) +{ + surface->refcount++; +} + +Canvas::Canvas(const Canvas & other): + surface(other.surface), + renderArea(other.renderArea) +{ + surface->refcount++; +} + +Canvas::Canvas(Canvas && other): + surface(other.surface), + renderArea(other.renderArea) +{ + surface->refcount++; +} + +Canvas::Canvas(const Canvas & other, const Rect & newClipRect): + Canvas(other) +{ + renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft()); +} + +Canvas::Canvas(const Point & size): + renderArea(Point(0,0), size), + surface(CSDL_Ext::newSurface(size.x, size.y)) +{ + CSDL_Ext::fillSurface(surface, CSDL_Ext::toSDL(Colors::TRANSPARENCY) ); + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); +} + +Canvas Canvas::createFromSurface(SDL_Surface * surface) +{ + return Canvas(surface); +} + +void Canvas::applyTransparency(bool on) +{ + if (on) + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND); + else + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); +} + +void Canvas::applyGrayscale() +{ + CSDL_Ext::convertToGrayscale(surface, renderArea); +} + +Canvas::~Canvas() +{ + SDL_FreeSurface(surface); +} + +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); +} + +void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) +{ + assert(image); + if (image) + image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect); +} + +void Canvas::draw(const Canvas & image, const Point & pos) +{ + CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); +} + +void Canvas::drawTransparent(const Canvas & image, const Point & pos, double transparency) +{ + SDL_BlendMode oldMode; + + 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); + 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_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); +} + +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)); +} + +void Canvas::drawBorder(const Rect & target, const ColorRGBA & color, int width) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width); +} + +void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + 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)); + CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.bottomLeft(), CSDL_Ext::toSDL(color)); + CSDL_Ext::drawLineDashed(surface, realTarget.topRight(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); +} + +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ) +{ + 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); + } +} + +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ) +{ + 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); + } +} + +void Canvas::drawColor(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + 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(); + + CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); +} + +SDL_Surface * Canvas::getInternalSurface() +{ + return surface; +} + +Rect Canvas::getRenderArea() const +{ + return renderArea; +} diff --git a/client/render/Canvas.h b/client/render/Canvas.h index b093d3123..8a4abcf67 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -1,102 +1,107 @@ -/* - * Canvas.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/TextAlignment.h" -#include "../../lib/Rect.h" -#include "../../lib/Color.h" - -struct SDL_Color; -struct SDL_Surface; -class IImage; -enum EFonts : int; - -/// Class that represents surface for drawing on -class Canvas -{ - /// Target surface - SDL_Surface * surface; - - /// Current rendering area, all rendering operations will be moved into selected area - Rect renderArea; - - /// constructs canvas using existing surface. Caller maintains ownership on the surface - explicit Canvas(SDL_Surface * surface); - - /// copy contructor - Canvas(const Canvas & other); - -public: - Canvas & operator = (const Canvas & other) = delete; - Canvas & operator = (Canvas && other) = delete; - - /// move contructor - 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); - - /// 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); - - ~Canvas(); - - /// if set to true, drawing this canvas onto another canvas will use alpha channel information - void applyTransparency(bool on); - - /// applies grayscale filter onto current image - void applyGrayscale(); - - /// renders image onto this canvas at specified position - void draw(const std::shared_ptr& image, const Point & pos); - - /// renders section of image bounded by sourceRect at specified position - void draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect); - - /// renders another canvas onto this canvas - void draw(const Canvas &image, const Point & pos); - - /// renders another canvas onto this canvas with transparency - void drawTransparent(const Canvas & image, const Point & pos, double transparency); - - /// renders another canvas onto this canvas with scaling - void drawScaled(const Canvas &image, const Point & pos, const Point & targetSize); - - /// renders single pixels with specified color - void drawPoint(const Point & dest, const ColorRGBA & color); - - /// 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 SDL_Color & color, int width = 1); - - /// renders rectangular, dashed border in specified location - void drawBorderDashed(const Rect & target, const ColorRGBA & color); - - /// renders single line of text with specified parameters - void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text ); - - /// renders multiple lines of text with specified parameters - void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector & text ); - - /// fills selected area with solid color, ignoring any transparency - void drawColor(const Rect & target, const SDL_Color & color); - - /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. - SDL_Surface * getInternalSurface(); -}; +/* + * Canvas.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/TextAlignment.h" +#include "../../lib/Rect.h" +#include "../../lib/Color.h" + +struct SDL_Surface; +class IImage; +enum EFonts : int; + +/// Class that represents surface for drawing on +class Canvas +{ + /// Target surface + SDL_Surface * surface; + + /// Current rendering area, all rendering operations will be moved into selected area + Rect renderArea; + + /// constructs canvas using existing surface. Caller maintains ownership on the surface + explicit Canvas(SDL_Surface * surface); + + /// copy contructor + Canvas(const Canvas & other); + +public: + Canvas & operator = (const Canvas & other) = delete; + Canvas & operator = (Canvas && other) = delete; + + /// move contructor + 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); + + /// 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); + + ~Canvas(); + + /// if set to true, drawing this canvas onto another canvas will use alpha channel information + void applyTransparency(bool on); + + /// applies grayscale filter onto current image + void applyGrayscale(); + + /// renders image onto this canvas at specified position + void draw(const std::shared_ptr& image, const Point & pos); + + /// renders section of image bounded by sourceRect at specified position + void draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect); + + /// renders another canvas onto this canvas + void draw(const Canvas &image, const Point & pos); + + /// renders another canvas onto this canvas with transparency + void drawTransparent(const Canvas & image, const Point & pos, double transparency); + + /// renders another canvas onto this canvas with scaling + void drawScaled(const Canvas &image, const Point & pos, const Point & targetSize); + + /// renders single pixels with specified color + void drawPoint(const Point & dest, const ColorRGBA & color); + + /// 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); + + /// renders rectangular, dashed border in specified location + void drawBorderDashed(const Rect & target, const ColorRGBA & color); + + /// renders single line of text with specified parameters + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ); + + /// renders multiple lines of text with specified parameters + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); + + /// fills selected area with solid color + void drawColor(const Rect & target, const ColorRGBA & color); + + /// fills selected area with blended color + void drawColorBlended(const Rect & target, const ColorRGBA & color); + + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. + SDL_Surface * getInternalSurface(); + + /// get the render area + Rect getRenderArea() const; +}; diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index 68a2989d5..d6234116f 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -1,162 +1,161 @@ -/* - * Canvas.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 "ColorFilter.h" - -#include - -#include "../../lib/JsonNode.h" - -SDL_Color ColorFilter::shiftColor(const SDL_Color & in) const -{ - int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; - int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; - int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a; - int a_out = in.a * a; - - vstd::abetween(r_out, 0, 255); - vstd::abetween(g_out, 0, 255); - vstd::abetween(b_out, 0, 255); - vstd::abetween(a_out, 0, 255); - - return { - static_cast(r_out), - static_cast(g_out), - static_cast(b_out), - static_cast(a_out) - }; -} - -bool ColorFilter::operator != (const ColorFilter & other) const -{ - return !(this->operator==(other)); -} - -bool ColorFilter::operator == (const ColorFilter & other) const -{ - return - r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && - g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && - b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && - a == other.a; -} - -ColorFilter ColorFilter::genEmptyShifter( ) -{ - return genAlphaShifter( 1.f); -} - -ColorFilter ColorFilter::genAlphaShifter( float alpha ) -{ - return genMuxerShifter( - { 1.f, 0.f, 0.f, 0.f }, - { 0.f, 1.f, 0.f, 0.f }, - { 0.f, 0.f, 1.f, 0.f }, - alpha); -} - -ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ) -{ - return genMuxerShifter( - { maxR - minR, 0.f, 0.f, minR }, - { 0.f, maxG - minG, 0.f, minG }, - { 0.f, 0.f, maxB - minB, minB }, - 1.f); -} - -ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) -{ - return ColorFilter(r, g, b, a); -} - -ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power) -{ - auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer - { - return { - vstd::lerp(left.r, right.r, power), - vstd::lerp(left.g, right.g, power), - vstd::lerp(left.b, right.b, power), - vstd::lerp(left.a, right.a, power) - }; - }; - - return genMuxerShifter( - lerpMuxer(left.r, right.r), - lerpMuxer(left.g, right.g), - lerpMuxer(left.b, right.b), - vstd::lerp(left.a, right.a, power) - ); -} - -ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right) -{ - // matrix multiplication - ChannelMuxer r{ - left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b, - left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b, - left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b, - left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a, - }; - - ChannelMuxer g{ - left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b, - left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b, - left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b, - left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a, - }; - - ChannelMuxer b{ - left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b, - left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b, - left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b, - left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a, - }; - - float a = left.a * right.a; - return genMuxerShifter(r,g,b,a); -} - -ColorFilter ColorFilter::genFromJson(const JsonNode & entry) -{ - ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f }; - ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f }; - ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f }; - float a{ 1.0}; - - if (!entry["red"].isNull()) - { - r.r = entry["red"].Vector()[0].Float(); - r.g = entry["red"].Vector()[1].Float(); - r.b = entry["red"].Vector()[2].Float(); - r.a = entry["red"].Vector()[3].Float(); - } - - if (!entry["red"].isNull()) - { - g.r = entry["green"].Vector()[0].Float(); - g.g = entry["green"].Vector()[1].Float(); - g.b = entry["green"].Vector()[2].Float(); - g.a = entry["green"].Vector()[3].Float(); - } - - if (!entry["red"].isNull()) - { - b.r = entry["blue"].Vector()[0].Float(); - b.g = entry["blue"].Vector()[1].Float(); - b.b = entry["blue"].Vector()[2].Float(); - b.a = entry["blue"].Vector()[3].Float(); - } - - if (!entry["alpha"].isNull()) - a = entry["alpha"].Float(); - - return genMuxerShifter(r,g,b,a); -} +/* + * Canvas.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 "ColorFilter.h" + +#include "../../lib/JsonNode.h" +#include "../../lib/Color.h" + +ColorRGBA ColorFilter::shiftColor(const ColorRGBA & in) const +{ + int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; + int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; + int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a; + int a_out = in.a * a; + + vstd::abetween(r_out, 0, 255); + vstd::abetween(g_out, 0, 255); + vstd::abetween(b_out, 0, 255); + vstd::abetween(a_out, 0, 255); + + return { + static_cast(r_out), + static_cast(g_out), + static_cast(b_out), + static_cast(a_out) + }; +} + +bool ColorFilter::operator != (const ColorFilter & other) const +{ + return !(this->operator==(other)); +} + +bool ColorFilter::operator == (const ColorFilter & other) const +{ + return + r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && + g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && + b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && + a == other.a; +} + +ColorFilter ColorFilter::genEmptyShifter( ) +{ + return genAlphaShifter( 1.f); +} + +ColorFilter ColorFilter::genAlphaShifter( float alpha ) +{ + return genMuxerShifter( + { 1.f, 0.f, 0.f, 0.f }, + { 0.f, 1.f, 0.f, 0.f }, + { 0.f, 0.f, 1.f, 0.f }, + alpha); +} + +ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ) +{ + return genMuxerShifter( + { maxR - minR, 0.f, 0.f, minR }, + { 0.f, maxG - minG, 0.f, minG }, + { 0.f, 0.f, maxB - minB, minB }, + 1.f); +} + +ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) +{ + return ColorFilter(r, g, b, a); +} + +ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power) +{ + auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer + { + return { + vstd::lerp(left.r, right.r, power), + vstd::lerp(left.g, right.g, power), + vstd::lerp(left.b, right.b, power), + vstd::lerp(left.a, right.a, power) + }; + }; + + return genMuxerShifter( + lerpMuxer(left.r, right.r), + lerpMuxer(left.g, right.g), + lerpMuxer(left.b, right.b), + vstd::lerp(left.a, right.a, power) + ); +} + +ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right) +{ + // matrix multiplication + ChannelMuxer r{ + left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b, + left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b, + left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b, + left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a, + }; + + ChannelMuxer g{ + left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b, + left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b, + left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b, + left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a, + }; + + ChannelMuxer b{ + left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b, + left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b, + left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b, + left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a, + }; + + float a = left.a * right.a; + return genMuxerShifter(r,g,b,a); +} + +ColorFilter ColorFilter::genFromJson(const JsonNode & entry) +{ + ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f }; + ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f }; + ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f }; + float a{ 1.0}; + + if (!entry["red"].isNull()) + { + r.r = entry["red"].Vector()[0].Float(); + r.g = entry["red"].Vector()[1].Float(); + r.b = entry["red"].Vector()[2].Float(); + r.a = entry["red"].Vector()[3].Float(); + } + + if (!entry["green"].isNull()) + { + g.r = entry["green"].Vector()[0].Float(); + g.g = entry["green"].Vector()[1].Float(); + g.b = entry["green"].Vector()[2].Float(); + g.a = entry["green"].Vector()[3].Float(); + } + + if (!entry["blue"].isNull()) + { + b.r = entry["blue"].Vector()[0].Float(); + b.g = entry["blue"].Vector()[1].Float(); + b.b = entry["blue"].Vector()[2].Float(); + b.a = entry["blue"].Vector()[3].Float(); + } + + if (!entry["alpha"].isNull()) + a = entry["alpha"].Float(); + + return genMuxerShifter(r,g,b,a); +} diff --git a/client/render/ColorFilter.h b/client/render/ColorFilter.h index bfca82695..5df63ec67 100644 --- a/client/render/ColorFilter.h +++ b/client/render/ColorFilter.h @@ -1,66 +1,65 @@ -/* - * ColorFilter.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 - -struct SDL_Color; - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -/// Base class for applying palette transformation on images -class ColorFilter -{ - struct ChannelMuxer { - float r, g, b, a; - }; - - ChannelMuxer r; - ChannelMuxer g; - ChannelMuxer b; - float a; - - ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a): - r(r), g(g), b(b), a(a) - {} -public: - SDL_Color shiftColor(const SDL_Color & in) const; - - bool operator == (const ColorFilter & other) const; - bool operator != (const ColorFilter & other) const; - - /// Generates empty object that has no effect on image - static ColorFilter genEmptyShifter(); - - /// Generates object that changes alpha (transparency) of the image - static ColorFilter genAlphaShifter( float alpha ); - - /// Generates object that transforms each channel independently - static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); - - /// Generates object that performs arbitrary mixing between any channels - static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); - - /// Combines 2 mixers into a single object - static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right); - - /// Scales down strength of a shifter to a specified factor - static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power); - - /// Generates object using supplied Json config - static ColorFilter genFromJson(const JsonNode & entry); -}; - -struct ColorMuxerEffect -{ - std::vector filters; - std::vector timePoints; -}; +/* + * ColorFilter.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; +class ColorRGBA; +VCMI_LIB_NAMESPACE_END + +/// Base class for applying palette transformation on images +class ColorFilter +{ + struct ChannelMuxer { + float r, g, b, a; + }; + + ChannelMuxer r; + ChannelMuxer g; + ChannelMuxer b; + float a; + + ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a): + r(r), g(g), b(b), a(a) + {} +public: + ColorRGBA shiftColor(const ColorRGBA & in) const; + + bool operator == (const ColorFilter & other) const; + bool operator != (const ColorFilter & other) const; + + /// Generates empty object that has no effect on image + static ColorFilter genEmptyShifter(); + + /// Generates object that changes alpha (transparency) of the image + static ColorFilter genAlphaShifter( float alpha ); + + /// Generates object that transforms each channel independently + static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); + + /// Generates object that performs arbitrary mixing between any channels + static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); + + /// Combines 2 mixers into a single object + static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right); + + /// Scales down strength of a shifter to a specified factor + static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power); + + /// Generates object using supplied Json config + static ColorFilter genFromJson(const JsonNode & entry); +}; + +struct ColorMuxerEffect +{ + std::vector filters; + std::vector timePoints; +}; diff --git a/client/render/Colors.cpp b/client/render/Colors.cpp index 57b7f2c5e..0c239e879 100644 --- a/client/render/Colors.cpp +++ b/client/render/Colors.cpp @@ -10,18 +10,43 @@ #include "StdInc.h" #include "Colors.h" +#include "../../lib/JsonNode.h" -#include +const ColorRGBA Colors::YELLOW = { 229, 215, 123, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::WHITE = { 255, 243, 222, 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 }; +const ColorRGBA Colors::ORANGE = { 232, 184, 32, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::BRIGHT_YELLOW = { 242, 226, 110, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::DEFAULT_KEY_COLOR = {0, 255, 255, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::RED = {255, 0, 0, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::PURPLE = {255, 75, 125, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::BLACK = {0, 0, 0, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::TRANSPARENCY = {0, 0, 0, ColorRGBA::ALPHA_TRANSPARENT}; -const SDL_Color Colors::YELLOW = { 229, 215, 123, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::WHITE = { 255, 243, 222, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::METALLIC_GOLD = { 173, 142, 66, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::GREEN = { 0, 255, 0, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::CYAN = { 0, 255, 255, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::ORANGE = { 232, 184, 32, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::BRIGHT_YELLOW = { 242, 226, 110, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::DEFAULT_KEY_COLOR = {0, 255, 255, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::RED = {255, 0, 0, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::PURPLE = {255, 75, 125, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::BLACK = {0, 0, 0, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::TRANSPARENCY = {0, 0, 0, SDL_ALPHA_TRANSPARENT}; +std::optional Colors::parseColor(std::string text) +{ + std::smatch match; + std::regex expr("^#([0-9a-fA-F]{6})$"); + ui8 rgb[3] = {0, 0, 0}; + if(std::regex_search(text, match, expr)) + { + std::string tmp = boost::algorithm::unhex(match[1].str()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + + static const JsonNode config(JsonPath::builtin("CONFIG/textColors")); + auto colors = config["colors"].Struct(); + for(auto & color : colors) { + if(boost::algorithm::to_lower_copy(color.first) == boost::algorithm::to_lower_copy(text)) + { + std::string tmp = boost::algorithm::unhex(color.second.String()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + } + + return std::nullopt; +} \ No newline at end of file diff --git a/client/render/Colors.h b/client/render/Colors.h index 6f80f7352..7d890bc7e 100644 --- a/client/render/Colors.h +++ b/client/render/Colors.h @@ -1,52 +1,55 @@ -/* - * ICursor.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 - -struct SDL_Color; - -/** - * The colors class defines color constants of type SDL_Color. - */ -class Colors -{ -public: - /** the h3 yellow color, typically used for headlines */ - static const SDL_Color YELLOW; - - /** the standard h3 white color */ - static const SDL_Color WHITE; - - /** the metallic gold color used mostly as a border around buttons */ - static const SDL_Color METALLIC_GOLD; - - /** green color used for in-game console */ - static const SDL_Color GREEN; - - /** the h3 orange color, used for blocked buttons */ - static const SDL_Color ORANGE; - - /** the h3 bright yellow color, used for selection border */ - static const SDL_Color BRIGHT_YELLOW; - - /** default key color for all 8 & 24 bit graphics */ - static const SDL_Color DEFAULT_KEY_COLOR; - - /// Selected creature card - static const SDL_Color RED; - - /// Minimap border - static const SDL_Color PURPLE; - - static const SDL_Color CYAN; - - static const SDL_Color BLACK; - - static const SDL_Color TRANSPARENCY; -}; +/* + * ICursor.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/Color.h" + +/** + * The colors class defines color constants of type ColorRGBA. + */ +class Colors +{ +public: + /** the h3 yellow color, typically used for headlines */ + static const ColorRGBA YELLOW; + + /** the standard h3 white color */ + static const ColorRGBA WHITE; + + /** the metallic gold color used mostly as a border around buttons */ + static const ColorRGBA METALLIC_GOLD; + + /** green color used for in-game console */ + static const ColorRGBA GREEN; + + /** the h3 orange color, used for blocked buttons */ + static const ColorRGBA ORANGE; + + /** the h3 bright yellow color, used for selection border */ + static const ColorRGBA BRIGHT_YELLOW; + + /** default key color for all 8 & 24 bit graphics */ + static const ColorRGBA DEFAULT_KEY_COLOR; + + /// Selected creature card + static const ColorRGBA RED; + + /// Minimap border + static const ColorRGBA PURPLE; + + static const ColorRGBA CYAN; + + static const ColorRGBA BLACK; + + static const ColorRGBA TRANSPARENCY; + + /// parse color + static std::optional parseColor(std::string text); +}; diff --git a/client/render/EFont.h b/client/render/EFont.h new file mode 100644 index 000000000..7f5dfee41 --- /dev/null +++ b/client/render/EFont.h @@ -0,0 +1,15 @@ +/* + * EFonts.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 + +enum EFonts : int +{ + 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 6212b357b..603972feb 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -1,302 +1,294 @@ -/* - * Graphics.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 "Graphics.h" - -#include -#include -#include -#include -#include -#include -#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 "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CBinaryReader.h" -#include "../lib/CModHandler.h" -#include "CGameInfo.h" -#include "../lib/VCMI_Lib.h" -#include "../CCallback.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/JsonNode.h" -#include "../lib/vcmi_endian.h" -#include "../lib/CStopWatch.h" -#include "../lib/CHeroHandler.h" - -#include - -Graphics * graphics = nullptr; - -void Graphics::loadPaletteAndColors() -{ - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); - std::string pals((char*)textFile.first.get(), textFile.second); - - playerColorPalette = new SDL_Color[256]; - neutralColor = new SDL_Color; - playerColors = new SDL_Color[PlayerColor::PLAYER_LIMIT_I]; - int startPoint = 24; //beginning byte; used to read - for(int i=0; i<256; ++i) - { - SDL_Color col; - col.r = pals[startPoint++]; - col.g = pals[startPoint++]; - col.b = pals[startPoint++]; - col.a = SDL_ALPHA_OPAQUE; - startPoint++; - playerColorPalette[i] = col; - } - - neutralColorPalette = new SDL_Color[32]; - - auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); - CBinaryReader reader(stream.get()); - - for(int i=0; i<32; ++i) - { - neutralColorPalette[i].r = reader.readUInt8(); - neutralColorPalette[i].g = reader.readUInt8(); - neutralColorPalette[i].b = reader.readUInt8(); - reader.readUInt8(); // this is "flags" entry, not alpha - neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; - } - //colors initialization - SDL_Color colors[] = { - {0xff,0, 0, SDL_ALPHA_OPAQUE}, - {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, - {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, - {0x42,0x94,0x29,SDL_ALPHA_OPAQUE}, - - {0xff,0x84,0, SDL_ALPHA_OPAQUE}, - {0x8c,0x29,0xa5,SDL_ALPHA_OPAQUE}, - {0x09,0x9c,0xa5,SDL_ALPHA_OPAQUE}, - {0xc6,0x7b,0x8c,SDL_ALPHA_OPAQUE}}; - - for(int i=0;i<8;i++) - { - playerColors[i] = colors[i]; - } - //gray - neutralColor->r = 0x84; - neutralColor->g = 0x84; - neutralColor->b = 0x84; - neutralColor->a = SDL_ALPHA_OPAQUE; -} - -void Graphics::initializeBattleGraphics() -{ - auto allConfigs = VLC->modh->getActiveMods(); - allConfigs.insert(allConfigs.begin(), CModHandler::scopeBuiltin()); - for(auto & mod : allConfigs) - { - if(!CResourceHandler::get(mod)->existsResource(ResourceID("config/battles_graphics.json"))) - continue; - - const JsonNode config(mod, ResourceID("config/battles_graphics.json")); - - //initialization of AC->def name mapping - if(!config["ac_mapping"].isNull()) - for(const JsonNode &ac : config["ac_mapping"].Vector()) - { - int ACid = static_cast(ac["id"].Float()); - std::vector< std::string > toAdd; - - for(const JsonNode &defname : ac["defnames"].Vector()) - { - toAdd.push_back(defname.String()); - } - - battleACToDef[ACid] = toAdd; - } - } -} -Graphics::Graphics() -{ - loadFonts(); - loadPaletteAndColors(); - initializeBattleGraphics(); - loadErmuToPicture(); - initializeImageLists(); - - //(!) do not load any CAnimation here -} - -Graphics::~Graphics() -{ - delete[] playerColors; - delete neutralColor; - delete[] playerColorPalette; - delete[] neutralColorPalette; -} - -void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) -{ - if(sur->format->palette) - { - SDL_Color * palette = nullptr; - if(player < PlayerColor::PLAYER_LIMIT) - { - palette = playerColorPalette + 32*player.getNum(); - } - else if(player == PlayerColor::NEUTRAL) - { - palette = neutralColorPalette; - } - else - { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); - 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 - } - 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!"); - } -} - -void Graphics::loadFonts() -{ - const JsonNode config(ResourceID("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); - } -} - -void Graphics::loadErmuToPicture() -{ - //loading ERMU to picture - const JsonNode config(ResourceID("config/ERMU_to_picture.json")); - int etp_idx = 0; - for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { - int idx = 0; - for(const JsonNode &n : etp.Vector()) { - ERMUtoPicture[idx][etp_idx] = n.String(); - idx ++; - } - assert (idx == std::size(ERMUtoPicture)); - - etp_idx ++; - } - 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 std::string & path) -{ - ResourceID animationPath(path, EResType::ANIMATION); - - if (cachedAnimations.count(animationPath.getName()) != 0) - return cachedAnimations.at(animationPath.getName()); - - auto newAnimation = std::make_shared(animationPath.getName()); - - newAnimation->preload(); - cachedAnimations[animationPath.getName()] = newAnimation; - return newAnimation; -} +/* + * Graphics.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 "Graphics.h" + +#include +#include +#include +#include +#include +#include +#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/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/JsonNode.h" +#include "../lib/vcmi_endian.h" +#include "../lib/CStopWatch.h" +#include "../lib/CHeroHandler.h" + +#include + +Graphics * graphics = nullptr; + +void Graphics::loadPaletteAndColors() +{ + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); + std::string pals((char*)textFile.first.get(), textFile.second); + + int startPoint = 24; //beginning byte; used to read + for(int i=0; i<8; ++i) + { + for(int j=0; j<32; ++j) + { + ColorRGBA col; + col.r = pals[startPoint++]; + col.g = pals[startPoint++]; + col.b = pals[startPoint++]; + col.a = SDL_ALPHA_OPAQUE; + startPoint++; + playerColorPalette[i][j] = col; + } + } + + auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); + CBinaryReader reader(stream.get()); + + for(int i=0; i<32; ++i) + { + neutralColorPalette[i].r = reader.readUInt8(); + neutralColorPalette[i].g = reader.readUInt8(); + neutralColorPalette[i].b = reader.readUInt8(); + reader.readUInt8(); // this is "flags" entry, not alpha + neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; + } + //colors initialization + ColorRGBA colors[] = { + {0xff,0, 0, SDL_ALPHA_OPAQUE}, + {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, + {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, + {0x42,0x94,0x29,SDL_ALPHA_OPAQUE}, + + {0xff,0x84,0, SDL_ALPHA_OPAQUE}, + {0x8c,0x29,0xa5,SDL_ALPHA_OPAQUE}, + {0x09,0x9c,0xa5,SDL_ALPHA_OPAQUE}, + {0xc6,0x7b,0x8c,SDL_ALPHA_OPAQUE}}; + + for(int i=0;i<8;i++) + { + playerColors[i] = colors[i]; + } + //gray + neutralColor.r = 0x84; + neutralColor.g = 0x84; + neutralColor.b = 0x84; + neutralColor.a = SDL_ALPHA_OPAQUE; +} + +void Graphics::initializeBattleGraphics() +{ + auto allConfigs = VLC->modh->getActiveMods(); + allConfigs.insert(allConfigs.begin(), ModScope::scopeBuiltin()); + for(auto & mod : allConfigs) + { + if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) + continue; + + const JsonNode config(mod, JsonPath::builtin("config/battles_graphics.json")); + + //initialization of AC->def name mapping + if(!config["ac_mapping"].isNull()) + for(const JsonNode &ac : config["ac_mapping"].Vector()) + { + int ACid = static_cast(ac["id"].Float()); + std::vector< std::string > toAdd; + + for(const JsonNode &defname : ac["defnames"].Vector()) + { + toAdd.push_back(defname.String()); + } + + battleACToDef[ACid] = toAdd; + } + } +} +Graphics::Graphics() +{ + loadFonts(); + loadPaletteAndColors(); + initializeBattleGraphics(); + loadErmuToPicture(); + initializeImageLists(); + + //(!) do not load any CAnimation here +} + +void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) +{ + if(sur->format->palette) + { + 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 + } + 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!"); + } +} + +void Graphics::loadFonts() +{ + 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); + } +} + +void Graphics::loadErmuToPicture() +{ + //loading ERMU to picture + const JsonNode config(JsonPath::builtin("config/ERMU_to_picture.json")); + int etp_idx = 0; + for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { + int idx = 0; + for(const JsonNode &n : etp.Vector()) { + ERMUtoPicture[idx][etp_idx] = n.String(); + idx ++; + } + assert (idx == std::size(ERMUtoPicture)); + + etp_idx ++; + } + 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 380c539fb..4170f8e1a 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -1,79 +1,77 @@ -/* - * Graphics.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 "IFont.h" -#include "../lib/GameConstants.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; -struct SDL_Color; -class CAnimation; - -enum EFonts : int -{ - FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD -}; - -/// 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 std::string & path); - - //Fonts - static const int FONTS_NUMBER = 9; - std::array< std::shared_ptr, FONTS_NUMBER> fonts; - - //various graphics - SDL_Color * playerColors; //array [8] - SDL_Color * neutralColor; - SDL_Color * playerColorPalette; //palette to make interface colors good - array of size [256] - SDL_Color * neutralColorPalette; - - 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 - std::map< int, std::vector < std::string > > battleACToDef; //maps AC format to vector of appropriate def names - - //functions - Graphics(); - ~Graphics(); - - void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player -}; - -extern Graphics * graphics; +/* + * Graphics.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/GameConstants.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; + +/// 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 + std::array playerColors; + std::array playerColorPalette; //palette to make interface colors good - array of size [256] + + 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 + std::map< int, std::vector < std::string > > battleACToDef; //maps AC format to vector of appropriate def names + + //functions + Graphics(); + + void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player +}; + +extern Graphics * graphics; diff --git a/client/render/ICursor.h b/client/render/ICursor.h index 6d37b3e37..af9e2f1c6 100644 --- a/client/render/ICursor.h +++ b/client/render/ICursor.h @@ -1,28 +1,28 @@ -/* - * ICursor.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 Point; -VCMI_LIB_NAMESPACE_END - -class IImage; - -class ICursor -{ -public: - virtual ~ICursor() = default; - - virtual void setImage(std::shared_ptr image, const Point & pivotOffset) = 0; - virtual void setCursorPosition( const Point & newPos ) = 0; - virtual void render() = 0; - virtual void setVisible( bool on) = 0; -}; - +/* + * ICursor.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 Point; +VCMI_LIB_NAMESPACE_END + +class IImage; + +class ICursor +{ +public: + virtual ~ICursor() = default; + + virtual void setImage(std::shared_ptr image, const Point & pivotOffset) = 0; + virtual void setCursorPosition( const Point & newPos ) = 0; + virtual void render() = 0; + virtual void setVisible( bool on) = 0; +}; + diff --git a/client/render/IFont.cpp b/client/render/IFont.cpp index 2151b542a..5cc60928b 100644 --- a/client/render/IFont.cpp +++ b/client/render/IFont.cpp @@ -25,24 +25,24 @@ size_t IFont::getStringWidth(const std::string & data) const return width; } -void IFont::renderTextLeft(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLeft(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { renderText(surface, data, color, pos); } -void IFont::renderTextRight(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextRight(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { Point size((int)getStringWidth(data), (int)getLineHeight()); renderText(surface, data, color, pos - size); } -void IFont::renderTextCenter(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextCenter(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { Point size((int)getStringWidth(data), (int)getLineHeight()); renderText(surface, data, color, pos - size / 2); } -void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; @@ -53,7 +53,7 @@ void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; currPos.y -= (int)data.size() * (int)getLineHeight(); @@ -65,7 +65,7 @@ void IFont::renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +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; diff --git a/client/render/IFont.h b/client/render/IFont.h index 4a1b92863..80575de93 100644 --- a/client/render/IFont.h +++ b/client/render/IFont.h @@ -11,16 +11,16 @@ VCMI_LIB_NAMESPACE_BEGIN class Point; +class ColorRGBA; VCMI_LIB_NAMESPACE_END struct SDL_Surface; -struct SDL_Color; class IFont { protected: /// Internal function to render font, see renderTextLeft - virtual void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const = 0; + virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0; public: virtual ~IFont() @@ -40,17 +40,17 @@ public: * @param pos - position of rendered font */ /// pos = topleft corner of the text - void renderTextLeft(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextLeft(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = center of the text - void renderTextRight(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextRight(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = bottomright corner of the text - void renderTextCenter(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextCenter(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = topleft corner of the text - void renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; /// pos = center of the text - void renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; /// pos = bottomright corner of the text - void renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; }; diff --git a/client/render/IImage.h b/client/render/IImage.h index c7fde1706..adf5126cd 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -1,94 +1,88 @@ -/* - * IImage.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; -class Rect; -class Point; - -VCMI_LIB_NAMESPACE_END - -struct SDL_Surface; -struct SDL_Color; -class ColorFilter; - -/// Defines which blit method will be selected when image is used for rendering -enum class EImageBlitMode : uint8_t -{ - /// 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 - COLORKEY, - - /// Image might have full alpha transparency range, e.g. shadows - ALPHA -}; - -/* - * Base class for images, can be used for non-animation pictures as well - */ -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 std::shared_ptr scaleFast(const Point & size) const = 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; - - //test transparency of specific pixel - virtual bool isTransparent(const Point & coords) const = 0; - - virtual Point dimensions() const = 0; - int width() const; - int height() const; - - //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 horizontalFlip() = 0; - virtual void verticalFlip() = 0; - virtual void doubleFlip() = 0; - - IImage(); - virtual ~IImage(); - - /// loads image from specified file. Returns 0-sized images on failure - static std::shared_ptr createFromFile( const std::string & path ); - static std::shared_ptr createFromFile( const std::string & path, EImageBlitMode mode ); - - /// temporary compatibility method. Creates IImage from existing SDL_Surface - /// Surface will be shared, called must still free it with SDL_FreeSurface - static std::shared_ptr createFromSurface( SDL_Surface * source ); -}; - +/* + * IImage.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" + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; +class Rect; +class Point; +class ColorRGBA; + +VCMI_LIB_NAMESPACE_END + +struct SDL_Surface; +class ColorFilter; + +/// Defines which blit method will be selected when image is used for rendering +enum class EImageBlitMode +{ + /// 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 + COLORKEY, + + /// Image might have full alpha transparency range, e.g. shadows + ALPHA +}; + +/* + * Base class for images, can be used for non-animation pictures as well + */ +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 std::shared_ptr scaleFast(const Point & size) const = 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; + + //test transparency of specific pixel + virtual bool isTransparent(const Point & coords) const = 0; + + virtual Point dimensions() const = 0; + int width() const; + int height() const; + + //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 horizontalFlip() = 0; + virtual void verticalFlip() = 0; + virtual void doubleFlip() = 0; + + IImage() = default; + virtual ~IImage() = default; +}; + diff --git a/client/render/IImageLoader.h b/client/render/IImageLoader.h index 966a5be26..7cd950091 100644 --- a/client/render/IImageLoader.h +++ b/client/render/IImageLoader.h @@ -1,34 +1,34 @@ -/* - * IImageLoader.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 Point; -VCMI_LIB_NAMESPACE_END - -class SDLImage; - -struct SDL_Color; - -class IImageLoader -{ -public: - //load size raw pixels from data - virtual void load(size_t size, const ui8 * data) = 0; - //set size pixels to color - virtual void load(size_t size, ui8 color=0) = 0; - - virtual void endLine() = 0; - //init image with these sizes and palette - virtual void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) = 0; - - virtual ~IImageLoader() = default; -}; +/* + * IImageLoader.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 Point; +VCMI_LIB_NAMESPACE_END + +class SDLImage; + +struct SDL_Color; + +class IImageLoader +{ +public: + //load size raw pixels from data + virtual void load(size_t size, const ui8 * data) = 0; + //set size pixels to color + virtual void load(size_t size, ui8 color=0) = 0; + + virtual void endLine() = 0; + //init image with these sizes and palette + virtual void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) = 0; + + virtual ~IImageLoader() = default; +}; diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h new file mode 100644 index 000000000..880295170 --- /dev/null +++ b/client/render/IRenderHandler.h @@ -0,0 +1,38 @@ +/* + * IRenderHandler.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" + +struct SDL_Surface; + +class IImage; +class CAnimation; +enum class EImageBlitMode; + +class IRenderHandler : public boost::noncopyable +{ +public: + virtual ~IRenderHandler() = default; + + /// Loads image using given path + virtual std::shared_ptr loadImage(const ImagePath & path) = 0; + virtual std::shared_ptr loadImage(const ImagePath & path, 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; + + /// Creates empty CAnimation + virtual std::shared_ptr createAnimation() = 0; +}; diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index 51cc3dda2..49e5cd95e 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -40,4 +40,7 @@ public: /// Dimensions of render output virtual Point getRenderResolution() const = 0; + + /// Window has focus + virtual bool hasFocus() = 0; }; diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index fd4be26c1..36a29d174 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -14,17 +14,17 @@ #include "../CGameInfo.h" #include "../render/Colors.h" -#include "../../lib/CModHandler.h" #include "../../lib/Languages.h" #include "../../lib/Rect.h" #include "../../lib/TextOperations.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/vcmi_endian.h" #include "../../lib/VCMI_Lib.h" #include -void CBitmapFont::loadModFont(const std::string & modName, const ResourceID & resource) +void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & resource) { if (!CResourceHandler::get(modName)->existsResource(resource)) { @@ -72,7 +72,7 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourceID & re CBitmapFont::CBitmapFont(const std::string & filename): maxHeight(0) { - ResourceID resource("data/" + filename, EResType::BMP_FONT); + ResourcePath resource("data/" + filename, EResType::BMP_FONT); loadModFont("core", resource); @@ -118,7 +118,7 @@ bool CBitmapFont::canRepresentString(const std::string & data) const return true; } -void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) 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); @@ -168,7 +168,7 @@ void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & char posX += character.rightOffset; } -void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { if (data.empty()) return; diff --git a/client/renderSDL/CBitmapFont.h b/client/renderSDL/CBitmapFont.h index 083841160..b5fa2b4f4 100644 --- a/client/renderSDL/CBitmapFont.h +++ b/client/renderSDL/CBitmapFont.h @@ -12,7 +12,7 @@ #include "../render/IFont.h" VCMI_LIB_NAMESPACE_BEGIN -class ResourceID; +class ResourcePath; VCMI_LIB_NAMESPACE_END class CBitmapFont : public IFont @@ -31,10 +31,10 @@ class CBitmapFont : public IFont std::unordered_map chars; uint32_t maxHeight; - void loadModFont(const std::string & modName, const ResourceID & resource); + void loadModFont(const std::string & modName, const ResourcePath & resource); - void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) const; - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + 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); diff --git a/client/renderSDL/CBitmapHanFont.cpp b/client/renderSDL/CBitmapHanFont.cpp index 031a7c301..cb527d054 100644 --- a/client/renderSDL/CBitmapHanFont.cpp +++ b/client/renderSDL/CBitmapHanFont.cpp @@ -35,7 +35,7 @@ size_t CBitmapHanFont::getCharacterIndex(ui8 first, ui8 second) const return (first - 0x81) * (12*16 - 2) + (second - 0x40); } -void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const +void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const ColorRGBA & color, int &posX, int &posY) const { //TODO: somewhat duplicated with CBitmapFont::renderCharacter(); Rect clipRect; @@ -76,7 +76,7 @@ void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, posX += (int)size + 1; } -void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +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; @@ -98,7 +98,7 @@ void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, CBitmapHanFont::CBitmapHanFont(const JsonNode &config): fallback(new CBitmapFont(config["fallback"].String())), - data(CResourceHandler::get()->load(ResourceID("data/" + config["name"].String(), EResType::OTHER))->readAll()), + 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 diff --git a/client/renderSDL/CBitmapHanFont.h b/client/renderSDL/CBitmapHanFont.h index 32b7e081f..707c360b2 100644 --- a/client/renderSDL/CBitmapHanFont.h +++ b/client/renderSDL/CBitmapHanFont.h @@ -30,8 +30,8 @@ class CBitmapHanFont : public IFont size_t getCharacterDataOffset(size_t index) const; size_t getCharacterIndex(ui8 first, ui8 second) const; - void renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const; - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + 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); diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index d13d0f5bc..b9d19aa12 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -24,7 +24,7 @@ std::pair, ui64> CTrueTypeFont::loadData(const JsonNode & config) { std::string filename = "Data/" + config["file"].String(); - return CResourceHandler::get()->load(ResourceID(filename, EResType::TTF_FONT))->readAll(); + return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll(); } TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) @@ -72,7 +72,7 @@ CTrueTypeFont::~CTrueTypeFont() = default; size_t CTrueTypeFont::getLineHeight() const { if (fallbackFont) - fallbackFont->getLineHeight(); + return fallbackFont->getLineHeight(); return TTF_FontHeight(font.get()); } @@ -98,7 +98,7 @@ size_t CTrueTypeFont::getStringWidth(const std::string & data) const return width; } -void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { if (fallbackFont && fallbackFont->canRepresentString(data)) { @@ -113,9 +113,9 @@ void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, { SDL_Surface * rendered; if (blended) - rendered = TTF_RenderUTF8_Blended(font.get(), data.c_str(), color); + rendered = TTF_RenderUTF8_Blended(font.get(), data.c_str(), CSDL_Ext::toSDL(color)); else - rendered = TTF_RenderUTF8_Solid(font.get(), data.c_str(), color); + rendered = TTF_RenderUTF8_Solid(font.get(), data.c_str(), CSDL_Ext::toSDL(color)); assert(rendered); diff --git a/client/renderSDL/CTrueTypeFont.h b/client/renderSDL/CTrueTypeFont.h index bd3fc4a53..804217a0b 100644 --- a/client/renderSDL/CTrueTypeFont.h +++ b/client/renderSDL/CTrueTypeFont.h @@ -32,7 +32,7 @@ class CTrueTypeFont : public IFont TTF_Font * loadFont(const JsonNode & config); int getFontStyle(const JsonNode & config); - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; public: CTrueTypeFont(const JsonNode & fontConfig); ~CTrueTypeFont(); diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index d68ed5f62..aed082adc 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -1,77 +1,77 @@ -/* - * CursorHardware.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 "CursorHardware.h" - -#include "../gui/CGuiHandler.h" -#include "../render/Colors.h" -#include "../render/IImage.h" -#include "SDL_Extensions.h" - -#include -#include - -CursorHardware::CursorHardware(): - cursor(nullptr) -{ - SDL_ShowCursor(SDL_DISABLE); -} - -CursorHardware::~CursorHardware() -{ - if(cursor) - SDL_FreeCursor(cursor); -} - -void CursorHardware::setVisible(bool on) -{ - GH.dispatchMainThread([on]() - { - if (on) - SDL_ShowCursor(SDL_ENABLE); - else - SDL_ShowCursor(SDL_DISABLE); - }); -} - -void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) -{ - auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y); - - CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY); - - image->draw(cursorSurface); - - auto oldCursor = cursor; - cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y); - - if (!cursor) - logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); - - SDL_FreeSurface(cursorSurface); - - GH.dispatchMainThread([this, oldCursor](){ - SDL_SetCursor(cursor); - - if (oldCursor) - SDL_FreeCursor(oldCursor); - }); -} - -void CursorHardware::setCursorPosition( const Point & newPos ) -{ - //no-op -} - -void CursorHardware::render() -{ - //no-op -} +/* + * CursorHardware.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 "CursorHardware.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Colors.h" +#include "../render/IImage.h" +#include "SDL_Extensions.h" + +#include +#include + +CursorHardware::CursorHardware(): + cursor(nullptr) +{ + SDL_ShowCursor(SDL_DISABLE); +} + +CursorHardware::~CursorHardware() +{ + if(cursor) + SDL_FreeCursor(cursor); +} + +void CursorHardware::setVisible(bool on) +{ + GH.dispatchMainThread([on]() + { + if (on) + SDL_ShowCursor(SDL_ENABLE); + else + SDL_ShowCursor(SDL_DISABLE); + }); +} + +void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) +{ + auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y); + + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); + + image->draw(cursorSurface); + + auto oldCursor = cursor; + cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y); + + if (!cursor) + logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); + + SDL_FreeSurface(cursorSurface); + + GH.dispatchMainThread([this, oldCursor](){ + SDL_SetCursor(cursor); + + if (oldCursor) + SDL_FreeCursor(oldCursor); + }); +} + +void CursorHardware::setCursorPosition( const Point & newPos ) +{ + //no-op +} + +void CursorHardware::render() +{ + //no-op +} diff --git a/client/renderSDL/CursorHardware.h b/client/renderSDL/CursorHardware.h index 8f0aa2f19..c4e311778 100644 --- a/client/renderSDL/CursorHardware.h +++ b/client/renderSDL/CursorHardware.h @@ -1,36 +1,36 @@ -/* - * CursorHardware.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 CAnimation; -class IImage; -struct SDL_Surface; -struct SDL_Texture; -struct SDL_Cursor; - -#include "../../lib/Point.h" -#include "../render/ICursor.h" - -class CursorHardware : public ICursor -{ - std::shared_ptr cursorImage; - - SDL_Cursor * cursor; - -public: - CursorHardware(); - ~CursorHardware(); - - void setImage(std::shared_ptr image, const Point & pivotOffset) override; - void setCursorPosition( const Point & newPos ) override; - void render() override; - void setVisible( bool on) override; -}; - +/* + * CursorHardware.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 CAnimation; +class IImage; +struct SDL_Surface; +struct SDL_Texture; +struct SDL_Cursor; + +#include "../../lib/Point.h" +#include "../render/ICursor.h" + +class CursorHardware : public ICursor +{ + std::shared_ptr cursorImage; + + SDL_Cursor * cursor; + +public: + CursorHardware(); + ~CursorHardware(); + + void setImage(std::shared_ptr image, const Point & pivotOffset) override; + void setCursorPosition( const Point & newPos ) override; + void render() override; + void setVisible( bool on) override; +}; + diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index 5b90709cf..78b9e1250 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -1,102 +1,102 @@ -/* - * CursorSoftware.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 "CursorSoftware.h" - -#include "../render/Colors.h" -#include "../render/IImage.h" -#include "../CMT.h" -#include "SDL_Extensions.h" - -#include -#include - -void CursorSoftware::render() -{ - //texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads - if (needUpdate) - updateTexture(); - - Point renderPos = pos - pivot; - - SDL_Rect destRect; - destRect.x = renderPos.x; - destRect.y = renderPos.y; - destRect.w = 40; - destRect.h = 40; - - SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect); -} - -void CursorSoftware::createTexture(const Point & dimensions) -{ - if(cursorTexture) - SDL_DestroyTexture(cursorTexture); - - if (cursorSurface) - SDL_FreeSurface(cursorSurface); - - cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); - cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); - - SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE); - SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND); -} - -void CursorSoftware::updateTexture() -{ - if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) - createTexture(cursorImage->dimensions()); - - CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY); - - cursorImage->draw(cursorSurface); - SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch); - needUpdate = false; -} - -void CursorSoftware::setImage(std::shared_ptr image, const Point & pivotOffset) -{ - assert(image != nullptr); - cursorImage = image; - pivot = pivotOffset; - needUpdate = true; -} - -void CursorSoftware::setCursorPosition( const Point & newPos ) -{ - pos = newPos; -} - -void CursorSoftware::setVisible(bool on) -{ - visible = on; -} - -CursorSoftware::CursorSoftware(): - cursorTexture(nullptr), - cursorSurface(nullptr), - needUpdate(false), - visible(false), - pivot(0,0) -{ - SDL_ShowCursor(SDL_DISABLE); -} - -CursorSoftware::~CursorSoftware() -{ - if(cursorTexture) - SDL_DestroyTexture(cursorTexture); - - if (cursorSurface) - SDL_FreeSurface(cursorSurface); -} - +/* + * CursorSoftware.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 "CursorSoftware.h" + +#include "../render/Colors.h" +#include "../render/IImage.h" +#include "../CMT.h" +#include "SDL_Extensions.h" + +#include +#include + +void CursorSoftware::render() +{ + //texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads + if (needUpdate) + updateTexture(); + + Point renderPos = pos - pivot; + + SDL_Rect destRect; + destRect.x = renderPos.x; + destRect.y = renderPos.y; + destRect.w = 40; + destRect.h = 40; + + SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect); +} + +void CursorSoftware::createTexture(const Point & dimensions) +{ + if(cursorTexture) + SDL_DestroyTexture(cursorTexture); + + if (cursorSurface) + SDL_FreeSurface(cursorSurface); + + cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); + cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + + SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE); + SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND); +} + +void CursorSoftware::updateTexture() +{ + if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) + createTexture(cursorImage->dimensions()); + + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); + + cursorImage->draw(cursorSurface); + SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch); + needUpdate = false; +} + +void CursorSoftware::setImage(std::shared_ptr image, const Point & pivotOffset) +{ + assert(image != nullptr); + cursorImage = image; + pivot = pivotOffset; + needUpdate = true; +} + +void CursorSoftware::setCursorPosition( const Point & newPos ) +{ + pos = newPos; +} + +void CursorSoftware::setVisible(bool on) +{ + visible = on; +} + +CursorSoftware::CursorSoftware(): + cursorTexture(nullptr), + cursorSurface(nullptr), + needUpdate(false), + visible(false), + pivot(0,0) +{ + SDL_ShowCursor(SDL_DISABLE); +} + +CursorSoftware::~CursorSoftware() +{ + if(cursorTexture) + SDL_DestroyTexture(cursorTexture); + + if (cursorSurface) + SDL_FreeSurface(cursorSurface); +} + diff --git a/client/renderSDL/CursorSoftware.h b/client/renderSDL/CursorSoftware.h index f33add2f6..44080e3e2 100644 --- a/client/renderSDL/CursorSoftware.h +++ b/client/renderSDL/CursorSoftware.h @@ -1,44 +1,44 @@ -/* - * CursorSoftware.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 CAnimation; -class IImage; -struct SDL_Surface; -struct SDL_Texture; -struct SDL_Cursor; - -#include "../../lib/Point.h" -#include "../render/ICursor.h" - -class CursorSoftware : public ICursor -{ - std::shared_ptr cursorImage; - - SDL_Texture * cursorTexture; - SDL_Surface * cursorSurface; - - Point pos; - Point pivot; - bool needUpdate; - bool visible; - - void createTexture(const Point & dimensions); - void updateTexture(); -public: - CursorSoftware(); - ~CursorSoftware(); - - void setImage(std::shared_ptr image, const Point & pivotOffset) override; - void setCursorPosition( const Point & newPos ) override; - void render() override; - void setVisible( bool on) override; -}; - +/* + * CursorSoftware.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 CAnimation; +class IImage; +struct SDL_Surface; +struct SDL_Texture; +struct SDL_Cursor; + +#include "../../lib/Point.h" +#include "../render/ICursor.h" + +class CursorSoftware : public ICursor +{ + std::shared_ptr cursorImage; + + SDL_Texture * cursorTexture; + SDL_Surface * cursorSurface; + + Point pos; + Point pivot; + bool needUpdate; + bool visible; + + void createTexture(const Point & dimensions); + void updateTexture(); +public: + CursorSoftware(); + ~CursorSoftware(); + + void setImage(std::shared_ptr image, const Point & pivotOffset) override; + void setCursorPosition( const Point & newPos ) override; + void render() override; + void setVisible( bool on) override; +}; + diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp new file mode 100644 index 000000000..87eb99091 --- /dev/null +++ b/client/renderSDL/RenderHandler.cpp @@ -0,0 +1,40 @@ +/* + * RenderHandler.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 "RenderHandler.h" + +#include "../render/CAnimation.h" +#include "SDLImage.h" + + +std::shared_ptr RenderHandler::loadImage(const ImagePath & path) +{ + return loadImage(path, EImageBlitMode::ALPHA); +} + +std::shared_ptr RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode) +{ + return std::make_shared(path, mode); +} + +std::shared_ptr RenderHandler::createImage(SDL_Surface * source) +{ + return std::make_shared(source, EImageBlitMode::ALPHA); +} + +std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path) +{ + return std::make_shared(path); +} + +std::shared_ptr RenderHandler::createAnimation() +{ + return std::make_shared(); +} diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h new file mode 100644 index 000000000..76e30ee9c --- /dev/null +++ b/client/renderSDL/RenderHandler.h @@ -0,0 +1,25 @@ +/* + * RenderHandler.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/IRenderHandler.h" + +class RenderHandler : public IRenderHandler +{ +public: + std::shared_ptr loadImage(const ImagePath & path) override; + std::shared_ptr loadImage(const ImagePath & 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; +}; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index cc2a0d682..e8930abd3 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -1,376 +1,356 @@ -/* - * SDLImage.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 "SDLImage.h" - -#include "SDLImageLoader.h" -#include "SDL_Extensions.h" - -#include "../render/ColorFilter.h" -#include "../render/CBitmapHandler.h" -#include "../render/CDefFile.h" -#include "../render/Graphics.h" - -#include "../../lib/JsonNode.h" - -#include - -class SDLImageLoader; - -std::shared_ptr IImage::createFromFile( const std::string & path ) -{ - return createFromFile(path, EImageBlitMode::ALPHA); -} - -std::shared_ptr IImage::createFromFile( const std::string & path, EImageBlitMode mode ) -{ - return std::shared_ptr(new SDLImage(path, mode)); -} - -std::shared_ptr IImage::createFromSurface( SDL_Surface * source ) -{ - return std::shared_ptr(new SDLImage(source, EImageBlitMode::ALPHA)); -} - -IImage::IImage() = default; -IImage::~IImage() = default; - -int IImage::width() const -{ - return dimensions().x; -} - -int IImage::height() const -{ - return dimensions().y; -} - -SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - SDLImageLoader loader(this); - data->loadFrame(frame, group, loader); - - savePalette(); - setBlitMode(EImageBlitMode::ALPHA); -} - -SDLImage::SDLImage(SDL_Surface * from, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - 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) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - std::string filename = conf["file"].String(); - - surf = BitmapHandler::loadBitmap(filename); - - 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(std::string filename, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - surf = BitmapHandler::loadBitmap(filename); - - if(surf == nullptr) - { - logGlobal->error("Error: failed to load image %s", filename); - return; - } - else - { - savePalette(); - setBlitMode(mode); - fullSize.x = surf->w; - fullSize.y = surf->h; - } -} - -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 -{ - if (!surf) - return; - - Rect sourceRect(0, 0, surf->w, surf->h); - - Point destShift(0, 0); - - if(src) - { - if(src->x < margins.x) - destShift.x += margins.x - src->x; - - if(src->y < margins.y) - destShift.y += margins.y - src->y; - - sourceRect = Rect(*src).intersect(Rect(margins.x, margins.y, surf->w, surf->h)); - - sourceRect -= margins; - } - else - destShift = margins; - - if(dest) - destShift += dest->topLeft(); - - uint8_t perSurfaceAlpha; - if (SDL_GetSurfaceAlphaMod(surf, &perSurfaceAlpha) != 0) - logGlobal->error("SDL_GetSurfaceAlphaMod faied! %s", SDL_GetError()); - - if(surf->format->BitsPerPixel == 8 && perSurfaceAlpha == SDL_ALPHA_OPAQUE && blitMode == EImageBlitMode::ALPHA) - { - CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift); - } - else - { - CSDL_Ext::blitSurface(surf, sourceRect, where, destShift); - } -} - -std::shared_ptr SDLImage::scaleFast(const Point & size) const -{ - float scaleX = float(size.x) / width(); - float scaleY = float(size.y) / height(); - - auto scaled = CSDL_Ext::scaleSurfaceFast(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]); - else if(scaled->format && scaled->format->Amask) - SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND);//just in case - else - CSDL_Ext::setDefaultColorKey(scaled);//just in case - - SDLImage * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); - - ret->fullSize.x = (int) round((float)fullSize.x * scaleX); - ret->fullSize.y = (int) round((float)fullSize.y * scaleY); - - ret->margins.x = (int) round((float)margins.x * scaleX); - ret->margins.y = (int) round((float)margins.y * scaleY); - - // erase our own reference - SDL_FreeSurface(scaled); - - return std::shared_ptr(ret); -} - -void SDLImage::exportBitmap(const boost::filesystem::path& path) const -{ - SDL_SaveBMP(surf, path.string().c_str()); -} - -void SDLImage::playerColored(PlayerColor player) -{ - graphics->blueToPlayersAdv(surf, player); -} - -void SDLImage::setAlpha(uint8_t value) -{ - CSDL_Ext::setAlpha (surf, value); - if (value != 255) - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); -} - -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 < PlayerColor::PLAYER_LIMIT || player==PlayerColor::NEUTRAL) - CSDL_Ext::setPlayerColor(surf, player); -} - -bool SDLImage::isTransparent(const Point & coords) const -{ - return CSDL_Ext::isTransparent(surf, coords.x, coords.y); -} - -Point SDLImage::dimensions() const -{ - return fullSize; -} - -void SDLImage::horizontalFlip() -{ - margins.y = fullSize.y - surf->h - margins.y; - - //todo: modify in-place - SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); - SDL_FreeSurface(surf); - surf = flipped; -} - -void SDLImage::verticalFlip() -{ - margins.x = fullSize.x - surf->w - margins.x; - - //todo: modify in-place - SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); - SDL_FreeSurface(surf); - surf = flipped; -} - -void SDLImage::doubleFlip() -{ - horizontalFlip(); - verticalFlip(); -} - -// Keep the original palette, in order to do color switching operation -void SDLImage::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); - - SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, DEFAULT_PALETTE_COLORS); -} - -void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) -{ - if(surf->format->palette) - { - std::vector shifterColors(colorsToMove); - - for(uint32_t i=0; icolors[firstColorID + i]; - } - CSDL_Ext::setColors(surf, shifterColors.data(), firstColorID, colorsToMove); - } -} - -void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) -{ - if(originalPalette == nullptr) - return; - - SDL_Palette* palette = surf->format->palette; - - // Note: here we skip first colors in the palette that are predefined in H3 images - for(int i = 0; i < palette->ncolors; i++) - { - if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - continue; - - palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]); - } -} - -void SDLImage::resetPalette() -{ - 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); -} - -void SDLImage::resetPalette( int colorID ) -{ - 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); -} - -void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) -{ - if(surf->format->palette) - { - 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] = specialPalette[i]; - } - } -} - -SDLImage::~SDLImage() -{ - SDL_FreeSurface(surf); - - if(originalPalette != nullptr) - { - SDL_FreePalette(originalPalette); - originalPalette = nullptr; - } -} - +/* + * SDLImage.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 "SDLImage.h" + +#include "SDLImageLoader.h" +#include "SDL_Extensions.h" + +#include "../render/ColorFilter.h" +#include "../render/CBitmapHandler.h" +#include "../render/CDefFile.h" +#include "../render/Graphics.h" + +#include "../../lib/JsonNode.h" + +#include + +class SDLImageLoader; + +int IImage::width() const +{ + return dimensions().x; +} + +int IImage::height() const +{ + return dimensions().y; +} + +SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + SDLImageLoader loader(this); + data->loadFrame(frame, group, loader); + + savePalette(); + setBlitMode(EImageBlitMode::ALPHA); +} + +SDLImage::SDLImage(SDL_Surface * from, EImageBlitMode mode) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + 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) + : 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) +{ + surf = BitmapHandler::loadBitmap(filename); + + if(surf == nullptr) + { + logGlobal->error("Error: failed to load image %s", filename.getOriginalName()); + return; + } + else + { + savePalette(); + setBlitMode(mode); + fullSize.x = surf->w; + fullSize.y = surf->h; + } +} + +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 +{ + if (!surf) + return; + + Rect sourceRect(0, 0, surf->w, surf->h); + + Point destShift(0, 0); + + if(src) + { + if(src->x < margins.x) + destShift.x += margins.x - src->x; + + if(src->y < margins.y) + destShift.y += margins.y - src->y; + + sourceRect = Rect(*src).intersect(Rect(margins.x, margins.y, surf->w, surf->h)); + + sourceRect -= margins; + } + else + destShift = margins; + + if(dest) + destShift += dest->topLeft(); + + uint8_t perSurfaceAlpha; + if (SDL_GetSurfaceAlphaMod(surf, &perSurfaceAlpha) != 0) + logGlobal->error("SDL_GetSurfaceAlphaMod faied! %s", SDL_GetError()); + + if(surf->format->BitsPerPixel == 8 && perSurfaceAlpha == SDL_ALPHA_OPAQUE && blitMode == EImageBlitMode::ALPHA) + { + CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift); + } + else + { + CSDL_Ext::blitSurface(surf, sourceRect, where, destShift); + } +} + +std::shared_ptr SDLImage::scaleFast(const Point & size) const +{ + float scaleX = float(size.x) / width(); + float scaleY = float(size.y) / height(); + + auto scaled = CSDL_Ext::scaleSurfaceFast(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]); + else if(scaled->format && scaled->format->Amask) + SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND);//just in case + else + CSDL_Ext::setDefaultColorKey(scaled);//just in case + + SDLImage * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); + + ret->fullSize.x = (int) round((float)fullSize.x * scaleX); + ret->fullSize.y = (int) round((float)fullSize.y * scaleY); + + ret->margins.x = (int) round((float)margins.x * scaleX); + ret->margins.y = (int) round((float)margins.y * scaleY); + + // erase our own reference + SDL_FreeSurface(scaled); + + return std::shared_ptr(ret); +} + +void SDLImage::exportBitmap(const boost::filesystem::path& path) const +{ + SDL_SaveBMP(surf, path.string().c_str()); +} + +void SDLImage::playerColored(PlayerColor player) +{ + graphics->blueToPlayersAdv(surf, player); +} + +void SDLImage::setAlpha(uint8_t value) +{ + CSDL_Ext::setAlpha (surf, value); + if (value != 255) + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); +} + +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 +{ + return CSDL_Ext::isTransparent(surf, coords.x, coords.y); +} + +Point SDLImage::dimensions() const +{ + return fullSize; +} + +void SDLImage::horizontalFlip() +{ + margins.y = fullSize.y - surf->h - margins.y; + + //todo: modify in-place + SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); + SDL_FreeSurface(surf); + surf = flipped; +} + +void SDLImage::verticalFlip() +{ + margins.x = fullSize.x - surf->w - margins.x; + + //todo: modify in-place + SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); + SDL_FreeSurface(surf); + surf = flipped; +} + +void SDLImage::doubleFlip() +{ + horizontalFlip(); + verticalFlip(); +} + +// Keep the original palette, in order to do color switching operation +void SDLImage::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); + + SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, DEFAULT_PALETTE_COLORS); +} + +void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{ + if(surf->format->palette) + { + std::vector shifterColors(colorsToMove); + + for(uint32_t i=0; icolors[firstColorID + i]; + } + CSDL_Ext::setColors(surf, shifterColors.data(), firstColorID, colorsToMove); + } +} + +void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) +{ + if(originalPalette == nullptr) + return; + + SDL_Palette* palette = surf->format->palette; + + // Note: here we skip first colors in the palette that are predefined in H3 images + for(int i = 0; i < palette->ncolors; i++) + { + 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]))); + } +} + +void SDLImage::resetPalette() +{ + 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); +} + +void SDLImage::resetPalette( int colorID ) +{ + 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); +} + +void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) +{ + if(surf->format->palette) + { + 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]); + } + } +} + +SDLImage::~SDLImage() +{ + SDL_FreeSurface(surf); + + if(originalPalette != nullptr) + { + SDL_FreePalette(originalPalette); + originalPalette = nullptr; + } +} + diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 74a28e003..05c516fa2 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -1,83 +1,83 @@ -/* - * SDLImage.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 "../../lib/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -class CDefFile; - -struct SDL_Surface; -struct SDL_Palette; - -/* - * Wrapper around SDL_Surface - */ -class SDLImage : public IImage -{ -public: - - const static int DEFAULT_PALETTE_COLORS = 256; - - //Surface without empty borders - SDL_Surface * surf; - //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(std::string 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(); - - // 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; - 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; - - friend class SDLImageLoader; - -private: - SDL_Palette * originalPalette; -}; +/* + * SDLImage.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 "../../lib/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class CDefFile; + +struct SDL_Surface; +struct SDL_Palette; + +/* + * Wrapper around SDL_Surface + */ +class SDLImage : public IImage +{ +public: + + const static int DEFAULT_PALETTE_COLORS = 256; + + //Surface without empty borders + SDL_Surface * surf; + //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(); + + // 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; + 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; + + friend class SDLImageLoader; + +private: + SDL_Palette * originalPalette; +}; diff --git a/client/renderSDL/SDLImageLoader.cpp b/client/renderSDL/SDLImageLoader.cpp index c951dcc2d..30e288f38 100644 --- a/client/renderSDL/SDLImageLoader.cpp +++ b/client/renderSDL/SDLImageLoader.cpp @@ -1,74 +1,74 @@ -/* - * SDLImageLoader.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 "SDLImageLoader.h" - -#include "SDLImage.h" - -#include "../../lib/Point.h" - -#include - -SDLImageLoader::SDLImageLoader(SDLImage * Img): - image(Img), - lineStart(nullptr), - position(nullptr) -{ -} - -void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) -{ - //Init image - image->surf = SDL_CreateRGBSurface(0, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); - image->margins = Margins; - image->fullSize = FullSize; - - //Prepare surface - SDL_Palette * p = SDL_AllocPalette(SDLImage::DEFAULT_PALETTE_COLORS); - SDL_SetPaletteColors(p, pal, 0, SDLImage::DEFAULT_PALETTE_COLORS); - SDL_SetSurfacePalette(image->surf, p); - SDL_FreePalette(p); - - SDL_LockSurface(image->surf); - lineStart = position = (ui8*)image->surf->pixels; -} - -inline void SDLImageLoader::load(size_t size, const ui8 * data) -{ - if (size) - { - memcpy((void *)position, data, size); - position += size; - } -} - -inline void SDLImageLoader::load(size_t size, ui8 color) -{ - if (size) - { - memset((void *)position, color, size); - position += size; - } -} - -inline void SDLImageLoader::endLine() -{ - lineStart += image->surf->pitch; - position = lineStart; -} - -SDLImageLoader::~SDLImageLoader() -{ - SDL_UnlockSurface(image->surf); - SDL_SetColorKey(image->surf, SDL_TRUE, 0); - //TODO: RLE if compressed and bpp>1 -} - +/* + * SDLImageLoader.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 "SDLImageLoader.h" + +#include "SDLImage.h" + +#include "../../lib/Point.h" + +#include + +SDLImageLoader::SDLImageLoader(SDLImage * Img): + image(Img), + lineStart(nullptr), + position(nullptr) +{ +} + +void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) +{ + //Init image + image->surf = SDL_CreateRGBSurface(0, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); + image->margins = Margins; + image->fullSize = FullSize; + + //Prepare surface + SDL_Palette * p = SDL_AllocPalette(SDLImage::DEFAULT_PALETTE_COLORS); + SDL_SetPaletteColors(p, pal, 0, SDLImage::DEFAULT_PALETTE_COLORS); + SDL_SetSurfacePalette(image->surf, p); + SDL_FreePalette(p); + + SDL_LockSurface(image->surf); + lineStart = position = (ui8*)image->surf->pixels; +} + +inline void SDLImageLoader::load(size_t size, const ui8 * data) +{ + if (size) + { + memcpy((void *)position, data, size); + position += size; + } +} + +inline void SDLImageLoader::load(size_t size, ui8 color) +{ + if (size) + { + memset((void *)position, color, size); + position += size; + } +} + +inline void SDLImageLoader::endLine() +{ + lineStart += image->surf->pitch; + position = lineStart; +} + +SDLImageLoader::~SDLImageLoader() +{ + SDL_UnlockSurface(image->surf); + SDL_SetColorKey(image->surf, SDL_TRUE, 0); + //TODO: RLE if compressed and bpp>1 +} + diff --git a/client/renderSDL/SDLImageLoader.h b/client/renderSDL/SDLImageLoader.h index 5a5b6e97f..6e4cca11b 100644 --- a/client/renderSDL/SDLImageLoader.h +++ b/client/renderSDL/SDLImageLoader.h @@ -1,32 +1,32 @@ -/* - * SDLImageLoader.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/IImageLoader.h" - -class SDLImageLoader : public IImageLoader -{ - SDLImage * image; - ui8 * lineStart; - ui8 * position; -public: - //load size raw pixels from data - void load(size_t size, const ui8 * data); - //set size pixels to color - void load(size_t size, ui8 color=0); - void endLine(); - //init image with these sizes and palette - void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); - - SDLImageLoader(SDLImage * Img); - ~SDLImageLoader(); -}; - - +/* + * SDLImageLoader.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/IImageLoader.h" + +class SDLImageLoader : public IImageLoader +{ + SDLImage * image; + ui8 * lineStart; + ui8 * position; +public: + //load size raw pixels from data + void load(size_t size, const ui8 * data); + //set size pixels to color + void load(size_t size, ui8 color=0); + void endLine(); + //init image with these sizes and palette + void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); + + SDLImageLoader(SDLImage * Img); + ~SDLImageLoader(); +}; + + diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index b4d7f1249..300611d70 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -1,876 +1,869 @@ -/* - * SDL_Extensions.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 "SDL_Extensions.h" - -#include "SDL_PixelAccess.h" - -#include "../render/Graphics.h" -#include "../render/Colors.h" -#include "../CMT.h" - -#include - -Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) -{ - return Rect(Point(rect.x, rect.y), Point(rect.w, rect.h)); -} - -SDL_Rect CSDL_Ext::toSDL(const Rect & rect) -{ - SDL_Rect result; - result.x = rect.x; - result.y = rect.y; - result.w = rect.w; - result.h = rect.h; - return result; -} - -ColorRGBA CSDL_Ext::fromSDL(const SDL_Color & color) -{ - return { color.r, color.g, color.b, color.a }; -} - -SDL_Color CSDL_Ext::toSDL(const ColorRGBA & color) -{ - SDL_Color result; - result.r = color.r; - result.g = color.g; - result.b = color.b; - result.a = color.a; - - return result; -} - -void CSDL_Ext::setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors) -{ - SDL_SetPaletteColors(surface->format->palette,colors,firstcolor,ncolors); -} - -void CSDL_Ext::setAlpha(SDL_Surface * bg, int value) -{ - SDL_SetSurfaceAlphaMod(bg, value); -} - -void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) -{ - 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, NULL, NULL)) - logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); - SDL_RenderPresent(mainRenderer); - -} - -SDL_Surface * CSDL_Ext::newSurface(int w, int h) -{ - 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); - - 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); - - handleFatalError(message, true); - } - - if (mod->format->palette) - { - assert(ret->format->palette); - 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; -} - -SDL_Surface * CSDL_Ext::copySurface(SDL_Surface * mod) //returns copy of given surface -{ - //return SDL_DisplayFormat(mod); - return SDL_ConvertSurface(mod, mod->format, mod->flags); -} - -template -SDL_Surface * CSDL_Ext::createSurfaceWithBpp(int width, int height) -{ - uint32_t rMask = 0, gMask = 0, bMask = 0, aMask = 0; - - Channels::px::r.set((uint8_t*)&rMask, 255); - Channels::px::g.set((uint8_t*)&gMask, 255); - Channels::px::b.set((uint8_t*)&bMask, 255); - Channels::px::a.set((uint8_t*)&aMask, 255); - - return SDL_CreateRGBSurface(0, width, height, bpp * 8, rMask, gMask, bMask, aMask); -} - -void CSDL_Ext::blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst) -{ - CSDL_Ext::blitSurface(src, dst, Point(x, y)); -} - -void CSDL_Ext::blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst) -{ - if (src) - blitAt(src,pos.x,pos.y,dst); -} - -// Vertical flip -SDL_Surface * CSDL_Ext::verticalFlip(SDL_Surface * toRot) -{ - SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); - - SDL_LockSurface(ret); - SDL_LockSurface(toRot); - - const int bpp = ret->format->BytesPerPixel; - - char * src = reinterpret_cast(toRot->pixels); - char * dst = reinterpret_cast(ret->pixels); - - for(int i=0; ih; i++) - { - //FIXME: optimization bugged -// if (bpp == 1) -// { -// // much faster for 8-bit surfaces (majority of our data) -// std::reverse_copy(src, src + toRot->pitch, dst); -// } -// else -// { - char * srcPxl = src; - char * dstPxl = dst + ret->w * bpp; - - for(int j=0; jw; j++) - { - dstPxl -= bpp; - std::copy(srcPxl, srcPxl + bpp, dstPxl); - srcPxl += bpp; - } -// } - src += toRot->pitch; - dst += ret->pitch; - } - SDL_UnlockSurface(ret); - SDL_UnlockSurface(toRot); - return ret; -} - -// Horizontal flip -SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) -{ - SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); - SDL_LockSurface(ret); - SDL_LockSurface(toRot); - char * src = reinterpret_cast(toRot->pixels); - char * dst = reinterpret_cast(ret->pixels) + ret->h * ret->pitch; - - for(int i=0; ih; i++) - { - dst -= ret->pitch; - std::copy(src, src + toRot->pitch, dst); - src += toRot->pitch; - } - SDL_UnlockSurface(ret); - SDL_UnlockSurface(toRot); - return ret; -} - -uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte) -{ - int bpp = surface->format->BytesPerPixel; - /* Here p is the address to the pixel we want to retrieve */ - uint8_t *p = (uint8_t *)surface->pixels + y * surface->pitch + x * bpp; - - switch(bpp) - { - case 1: - if(colorByte) - return colorTouint32_t(surface->format->palette->colors+(*p)); - else - return *p; - - case 2: - return *(uint16_t *)p; - - case 3: - return p[0] | p[1] << 8 | p[2] << 16; - - case 4: - return *(uint32_t *)p; - - default: - return 0; // shouldn't happen, but avoids warnings - } -} - -template -int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput) -{ - SDL_Rect srcRectInstance = CSDL_Ext::toSDL(srcRectInput); - SDL_Rect dstRectInstance = CSDL_Ext::toSDL(Rect(dstPointInput, srcRectInput.dimensions())); - - SDL_Rect * srcRect =&srcRectInstance; - SDL_Rect * dstRect =&dstRectInstance; - - /* Make sure the surfaces aren't locked */ - if ( ! src || ! dst ) - { - SDL_SetError("SDL_UpperBlit: passed a nullptr surface"); - return -1; - } - - if ( src->locked || dst->locked ) - { - SDL_SetError("Surfaces must not be locked during blit"); - return -1; - } - - if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok - { - SDL_Rect fulldst; - int srcx, srcy, w, h; - - /* If the destination rectangle is nullptr, use the entire dest surface */ - if ( dstRect == nullptr ) - { - fulldst.x = fulldst.y = 0; - dstRect = &fulldst; - } - - /* clip the source rectangle to the source surface */ - if(srcRect) - { - int maxw, maxh; - - srcx = srcRect->x; - w = srcRect->w; - if(srcx < 0) - { - w += srcx; - dstRect->x -= srcx; - srcx = 0; - } - maxw = src->w - srcx; - if(maxw < w) - w = maxw; - - srcy = srcRect->y; - h = srcRect->h; - if(srcy < 0) - { - h += srcy; - dstRect->y -= srcy; - srcy = 0; - } - maxh = src->h - srcy; - if(maxh < h) - h = maxh; - - } - else - { - srcx = srcy = 0; - w = src->w; - h = src->h; - } - - /* clip the destination rectangle against the clip rectangle */ - { - SDL_Rect *clip = &dst->clip_rect; - int dx, dy; - - dx = clip->x - dstRect->x; - if(dx > 0) - { - w -= dx; - dstRect->x += dx; - srcx += dx; - } - dx = dstRect->x + w - clip->x - clip->w; - if(dx > 0) - w -= dx; - - dy = clip->y - dstRect->y; - if(dy > 0) - { - h -= dy; - dstRect->y += dy; - srcy += dy; - } - dy = dstRect->y + h - clip->y - clip->h; - if(dy > 0) - h -= dy; - } - - if(w > 0 && h > 0) - { - dstRect->w = w; - dstRect->h = h; - - if(SDL_LockSurface(dst)) - return -1; //if we cannot lock the surface - - const SDL_Color *colors = src->format->palette->colors; - 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) - { - uint8_t *color = colory; - uint8_t *p = py; - - for(int x = w; x; x--) - { - const SDL_Color &tbc = colors[*color++]; //color to blit - ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); - } - } - SDL_UnlockSurface(dst); - } - } - return 0; -} - -int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint) -{ - switch(dst->format->BytesPerPixel) - { - 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; - } -} - -uint32_t CSDL_Ext::colorTouint32_t(const SDL_Color * color) -{ - uint32_t ret = 0; - ret+=color->a; - ret<<=8; //*=256 - ret+=color->b; - ret<<=8; //*=256 - ret+=color->g; - ret<<=8; //*=256 - ret+=color->r; - return ret; -} - -static void drawLineXDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) -{ - double length(x2 - x1); - - for(int x = x1; x <= x2; x++) - { - double f = (x - x1) / length; - int y = vstd::lerp(y1, y2, f); - - if (std::abs(x - x1) % 5 != 4) - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); - } -} - -static void drawLineYDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) -{ - double length(y2 - y1); - - for(int y = y1; y <= y2; y++) - { - double f = (y - y1) / length; - int x = vstd::lerp(x1, x2, f); - - if (std::abs(y - y1) % 5 != 4) - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); - } -} - -static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) -{ - double length(x2 - x1); - for(int x = x1; x <= x2; x++) - { - double f = (x - x1) / length; - int y = vstd::lerp(y1, y2, f); - - uint8_t r = vstd::lerp(color1.r, color2.r, f); - uint8_t g = vstd::lerp(color1.g, color2.g, f); - uint8_t b = vstd::lerp(color1.b, color2.b, f); - 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); - } -} - -static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) -{ - double length(y2 - y1); - for(int y = y1; y <= y2; y++) - { - double f = (y - y1) / length; - int x = vstd::lerp(x1, x2, f); - - uint8_t r = vstd::lerp(color1.r, color2.r, f); - uint8_t g = vstd::lerp(color1.g, color2.g, f); - uint8_t b = vstd::lerp(color1.b, color2.b, f); - 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); - } -} - -void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2) -{ - //FIXME: duplicated code with drawLineDashed - int width = std::abs(from.x - dest.x); - int height = std::abs(from.y - dest.y); - - if ( width == 0 && height == 0) - { - uint8_t *p = CSDL_Ext::getPxPtr(sur, from.x, from.y); - ColorPutter<4, 0>::PutColorAlpha(p, color1); - return; - } - - if (width > height) - { - if ( from.x < dest.x) - drawLineX(sur, from.x, from.y, dest.x, dest.y, color1, color2); - 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); - } -} - -void CSDL_Ext::drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color) -{ - //FIXME: duplicated code with drawLine - int width = std::abs(from.x - dest.x); - int height = std::abs(from.y - dest.y); - - if ( width == 0 && height == 0) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, from.x, from.y, color.r, color.g, color.b); - return; - } - - if (width > height) - { - if ( from.x < dest.x) - drawLineXDashed(sur, from.x, from.y, dest.x, dest.y, color); - else - drawLineXDashed(sur, dest.x, dest.y, from.x, from.y, color); - } - else - { - if ( from.y < dest.y) - drawLineYDashed(sur, from.x, from.y, dest.x, dest.y, color); - else - drawLineYDashed(sur, dest.x, dest.y, from.x, from.y, color); - } -} - -void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color &color, int depth) -{ - depth = std::max(1, depth); - for(int depthIterator = 0; depthIterator < depth; depthIterator++) - { - for(int i = 0; i < w; i++) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+depthIterator,color.r,color.g,color.b); - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+h-1-depthIterator,color.r,color.g,color.b); - } - for(int i = 0; i < h; i++) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+depthIterator,y+i,color.r,color.g,color.b); - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+w-1-depthIterator,y+i,color.r,color.g,color.b); - } - } -} - -void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &color, int depth) -{ - drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); -} - -void CSDL_Ext::setPlayerColor(SDL_Surface * sur, PlayerColor player) -{ - if(player==PlayerColor::UNFLAGGABLE) - return; - if(sur->format->BitsPerPixel==8) - { - SDL_Color *color = (player == PlayerColor::NEUTRAL - ? graphics->neutralColor - : &graphics->playerColors[player.getNum()]); - CSDL_Ext::setColors(sur, color, 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) - 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) -{ - return (uint8_t *)srf->pixels + y * srf->pitch + x * srf->format->BytesPerPixel; -} - -bool CSDL_Ext::isTransparent( SDL_Surface * srf, const Point & position ) -{ - return isTransparent(srf, position.x, position.y); -} - -bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) -{ - if (x < 0 || y < 0 || x >= srf->w || y >= srf->h) - return true; - - SDL_Color color; - - SDL_GetRGBA(CSDL_Ext::getPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); - - bool pixelTransparent = color.a < 128; - bool pixelCyan = (color.r == 0 && color.g == 255 && color.b == 255); - - return pixelTransparent || pixelCyan; -} - -void CSDL_Ext::VflipSurf(SDL_Surface * surf) -{ - char buf[4]; //buffer - int bpp = surf->format->BytesPerPixel; - for (int y=0; yh; ++y) - { - char * base = (char*)surf->pixels + y * surf->pitch; - for (int x=0; xw/2; ++x) - { - memcpy(buf, base + x * bpp, bpp); - memcpy(base + x * bpp, base + (surf->w - x - 1) * bpp, bpp); - memcpy(base + (surf->w - x - 1) * bpp, buf, bpp); - } - } -} - -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); - - 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; - } -} - -void CSDL_Ext::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) -{ - const SDL_Rect & rect = ekran->clip_rect; - - if(x >= rect.x && x < rect.w + rect.x - && y >= rect.y && y < rect.h + rect.y) - CSDL_Ext::putPixelWithoutRefresh(ekran, x, y, R, G, B, A); -} - -template -void CSDL_Ext::convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect ) -{ - uint8_t * pixels = static_cast(surf->pixels); - - for(int yp = rect.top(); yp < rect.bottom(); ++yp) - { - uint8_t * pixel_from = pixels + yp * surf->pitch + rect.left() * surf->format->BytesPerPixel; - uint8_t * pixel_dest = pixels + yp * surf->pitch + rect.right() * surf->format->BytesPerPixel; - - for (uint8_t * pixel = pixel_from; pixel < pixel_dest; pixel += surf->format->BytesPerPixel) - { - int r = Channels::px::r.get(pixel); - int g = Channels::px::g.get(pixel); - int b = Channels::px::b.get(pixel); - - int gray = static_cast(0.299 * r + 0.587 * g + 0.114 *b); - - Channels::px::r.set(pixel, gray); - Channels::px::g.set(pixel, gray); - Channels::px::b.set(pixel, gray); - } - } -} - -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 = float(surf->w) / float(ret->w), - factorY = float(surf->h) / float(ret->h); - - for(int y = 0; y < ret->h; y++) - { - for(int x = 0; x < ret->w; x++) - { - //coordinates we want to calculate - int origX = static_cast(floor(factorX * x)), - 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) -{ - if (!surf || !width || !height) - return nullptr; - - if (surf->format->palette) - return scaleSurfaceFast(surf, width, height); - - //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 2: scaleSurfaceInternal<2>(surf, ret); break; - case 3: scaleSurfaceInternal<3>(surf, ret); break; - case 4: scaleSurfaceInternal<4>(surf, ret); break; - } - - return ret; -} - -void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint) -{ - SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput); - SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(dstPoint, srcRectInput.dimensions())); - - int result = SDL_UpperBlit(src, &srcRect, dst, &dstRect); - - if (result != 0) - logGlobal->error("SDL_UpperBlit failed! %s", SDL_GetError()); -} - -void CSDL_Ext::blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest) -{ - Rect allSurface( Point(0,0), Point(src->w, src->h)); - - blitSurface(src, allSurface, dst, dest); -} - -void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) -{ - Rect allSurface( Point(0,0), Point(dst->w, dst->h)); - - fillRect(dst, allSurface, color); -} - -void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color ) -{ - SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); - - uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - SDL_FillRect(dst, &newRect, sdlColor); -} - -SDL_Color CSDL_Ext::makeColor(ui8 r, ui8 g, ui8 b, ui8 a) -{ - SDL_Color ret = {r, g, b, a}; - return ret; -} - -STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) -{ - return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); -} - -void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) -{ - uint32_t key = mapColor(surface,color); - SDL_SetColorKey(surface, SDL_TRUE, key); -} - -void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) -{ - setColorKey(surface, Colors::DEFAULT_KEY_COLOR); -} - -void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) -{ - uint32_t key = mapColor(surface, Colors::DEFAULT_KEY_COLOR); - auto & color = surface->format->palette->colors[key]; - - // set color key only if exactly such color was found - if (color.r == Colors::DEFAULT_KEY_COLOR.r && color.g == Colors::DEFAULT_KEY_COLOR.g && color.b == Colors::DEFAULT_KEY_COLOR.b) - { - SDL_SetColorKey(surface, SDL_TRUE, key); - color.a = SDL_ALPHA_TRANSPARENT; - } -} - -void CSDL_Ext::setClipRect(SDL_Surface * src, const Rect & other) -{ - SDL_Rect rect = CSDL_Ext::toSDL(other); - - SDL_SetClipRect(src, &rect); -} - -void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) -{ - SDL_Rect rect; - - SDL_GetClipRect(src, &rect); - - other = CSDL_Ext::fromSDL(rect); -} - -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); - +/* + * SDL_Extensions.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 "SDL_Extensions.h" + +#include "SDL_PixelAccess.h" + +#include "../render/Graphics.h" +#include "../render/Colors.h" +#include "../CMT.h" + +#include "../../lib/GameConstants.h" + +#include + +Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) +{ + return Rect(Point(rect.x, rect.y), Point(rect.w, rect.h)); +} + +SDL_Rect CSDL_Ext::toSDL(const Rect & rect) +{ + SDL_Rect result; + result.x = rect.x; + result.y = rect.y; + result.w = rect.w; + result.h = rect.h; + return result; +} + +ColorRGBA CSDL_Ext::fromSDL(const SDL_Color & color) +{ + return { color.r, color.g, color.b, color.a }; +} + +SDL_Color CSDL_Ext::toSDL(const ColorRGBA & color) +{ + SDL_Color result; + result.r = color.r; + result.g = color.g; + result.b = color.b; + result.a = color.a; + + return result; +} + +void CSDL_Ext::setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors) +{ + SDL_SetPaletteColors(surface->format->palette,colors,firstcolor,ncolors); +} + +void CSDL_Ext::setAlpha(SDL_Surface * bg, int value) +{ + SDL_SetSurfaceAlphaMod(bg, value); +} + +void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) +{ + 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); + +} + +SDL_Surface * CSDL_Ext::newSurface(int w, int h) +{ + 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); + + 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); + + handleFatalError(message, true); + } + + if (mod->format->palette) + { + assert(ret->format->palette); + 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; +} + +SDL_Surface * CSDL_Ext::copySurface(SDL_Surface * mod) //returns copy of given surface +{ + //return SDL_DisplayFormat(mod); + return SDL_ConvertSurface(mod, mod->format, mod->flags); +} + +template +SDL_Surface * CSDL_Ext::createSurfaceWithBpp(int width, int height) +{ + uint32_t rMask = 0, gMask = 0, bMask = 0, aMask = 0; + + Channels::px::r.set((uint8_t*)&rMask, 255); + Channels::px::g.set((uint8_t*)&gMask, 255); + Channels::px::b.set((uint8_t*)&bMask, 255); + Channels::px::a.set((uint8_t*)&aMask, 255); + + return SDL_CreateRGBSurface(0, width, height, bpp * 8, rMask, gMask, bMask, aMask); +} + +void CSDL_Ext::blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst) +{ + CSDL_Ext::blitSurface(src, dst, Point(x, y)); +} + +void CSDL_Ext::blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst) +{ + if (src) + blitAt(src,pos.x,pos.y,dst); +} + +// Vertical flip +SDL_Surface * CSDL_Ext::verticalFlip(SDL_Surface * toRot) +{ + SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); + + SDL_LockSurface(ret); + SDL_LockSurface(toRot); + + const int bpp = ret->format->BytesPerPixel; + + char * src = reinterpret_cast(toRot->pixels); + char * dst = reinterpret_cast(ret->pixels); + + for(int i=0; ih; i++) + { + //FIXME: optimization bugged +// if (bpp == 1) +// { +// // much faster for 8-bit surfaces (majority of our data) +// std::reverse_copy(src, src + toRot->pitch, dst); +// } +// else +// { + char * srcPxl = src; + char * dstPxl = dst + ret->w * bpp; + + for(int j=0; jw; j++) + { + dstPxl -= bpp; + std::copy(srcPxl, srcPxl + bpp, dstPxl); + srcPxl += bpp; + } +// } + src += toRot->pitch; + dst += ret->pitch; + } + SDL_UnlockSurface(ret); + SDL_UnlockSurface(toRot); + return ret; +} + +// Horizontal flip +SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) +{ + SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); + SDL_LockSurface(ret); + SDL_LockSurface(toRot); + char * src = reinterpret_cast(toRot->pixels); + char * dst = reinterpret_cast(ret->pixels) + ret->h * ret->pitch; + + for(int i=0; ih; i++) + { + dst -= ret->pitch; + std::copy(src, src + toRot->pitch, dst); + src += toRot->pitch; + } + SDL_UnlockSurface(ret); + SDL_UnlockSurface(toRot); + return ret; +} + +uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte) +{ + int bpp = surface->format->BytesPerPixel; + /* Here p is the address to the pixel we want to retrieve */ + uint8_t *p = (uint8_t *)surface->pixels + y * surface->pitch + x * bpp; + + switch(bpp) + { + case 1: + if(colorByte) + return colorTouint32_t(surface->format->palette->colors+(*p)); + else + return *p; + + case 2: + return *(uint16_t *)p; + + case 3: + return p[0] | p[1] << 8 | p[2] << 16; + + case 4: + return *(uint32_t *)p; + + default: + return 0; // shouldn't happen, but avoids warnings + } +} + +template +int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput) +{ + SDL_Rect srcRectInstance = CSDL_Ext::toSDL(srcRectInput); + SDL_Rect dstRectInstance = CSDL_Ext::toSDL(Rect(dstPointInput, srcRectInput.dimensions())); + + SDL_Rect * srcRect =&srcRectInstance; + SDL_Rect * dstRect =&dstRectInstance; + + /* Make sure the surfaces aren't locked */ + if ( ! src || ! dst ) + { + SDL_SetError("SDL_UpperBlit: passed a nullptr surface"); + return -1; + } + + if ( src->locked || dst->locked ) + { + SDL_SetError("Surfaces must not be locked during blit"); + return -1; + } + + if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok + { + SDL_Rect fulldst; + int srcx, srcy, w, h; + + /* If the destination rectangle is nullptr, use the entire dest surface */ + if ( dstRect == nullptr ) + { + fulldst.x = fulldst.y = 0; + dstRect = &fulldst; + } + + /* clip the source rectangle to the source surface */ + if(srcRect) + { + int maxw, maxh; + + srcx = srcRect->x; + w = srcRect->w; + if(srcx < 0) + { + w += srcx; + dstRect->x -= srcx; + srcx = 0; + } + maxw = src->w - srcx; + if(maxw < w) + w = maxw; + + srcy = srcRect->y; + h = srcRect->h; + if(srcy < 0) + { + h += srcy; + dstRect->y -= srcy; + srcy = 0; + } + maxh = src->h - srcy; + if(maxh < h) + h = maxh; + + } + else + { + srcx = srcy = 0; + w = src->w; + h = src->h; + } + + /* clip the destination rectangle against the clip rectangle */ + { + SDL_Rect *clip = &dst->clip_rect; + int dx, dy; + + dx = clip->x - dstRect->x; + if(dx > 0) + { + w -= dx; + dstRect->x += dx; + srcx += dx; + } + dx = dstRect->x + w - clip->x - clip->w; + if(dx > 0) + w -= dx; + + dy = clip->y - dstRect->y; + if(dy > 0) + { + h -= dy; + dstRect->y += dy; + srcy += dy; + } + dy = dstRect->y + h - clip->y - clip->h; + if(dy > 0) + h -= dy; + } + + if(w > 0 && h > 0) + { + dstRect->w = w; + dstRect->h = h; + + if(SDL_LockSurface(dst)) + return -1; //if we cannot lock the surface + + const SDL_Color *colors = src->format->palette->colors; + 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) + { + uint8_t *color = colory; + uint8_t *p = py; + + for(int x = w; x; x--) + { + const SDL_Color &tbc = colors[*color++]; //color to blit + ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); + } + } + SDL_UnlockSurface(dst); + } + } + return 0; +} + +int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint) +{ + switch(dst->format->BytesPerPixel) + { + 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; + } +} + +uint32_t CSDL_Ext::colorTouint32_t(const SDL_Color * color) +{ + uint32_t ret = 0; + ret+=color->a; + ret<<=8; //*=256 + ret+=color->b; + ret<<=8; //*=256 + ret+=color->g; + ret<<=8; //*=256 + ret+=color->r; + return ret; +} + +static void drawLineXDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) +{ + double length(x2 - x1); + + for(int x = x1; x <= x2; x++) + { + double f = (x - x1) / length; + int y = vstd::lerp(y1, y2, f); + + if (std::abs(x - x1) % 5 != 4) + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); + } +} + +static void drawLineYDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) +{ + double length(y2 - y1); + + for(int y = y1; y <= y2; y++) + { + double f = (y - y1) / length; + int x = vstd::lerp(x1, x2, f); + + if (std::abs(y - y1) % 5 != 4) + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); + } +} + +static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) +{ + double length(x2 - x1); + for(int x = x1; x <= x2; x++) + { + double f = (x - x1) / length; + int y = vstd::lerp(y1, y2, f); + + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + 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); + } +} + +static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) +{ + double length(y2 - y1); + for(int y = y1; y <= y2; y++) + { + double f = (y - y1) / length; + int x = vstd::lerp(x1, x2, f); + + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + 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); + } +} + +void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2) +{ + //FIXME: duplicated code with drawLineDashed + int width = std::abs(from.x - dest.x); + int height = std::abs(from.y - dest.y); + + if ( width == 0 && height == 0) + { + uint8_t *p = CSDL_Ext::getPxPtr(sur, from.x, from.y); + ColorPutter<4, 0>::PutColorAlpha(p, color1); + return; + } + + if (width > height) + { + if ( from.x < dest.x) + drawLineX(sur, from.x, from.y, dest.x, dest.y, color1, color2); + 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); + } +} + +void CSDL_Ext::drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color) +{ + //FIXME: duplicated code with drawLine + int width = std::abs(from.x - dest.x); + int height = std::abs(from.y - dest.y); + + if ( width == 0 && height == 0) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, from.x, from.y, color.r, color.g, color.b); + return; + } + + if (width > height) + { + if ( from.x < dest.x) + drawLineXDashed(sur, from.x, from.y, dest.x, dest.y, color); + else + drawLineXDashed(sur, dest.x, dest.y, from.x, from.y, color); + } + else + { + if ( from.y < dest.y) + drawLineYDashed(sur, from.x, from.y, dest.x, dest.y, color); + else + drawLineYDashed(sur, dest.x, dest.y, from.x, from.y, color); + } +} + +void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color &color, int depth) +{ + depth = std::max(1, depth); + for(int depthIterator = 0; depthIterator < depth; depthIterator++) + { + for(int i = 0; i < w; i++) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+depthIterator,color.r,color.g,color.b); + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+h-1-depthIterator,color.r,color.g,color.b); + } + for(int i = 0; i < h; i++) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+depthIterator,y+i,color.r,color.g,color.b); + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+w-1-depthIterator,y+i,color.r,color.g,color.b); + } + } +} + +void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &color, int depth) +{ + drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); +} + +void CSDL_Ext::setPlayerColor(SDL_Surface * sur, const PlayerColor & player) +{ + 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) + 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) +{ + return (uint8_t *)srf->pixels + y * srf->pitch + x * srf->format->BytesPerPixel; +} + +bool CSDL_Ext::isTransparent( SDL_Surface * srf, const Point & position ) +{ + return isTransparent(srf, position.x, position.y); +} + +bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) +{ + if (x < 0 || y < 0 || x >= srf->w || y >= srf->h) + return true; + + SDL_Color color; + + SDL_GetRGBA(CSDL_Ext::getPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); + + bool pixelTransparent = color.a < 128; + bool pixelCyan = (color.r == 0 && color.g == 255 && color.b == 255); + + return pixelTransparent || pixelCyan; +} + +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); + + 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; + } +} + +void CSDL_Ext::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) +{ + const SDL_Rect & rect = ekran->clip_rect; + + if(x >= rect.x && x < rect.w + rect.x + && y >= rect.y && y < rect.h + rect.y) + CSDL_Ext::putPixelWithoutRefresh(ekran, x, y, R, G, B, A); +} + +template +void CSDL_Ext::convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect ) +{ + uint8_t * pixels = static_cast(surf->pixels); + + for(int yp = rect.top(); yp < rect.bottom(); ++yp) + { + uint8_t * pixel_from = pixels + yp * surf->pitch + rect.left() * surf->format->BytesPerPixel; + uint8_t * pixel_dest = pixels + yp * surf->pitch + rect.right() * surf->format->BytesPerPixel; + + for (uint8_t * pixel = pixel_from; pixel < pixel_dest; pixel += surf->format->BytesPerPixel) + { + int r = Channels::px::r.get(pixel); + int g = Channels::px::g.get(pixel); + int b = Channels::px::b.get(pixel); + + int gray = static_cast(0.299 * r + 0.587 * g + 0.114 *b); + + Channels::px::r.set(pixel, gray); + Channels::px::g.set(pixel, gray); + Channels::px::b.set(pixel, gray); + } + } +} + +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 = float(surf->w) / float(ret->w), + factorY = float(surf->h) / float(ret->h); + + for(int y = 0; y < ret->h; y++) + { + for(int x = 0; x < ret->w; x++) + { + //coordinates we want to calculate + int origX = static_cast(floor(factorX * x)), + 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) +{ + if (!surf || !width || !height) + return nullptr; + + if (surf->format->palette) + return scaleSurfaceFast(surf, width, height); + + //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 2: scaleSurfaceInternal<2>(surf, ret); break; + case 3: scaleSurfaceInternal<3>(surf, ret); break; + case 4: scaleSurfaceInternal<4>(surf, ret); break; + } + + return ret; +} + +void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint) +{ + SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput); + SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(dstPoint, srcRectInput.dimensions())); + + int result = SDL_UpperBlit(src, &srcRect, dst, &dstRect); + + if (result != 0) + logGlobal->error("SDL_UpperBlit failed! %s", SDL_GetError()); +} + +void CSDL_Ext::blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest) +{ + Rect allSurface( Point(0,0), Point(src->w, src->h)); + + blitSurface(src, allSurface, dst, dest); +} + +void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) +{ + Rect allSurface( Point(0,0), Point(dst->w, dst->h)); + + fillRect(dst, allSurface, color); +} + +void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) +{ + SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); + + uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); + SDL_FillRect(dst, &newRect, sdlColor); +} + +void CSDL_Ext::fillRectBlended( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) +{ + SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); + uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); + + SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); + SDL_FillRect(tmp, nullptr, sdlColor); + SDL_BlitSurface(tmp, nullptr, dst, &newRect); + SDL_FreeSurface(tmp); +} + +STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) +{ + return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); +} + +void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) +{ + uint32_t key = mapColor(surface,color); + SDL_SetColorKey(surface, SDL_TRUE, key); +} + +void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) +{ + setColorKey(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); +} + +void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) +{ + uint32_t key = mapColor(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); + auto & color = surface->format->palette->colors[key]; + + // set color key only if exactly such color was found + if (color.r == Colors::DEFAULT_KEY_COLOR.r && color.g == Colors::DEFAULT_KEY_COLOR.g && color.b == Colors::DEFAULT_KEY_COLOR.b) + { + SDL_SetColorKey(surface, SDL_TRUE, key); + color.a = SDL_ALPHA_TRANSPARENT; + } +} + +void CSDL_Ext::setClipRect(SDL_Surface * src, const Rect & other) +{ + SDL_Rect rect = CSDL_Ext::toSDL(other); + + SDL_SetClipRect(src, &rect); +} + +void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) +{ + SDL_Rect rect; + + SDL_GetClipRect(src, &rect); + + other = CSDL_Ext::fromSDL(rect); +} + +template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); +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 b5a6882cc..c85458c09 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -1,133 +1,132 @@ -/* - * SDL_Extensions.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/GameConstants.h" -#include "../../lib/Rect.h" -#include "../../lib/Color.h" - -struct SDL_Rect; -struct SDL_Window; -struct SDL_Renderer; -struct SDL_Texture; -struct SDL_Surface; -struct SDL_Color; - -VCMI_LIB_NAMESPACE_BEGIN - -class Rect; -class Point; - -VCMI_LIB_NAMESPACE_END - -namespace CSDL_Ext -{ - -/// creates Rect using provided rect -Rect fromSDL(const SDL_Rect & rect); - -/// creates SDL_Rect using provided rect -SDL_Rect toSDL(const Rect & rect); - -/// creates Color using provided SDL_Color -ColorRGBA fromSDL(const SDL_Color & color); - -/// creates SDL_Color using provided Color -SDL_Color toSDL(const ColorRGBA & color); - -void setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors); -void setAlpha(SDL_Surface * bg, int value); - -using TColorPutter = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &); -using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &, const uint8_t &); - - void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst); - void blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst); - - void setClipRect(SDL_Surface * src, const Rect & other); - void getClipRect(SDL_Surface * src, Rect & other); - - void blitSurface(SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dest); - void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); - - void fillSurface(SDL_Surface * dst, const SDL_Color & color); - void fillRect(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); - - SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip - SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip - uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false); - bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position - 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 - - 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 - uint32_t colorTouint32_t(const SDL_Color * color); //little endian only - SDL_Color makeColor(ui8 r, ui8 g, ui8 b, ui8 a); - - void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2); - 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, 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 * 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 - void VflipSurf(SDL_Surface * surf); //fluipis given surface by vertical axis - - //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 - SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); - - template - void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); - void convertToGrayscale(SDL_Surface * surf, const Rect & rect); - - void setColorKey(SDL_Surface * surface, SDL_Color color); - - ///set key-color to 0,255,255 - void setDefaultColorKey(SDL_Surface * surface); - ///set key-color to 0,255,255 only if it exactly mapped - void setDefaultColorKeyPresize(SDL_Surface * surface); - - /// helper that will safely set and un-set ClipRect for SDL_Surface - class CClipRectGuard: boost::noncopyable - { - SDL_Surface * surf; - Rect oldRect; - - public: - CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface) - { - CSDL_Ext::getClipRect(surf, oldRect); - CSDL_Ext::setClipRect(surf, rect); - } - - ~CClipRectGuard() - { - CSDL_Ext::setClipRect(surf, oldRect); - } - }; -} +/* + * SDL_Extensions.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/Color.h" + +struct SDL_Rect; +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Texture; +struct SDL_Surface; +struct SDL_Color; + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; +class Rect; +class Point; + +VCMI_LIB_NAMESPACE_END + +namespace CSDL_Ext +{ + +/// creates Rect using provided rect +Rect fromSDL(const SDL_Rect & rect); + +/// creates SDL_Rect using provided rect +SDL_Rect toSDL(const Rect & rect); + +/// creates Color using provided SDL_Color +ColorRGBA fromSDL(const SDL_Color & color); + +/// creates SDL_Color using provided Color +SDL_Color toSDL(const ColorRGBA & color); + +void setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors); +void setAlpha(SDL_Surface * bg, int value); + +using TColorPutter = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &); +using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &, const uint8_t &); + + void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst); + void blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst); + + void setClipRect(SDL_Surface * src, const Rect & other); + void getClipRect(SDL_Surface * src, Rect & other); + + void blitSurface(SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dest); + void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); + + void fillSurface(SDL_Surface * dst, const SDL_Color & color); + 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); + + SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip + SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip + uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false); + bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position + 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 + + 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 + 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 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 * 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 + SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); + + template + void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); + void convertToGrayscale(SDL_Surface * surf, const Rect & rect); + + void setColorKey(SDL_Surface * surface, SDL_Color color); + + ///set key-color to 0,255,255 + void setDefaultColorKey(SDL_Surface * surface); + ///set key-color to 0,255,255 only if it exactly mapped + void setDefaultColorKeyPresize(SDL_Surface * surface); + + /// helper that will safely set and un-set ClipRect for SDL_Surface + class CClipRectGuard: boost::noncopyable + { + SDL_Surface * surf; + Rect oldRect; + + public: + CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface) + { + CSDL_Ext::getClipRect(surf, oldRect); + CSDL_Ext::setClipRect(surf, rect); + } + + ~CClipRectGuard() + { + CSDL_Ext::setClipRect(surf, oldRect); + } + }; +} diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 623289074..1371c51c7 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -274,8 +274,14 @@ void ScreenHandler::initializeWindow() handleFatalError(message, true); } - //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible - mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), 0); + // create first available renderer if no preferred one is set + // use no SDL_RENDERER_SOFTWARE or SDL_RENDERER_ACCELERATED flag, so HW accelerated will be preferred but SW renderer will also be possible + uint32_t rendererFlags = 0; + if(settings["video"]["vsync"].Bool()) + { + rendererFlags |= SDL_RENDERER_PRESENTVSYNC; + } + mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), rendererFlags); if(mainRenderer == nullptr) throw std::runtime_error("Unable to create renderer\n"); @@ -565,3 +571,9 @@ std::vector ScreenHandler::getSupportedResolutions( int displayIndex) con return result; } + +bool ScreenHandler::hasFocus() +{ + ui32 flags = SDL_GetWindowFlags(mainWindow); + return flags & SDL_WINDOW_INPUT_FOCUS; +} diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index c7a057144..fb3d6a334 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -86,6 +86,9 @@ public: /// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS Point getRenderResolution() const final; + /// Window has focus + bool hasFocus() final; + std::vector getSupportedResolutions() const final; std::vector getSupportedResolutions(int displayIndex) const; std::tuple getSupportedScalingRange() const final; diff --git a/client/resource.h b/client/resource.h index caafaed7d..b5eedceac 100644 --- a/client/resource.h +++ b/client/resource.h @@ -1,16 +1,16 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by VCMI_client.rc -// -#define IDI_ICON1 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by VCMI_client.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index b33bd8e6a..b4706fa3a 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -25,6 +25,7 @@ #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" @@ -60,29 +61,17 @@ void CButton::update() redraw(); } -void CButton::setBorderColor(std::optional borderColor) +void CButton::setBorderColor(std::optional newBorderColor) { - setBorderColor(borderColor, borderColor, borderColor, borderColor); + borderColor = newBorderColor; } -void CButton::setBorderColor(std::optional normalBorderColor, - std::optional pressedBorderColor, - std::optional blockedBorderColor, - std::optional highlightedBorderColor) -{ - stateToBorderColor[NORMAL] = normalBorderColor; - stateToBorderColor[PRESSED] = pressedBorderColor; - stateToBorderColor[BLOCKED] = blockedBorderColor; - stateToBorderColor[HIGHLIGHTED] = highlightedBorderColor; - update(); -} - -void CButton::addCallback(std::function callback) +void CButton::addCallback(const std::function & callback) { this->callback += callback; } -void CButton::addTextOverlay(const std::string & Text, EFonts font, SDL_Color color) +void CButton::addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); addOverlay(std::make_shared(pos.w/2, pos.h/2, font, ETextAlignment::CENTER, color, Text)); @@ -101,7 +90,7 @@ void CButton::addOverlay(std::shared_ptr newOverlay) update(); } -void CButton::addImage(std::string filename) +void CButton::addImage(const AnimationPath & filename) { imageNames.push_back(filename); } @@ -251,7 +240,7 @@ void CButton::hover (bool on) } } -CButton::CButton(Point position, const std::string &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): +CButton::CButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): CKeyShortcut(key), callback(Callback) { @@ -287,7 +276,7 @@ void CButton::setIndex(size_t index) if (index == currentImage || index>=imageNames.size()) return; currentImage = index; - auto anim = std::make_shared(imageNames[index]); + auto anim = GH.renderHandler().loadAnimation(imageNames[index]); setImage(anim); } @@ -309,7 +298,6 @@ void CButton::showAll(Canvas & to) { CIntObject::showAll(to); - auto borderColor = stateToBorderColor[getState()]; if (borderColor) to.drawBorder(Rect::createAround(pos, 1), *borderColor); } @@ -377,7 +365,7 @@ void CToggleBase::addCallback(std::function function) callback += function; } -CToggleButton::CToggleButton(Point position, const std::string &defName, const std::pair &help, +CToggleButton::CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList callback, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton), CToggleBase(callback) diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 042d158d8..9eea639fd 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -10,10 +10,9 @@ #pragma once #include "../gui/CIntObject.h" -#include "../render/Colors.h" +#include "../render/EFont.h" #include "../../lib/FunctionList.h" - -#include +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class Rect; @@ -37,15 +36,15 @@ public: BLOCKED=2, HIGHLIGHTED=3 }; -private: - std::vector imageNames;//store list of images that can be used by this button +protected: + std::vector imageNames;//store list of images that can be used by this button size_t currentImage; ButtonState state;//current state of button from enum std::array stateToIndex; // mapping of button state to index of frame in animation std::array hoverTexts; //texts for statusbar, if empty - first entry will be used - std::array, 4> stateToBorderColor; // mapping of button state to border color + std::optional borderColor; // mapping of button state to border color std::string helpBox; //for right-click help std::shared_ptr image; //image for this button @@ -64,24 +63,17 @@ public: hoverable,//if true, button will be highlighted when hovered (e.g. main menu) soundDisabled; - // sets border color for each button state; - // if it's set, the button will have 1-px border around it with this color - void setBorderColor(std::optional normalBorderColor, - std::optional pressedBorderColor, - std::optional blockedBorderColor, - std::optional highlightedBorderColor); - // sets the same border color for all button states. - void setBorderColor(std::optional borderColor); + void setBorderColor(std::optional borderColor); /// adds one more callback to on-click actions - void addCallback(std::function callback); + void addCallback(const std::function & callback); /// adds overlay on top of button image. Only one overlay can be active at once void addOverlay(std::shared_ptr newOverlay); - void addTextOverlay(const std::string & Text, EFonts font, SDL_Color color = Colors::WHITE); + void addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color); - void addImage(std::string filename); + void addImage(const AnimationPath & filename); void addHoverText(ButtonState state, std::string text); void setImageOrder(int state1, int state2, int state3, int state4); @@ -93,7 +85,7 @@ public: bool isHighlighted(); /// Constructor - CButton(Point position, const std::string & defName, const std::pair & help, + CButton(Point position, const AnimationPath & defName, const std::pair & help, CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); /// Appearance modifiers @@ -154,7 +146,7 @@ class CToggleButton : public CButton, public CToggleBase void setEnabled(bool enabled) override; public: - CToggleButton(Point position, const std::string &defName, const std::pair &help, + CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); void clickPressed(const Point & cursorPosition) override; diff --git a/client/widgets/CAltar.cpp b/client/widgets/CAltar.cpp new file mode 100644 index 000000000..a21766618 --- /dev/null +++ b/client/widgets/CAltar.cpp @@ -0,0 +1,466 @@ +/* + * CAltarWindow.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 "CAltar.h" + +#include "../widgets/CAltar.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../../CCallback.h" + +#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGMarket.h" + +CAltar::CAltar(const IMarket * market, const CGHeroInstance * hero) + : CTradeBase(market, hero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + // 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)); + // Total experience on the Altar + texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltar::makeDeal, this)); + expToLevel = std::make_shared(75, 477, FONT_SMALL, ETextAlignment::CENTER); + expForHero = std::make_shared(75, 545, FONT_SMALL, ETextAlignment::CENTER); +} + +void CAltar::deselect() +{ + hLeft = hRight = nullptr; + deal->block(true); +} + +CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) + : CAltar(market, hero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + labels.emplace_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); + labels.emplace_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); + selectedCost = std::make_shared(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + selectedArt = std::make_shared(Point(280, 442)); + + sacrificeAllButton = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), + CGI->generaltexth->zelp[571], std::bind(&CAltar::sacrificeAll, this)); + sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); + + sacrificeBackpackButton = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), + CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this)); + sacrificeBackpackButton->block(hero->artifactsInBackpack.empty()); + + arts = std::make_shared(Point(-365, -11)); + arts->setHero(hero); + + int slotNum = 0; + for(auto & altarSlotPos : posSlotsAltar) + { + auto altarSlot = std::make_shared(altarSlotPos, EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum++); + altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1); + altarSlot->subtitle = ""; + items.front().emplace_back(altarSlot); + } + + calcExpAltarForHero(); + deselect(); +}; + +TExpType CAltarArtifacts::calcExpAltarForHero() +{ + auto artifactsOfHero = std::dynamic_pointer_cast(arts); + TExpType expOnAltar(0); + for(const auto art : artifactsOfHero->artifactsOnAltar) + { + int dmp, expOfArt; + market->getOffer(art->artType->getId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP); + expOnAltar += expOfArt; + } + auto resultExp = hero->calculateXp(expOnAltar); + expForHero->setText(std::to_string(resultExp)); + return resultExp; +} + +void CAltarArtifacts::makeDeal() +{ + std::vector positions; + for(const auto art : arts->artifactsOnAltar) + { + positions.push_back(hero->getSlotByInstance(art)); + } + std::sort(positions.begin(), positions.end()); + std::reverse(positions.begin(), positions.end()); + + LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector(), std::vector(), hero); + arts->artifactsOnAltar.clear(); + + for(auto item : items[0]) + { + item->setID(-1); + item->subtitle = ""; + } + deal->block(true); + calcExpAltarForHero(); +} + +void CAltarArtifacts::sacrificeAll() +{ + std::vector> artsForMove; + for(const auto & slotInfo : arts->getHero()->artifactsWorn) + { + if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) + artsForMove.push_back(slotInfo.second.artifact); + } + for(auto artInst : artsForMove) + moveArtToAltar(nullptr, artInst); + arts->updateWornSlots(); + sacrificeBackpack(); +} + +void CAltarArtifacts::sacrificeBackpack() +{ + while(!arts->visibleArtSet.artifactsInBackpack.empty()) + { + if(!putArtOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact)) + break; + }; + calcExpAltarForHero(); +} + +void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art) +{ + if(art) + { + selectedArt->setArtifact(art); + int dmp, exp; + market->getOffer(art->getTypeId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP); + selectedCost->setText(std::to_string(hero->calculateXp(exp))); + } + else + { + selectedArt->setArtifact(nullptr); + selectedCost->setText(""); + } +} + +void CAltarArtifacts::moveArtToAltar(std::shared_ptr altarSlot, const CArtifactInstance * art) +{ + if(putArtOnAltar(altarSlot, art)) + { + CCS->curh->dragAndDropCursor(nullptr); + arts->unmarkSlots(); + } +} + +std::shared_ptr CAltarArtifacts::getAOHset() const +{ + return arts; +} + +bool CAltarArtifacts::putArtOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art) +{ + if(!art->artType->isTradable()) + { + logGlobal->warn("Cannot put special artifact on altar!"); + return false; + } + + if(!altarSlot || altarSlot->id != -1) + { + int slotIndex = -1; + while(items[0][++slotIndex]->id >= 0 && slotIndex + 1 < items[0].size()); + slotIndex = items[0][slotIndex]->id == -1 ? slotIndex : -1; + if(slotIndex < 0) + { + logGlobal->warn("No free slots on altar!"); + return false; + } + altarSlot = items[0][slotIndex]; + } + + int dmp, exp; + market->getOffer(art->artType->getId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP); + exp = static_cast(hero->calculateXp(exp)); + + arts->artifactsOnAltar.insert(art); + altarSlot->setArtInstance(art); + altarSlot->subtitle = std::to_string(exp); + + deal->block(false); + return true; +}; + +void CAltarArtifacts::onSlotClickPressed(std::shared_ptr altarSlot) +{ + const auto pickedArtInst = arts->getPickedArtifact(); + if(pickedArtInst) + { + arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); + moveArtToAltar(altarSlot, pickedArtInst); + } + else if(const CArtifactInstance * art = altarSlot->getArtInstance()) + { + const auto hero = arts->getHero(); + const auto slot = hero->getSlotByInstance(art); + assert(slot != ArtifactPosition::PRE_FIRST); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), + ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); + arts->pickedArtFromSlot = slot; + arts->artifactsOnAltar.erase(art); + altarSlot->setID(-1); + altarSlot->subtitle.clear(); + deal->block(!arts->artifactsOnAltar.size()); + } + calcExpAltarForHero(); +} + +CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero) + : CAltar(market, hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + labels.emplace_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, + boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); + labels.emplace_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); + texts.emplace_back(std::make_unique(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + lSubtitle = std::make_shared(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + rSubtitle = std::make_shared(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + unitsSlider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarCreatures::onUnitsSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); + maxUnits = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, unitsSlider)); + + unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0); + expPerUnit.resize(GameConstants::ARMY_SIZE, 0); + sacrificeAllButton = std::make_shared( + Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltar::sacrificeAll, this)); + + // Creating slots for hero creatures + for(int slotIdx = 0; slotIdx < GameConstants::ARMY_SIZE; slotIdx++) + { + CreatureID creatureId = CreatureID::NONE; + if(const auto & creature = hero->getCreature(SlotID(slotIdx))) + creatureId = creature->getId(); + else + continue; + + auto heroSlot = std::make_shared(posSlotsHero[slotIdx], EType::CREATURE, creatureId.num, true, slotIdx); + heroSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void + { + onSlotClickPressed(altarSlot, items[0], hLeft, hRight); + }; + heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(slotIdx))); + items[1].emplace_back(heroSlot); + } + + // Creating slots for creatures on altar + assert(items[1].size() <= posSlotsAltar.size()); + for(const auto & heroSlot : items[1]) + { + auto altarSlot = std::make_shared(posSlotsAltar[heroSlot->serial], EType::CREATURE_PLACEHOLDER, heroSlot->id, false, heroSlot->serial); + altarSlot->pos.w = heroSlot->pos.w; altarSlot->pos.h = heroSlot->pos.h; + altarSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void + { + onSlotClickPressed(altarSlot, items[1], hRight, hLeft); + }; + items[0].emplace_back(altarSlot); + } + + readExpValues(); + calcExpAltarForHero(); + deselect(); +}; + +void CAltarCreatures::readExpValues() +{ + int dump; + for(auto heroSlot : items[1]) + { + if(heroSlot->id >= 0) + market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP); + } +} + +void CAltarCreatures::updateControls() +{ + int sliderAmount = 0; + if(hLeft) + { + std::optional lastSlot; + for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++) + { + if(hero->getStackCount(slot) > unitsOnAltar[slot.num]) + { + if(lastSlot.has_value()) + { + lastSlot = std::nullopt; + break; + } + else + { + lastSlot = slot; + } + } + } + sliderAmount = hero->getStackCount(SlotID(hLeft->serial)); + if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial)) + sliderAmount--; + } + unitsSlider->setAmount(sliderAmount); + unitsSlider->block(!unitsSlider->getAmount()); + if(hLeft) + unitsSlider->scrollTo(unitsOnAltar[hLeft->serial]); + maxUnits->block(unitsSlider->getAmount() == 0); +} + +void CAltarCreatures::updateSubtitlesForSelected() +{ + if(hLeft) + lSubtitle->setText(std::to_string(unitsSlider->getValue())); + else + lSubtitle->setText(""); + if(hRight) + rSubtitle->setText(hRight->subtitle); + else + rSubtitle->setText(""); +} + +void CAltarCreatures::updateGarrison() +{ + std::set> empty; + getEmptySlots(empty); + removeItems(empty); + readExpValues(); + for(auto & heroSlot : items[1]) + heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(heroSlot->serial))); +} + +void CAltarCreatures::deselect() +{ + CAltar::deselect(); + unitsSlider->block(true); + maxUnits->block(true); + updateSubtitlesForSelected(); +} + +TExpType CAltarCreatures::calcExpAltarForHero() +{ + TExpType expOnAltar(0); + auto oneUnitExp = expPerUnit.begin(); + for(const auto units : unitsOnAltar) + expOnAltar += *oneUnitExp++ * units; + auto resultExp = hero->calculateXp(expOnAltar); + expForHero->setText(std::to_string(resultExp)); + return resultExp; +} + +void CAltarCreatures::makeDeal() +{ + deselect(); + unitsSlider->scrollTo(0); + expForHero->setText(std::to_string(0)); + + std::vector ids; + std::vector toSacrifice; + + for(int i = 0; i < unitsOnAltar.size(); i++) + { + if(unitsOnAltar[i]) + { + ids.push_back(SlotID(i)); + toSacrifice.push_back(unitsOnAltar[i]); + } + } + + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero); + + for(int & units : unitsOnAltar) + units = 0; + + for(auto heroSlot : items[0]) + { + heroSlot->setType(CREATURE_PLACEHOLDER); + heroSlot->subtitle = ""; + } +} + +void CAltarCreatures::sacrificeAll() +{ + std::optional lastSlot; + for(auto heroSlot : items[1]) + { + auto stackCount = hero->getStackCount(SlotID(heroSlot->serial)); + if(stackCount > unitsOnAltar[heroSlot->serial]) + { + if(!lastSlot.has_value()) + lastSlot = SlotID(heroSlot->serial); + unitsOnAltar[heroSlot->serial] = stackCount; + } + } + assert(lastSlot.has_value()); + unitsOnAltar[lastSlot.value().num]--; + + if(hRight) + unitsSlider->scrollTo(unitsOnAltar[hRight->serial]); + for(auto altarSlot : items[0]) + updateAltarSlot(altarSlot); + updateSubtitlesForSelected(); + + deal->block(calcExpAltarForHero() == 0); +} + +void CAltarCreatures::updateAltarSlot(std::shared_ptr slot) +{ + auto units = unitsOnAltar[slot->serial]; + slot->setType(units > 0 ? CREATURE : CREATURE_PLACEHOLDER); + slot->subtitle = units > 0 ? + boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : ""; +} + +void CAltarCreatures::onUnitsSliderMoved(int newVal) +{ + if(hLeft) + unitsOnAltar[hLeft->serial] = newVal; + if(hRight) + updateAltarSlot(hRight); + deal->block(calcExpAltarForHero() == 0); + updateControls(); + updateSubtitlesForSelected(); +} + +void CAltarCreatures::onSlotClickPressed(std::shared_ptr altarSlot, + std::vector> & oppositeSlots, + std::shared_ptr & hCurSide, std::shared_ptr & hOppSide) +{ + std::shared_ptr oppositeSlot; + for(const auto & slot : oppositeSlots) + if(slot->serial == altarSlot->serial) + { + oppositeSlot = slot; + break; + } + + if(hCurSide != altarSlot && oppositeSlot) + { + hCurSide = altarSlot; + hOppSide = oppositeSlot; + updateControls(); + updateSubtitlesForSelected(); + redraw(); + } +} diff --git a/client/widgets/CAltar.h b/client/widgets/CAltar.h new file mode 100644 index 000000000..685afa731 --- /dev/null +++ b/client/widgets/CAltar.h @@ -0,0 +1,103 @@ +/* + * CAltar.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 "../widgets/CArtifactsOfHeroAltar.h" +#include "../widgets/CTradeBase.h" + +class CSlider; + +class CAltar : public CTradeBase, public CIntObject +{ +public: + std::shared_ptr expToLevel; + std::shared_ptr expForHero; + std::shared_ptr sacrificeAllButton; + + CAltar(const IMarket * market, const CGHeroInstance * hero); + virtual ~CAltar() = default; + virtual void sacrificeAll() = 0; + virtual void deselect(); + virtual TExpType calcExpAltarForHero() = 0; +}; + +class CAltarArtifacts : public CAltar +{ +public: + CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void sacrificeBackpack(); + void setSelectedArtifact(const CArtifactInstance * art); + void moveArtToAltar(std::shared_ptr, const CArtifactInstance * art); + std::shared_ptr getAOHset() const; + +private: + std::shared_ptr selectedArt; + std::shared_ptr selectedCost; + std::shared_ptr sacrificeBackpackButton; + std::shared_ptr arts; + + const std::vector posSlotsAltar = + { + Point(317, 53), Point(371, 53), Point(425, 53), + Point(479, 53), Point(533, 53), Point(317, 123), + Point(371, 123), Point(425, 123), Point(479, 123), + Point(533, 123), Point(317, 193), Point(371, 193), + Point(425, 193), Point(479, 193), Point(533, 193), + Point(317, 263), Point(371, 263), Point(425, 263), + Point(479, 263), Point(533, 263), Point(398, 333), + Point(452, 333) + }; + + bool putArtOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); + void onSlotClickPressed(std::shared_ptr altarSlot); +}; + +class CAltarCreatures : public CAltar +{ +public: + CAltarCreatures(const IMarket * market, const CGHeroInstance * hero); + void updateGarrison(); + void deselect() override; + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void updateAltarSlot(std::shared_ptr slot); + +private: + std::shared_ptr maxUnits; + std::shared_ptr unitsSlider; + std::vector unitsOnAltar; + std::vector expPerUnit; + std::shared_ptr lSubtitle, rSubtitle; + + const std::vector posSlotsAltar = + { + Point(334, 110), Point(417, 110), Point(500, 110), + Point(334, 208), Point(417, 208), Point(500, 208), + Point(417, 306) + }; + const std::vector posSlotsHero = + { + Point(45, 110), Point(128, 110), Point(211, 110), + Point(45, 208), Point(128, 208), Point(211, 208), + Point(128, 306) + }; + + void readExpValues(); + void updateControls(); + void updateSubtitlesForSelected(); + void onUnitsSliderMoved(int newVal); + void onSlotClickPressed(std::shared_ptr altarSlot, + std::vector> & oppositeSlots, + std::shared_ptr & hCurSide, std::shared_ptr & hOppSide); +}; diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 5e443d959..334cd9f6e 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -18,6 +18,7 @@ #include "../windows/GUIClasses.h" #include "../render/Canvas.h" #include "../render/Colors.h" +#include "../render/IRenderHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" @@ -25,10 +26,11 @@ #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) { - baseType = -1; // By default we don't store any component ourArt = artInst; if(!artInst) { @@ -37,34 +39,62 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) hoverText = CGI->generaltexth->allTexts[507]; return; } - image->enable(); - image->setFrame(artInst->artType->getIconIndex()); + + imageIndex = artInst->artType->getIconIndex(); if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL) { auto spellID = artInst->getScrollSpellID(); - if(spellID.num >= 0) + assert(spellID.num >= 0); + + if(settings["general"]["enableUiEnhancements"].Bool()) { - // Add spell component info (used to provide a pic in r-click popup) - baseType = CComponent::spell; - type = spellID; - bonusValue = 0; + 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 { - baseType = CComponent::artifact; - type = artInst->getTypeId(); - bonusValue = 0; + 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) - : ourArt(Art) +CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) + : ourArt(art) + , locked(false) { - image = nullptr; pos += position; pos.w = pos.h = 44; + + 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(); + + selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION, 0, -1, -1); + selection->visible = false; } const CArtifactInstance * CArtPlace::getArt() @@ -72,26 +102,12 @@ const CArtifactInstance * CArtPlace::getArt() return ourArt; } -CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art) - : CArtPlace(position, Art), +CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art) + : CArtPlace(position, art), commanderOwner(commanderOwner), commanderSlotID(artSlot.num) { - createImage(); - setArtifact(Art); -} - -void CCommanderArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - - int imageIndex = 0; - if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared("artifact", imageIndex); - if(!ourArt) - image->disable(); + setArtifact(art); } void CCommanderArtPlace::returnArtToHeroCallback() @@ -104,10 +120,11 @@ void CCommanderArtPlace::returnArtToHeroCallback() } else { - ArtifactLocation src(commanderOwner->commander.get(), artifactPos); - ArtifactLocation dst(commanderOwner, freeSlot); + ArtifactLocation src(commanderOwner->id, artifactPos); + src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + ArtifactLocation dst(commanderOwner->id, freeSlot); - if(ourArt->canBePutAt(dst, true)) + if(ourArt->canBePutAt(commanderOwner, freeSlot, true)) { LOCPLINT->cb->swapArtifacts(src, dst); setArtifact(nullptr); @@ -128,20 +145,12 @@ void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition) CArtPlace::showPopupWindow(cursorPosition); } -void CCommanderArtPlace::setArtifact(const CArtifactInstance * art) +CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * art) + : CArtPlace(position, art) { - setInternals(art); } -CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * Art) - : CArtPlace(position, Art), - locked(false), - marked(false) -{ - createImage(); -} - -void CHeroArtPlace::lockSlot(bool on) +void CArtPlace::lockSlot(bool on) { if(locked == on) return; @@ -151,31 +160,24 @@ void CHeroArtPlace::lockSlot(bool on) if(on) image->setFrame(ArtifactID::ART_LOCK); else if(ourArt) - image->setFrame(ourArt->artType->getIconIndex()); + image->setFrame(imageIndex); else image->setFrame(0); } -bool CHeroArtPlace::isLocked() +bool CArtPlace::isLocked() const { return locked; } -void CHeroArtPlace::selectSlot(bool on) +void CArtPlace::selectSlot(bool on) { - if(marked == on) - return; - - marked = on; - if(on) - selection->enable(); - else - selection->disable(); + selection->visible = on; } -bool CHeroArtPlace::isMarked() const +bool CArtPlace::isSelected() const { - return marked; + return selection->visible; } void CHeroArtPlace::clickPressed(const Point & cursorPosition) @@ -186,27 +188,22 @@ void CHeroArtPlace::clickPressed(const Point & cursorPosition) void CHeroArtPlace::showPopupWindow(const Point & cursorPosition) { - if(rightClickCallback) - rightClickCallback(*this); + if(showPopupCallback) + showPopupCallback(*this); } -void CHeroArtPlace::showAll(Canvas & to) +void CArtPlace::showAll(Canvas & to) { - if(ourArt) - { - CIntObject::showAll(to); - } - - if(marked && isActive()) - to.drawBorder(pos, Colors::BRIGHT_YELLOW); + CIntObject::showAll(to); + selection->showAll(to); } -void CHeroArtPlace::setArtifact(const CArtifactInstance * art) +void CArtPlace::setArtifact(const CArtifactInstance * art) { setInternals(art); if(art) { - image->setFrame(locked ? ArtifactID::ART_LOCK : art->artType->getIconIndex()); + image->setFrame(locked ? static_cast(ArtifactID::ART_LOCK) : imageIndex); if(locked) // Locks should appear as empty. hoverText = CGI->generaltexth->allTexts[507]; @@ -236,41 +233,34 @@ void CHeroArtPlace::addCombinedArtInfo(std::map & arts) } } -void CHeroArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - si32 imageIndex = 0; - - if(locked) - imageIndex = ArtifactID::ART_LOCK; - else if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared("artifact", imageIndex); - if(!ourArt) - image->disable(); - - selection = std::make_shared("artifact", ArtifactID::ART_SELECTION); - selection->disable(); -} - bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot) { assert(hero); const auto art = hero->getArt(slot); assert(art); - auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), ArtifactUtils::isSlotEquipment(slot)); - for(const auto combinedArt : assemblyPossibilities) + if(hero->tempOwner != LOCPLINT->playerID) + return false; + + auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId()); + if(!assemblyPossibilities.empty()) { - LOCPLINT->showArtifactAssemblyDialog( - art->artType, - combinedArt, - std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId())); + 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()); - if(assemblyPossibilities.size() > 2) - logGlobal->warn("More than one possibility of assembling on %s... taking only first", art->artType->getNameTranslated()); + LOCPLINT->showArtifactAssemblyDialog(art->artType, combinedArt, onYesHandlers); + LOCPLINT->waitWhileDialog(); + if(assembleConfirmed) + break; + } + }); + askThread->detach(); return true; } return false; @@ -282,6 +272,9 @@ bool ArtifactUtilsClient::askToDisassemble(const CGHeroInstance * hero, const Ar 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)) diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index d8cc43aba..43bc4e729 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -19,7 +19,6 @@ class CArtifactSet; VCMI_LIB_NAMESPACE_END class CAnimImage; -class CButton; class CArtifactHolder { @@ -32,18 +31,24 @@ public: class CArtPlace : public LRClickableAreaWTextComp { +public: + CArtPlace(Point position, const CArtifactInstance * art = nullptr); + const CArtifactInstance* getArt(); + void lockSlot(bool on); + bool isLocked() const; + void selectSlot(bool on); + bool isSelected() const; + void showAll(Canvas & to) override; + void setArtifact(const CArtifactInstance * art); + protected: std::shared_ptr image; const CArtifactInstance * ourArt; + int imageIndex; + std::shared_ptr selection; + bool locked; void setInternals(const CArtifactInstance * artInst); - virtual void createImage()=0; - -public: - CArtPlace(Point position, const CArtifactInstance * Art = nullptr); - const CArtifactInstance * getArt(); - - virtual void setArtifact(const CArtifactInstance * art)=0; }; class CCommanderArtPlace : public CArtPlace @@ -52,42 +57,27 @@ protected: const CGHeroInstance * commanderOwner; ArtifactPosition commanderSlotID; - void createImage() override; void returnArtToHeroCallback(); public: - CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art = nullptr); + CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art = nullptr); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - void setArtifact(const CArtifactInstance * art) override; }; class CHeroArtPlace: public CArtPlace { public: - using ClickHandler = std::function; + using ClickFunctor = std::function; ArtifactPosition slot; - ClickHandler leftClickCallback; - ClickHandler rightClickCallback; + ClickFunctor leftClickCallback; + ClickFunctor showPopupCallback; - CHeroArtPlace(Point position, const CArtifactInstance * Art = nullptr); - void lockSlot(bool on); - bool isLocked(); - void selectSlot(bool on); - bool isMarked() const; + CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - void showAll(Canvas & to) override; - void setArtifact(const CArtifactInstance * art) override; void addCombinedArtInfo(std::map & arts); - -protected: - std::shared_ptr selection; - bool locked; - bool marked; - - void createImage() override; }; namespace ArtifactUtilsClient diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 190eb0cbf..a104974a2 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -10,12 +10,14 @@ #include "StdInc.h" #include "CArtifactsOfHeroAltar.h" +#include "Buttons.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) : visibleArtSet(ArtBearer::ArtBearer::HERO) @@ -26,6 +28,12 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) position, std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1)); pickedArtFromSlot = ArtifactPosition::PRE_FIRST; + + // The backpack is in the altar window above and to the right + for(auto & slot : backpack) + slot->moveBy(Point(2, -1)); + leftBackpackRoll->moveBy(Point(2, -1)); + rightBackpackRoll->moveBy(Point(2, -1)); }; CArtifactsOfHeroAltar::~CArtifactsOfHeroAltar() @@ -58,7 +66,7 @@ void CArtifactsOfHeroAltar::updateBackpackSlots() void CArtifactsOfHeroAltar::scrollBackpack(int offset) { CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, visibleArtSet); - safeRedraw(); + redraw(); } void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace) @@ -71,7 +79,7 @@ void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace) if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot)) pickedArtFromSlot = curHero->getSlotByInstance(art); assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, pickedArtFromSlot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, pickedArtFromSlot), ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } } @@ -88,7 +96,7 @@ void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot) if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS) { assert(curHero->getSlot(slot)->getArt()); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, slot), ArtifactLocation(curHero, pickedArtFromSlot)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, slot), ArtifactLocation(curHero->id, pickedArtFromSlot)); pickedArtFromSlot = ArtifactPosition::PRE_FIRST; } } @@ -109,4 +117,4 @@ void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst) getArtPlace(part.slot)->setArtifact(nullptr); } } -} \ No newline at end of file +} diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index ed25c1357..6a487799c 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -18,6 +18,7 @@ #include "../CPlayerInterface.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../CCallback.h" @@ -38,11 +39,11 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position) { const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % HERO_BACKPACK_WINDOW_SLOT_COLUMNS), slotSizeWithMargin * (artPlaceIdx / HERO_BACKPACK_WINDOW_SLOT_COLUMNS)); - backpackSlotsBackgrounds.emplace_back(std::make_shared("heroWindow/artifactSlotEmpty", pos)); + backpackSlotsBackgrounds.emplace_back(std::make_shared(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos)); artPlace = std::make_shared(pos); artPlace->setArtifact(nullptr); artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); artPlaceIdx++; } @@ -79,8 +80,8 @@ void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, co void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } void CArtifactsOfHeroBackpack::scrollBackpack(int offset) @@ -88,7 +89,7 @@ void CArtifactsOfHeroBackpack::scrollBackpack(int offset) if(backpackListBox) backpackListBox->resize(getActiveSlotRowsNum()); backpackPos += offset; - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + backpackPos); + auto slot = ArtifactPosition::BACKPACK_START + backpackPos; for(auto artPlace : backpack) { setSlotData(artPlace, slot, *curHero); diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 799135837..319c40e2f 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -22,6 +22,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroBase::CArtifactsOfHeroBase() : backpackPos(0), @@ -38,11 +39,11 @@ void CArtifactsOfHeroBase::putBackPickedArtifact() auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId()); if(slot == ArtifactPosition::PRE_FIRST) { - LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } else { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero, slot)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero->id, slot)); } } if(putBackPickedArtCallback) @@ -55,15 +56,15 @@ void CArtifactsOfHeroBase::setPutBackPickedArtifactCallback(PutBackPickedArtCall } void CArtifactsOfHeroBase::init( - CHeroArtPlace::ClickHandler lClickCallback, - CHeroArtPlace::ClickHandler rClickCallback, + CHeroArtPlace::ClickFunctor lClickCallback, + CHeroArtPlace::ClickFunctor showPopupCallback, const Point & position, - BpackScrollHandler scrollHandler) + BpackScrollFunctor scrollCallback) { // CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION_CAPTURING is removed OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); pos += position; - for(int g = 0; g < GameConstants::BACKPACK_START; g++) + for(int g = 0; g < ArtifactPosition::BACKPACK_START; g++) { artWorn[ArtifactPosition(g)] = std::make_shared(slotPos[g]); } @@ -78,18 +79,20 @@ void CArtifactsOfHeroBase::init( artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); artPlace.second->leftClickCallback = lClickCallback; - artPlace.second->rightClickCallback = rClickCallback; + artPlace.second->showPopupCallback = showPopupCallback; } for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); artPlace->leftClickCallback = lClickCallback; - artPlace->rightClickCallback = rClickCallback; + artPlace->showPopupCallback = showPopupCallback; } - leftBackpackRoll = std::make_shared(Point(379, 364), "hsbtns3.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(-1); }, EShortcut::MOVE_LEFT); - rightBackpackRoll = std::make_shared(Point(632, 364), "hsbtns5.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT); + leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(-1);}, EShortcut::MOVE_LEFT); + rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(+1);}, EShortcut::MOVE_RIGHT); leftBackpackRoll->block(true); rightBackpackRoll->block(true); + + setRedrawParent(true); } void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace) @@ -100,8 +103,8 @@ void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace) void CArtifactsOfHeroBase::rightClickArtPlace(CHeroArtPlace & artPlace) { - if(rightClickCallback) - rightClickCallback(*this, artPlace); + if(showPopupCallback) + showPopupCallback(*this, artPlace); } void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) @@ -127,7 +130,7 @@ const CGHeroInstance * CArtifactsOfHeroBase::getHero() const void CArtifactsOfHeroBase::scrollBackpack(int offset) { scrollBackpackForArtSet(offset, *curHero); - safeRedraw(); + redraw(); } void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSet & artSet) @@ -143,7 +146,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe }; slotInc inc_ring = [artsInBackpack](ArtifactPosition & slot) -> ArtifactPosition { - return ArtifactPosition(GameConstants::BACKPACK_START + (slot - GameConstants::BACKPACK_START + 1) % artsInBackpack); + return ArtifactPosition::BACKPACK_START + (slot - ArtifactPosition::BACKPACK_START + 1) % artsInBackpack; }; slotInc inc; if(scrollingPossible) @@ -158,7 +161,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe if(artsInBackpack) backpackPos %= artsInBackpack; - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + backpackPos); + auto slot = ArtifactPosition(ArtifactPosition::BACKPACK_START + backpackPos); for(auto artPlace : backpack) { setSlotData(artPlace, slot, artSet); @@ -172,21 +175,10 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe rightBackpackRoll->block(!scrollingPossible); } -void CArtifactsOfHeroBase::safeRedraw() -{ - if(isActive()) - { - if(parent) - parent->redraw(); - else - redraw(); - } -} - void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved) { for(auto artPlace : artWorn) - artPlace.second->selectSlot(art->artType->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); + artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); } void CArtifactsOfHeroBase::unmarkSlots() @@ -270,8 +262,10 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit { arts.insert(std::pair(combinedArt, 0)); for(const auto part : combinedArt->getConstituents()) - if(artSet.hasArt(part->getId(), true)) + { + if(artSet.hasArt(part->getId(), false)) arts.at(combinedArt)++; + } } artPlace->addCombinedArtInfo(arts); } diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index f7da73d79..a85b8ebff 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -11,19 +11,21 @@ #include "CArtifactHolder.h" +class CButton; + class CArtifactsOfHeroBase : public CIntObject { protected: using ArtPlacePtr = std::shared_ptr; - using BpackScrollHandler = std::function; + using BpackScrollFunctor = std::function; public: using ArtPlaceMap = std::map; - using ClickHandler = std::function; + using ClickFunctor = std::function; using PutBackPickedArtCallback = std::function; - ClickHandler leftClickCallback; - ClickHandler rightClickCallback; + ClickFunctor leftClickCallback; + ClickFunctor showPopupCallback; CArtifactsOfHeroBase(); virtual void putBackPickedArtifact(); @@ -33,7 +35,6 @@ public: virtual void setHero(const CGHeroInstance * hero); virtual const CGHeroInstance * getHero() const; virtual void scrollBackpack(int offset); - virtual void safeRedraw(); virtual void markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved = true); virtual void unmarkSlots(); virtual ArtPlacePtr getArtPlace(const ArtifactPosition & slot); @@ -53,17 +54,17 @@ protected: const std::vector slotPos = { - Point(509,30), Point(567,240), Point(509,80), //0-2 - Point(383,68), Point(564,183), Point(509,130), //3-5 - Point(431,68), Point(610,183), Point(515,295), //6-8 - Point(383,143), Point(399,194), Point(415,245), //9-11 - Point(431,296), Point(564,30), Point(610,30), //12-14 + Point(509,30), Point(568,242), Point(509,80), //0-2 + Point(383,69), Point(562,184), Point(509,131), //3-5 + Point(431,69), Point(610,184), Point(515,295), //6-8 + Point(383,143), Point(399,193), Point(415,244), //9-11 + Point(431,295), Point(564,30), Point(610,30), //12-14 Point(610,76), Point(610,122), Point(610,310), //15-17 - Point(381,296) //18 + Point(381,295) //18 }; - virtual void init(CHeroArtPlace::ClickHandler lClickCallback, CHeroArtPlace::ClickHandler rClickCallback, - const Point & position, BpackScrollHandler scrollHandler); + virtual void init(CHeroArtPlace::ClickFunctor lClickCallback, CHeroArtPlace::ClickFunctor showPopupCallback, + const Point & position, BpackScrollFunctor scrollCallback); // Assigns an artifacts to an artifact place depending on it's new slot ID virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet); virtual void scrollBackpackForArtSet(int offset, const CArtifactSet & artSet); diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index 059f196fa..14fd222f4 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -13,8 +13,10 @@ #include "Buttons.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector Backpack, std::shared_ptr leftScroll, std::shared_ptr rightScroll) @@ -29,16 +31,18 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); artPlace.second->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace.second->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace.second->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); } for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); } leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1)); rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1)); + + setRedrawParent(true); } CArtifactsOfHeroKingdom::~CArtifactsOfHeroKingdom() @@ -53,7 +57,7 @@ void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, con void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index fd8e78b3a..b4ac981b0 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -11,8 +11,10 @@ #include "CArtifactsOfHeroMain.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { @@ -35,6 +37,6 @@ void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); -} \ No newline at end of file + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); +} diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index 74878b348..94d04f512 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -30,12 +30,12 @@ void CArtifactsOfHeroMarket::scrollBackpack(int offset) { for(auto & artPlace : backpack) { - if(artPlace->isMarked()) + if(artPlace->isSelected()) { selectArtCallback(artPlace.get()); break; } } } - safeRedraw(); + redraw(); } \ No newline at end of file diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 1dbba2257..b3f725746 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -21,6 +21,8 @@ #include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../render/Canvas.h" +#include "../render/IFont.h" +#include "../render/Graphics.h" #include "../windows/CMessage.h" #include "../windows/InfoWindows.h" #include "../widgets/TextControls.h" @@ -28,42 +30,44 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.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/NetPacksBase.h" #include "../../lib/CArtHandler.h" #include "../../lib/CArtifactInstance.h" -CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font): - perDay(false) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font) { - init(Type, Subtype, Val, imageSize, font); + init(Type, Subtype, Val, imageSize, font, ""); +} + +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize, EFonts font) +{ + init(Type, Subtype, std::nullopt, imageSize, font, Val); } CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) - : perDay(false) { - if(c.id == Component::EComponentType::RESOURCE && c.when==-1) - perDay = true; - - init((Etype)c.id, c.subtype, c.val, imageSize, font); + init(c.type, c.subType, c.value, imageSize, font, ""); } -void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt) +void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, const std::string & ValText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); addUsedEvents(SHOW_POPUP); - compType = Type; - subtype = Subtype; - val = Val; + data.type = Type; + data.subType = Subtype; + data.value = Val; + + customSubtitle = ValText; size = imageSize; font = fnt; - assert(compType < typeInvalid); assert(size < sizeInvalid); setSurface(getFileName()[size], (int)getIndex()); @@ -84,6 +88,9 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts if (size < small) max = 30; + if(Type == ComponentType::RESOURCE && !ValText.empty()) + max = 80; + std::vector textLines = CMessage::breakText(getSubtitle(), std::max(max, pos.w), font); for(auto & line : textLines) { @@ -100,156 +107,212 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts } } -const std::vector CComponent::getFileName() +std::vector CComponent::getFileName() const { - static const std::string primSkillsArr [] = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; - static const std::string secSkillsArr [] = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; - static const std::string resourceArr [] = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; - static const std::string creatureArr [] = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; - static const std::string artifactArr[] = {"Artifact", "Artifact", "Artifact", "Artifact"}; - static const std::string spellsArr [] = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; - static const std::string moraleArr [] = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; - static const std::string luckArr [] = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; - static const std::string heroArr [] = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; - static const std::string flagArr [] = {"CREST58", "CREST58", "CREST58", "CREST58"}; + static const std::array primSkillsArr = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; + static const std::array secSkillsArr = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; + static const std::array resourceArr = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; + static const std::array creatureArr = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; + static const std::array artifactArr = {"Artifact", "Artifact", "Artifact", "Artifact"}; + static const std::array spellsArr = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; + static const std::array moraleArr = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; + static const std::array luckArr = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; + static const std::array heroArr = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; + static const std::array flagArr = {"CREST58", "CREST58", "CREST58", "CREST58"}; - auto gen = [](const std::string * arr) + auto gen = [](const std::array & arr) -> std::vector { - return std::vector(arr, arr + 4); + return { AnimationPath::builtin(arr[0]), AnimationPath::builtin(arr[1]), AnimationPath::builtin(arr[2]), AnimationPath::builtin(arr[3]) }; }; - switch(compType) + switch(data.type) { - case primskill: return gen(primSkillsArr); - case secskill: return gen(secSkillsArr); - case resource: return gen(resourceArr); - case creature: return gen(creatureArr); - case artifact: return gen(artifactArr); - case experience: return gen(primSkillsArr); - case spell: return gen(spellsArr); - case morale: return gen(moraleArr); - case luck: return gen(luckArr); - case building: return std::vector(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons); - case hero: return gen(heroArr); - case flag: return gen(flagArr); + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::MANA: + case ComponentType::LEVEL: + return gen(primSkillsArr); + case ComponentType::SEC_SKILL: + return gen(secSkillsArr); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return gen(resourceArr); + case ComponentType::CREATURE: + return gen(creatureArr); + case ComponentType::ARTIFACT: + return gen(artifactArr); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return gen(spellsArr); + case ComponentType::MORALE: + return gen(moraleArr); + case ComponentType::LUCK: + return gen(luckArr); + case ComponentType::BUILDING: + return std::vector(4, (*CGI->townh)[data.subType.as().getFaction()]->town->clientInfo.buildingsIcons); + case ComponentType::HERO_PORTRAIT: + return gen(heroArr); + case ComponentType::FLAG: + return gen(flagArr); + default: + assert(0); + return {}; } - assert(0); - return std::vector(); } -size_t CComponent::getIndex() +size_t CComponent::getIndex() const { - switch(compType) + switch(data.type) { - case primskill: return subtype; - case secskill: return subtype*3 + 3 + val - 1; - case resource: return subtype; - case creature: return CGI->creatures()->getByIndex(subtype)->getIconIndex(); - case artifact: return CGI->artifacts()->getByIndex(subtype)->getIconIndex(); - case experience: return 4; - case spell: return (size < large) ? subtype + 1 : subtype; - case morale: return val+3; - case luck: return val+3; - case building: return val; - case hero: return subtype; - case flag: return subtype; + case ComponentType::PRIM_SKILL: + return data.subType.getNum(); + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return 4; // for whatever reason, in H3 experience icon is located in primary skills icons + case ComponentType::MANA: + return 5; // for whatever reason, in H3 mana points icon is located in primary skills icons + case ComponentType::SEC_SKILL: + return data.subType.getNum() * 3 + 3 + data.value.value_or(0) - 1; + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return data.subType.getNum(); + case ComponentType::CREATURE: + return CGI->creatures()->getById(data.subType.as())->getIconIndex(); + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getIconIndex(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return (size < large) ? data.subType.getNum() + 1 : data.subType.getNum(); + case ComponentType::MORALE: + return data.value.value_or(0) + 3; + case ComponentType::LUCK: + return data.value.value_or(0) + 3; + case ComponentType::BUILDING: + return data.subType.as().getBuilding(); + case ComponentType::HERO_PORTRAIT: + return CGI->heroTypes()->getById(data.subType.as())->getIconIndex(); + case ComponentType::FLAG: + return data.subType.getNum(); + default: + assert(0); + return 0; } - assert(0); - return 0; } -std::string CComponent::getDescription() +std::string CComponent::getDescription() const { - switch(compType) + switch(data.type) { - case primskill: return (subtype < 4)? CGI->generaltexth->arraytxt[2+subtype] //Primary skill - : CGI->generaltexth->allTexts[149]; //mana - case secskill: return CGI->skillh->getByIndex(subtype)->getDescriptionTranslated(val); - case resource: return CGI->generaltexth->allTexts[242]; - case creature: return ""; - case artifact: - { - auto artID = ArtifactID(subtype); - auto description = VLC->arth->objects[artID]->getDescriptionTranslated(); - if(artID == ArtifactID::SPELL_SCROLL) + case ComponentType::PRIM_SKILL: + return CGI->generaltexth->arraytxt[2+data.subType.getNum()]; + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return CGI->generaltexth->allTexts[241]; + case ComponentType::MANA: + return CGI->generaltexth->allTexts[149]; + case ComponentType::SEC_SKILL: + return CGI->skillh->getByIndex(data.subType.getNum())->getDescriptionTranslated(data.value.value_or(0)); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return CGI->generaltexth->allTexts[242]; + case ComponentType::CREATURE: + return ""; + case ComponentType::ARTIFACT: + return VLC->artifacts()->getById(data.subType.as())->getDescriptionTranslated(); + case ComponentType::SPELL_SCROLL: { - ArtifactUtils::insertScrrollSpellName(description, SpellID(val)); + auto description = VLC->arth->objects[ArtifactID::SPELL_SCROLL]->getDescriptionTranslated(); + ArtifactUtils::insertScrrollSpellName(description, data.subType.as()); + return description; } - return description; - } - case experience: return CGI->generaltexth->allTexts[241]; - case spell: return (*CGI->spellh)[subtype]->getDescriptionTranslated(val); - case morale: return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)]; - case luck: return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)]; - case building: return (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]->getDescriptionTranslated(); - case hero: return ""; - case flag: return ""; - } - assert(0); - return ""; -} - -std::string CComponent::getSubtitle() -{ - if(!perDay) - return getSubtitleInternal(); - - std::string ret = CGI->generaltexth->allTexts[3]; - boost::replace_first(ret, "%d", getSubtitleInternal()); - return ret; -} - -std::string CComponent::getSubtitleInternal() -{ - //FIXME: some of these are horrible (e.g creature) - switch(compType) - { - case primskill: return boost::str(boost::format("%+d %s") % val % (subtype < 4 ? CGI->generaltexth->primarySkillNames[subtype] : CGI->generaltexth->allTexts[387])); - case secskill: return CGI->generaltexth->levels[val-1] + "\n" + CGI->skillh->getByIndex(subtype)->getNameTranslated(); - case resource: return std::to_string(val); - case creature: + case ComponentType::SPELL: + return VLC->spells()->getById(data.subType.as())->getDescriptionTranslated(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: + return CGI->generaltexth->heroscrn[ 7 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; + case ComponentType::BUILDING: { - auto creature = CGI->creh->getByIndex(subtype); - if ( val ) - return std::to_string(val) + " " + (val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); + auto index = data.subType.as(); + return (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]->getDescriptionTranslated(); + } + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return ""; + default: + assert(0); + return ""; + } +} + +std::string CComponent::getSubtitle() const +{ + if (!customSubtitle.empty()) + return customSubtitle; + + switch(data.type) + { + case ComponentType::PRIM_SKILL: + if (data.value) + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->primarySkillNames[data.subType.getNum()]); else - return val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated(); - } - case artifact: return CGI->artifacts()->getByIndex(subtype)->getNameTranslated(); - case experience: + return CGI->generaltexth->primarySkillNames[data.subType.getNum()]; + case ComponentType::EXPERIENCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::LEVEL: { - if(subtype == 1) //+1 level - tree of knowledge - { - std::string level = CGI->generaltexth->allTexts[442]; - boost::replace_first(level, "1", std::to_string(val)); - return level; - } + std::string level = CGI->generaltexth->allTexts[442]; + boost::replace_first(level, "1", std::to_string(data.value.value_or(0))); + return level; + } + case ComponentType::MANA: + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->allTexts[387]); + case ComponentType::SEC_SKILL: + return CGI->generaltexth->levels[data.value.value_or(0)-1] + "\n" + CGI->skillh->getById(data.subType.as())->getNameTranslated(); + case ComponentType::RESOURCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::RESOURCE_PER_DAY: + return boost::str(boost::format(CGI->generaltexth->allTexts[3]) % data.value.value_or(0)); + case ComponentType::CREATURE: + { + auto creature = CGI->creh->getById(data.subType.as()); + if(data.value) + return std::to_string(*data.value) + " " + (*data.value > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); else - { - return std::to_string(val); //amount of experience OR level required for seer hut; - } + return creature->getNamePluralTranslated(); } - case spell: return CGI->spells()->getByIndex(subtype)->getNameTranslated(); - case morale: return ""; - case luck: return ""; - case building: - { - auto building = (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]; - if(!building) + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return CGI->spells()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::MORALE: + return ""; + case ComponentType::LUCK: + return ""; + case ComponentType::BUILDING: { - logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[subtype]->town->faction->getNameTranslated(), val); - return (boost::format("Missing building #%d") % val).str(); + auto index = data.subType.as(); + auto building = (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]; + if(!building) + { + logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[index.getFaction()]->town->faction->getNameTranslated(), index.getBuilding().getNum()); + return (boost::format("Missing building #%d") % index.getBuilding().getNum()).str(); + } + return building->getNameTranslated(); } - return building->getNameTranslated(); - } - case hero: return ""; - case flag: return CGI->generaltexth->capColors[subtype]; + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return CGI->generaltexth->capColors[data.subType.as().getNum()]; + default: + assert(0); + return ""; } - logGlobal->error("Invalid CComponent type: %d", (int)compType); - return ""; } -void CComponent::setSurface(std::string defName, int imgPos) +void CComponent::setSurface(const AnimationPath & defName, int imgPos) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); image = std::make_shared(defName, imgPos); @@ -267,6 +330,12 @@ void CSelectableComponent::clickPressed(const Point & cursorPosition) onSelect(); } +void CSelectableComponent::clickDouble(const Point & cursorPosition) +{ + if(onChoose) + onChoose(); +} + void CSelectableComponent::init() { selected = false; @@ -276,15 +345,15 @@ CSelectableComponent::CSelectableComponent(const Component &c, std::function OnSelect): +CSelectableComponent::CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize, std::function OnSelect): CComponent(Type,Sub,Val, imageSize),onSelect(OnSelect) { setRedrawParent(true); - addUsedEvents(LCLICK | KEYBOARD); + addUsedEvents(LCLICK | DOUBLECLICK | KEYBOARD); init(); } diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 5e32b6fcd..f41ea3264 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -10,6 +10,9 @@ #pragma once #include "../gui/CIntObject.h" +#include "../render/EFont.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,11 +27,6 @@ class CLabel; class CComponent : public virtual CIntObject { public: - enum Etype - { - primskill, secskill, resource, creature, artifact, experience, spell, morale, luck, building, hero, flag, typeInvalid - }; - //NOTE: not all types have exact these sizes or have less than 4 of them. In such cases closest one will be used enum ESize { @@ -42,27 +40,24 @@ public: private: std::vector> lines; - size_t getIndex(); - const std::vector getFileName(); - void setSurface(std::string defName, int imgPos); - std::string getSubtitleInternal(); + size_t getIndex() const; + std::vector getFileName() const; + void setSurface(const AnimationPath & defName, int imgPos); - void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL); + void init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font, const std::string & ValText); public: std::shared_ptr image; - - Etype compType; //component type + Component data; + std::string customSubtitle; ESize size; //component size. EFonts font; //Font size of label - int subtype; //type-dependant subtype. See getSomething methods for details - int val; // value \ strength \ amount of component. See getSomething methods for details - bool perDay; // add "per day" text to subtitle - std::string getDescription(); - std::string getSubtitle(); + std::string getDescription() const; + std::string getSubtitle() const; - CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val = std::nullopt, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize=large, EFonts font = FONT_SMALL); CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL); void showPopupWindow(const Point & cursorPosition) override; //call-in @@ -75,12 +70,14 @@ class CSelectableComponent : public CComponent, public CKeyShortcut public: bool selected; //if true, this component is selected std::function onSelect; //function called on selection change + std::function onChoose; //function called on doubleclick void showAll(Canvas & to) override; void select(bool on); void clickPressed(const Point & cursorPosition) override; //call-in - CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); + void clickDouble(const Point & cursorPosition) override; //call-in + CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); CSelectableComponent(const Component & c, std::function OnSelect = nullptr); }; diff --git a/client/widgets/CExchangeController.cpp b/client/widgets/CExchangeController.cpp new file mode 100644 index 000000000..23c9019d8 --- /dev/null +++ b/client/widgets/CExchangeController.cpp @@ -0,0 +1,119 @@ +/* + * CExchangeController.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 "CExchangeController.h" + +#include "../CPlayerInterface.h" + +#include "../widgets/CGarrisonInt.h" + +#include "../../CCallback.h" + +#include "../lib/mapObjects/CGHeroInstance.h" + +CExchangeController::CExchangeController(ObjectInstanceID hero1, ObjectInstanceID hero2) + : left(LOCPLINT->cb->getHero(hero1)) + , right(LOCPLINT->cb->getHero(hero2)) +{ +} + +void CExchangeController::swapArmy() +{ + auto getStacks = [](const CArmedInstance * source) -> std::vector> + { + auto slots = source->Slots(); + return std::vector>(slots.begin(), slots.end()); + }; + + auto leftSlots = getStacks(left); + auto rightSlots = getStacks(right); + + auto i = leftSlots.begin(), j = rightSlots.begin(); + + for(; i != leftSlots.end() && j != rightSlots.end(); i++, j++) + { + LOCPLINT->cb->swapCreatures(left, right, i->first, j->first); + } + + if(i != leftSlots.end()) + { + auto freeSlots = right->getFreeSlots(); + auto slot = freeSlots.begin(); + + for(; i != leftSlots.end() && slot != freeSlots.end(); i++, slot++) + { + LOCPLINT->cb->swapCreatures(left, right, i->first, *slot); + } + } + else if(j != rightSlots.end()) + { + auto freeSlots = left->getFreeSlots(); + auto slot = freeSlots.begin(); + + for(; j != rightSlots.end() && slot != freeSlots.end(); j++, slot++) + { + LOCPLINT->cb->swapCreatures(left, right, *slot, j->first); + } + } +} + +void CExchangeController::moveArmy(bool leftToRight, std::optional heldSlot) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + + if(!heldSlot.has_value()) + { + auto weakestSlot = vstd::minElementByFun(source->Slots(), + [](const std::pair & s) -> int + { + return s.second->getCreatureID().toCreature()->getAIValue(); + }); + heldSlot = weakestSlot->first; + } + LOCPLINT->cb->bulkMoveArmy(source->id, target->id, heldSlot.value()); +} + +void CExchangeController::moveStack(bool leftToRight, SlotID sourceSlot) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + auto creature = source->getCreature(sourceSlot); + + if(creature == nullptr) + return; + + SlotID targetSlot = target->getSlotFor(creature); + if(targetSlot.validSlot()) + { + if(source->stacksCount() == 1 && source->needsLastStack()) + { + LOCPLINT->cb->splitStack(source, target, sourceSlot, targetSlot, + target->getStackCount(targetSlot) + source->getStackCount(sourceSlot) - 1); + } + else + { + LOCPLINT->cb->mergeOrSwapStacks(source, target, sourceSlot, targetSlot); + } + } +} + +void CExchangeController::swapArtifacts(bool equipped, bool baclpack) +{ + LOCPLINT->cb->bulkMoveArtifacts(left->id, right->id, true, equipped, baclpack); +} + +void CExchangeController::moveArtifacts(bool leftToRight, bool equipped, bool baclpack) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + + LOCPLINT->cb->bulkMoveArtifacts(source->id, target->id, false, equipped, baclpack); +} diff --git a/client/widgets/CExchangeController.h b/client/widgets/CExchangeController.h new file mode 100644 index 000000000..928725f1b --- /dev/null +++ b/client/widgets/CExchangeController.h @@ -0,0 +1,30 @@ +/* + * CExchangeController.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 "CWindowWithArtifacts.h" + +class CCallback; + +class CExchangeController +{ +public: + CExchangeController(ObjectInstanceID hero1, ObjectInstanceID hero2); + void swapArmy(); + void moveArmy(bool leftToRight, std::optional heldSlot); + void moveStack(bool leftToRight, SlotID sourceSlot); + void swapArtifacts(bool equipped, bool baclpack); + void moveArtifacts(bool leftToRight, bool equipped, bool baclpack); + +private: + const CGHeroInstance * left; + const CGHeroInstance * right; +}; diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 59271b7cb..bf444ede9 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -17,6 +17,7 @@ #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../render/IImage.h" +#include "../render/Graphics.h" #include "../windows/CCreatureWindow.h" #include "../windows/GUIClasses.h" #include "../CGameInfo.h" @@ -29,6 +30,7 @@ #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" @@ -163,7 +165,7 @@ bool CGarrisonSlot::viewInfo() UpgradeInfo pom; LOCPLINT->cb->fillUpgradeInfo(getObj(), ID, pom); - bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID>=0; //upgrade is possible + bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID != CreatureID::NONE; //upgrade is possible std::function upgr = nullptr; auto dism = getDismiss(); if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); }; @@ -194,16 +196,18 @@ bool CGarrisonSlot::highlightOrDropArtifact() artSelected = true; if (myStack) // try dropping the artifact only if the slot isn't empty { - ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT); - if(pickedArtInst->canBePutAt(dst, true)) + ArtifactLocation src(srcHero->id, ArtifactPosition::TRANSITION_POS); + ArtifactLocation dst(getObj()->id, ArtifactPosition::CREATURE_SLOT); + dst.creature = getSlot(); + + if(pickedArtInst->canBePutAt(myStack, ArtifactPosition::CREATURE_SLOT, true)) { //equip clicked stack - if(dst.getArt()) + if(auto dstArt = myStack->getArt(ArtifactPosition::CREATURE_SLOT)) { //creature can wear only one active artifact //if we are placing a new one, the old one will be returned to the hero's backpack - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero, - ArtifactUtils::getArtBackpackPosition(srcHero, dst.getArt()->getTypeId()))); + LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero->id, + ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()))); } LOCPLINT->cb->swapArtifacts(src, dst); } @@ -421,13 +425,14 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa pos.x += x; pos.y += y; - std::string imgName = owner->smallIcons ? "cprsmall" : "TWCRPORT"; + AnimationPath imgName = AnimationPath::builtin(owner->smallIcons ? "cprsmall" : "TWCRPORT"); creatureImage = std::make_shared(graphics->getAnimation(imgName), 0); creatureImage->disable(); selectionImage = std::make_shared(graphics->getAnimation(imgName), 1); selectionImage->disable(); + selectionImage->center(creatureImage->pos.center()); if(Owner->smallIcons) { diff --git a/client/widgets/CTradeBase.cpp b/client/widgets/CTradeBase.cpp new file mode 100644 index 000000000..e1e7aacde --- /dev/null +++ b/client/widgets/CTradeBase.cpp @@ -0,0 +1,285 @@ +/* + * CTradeBase.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 "CTradeBase.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../widgets/TextControls.h" +#include "../windows/InfoWindows.h" + +#include "../CGameInfo.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +CTradeBase::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) + : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos) + , type(EType(-1)) // set to invalid, will be corrected in setType + , id(ID) + , serial(Serial) + , left(Left) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + downSelection = false; + hlp = nullptr; + setType(Type); + if(image) + { + this->pos.w = image->pos.w; + this->pos.h = image->pos.h; + } +} + +void CTradeBase::CTradeableItem::setType(EType newType) +{ + if(type != newType) + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + type = newType; + + if(getIndex() < 0) + { + image = std::make_shared(getFilename(), 0); + image->disable(); + } + else + { + image = std::make_shared(getFilename(), getIndex()); + } + } +} + +void CTradeBase::CTradeableItem::setID(int newID) +{ + if(id != newID) + { + id = newID; + if(image) + { + int index = getIndex(); + if(index < 0) + image->disable(); + else + { + image->enable(); + image->setFrame(index); + } + } + } +} + +AnimationPath CTradeBase::CTradeableItem::getFilename() +{ + switch(type) + { + case RESOURCE: + return AnimationPath::builtin("RESOURCE"); + case PLAYER: + return AnimationPath::builtin("CREST58"); + case ARTIFACT_TYPE: + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + return AnimationPath::builtin("artifact"); + case CREATURE: + return AnimationPath::builtin("TWCRPORT"); + default: + return {}; + } +} + +int CTradeBase::CTradeableItem::getIndex() +{ + if(id < 0) + return -1; + + switch(type) + { + case RESOURCE: + case PLAYER: + return id; + case ARTIFACT_TYPE: + case ARTIFACT_INSTANCE: + case ARTIFACT_PLACEHOLDER: + return CGI->artifacts()->getByIndex(id)->getIconIndex(); + case CREATURE: + return CGI->creatures()->getByIndex(id)->getIconIndex(); + default: + return -1; + } +} + +void CTradeBase::CTradeableItem::showAll(Canvas & to) +{ + Point posToBitmap; + Point posToSubCenter; + + switch (type) + { + case RESOURCE: + posToBitmap = Point(19, 9); + posToSubCenter = Point(36, 59); + break; + case CREATURE_PLACEHOLDER: + case CREATURE: + posToSubCenter = Point(29, 77); + break; + case PLAYER: + posToSubCenter = Point(31, 76); + break; + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + posToSubCenter = Point(19, 54); + if (downSelection) + posToSubCenter.y += 8; + break; + case ARTIFACT_TYPE: + posToSubCenter = Point(19, 58); + break; + } + + if(image) + { + image->moveTo(pos.topLeft() + posToBitmap); + CIntObject::showAll(to); + } + + to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); +} + +void CTradeBase::CTradeableItem::clickPressed(const Point& cursorPosition) +{ + if(clickPressedCallback) + clickPressedCallback(shared_from_this()); +} + +void CTradeBase::CTradeableItem::showAllAt(const Point& dstPos, const std::string& customSub, Canvas& to) +{ + Rect oldPos = pos; + std::string oldSub = subtitle; + downSelection = true; + + moveTo(dstPos); + subtitle = customSub; + showAll(to); + + downSelection = false; + moveTo(oldPos.topLeft()); + subtitle = oldSub; +} + +void CTradeBase::CTradeableItem::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + return; + } + + switch(type) + { + case CREATURE: + case CREATURE_PLACEHOLDER: + GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); + break; + case ARTIFACT_PLACEHOLDER: + if(id < 0) + GH.statusbar()->write(CGI->generaltexth->zelp[582].first); + else + GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); + break; + } +} + +void CTradeBase::CTradeableItem::showPopupWindow(const Point& cursorPosition) +{ + switch(type) + { + case CREATURE: + case CREATURE_PLACEHOLDER: + break; + case ARTIFACT_TYPE: + case 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. + if (id >= 0) + CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); + break; + } +} + +std::string CTradeBase::CTradeableItem::getName(int number) const +{ + switch(type) + { + case PLAYER: + return CGI->generaltexth->capColors[id]; + case RESOURCE: + return CGI->generaltexth->restypes[id]; + case CREATURE: + if (number == 1) + return CGI->creh->objects[id]->getNameSingularTranslated(); + else + return CGI->creh->objects[id]->getNamePluralTranslated(); + case ARTIFACT_TYPE: + case ARTIFACT_INSTANCE: + return CGI->artifacts()->getByIndex(id)->getNameTranslated(); + } + logGlobal->error("Invalid trade item type: %d", (int)type); + return ""; +} + +const CArtifactInstance* CTradeBase::CTradeableItem::getArtInstance() const +{ + switch(type) + { + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + return hlp; + default: + return nullptr; + } +} + +void CTradeBase::CTradeableItem::setArtInstance(const CArtifactInstance * art) +{ + assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE); + hlp = art; + if(art) + setID(art->artType->getId()); + else + setID(-1); +} + +CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero) + : market(market) + , hero(hero) +{ +} + +void CTradeBase::removeItems(const std::set> & toRemove) +{ + for(auto item : toRemove) + removeItem(item); +} + +void CTradeBase::removeItem(std::shared_ptr item) +{ + items[item->left] -= item; + + if(hRight == item) + hRight.reset(); +} + +void CTradeBase::getEmptySlots(std::set> & toRemove) +{ + for(auto item : items[1]) + if(!hero->getStackCount(SlotID(item->serial))) + toRemove.insert(item); +} diff --git a/client/widgets/CTradeBase.h b/client/widgets/CTradeBase.h new file mode 100644 index 000000000..75079fe0f --- /dev/null +++ b/client/widgets/CTradeBase.h @@ -0,0 +1,88 @@ +/* + * CTradeBase.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 "Images.h" + +#include "../../lib/FunctionList.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IMarket; +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CTextBox; + +class CTradeBase +{ +public: + enum EType + { + RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE + }; + + class CTradeableItem : public CIntObject, public std::enable_shared_from_this + { + std::shared_ptr image; + AnimationPath getFilename(); + int getIndex(); + public: + const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact + EType type; + int id; + const int serial; + const bool left; + std::string subtitle; //empty if default + std::function altarSlot)> clickPressedCallback; + + void setType(EType newType); + void setID(int newID); + + const CArtifactInstance* getArtInstance() const; + void setArtInstance(const CArtifactInstance * art); + + CFunctionList callback; + bool downSelection; + + void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); + + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + void showAll(Canvas & to) override; + void clickPressed(const Point & cursorPosition) override; + std::string getName(int number = -1) const; + CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial); + }; + + const IMarket * market; + const CGHeroInstance * hero; + + //all indexes: 1 = left, 0 = right + std::array>, 2> items; + + //highlighted items (nullptr if no highlight) + std::shared_ptr hLeft; + std::shared_ptr hRight; + std::shared_ptr deal; + + CTradeBase(const IMarket * market, const CGHeroInstance * hero); + void removeItems(const std::set> & toRemove); + void removeItem(std::shared_ptr item); + void getEmptySlots(std::set> & toRemove); + virtual void makeDeal() = 0; + +protected: + std::vector> labels; + std::vector> buttons; + std::vector> texts; +}; diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 3a9c3156a..bf5ca043e 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -14,6 +14,10 @@ #include "../gui/CursorHandler.h" #include "../gui/WindowHandler.h" +#include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" + #include "CComponent.h" #include "../windows/CHeroWindow.h" @@ -25,20 +29,27 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/CConfigHandler.h" void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) +{ + artSets.emplace_back(artSet); +} + +void CWindowWithArtifacts::addSetAndCallbacks(CArtifactsOfHeroPtr artSet) { CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackHandler = []() -> void { CCS->curh->dragAndDropCursor(nullptr); }; - artSets.emplace_back(artSet); + addSet(artSet); std::visit([this, artPutBackHandler](auto artSetWeak) { auto artSet = artSetWeak.lock(); artSet->leftClickCallback = std::bind(&CWindowWithArtifacts::leftClickArtPlaceHero, this, _1, _2); - artSet->rightClickCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2); + artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2); artSet->setPutBackPickedArtifactCallback(artPutBackHandler); }, artSet); } @@ -84,7 +95,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT) { // The Catapult must be equipped - std::vector> catapult(1, std::make_shared(CComponent::artifact, 3, 0)); + std::vector> catapult(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT))); LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult); return false; } @@ -111,8 +122,8 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst if(pickedArtInst) { - auto srcLoc = ArtifactLocation(heroPickedArt, ArtifactPosition::TRANSITION_POS); - auto dstLoc = ArtifactLocation(hero, artPlace.slot); + auto srcLoc = ArtifactLocation(heroPickedArt->id, ArtifactPosition::TRANSITION_POS); + auto dstLoc = ArtifactLocation(hero->id, artPlace.slot); if(ArtifactUtils::isSlotBackpack(artPlace.slot)) { @@ -130,7 +141,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst } } // Check if artifact transfer is possible - else if(pickedArtInst->canBePutAt(dstLoc, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) + else if(pickedArtInst->canBePutAt(hero, artPlace.slot, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) { isTransferAllowed = true; } @@ -244,7 +255,7 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns void CWindowWithArtifacts::artifactRemoved(const ArtifactLocation & artLoc) { - updateSlots(artLoc.slot); + updateSlots(); } void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) @@ -259,9 +270,9 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const // we have a different artifact may look surprising... but it's valid. auto pickedArtInst = std::get(curState.value()); - assert(!pickedArtInst || destLoc.isHolder(std::get(curState.value()))); + assert(!pickedArtInst || destLoc.artHolder == std::get(curState.value())->id); - auto artifactMovedBody = [this, withRedraw, &srcLoc, &destLoc, &pickedArtInst](auto artSetWeak) -> void + auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void { auto artSetPtr = artSetWeak.lock(); if(artSetPtr) @@ -269,12 +280,17 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const const auto hero = artSetPtr->getHero(); if(pickedArtInst) { - if(artSetPtr->isActive()) + markPossibleSlots(); + + if(pickedArtInst->getTypeId() == ArtifactID::SPELL_SCROLL && pickedArtInst->getScrollSpellID().num >= 0 && settings["general"]["enableUiEnhancements"].Bool()) { - CCS->curh->dragAndDropCursor("artifact", pickedArtInst->artType->getIconIndex()); - if(srcLoc.isHolder(hero) || !std::is_same_v>) - artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin("spellscr")); + anim->load(pickedArtInst->getScrollSpellID().num); + std::shared_ptr img = anim->getImage(pickedArtInst->getScrollSpellID().num); + CCS->curh->dragAndDropCursor(img->scaleFast(Point(44, 34))); } + else + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); } else { @@ -296,11 +312,11 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const { cew->updateWidgets(); } - artSetPtr->safeRedraw(); + artSetPtr->redraw(); } // Make sure the status bar is updated so it does not display old text - if(destLoc.getHolderArtSet() == hero) + if(destLoc.artHolder == hero->id) { if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot)) artPlace->hover(true); @@ -314,26 +330,24 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation & artLoc) { - updateSlots(artLoc.slot); + updateSlots(); } void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation & artLoc) { - updateSlots(artLoc.slot); + markPossibleSlots(); + updateSlots(); } -void CWindowWithArtifacts::updateSlots(const ArtifactPosition & slot) +void CWindowWithArtifacts::updateSlots() { - auto updateSlotBody = [slot](auto artSetWeak) -> void + auto updateSlotBody = [](auto artSetWeak) -> void { if(const auto artSetPtr = artSetWeak.lock()) { - if(ArtifactUtils::isSlotEquipment(slot)) - artSetPtr->updateWornSlots(); - else if(ArtifactUtils::isSlotBackpack(slot)) - artSetPtr->updateBackpackSlots(); - - artSetPtr->safeRedraw(); + artSetPtr->updateWornSlots(); + artSetPtr->updateBackpackSlots(); + artSetPtr->redraw(); } }; @@ -395,3 +409,26 @@ std::optional CWindowWithArtifacts::f } return res; } + +void CWindowWithArtifacts::markPossibleSlots() +{ + if(const auto pickedArtInst = getPickedArtifact()) + { + const auto heroArtOwner = getHeroPickedArtifact(); + auto artifactAssembledBody = [&pickedArtInst, &heroArtOwner](auto artSetWeak) -> void + { + 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); + } + } + }; + + for(auto artSetWeak : artSets) + std::visit(artifactAssembledBody, artSetWeak); + } +} diff --git a/client/widgets/CWindowWithArtifacts.h b/client/widgets/CWindowWithArtifacts.h index c5180c688..777e4537d 100644 --- a/client/widgets/CWindowWithArtifacts.h +++ b/client/widgets/CWindowWithArtifacts.h @@ -28,6 +28,7 @@ public: using CloseCallback = std::function; void addSet(CArtifactsOfHeroPtr artSet); + void addSetAndCallbacks(CArtifactsOfHeroPtr artSet); void addCloseCallback(CloseCallback callback); const CGHeroInstance * getHeroPickedArtifact(); const CArtifactInstance * getPickedArtifact(); @@ -39,11 +40,12 @@ public: void artifactDisassembled(const ArtifactLocation & artLoc) override; void artifactAssembled(const ArtifactLocation & artLoc) override; -private: +protected: std::vector artSets; CloseCallback closeCallback; - void updateSlots(const ArtifactPosition & slot); + void updateSlots(); std::optional> getState(); std::optional findAOHbyRef(CArtifactsOfHeroBase & artsInst); + void markPossibleSlots(); }; diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp new file mode 100644 index 000000000..f0150b6d1 --- /dev/null +++ b/client/widgets/ComboBox.cpp @@ -0,0 +1,185 @@ +/* + * ComboBox.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 "ComboBox.h" + +#include "Slider.h" +#include "Images.h" +#include "TextControls.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" + +ComboBox::DropDown::Item::Item(const JsonNode & config, ComboBox::DropDown & _dropDown, Point position) + : InterfaceObjectConfigurable(LCLICK | HOVER, position), + dropDown(_dropDown) +{ + build(config); + + if(auto w = widget("hoverImage")) + { + pos.w = w->pos.w; + pos.h = w->pos.h; + w->disable(); + } + setRedrawParent(true); +} + +void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) +{ + item = _item; + if(auto w = widget("labelName")) + { + if(dropDown.comboBox.getItemText) + w->setText(dropDown.comboBox.getItemText(idx, item)); + } +} + +void ComboBox::DropDown::Item::hover(bool on) +{ + auto h = widget("hoverImage"); + auto w = widget("labelName"); + if(h && w) + { + if(w->getText().empty() || on == false) + h->disable(); + else + h->enable(); + } + redraw(); +} + +void ComboBox::DropDown::Item::clickPressed(const Point & cursorPosition) +{ + if(isHovered()) + dropDown.setItem(item); +} + +void ComboBox::DropDown::Item::clickReleased(const Point & cursorPosition) +{ + dropDown.clickPressed(cursorPosition); + dropDown.clickReleased(cursorPosition); +} + +ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox, Point dropDownPosition): + InterfaceObjectConfigurable(LCLICK | HOVER), + comboBox(_comboBox) +{ + REGISTER_BUILDER("item", &ComboBox::DropDown::buildItem); + + if(comboBox.onConstructItems) + comboBox.onConstructItems(curItems); + + addCallback("sliderMove", std::bind(&ComboBox::DropDown::sliderMove, this, std::placeholders::_1)); + + pos = comboBox.pos + dropDownPosition; + + build(config); + + if(auto w = widget("slider")) + { + w->setAmount(curItems.size()); + } + + //FIXME: this should be done by InterfaceObjectConfigurable, but might have side-effects + pos = children.front()->pos; + for (auto const & child : children) + pos = pos.include(child->pos); + + updateListItems(); +} + +std::shared_ptr ComboBox::DropDown::buildItem(const JsonNode & config) +{ + auto position = readPosition(config["position"]); + items.push_back(std::make_shared(config, *this, position)); + return items.back(); +} + +void ComboBox::DropDown::sliderMove(int slidPos) +{ + auto w = widget("slider"); + if(!w) + return; // ignore spurious call when slider is being created + updateListItems(); + redraw(); +} + +bool ComboBox::DropDown::receiveEvent(const Point & position, int eventType) const +{ + if (eventType == LCLICK) + return true; // we want drop box to close when clicking outside drop box borders + + return CIntObject::receiveEvent(position, eventType); +} + +void ComboBox::DropDown::clickPressed(const Point & cursorPosition) +{ + if (!pos.isInside(cursorPosition)) + { + assert(GH.windows().isTopWindow(this)); + GH.windows().popWindows(1); + } +} + +void ComboBox::DropDown::updateListItems() +{ + int elemIdx = 0; + + if(auto w = widget("slider")) + elemIdx = w->getValue(); + + for(auto item : items) + { + if(elemIdx < curItems.size()) + { + item->updateItem(elemIdx, curItems[elemIdx]); + elemIdx++; + } + else + { + item->updateItem(elemIdx); + } + } +} + +void ComboBox::DropDown::setItem(const void * item) +{ + comboBox.setItem(item); + + assert(GH.windows().isTopWindow(this)); + GH.windows().popWindows(1); +} + +ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key, bool playerColoredButton): + CButton(position, defName, help, 0, key, playerColoredButton) +{ + addCallback([this, dropDownDescriptor, dropDownPosition]() + { + GH.windows().createAndPushWindow(dropDownDescriptor, *this, dropDownPosition); + }); +} + +void ComboBox::setItem(const void * item) +{ + auto w = std::dynamic_pointer_cast(overlay); + + if( w && getItemText) + addTextOverlay(getItemText(0, item), w->font, w->color); + + if(onSetItem) + onSetItem(item); +} + +void ComboBox::setItem(int id) +{ + std::vector tempItems; + onConstructItems(tempItems); + setItem(tempItems.at(id)); +} diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h new file mode 100644 index 000000000..cd3c1e883 --- /dev/null +++ b/client/widgets/ComboBox.h @@ -0,0 +1,72 @@ +/* + * ComboBox.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/InterfaceObjectConfigurable.h" +#include "Buttons.h" + +class ComboBox : public CButton +{ + class DropDown : public InterfaceObjectConfigurable + { + struct Item : public InterfaceObjectConfigurable + { + DropDown & dropDown; + const void * item = nullptr; + + Item(const JsonNode &, ComboBox::DropDown &, Point position); + void updateItem(int index, const void * item = nullptr); + + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; + }; + + friend struct Item; + + public: + DropDown(const JsonNode &, ComboBox &, Point dropDownPosition); + + bool receiveEvent(const Point & position, int eventType) const override; + void clickPressed(const Point & cursorPosition) override; + void setItem(const void *); + + void updateListItems(); + + private: + std::shared_ptr buildItem(const JsonNode & config); + + void sliderMove(int slidPos); + + ComboBox & comboBox; + std::vector> items; + std::vector curItems; + }; + + friend class DropDown; + + void setItem(const void *); + +public: + ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key = {}, bool playerColoredButton = false); + + //define this callback to fill input vector with data for the combo box + std::function &)> onConstructItems; + + //callback is called when item is selected and its value can be used + std::function onSetItem; + + //return text value from item data + std::function getItemText; + + void setItem(int id); + + void updateListItems(); +}; diff --git a/client/widgets/CreatureCostBox.cpp b/client/widgets/CreatureCostBox.cpp index 94c31e29d..546c71864 100644 --- a/client/widgets/CreatureCostBox.cpp +++ b/client/widgets/CreatureCostBox.cpp @@ -38,7 +38,7 @@ void CreatureCostBox::createItems(TResources res) TResources::nziterator iter(res); while(iter.valid()) { - ImagePtr image = std::make_shared("RESOURCE", iter->resType); + ImagePtr image = std::make_shared(AnimationPath::builtin("RESOURCE"), iter->resType); LabelPtr text = std::make_shared(15, 43, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "0"); resources.insert(std::make_pair(iter->resType, std::make_pair(text, image))); diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 9767717b7..8ae5795fb 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -15,6 +15,7 @@ #include "../gui/CGuiHandler.h" #include "../renderSDL/SDL_Extensions.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" @@ -34,7 +35,6 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) : bg(image) - , visible(true) , needRefresh(false) { pos += position; @@ -42,17 +42,16 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) pos.h = bg->height(); } -CPicture::CPicture( const std::string &bmpname, int x, int y ) +CPicture::CPicture( const ImagePath &bmpname, int x, int y ) : CPicture(bmpname, Point(x,y)) {} -CPicture::CPicture( const std::string &bmpname ) +CPicture::CPicture( const ImagePath & bmpname ) : CPicture(bmpname, Point(0,0)) {} -CPicture::CPicture( const std::string &bmpname, const Point & position ) - : bg(IImage::createFromFile(bmpname)) - , visible(true) +CPicture::CPicture( const ImagePath & bmpname, const Point & position ) + : bg(GH.renderHandler().loadImage(bmpname)) , needRefresh(false) { pos.x += position.x; @@ -80,13 +79,13 @@ CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, in void CPicture::show(Canvas & to) { - if (visible && needRefresh) + if (needRefresh) showAll(to); } void CPicture::showAll(Canvas & to) { - if(bg && visible) + if(bg) { if (srcRect.has_value()) to.draw(bg, pos.topLeft(), *srcRect); @@ -95,7 +94,7 @@ void CPicture::showAll(Canvas & to) } } -void CPicture::setAlpha(int value) +void CPicture::setAlpha(uint8_t value) { bg->setAlpha(value); } @@ -113,9 +112,9 @@ void CPicture::colorize(PlayerColor player) bg->playerColored(player); } -CFilledTexture::CFilledTexture(std::string imageName, Rect position): +CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position): CIntObject(0, position.topLeft()), - texture(IImage::createFromFile(imageName)) + texture(GH.renderHandler().loadImage(imageName)) { pos.w = position.w; pos.h = position.h; @@ -142,7 +141,7 @@ void CFilledTexture::showAll(Canvas & to) } } -FilledTexturePlayerColored::FilledTexturePlayerColored(std::string imageName, Rect position) +FilledTexturePlayerColored::FilledTexturePlayerColored(const ImagePath & imageName, Rect position) : CFilledTexture(imageName, position) { } @@ -171,14 +170,14 @@ void FilledTexturePlayerColored::playerColored(PlayerColor player) texture->adjustPalette(filters[player.getNum()], 0); } -CAnimImage::CAnimImage(const std::string & name, size_t Frame, size_t Group, int x, int y, ui8 Flags): +CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, int x, int y, ui8 Flags): frame(Frame), group(Group), flags(Flags) { pos.x += x; pos.y += y; - anim = std::make_shared(name); + anim = GH.renderHandler().loadAnimation(name); init(); } @@ -274,6 +273,18 @@ void CAnimImage::showAll(Canvas & to) } } +void CAnimImage::setAnimationPath(const AnimationPath & name, size_t frame) +{ + this->frame = frame; + anim = GH.renderHandler().loadAnimation(name); + init(); +} + +void CAnimImage::setScale(Point scale) +{ + scaledSize = scale; +} + void CAnimImage::setFrame(size_t Frame, size_t Group) { if (frame == Frame && group==Group) @@ -307,8 +318,8 @@ bool CAnimImage::isPlayerColored() const return player.has_value(); } -CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): - anim(std::make_shared(name)), +CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): + anim(GH.renderHandler().loadAnimation(name)), group(Group), frame(0), first(0), @@ -448,7 +459,7 @@ void CShowableAnim::setDuration(int durationMs) frameTimeTotal = durationMs/(last - first); } -CCreatureAnim::CCreatureAnim(int x, int y, std::string name, ui8 flags, ECreatureAnimType type): +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 { xOffset = 0; diff --git a/client/widgets/Images.h b/client/widgets/Images.h index d11dbab45..94fef0c1c 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -11,12 +11,12 @@ #include "../gui/CIntObject.h" #include "../battle/BattleConstants.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class Rect; VCMI_LIB_NAMESPACE_END -struct SDL_Color; class CAnimImage; class CLabel; class CAnimation; @@ -34,10 +34,6 @@ public: /// If set to true, iamge will be redrawn on each frame bool needRefresh; - /// If set to false, image will not be rendered - /// Deprecated, use CIntObject::disable()/enable() instead - bool visible; - std::shared_ptr getSurface() { return bg; @@ -50,13 +46,13 @@ public: CPicture(std::shared_ptr image, const Rect &SrcRext, int x = 0, int y = 0); //wrap subrect of given surface /// Loads image from specified file name - CPicture(const std::string & bmpname); - CPicture(const std::string & bmpname, const Point & position); - CPicture(const std::string & bmpname, int x, int y); + CPicture(const ImagePath & bmpname); + CPicture(const ImagePath & bmpname, const Point & position); + CPicture(const ImagePath & bmpname, int x, int y); /// set alpha value for whole surface. Note: may be messed up if surface is shared /// 0=transparent, 255=opaque - void setAlpha(int value); + void setAlpha(uint8_t value); void scaleTo(Point size); void colorize(PlayerColor player); @@ -72,7 +68,7 @@ protected: Rect imageArea; public: - CFilledTexture(std::string imageName, Rect position); + CFilledTexture(const ImagePath & imageName, Rect position); CFilledTexture(std::shared_ptr image, Rect position, Rect imageArea); void showAll(Canvas & to) override; @@ -81,7 +77,7 @@ public: class FilledTexturePlayerColored : public CFilledTexture { public: - FilledTexturePlayerColored(std::string imageName, Rect position); + FilledTexturePlayerColored(const ImagePath & imageName, Rect position); void playerColored(PlayerColor player); }; @@ -95,7 +91,7 @@ private: size_t frame; size_t group; ui8 flags; - const Point scaledSize; + Point scaledSize; /// If set, then image is colored using player-specific palette std::optional player; @@ -106,7 +102,7 @@ private: public: bool visible; - CAnimImage(const std::string & name, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); + 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(); @@ -124,6 +120,10 @@ public: bool isPlayerColored() const; void showAll(Canvas & to) override; + + void setAnimationPath(const AnimationPath & name, size_t frame); + + void setScale(Point scale); }; /// Base class for displaying animation, used as superclass for different animations @@ -167,7 +167,7 @@ public: //Set per-surface alpha, 0 = transparent, 255 = opaque void setAlpha(ui32 alphaValue); - CShowableAnim(int x, int y, std::string name, ui8 flags, ui32 frameTime, size_t Group=0, uint8_t alpha = UINT8_MAX); + 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 @@ -214,6 +214,6 @@ public: //clear queue and set animation to this sequence void clearAndSet(ECreatureAnimType type); - CCreatureAnim(int x, int y, std::string name, ui8 flags = 0, ECreatureAnimType = ECreatureAnimType::HOLDING); + CCreatureAnim(int x, int y, const AnimationPath & name, ui8 flags = 0, ECreatureAnimType = ECreatureAnimType::HOLDING); }; diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0fe9a83b9..ff1bc50d8 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -18,18 +18,21 @@ #include "../CPlayerInterface.h" #include "../CGameInfo.h" #include "../PlayerLocalState.h" +#include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" +#include "../windows/CTradeWindow.h" #include "../widgets/TextControls.h" #include "../widgets/CGarrisonInt.h" #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/gameState/InfoAboutArmy.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CGCreature.h" @@ -92,16 +95,16 @@ void LRClickableAreaWTextComp::clickPressed(const Point & cursorPosition) LOCPLINT->showInfoDialog(text, comp); } -LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, int BaseType) - : LRClickableAreaWText(Pos), baseType(BaseType), bonusValue(-1) +LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, ComponentType BaseType) + : LRClickableAreaWText(Pos) { - type = -1; + component.type = BaseType; } std::shared_ptr LRClickableAreaWTextComp::createComponent() const { - if(baseType >= 0) - return std::make_shared(CComponent::Etype(baseType), type, bonusValue); + if(component.type != ComponentType::NONE) + return std::make_shared(component); else return std::shared_ptr(); } @@ -117,9 +120,10 @@ void LRClickableAreaWTextComp::showPopupWindow(const Point & cursorPosition) LRClickableAreaWText::showPopupWindow(cursorPosition); //only if with-component variant not occurred } -CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero) +CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero) : CIntObject(LCLICK | HOVER), - hero(_hero) + hero(hero), + clickFunctor(nullptr) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -129,13 +133,24 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero) pos.h = 64; if(hero) - portrait = std::make_shared("PortraitsLarge", hero->portrait); + { + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex()); + clickFunctor = [hero]() -> void + { + LOCPLINT->openHeroWindow(hero); + }; + } +} + +void CHeroArea::addClickCallback(ClickFunctor callback) +{ + clickFunctor = callback; } void CHeroArea::clickPressed(const Point & cursorPosition) { - if(hero) - LOCPLINT->openHeroWindow(hero); + if(clickFunctor) + clickFunctor(); } void CHeroArea::hover(bool on) @@ -149,20 +164,35 @@ void CHeroArea::hover(bool on) void LRClickableAreaOpenTown::clickPressed(const Point & cursorPosition) { if(town) - { LOCPLINT->openTownWindow(town); - if ( type == 2 ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::VILLAGE_HALL); - else if ( type == 3 && town->fortLevel() ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::FORT); - } } LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town) - : LRClickableAreaWTextComp(Pos, -1), town(Town) + : LRClickableAreaWTextComp(Pos), town(Town) { } +void LRClickableArea::clickPressed(const Point & cursorPosition) +{ + if(onClick) + { + onClick(); + GH.input().hapticFeedback(); + } +} + +void LRClickableArea::showPopupWindow(const Point & cursorPosition) +{ + if(onPopup) + onPopup(); +} + +LRClickableArea::LRClickableArea(const Rect & Pos, std::function onClick, std::function onPopup) + : CIntObject(LCLICK | SHOW_POPUP), onClick(onClick), onPopup(onPopup) +{ + pos = Pos + pos.topLeft(); +} + void CMinorResDataBar::show(Canvas & to) { } @@ -204,7 +234,7 @@ CMinorResDataBar::CMinorResDataBar() pos.x = 7; pos.y = 575; - background = std::make_shared("KRESBAR.bmp"); + background = std::make_shared(ImagePath::builtin("KRESBAR.bmp")); background->colorize(LOCPLINT->playerID); pos.w = background->pos.w; @@ -236,7 +266,7 @@ void CArmyTooltip::init(const InfoAboutArmy &army) continue; } - icons.push_back(std::make_shared("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.type->getIconIndex(), 0, slotsPos[slot.first.getNum()].x, slotsPos[slot.first.getNum()].y)); std::string subtitle; if(army.army.isDetailed) @@ -279,7 +309,7 @@ CArmyTooltip::CArmyTooltip(Point pos, const CArmedInstance * army): void CHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 3, 2); if(hero.details) { @@ -289,8 +319,8 @@ void CHeroTooltip::init(const InfoAboutHero & hero) labels.push_back(std::make_shared(158, 98, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); - morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74); - luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91); + morale = std::make_shared(AnimationPath::builtin("IMRL22"), hero.details->morale + 3, 0, 5, 74); + luck = std::make_shared(AnimationPath::builtin("ILCK22"), hero.details->luck + 3, 0, 5, 91); } } @@ -317,7 +347,7 @@ CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstan void CInteractableHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2); + 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); if(hero.details) @@ -328,8 +358,8 @@ void CInteractableHeroTooltip::init(const InfoAboutHero & hero) labels.push_back(std::make_shared(158, 98, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); - morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74); - luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91); + morale = std::make_shared(AnimationPath::builtin("IMRL22"), hero.details->morale + 3, 0, 5, 74); + luck = std::make_shared(AnimationPath::builtin("ILCK22"), hero.details->luck + 3, 0, 5, 91); } } @@ -340,17 +370,17 @@ void CTownTooltip::init(const InfoAboutTown & town) //order of icons in def: fort, citadel, castle, no fort size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; - fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31); + fort = std::make_shared(AnimationPath::builtin("ITMCLS"), fortIndex, 0, 105, 31); assert(town.tType); size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - build = std::make_shared("itpt", iconIndex, 0, 3, 2); + build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); if(town.details) { - hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31); + hall = std::make_shared(AnimationPath::builtin("ITMTLS"), town.details->hallLevel, 0, 67, 31); if(town.details->goldIncome) { @@ -358,18 +388,18 @@ void CTownTooltip::init(const InfoAboutTown & town) std::to_string(town.details->goldIncome)); } if(town.details->garrisonedHero) //garrisoned hero icon - garrisonedHero = std::make_shared("TOWNQKGH", 149, 76); + garrisonedHero = std::make_shared(ImagePath::builtin("TOWNQKGH"), 149, 76); if(town.details->customRes)//silo is built { if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { - res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75); - res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::WOOD), 0, 7, 75); + res2 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), town.tType->primaryRes, 0, 7, 81); } } } @@ -389,50 +419,93 @@ CTownTooltip::CTownTooltip(Point pos, const CGTownInstance * town) CInteractableTownTooltip::CInteractableTownTooltip(Point pos, const CGTownInstance * town) { - init(InfoAboutTown(town, true)); + init(town); OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); 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 InfoAboutTown & town) +void CInteractableTownTooltip::init(const CGTownInstance * town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + const InfoAboutTown townInfo = InfoAboutTown(town, true); + int townId = town->id; + //order of icons in def: fort, citadel, castle, no fort - size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; + size_t fortIndex = townInfo.fortLevel ? townInfo.fortLevel - 1 : 3; - fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31); - - assert(town.tType); - - size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - - build = std::make_shared("itpt", iconIndex, 0, 3, 2); - title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town.name); - - if(town.details) + fort = std::make_shared(AnimationPath::builtin("ITMCLS"), fortIndex, 0, 105, 31); + fastArmyPurchase = std::make_shared(Rect(105, 31, 34, 34), [townId]() { - hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31); + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId) + std::make_shared(town)->enterToTheQuickRecruitmentWindow(); + } + }); + fastTavern = std::make_shared(Rect(3, 2, 58, 64), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId && town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + } + }); + 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)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); - if(town.details->goldIncome) + assert(townInfo.tType); + + size_t iconIndex = townInfo.tType->clientInfo.icons[townInfo.fortLevel > 0][townInfo.built >= CGI->settings()->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); + + if(townInfo.details) + { + hall = std::make_shared(AnimationPath::builtin("ITMTLS"), townInfo.details->hallLevel, 0, 67, 31); + fastTownHall = std::make_shared(Rect(67, 31, 34, 34), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId) + std::make_shared(town)->enterTownHall(); + } + }); + + if(townInfo.details->goldIncome) { income = std::make_shared(157, 58, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, - std::to_string(town.details->goldIncome)); + std::to_string(townInfo.details->goldIncome)); } - if(town.details->garrisonedHero) //garrisoned hero icon - garrisonedHero = std::make_shared("TOWNQKGH", 149, 76); + if(townInfo.details->garrisonedHero) //garrisoned hero icon + garrisonedHero = std::make_shared(ImagePath::builtin("TOWNQKGH"), 149, 76); - if(town.details->customRes)//silo is built + if(townInfo.details->customRes)//silo is built { - if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore + if(townInfo.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { - res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75); - res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::WOOD), 0, 7, 75); + res2 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), townInfo.tType->primaryRes, 0, 7, 81); } } } @@ -442,14 +515,16 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto creatureData = (*CGI->creh)[creature->stacks.begin()->second->getCreatureID()].get(); - creatureImage = std::make_shared(graphics->getAnimation("TWCRPORT"), creatureData->getIconIndex()); + auto creatureID = creature->getCreature(); + int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex(); + + creatureImage = std::make_shared(graphics->getAnimation(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; std::string textContent = isHeroSelected - ? creature->getHoverText(LOCPLINT->localState->getCurrentHero()) - : creature->getHoverText(LOCPLINT->playerID); + ? creature->getPopupText(LOCPLINT->localState->getCurrentHero()) + : creature->getPopupText(LOCPLINT->playerID); //TODO: window is bigger than OH3 //TODO: vertical alignment does not match H3. Commented below example that matches H3 for creatures count but supports only 1 line: @@ -463,20 +538,21 @@ void MoraleLuckBox::set(const AFactionMember * node) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - const int textId[] = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} + 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 - const int neutralDescr[] = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. - const int componentType[] = {CComponent::luck, CComponent::morale}; - const int hoverTextBase[] = {7, 4}; + const std::array neutralDescr = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. + const std::array componentType = {ComponentType::LUCK, ComponentType::MORALE}; + const std::array hoverTextBase = {7, 4}; TConstBonusListPtr modifierList = std::make_shared(); - bonusValue = 0; + + component.value = 0; if(node) - bonusValue = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); + component.value = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); - int mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good + int mrlt = (component.value>0)-(component.value<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good hoverText = CGI->generaltexth->heroscrn[hoverTextBase[morale] - mrlt]; - baseType = componentType[morale]; + component.type = componentType[morale]; text = CGI->generaltexth->arraytxt[textId[morale]]; boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]); @@ -484,27 +560,32 @@ void MoraleLuckBox::set(const AFactionMember * node) || node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING))) { text += CGI->generaltexth->arraytxt[113]; //unaffected by morale - bonusValue = 0; + component.value = 0; } else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE)) { auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE)); text += "\n" + noMorale->Description(); - bonusValue = 0; + 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(); - bonusValue = 0; + component.value = 0; } else { std::string addInfo = ""; for(auto & bonus : * modifierList) { - if(bonus->val) - addInfo += "\n" + bonus->Description(); + if(bonus->val) { + const std::string& description = bonus->Description(); + //arraytxt already contains \n + if (description.size() && description[0] != '\n') + addInfo += '\n'; + addInfo += description; + } } text = addInfo.empty() ? text + CGI->generaltexth->arraytxt[noneTxtId] @@ -516,7 +597,7 @@ void MoraleLuckBox::set(const AFactionMember * node) else imageName = morale ? "IMRL42" : "ILCK42"; - image = std::make_shared(imageName, bonusValue + 3); + image = std::make_shared(AnimationPath::builtin(imageName), *component.value + 3); image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon } @@ -524,7 +605,6 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) : morale(Morale), small(Small) { - bonusValue = 0; pos = r + pos.topLeft(); defActions = 255-DISPOSE; } @@ -568,3 +648,31 @@ void CCreaturePic::setAmount(int newAmount) else amount->setText(""); } + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color) : + color(color), colorLine(ColorRGBA()), drawLine(false) +{ + pos = position + pos.topLeft(); +} + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine) : + color(color), colorLine(colorLine), drawLine(true) +{ + pos = position + pos.topLeft(); +} + +void TransparentFilledRectangle::showAll(Canvas & to) +{ + to.drawColorBlended(pos, color); + if(drawLine) + to.drawBorder(pos, colorLine); +} + +SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) : + pos1(pos1), pos2(pos2), color(color) +{} + +void SimpleLine::showAll(Canvas & to) +{ + to.drawLine(pos1 + pos.topLeft(), pos2 + pos.topLeft(), color, color); +} diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index ed45f0ab7..28e306614 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -10,13 +10,18 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN class CGGarrison; class CGCreature; struct InfoAboutArmy; +struct InfoAboutHero; +struct InfoAboutTown; class CArmedInstance; +class CGTownInstance; +class CGHeroInstance; class AFactionMember; VCMI_LIB_NAMESPACE_END @@ -27,6 +32,7 @@ class CGarrisonInt; class CCreatureAnim; class CComponent; class CAnimImage; +class LRClickableArea; /// Shows a text by moving the mouse cursor over the object class CHoverableArea: public virtual CIntObject @@ -129,8 +135,13 @@ class CInteractableTownTooltip : public CIntObject std::shared_ptr res1; std::shared_ptr res2; std::shared_ptr garrison; + + std::shared_ptr fastTavern; + std::shared_ptr fastMarket; + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; - void init(const InfoAboutTown & town); + void init(const CGTownInstance * town); public: CInteractableTownTooltip(Point pos, const CGTownInstance * town); }; @@ -171,30 +182,33 @@ public: ~CMinorResDataBar(); }; -/// Opens hero window by left-clicking on it +/// Performs an action by left-clicking on it. Opens hero window by default class CHeroArea: public CIntObject { - const CGHeroInstance * hero; - std::shared_ptr portrait; - public: - CHeroArea(int x, int y, const CGHeroInstance * _hero); + using ClickFunctor = std::function; + CHeroArea(int x, int y, const CGHeroInstance * hero); + void addClickCallback(ClickFunctor callback); void clickPressed(const Point & cursorPosition) override; void hover(bool on) override; +private: + const CGHeroInstance * hero; + std::shared_ptr portrait; + ClickFunctor clickFunctor; + ClickFunctor showPopupHandler; }; /// Can interact on left and right mouse clicks class LRClickableAreaWTextComp: public LRClickableAreaWText { public: - int type; - int baseType; - int bonusValue; + Component component; + void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), int BaseType = -1); + LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), ComponentType baseType = ComponentType::NONE); std::shared_ptr createComponent() const; }; @@ -207,6 +221,17 @@ public: LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town); }; +/// Can do action on click +class LRClickableArea: public CIntObject +{ + std::function onClick; + std::function onPopup; +public: + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + LRClickableArea(const Rect & Pos, std::function onClick = nullptr, std::function onPopup = nullptr); +}; + class MoraleLuckBox : public LRClickableAreaWTextComp { std::shared_ptr image; @@ -218,3 +243,24 @@ public: MoraleLuckBox(bool Morale, const Rect &r, bool Small=false); }; + +class TransparentFilledRectangle : public CIntObject +{ + ColorRGBA color; + ColorRGBA colorLine; + bool drawLine; +public: + TransparentFilledRectangle(Rect position, ColorRGBA color); + TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine); + void showAll(Canvas & to) override; +}; + +class SimpleLine : public CIntObject +{ + Point pos1; + Point pos2; + ColorRGBA color; +public: + SimpleLine(Point pos1, Point pos2, ColorRGBA color); + void showAll(Canvas & to) override; +}; diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index 2d6aa478b..ea8743b91 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -21,16 +21,16 @@ #include "../../lib/CGeneralTextHandler.h" -RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback) +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; - inactiveImage = std::make_shared("radialMenu/itemInactive", Point(0, 0)); - selectedImage = std::make_shared("radialMenu/itemEmpty", Point(0, 0)); + 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)); - iconImage = std::make_shared("radialMenu/" + imageName, Point(0, 0)); + iconImage = std::make_shared(ImagePath::builtin("radialMenu/" + imageName), Point(0, 0)); pos = selectedImage->pos; selectedImage->setEnabled(false); @@ -42,13 +42,13 @@ void RadialMenuItem::setSelected(bool selected) inactiveImage->setEnabled(!selected); } -RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig): - centerPosition(positionToCenter) +RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool alternativeLayout): + centerPosition(positionToCenter), alternativeLayout(alternativeLayout) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos += positionToCenter; - Point itemSize = Point(70, 80); + Point itemSize = alternativeLayout ? Point(80, 70) : Point(70, 80); moveBy(-itemSize / 2); pos.w = itemSize.x; pos.h = itemSize.y; @@ -56,10 +56,11 @@ RadialMenu::RadialMenu(const Point & positionToCenter, const std::vectorpos); + pos = pos.include(statusBar->pos); fitToScreen(10); @@ -71,7 +72,7 @@ void RadialMenu::addItem(const Point & offset, bool enabled, const std::string & if (!enabled) return; - auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback); + auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback, alternativeLayout); item->moveBy(offset); diff --git a/client/widgets/RadialMenu.h b/client/widgets/RadialMenu.h index 854999280..1b8462fd9 100644 --- a/client/widgets/RadialMenu.h +++ b/client/widgets/RadialMenu.h @@ -26,6 +26,13 @@ struct RadialMenuConfig static constexpr Point ITEM_SW = Point(-40, +70); static constexpr Point ITEM_SE = Point(+40, +70); + static constexpr Point ITEM_ALT_NN = Point(0, -80); + static constexpr Point ITEM_ALT_SS = Point(0, +80); + static constexpr Point ITEM_ALT_NW = Point(-70, -40); + static constexpr Point ITEM_ALT_SW = Point(-70, +40); + static constexpr Point ITEM_ALT_NE = Point(+70, -40); + static constexpr Point ITEM_ALT_SE = Point(+70, +40); + Point itemPosition; bool enabled; std::string imageName; @@ -44,7 +51,7 @@ class RadialMenuItem : public CIntObject std::string hoverText; public: - RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback); + RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool alternativeLayout); void setSelected(bool selected); }; @@ -60,8 +67,10 @@ class RadialMenu : public CIntObject void addItem(const Point & offset, bool enabled, const std::string & path, const std::string & hoverText, const std::function & callback); std::shared_ptr findNearestItem(const Point & cursorPosition) const; + + bool alternativeLayout; public: - RadialMenu(const Point & positionToCenter, const std::vector & menuConfig); + RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool alternativeLayout = false); void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 6246e9df3..b8dfc98bf 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -17,6 +17,7 @@ #include "../gui/Shortcut.h" #include "../gui/CGuiHandler.h" #include "../render/Canvas.h" +#include "../render/Colors.h" void CSlider::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) { @@ -68,6 +69,11 @@ int CSlider::getValue() const return value; } +void CSlider::setValue(int to) +{ + scrollTo(value); +} + int CSlider::getCapacity() const { return capacity; @@ -118,7 +124,7 @@ void CSlider::scrollTo(int to) updateSliderPos(); - moved(to); + moved(getValue()); } void CSlider::clickPressed(const Point & cursorPosition) @@ -163,7 +169,7 @@ bool CSlider::receiveEvent(const Point &position, int eventType) const return testTarget.isInside(position); } -CSlider::CSlider(Point position, int totalw, std::function Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) +CSlider::CSlider(Point position, int totalw, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) : Scrollable(LCLICK | DRAG, position, orientation ), capacity(Capacity), amount(Amount), @@ -177,7 +183,7 @@ CSlider::CSlider(Point position, int totalw, std::function Moved, int if(style == BROWN) { - std::string name = getOrientation() == Orientation::HORIZONTAL ? "IGPCRDIV.DEF" : "OVBUTN2.DEF"; + AnimationPath name = AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "IGPCRDIV.DEF" : "OVBUTN2.DEF"); //NOTE: this images do not have "blocked" frames. They should be implemented somehow (e.g. palette transform or something...) left = std::make_shared(Point(), name, CButton::tooltip()); @@ -190,9 +196,9 @@ CSlider::CSlider(Point position, int totalw, std::function Moved, int } else { - left = std::make_shared(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBLF.DEF" : "SCNRBUP.DEF", CButton::tooltip()); - right = std::make_shared(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF", CButton::tooltip()); - slider = std::make_shared(Point(), "SCNRBSL.DEF", CButton::tooltip()); + left = std::make_shared(Point(), AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "SCNRBLF.DEF" : "SCNRBUP.DEF"), CButton::tooltip()); + right = std::make_shared(Point(), AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF"), CButton::tooltip()); + slider = std::make_shared(Point(), AnimationPath::builtin("SCNRBSL.DEF"), CButton::tooltip()); } slider->actOnDown = true; slider->soundDisabled = true; @@ -296,3 +302,31 @@ void CSlider::scrollToMax() { scrollTo(amount); } + +SliderNonlinear::SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style) + : CSlider(position, length, Moved, 1, values.size(), Value, orientation, style) + , scaledValues(values) +{ + +} + +int SliderNonlinear::getValue() const +{ + return scaledValues.at(CSlider::getValue()); +} + +void SliderNonlinear::setValue(int to) +{ + size_t nearest = 0; + + for(size_t i = 0; i < scaledValues.size(); ++i) + { + int nearestDistance = std::abs(to - scaledValues[nearest]); + int currentDistance = std::abs(to - scaledValues[i]); + + if(currentDistance < nearestDistance) + nearest = i; + } + + scrollTo(nearest); +} diff --git a/client/widgets/Slider.h b/client/widgets/Slider.h index f7a8cad10..b6e8be677 100644 --- a/client/widgets/Slider.h +++ b/client/widgets/Slider.h @@ -59,10 +59,11 @@ public: /// Amount modifier void setAmount(int to); + virtual void setValue(int to); /// Accessors int getAmount() const; - int getValue() const; + virtual int getValue() const; int getCapacity() const; void addCallback(std::function callback); @@ -80,7 +81,20 @@ public: /// @param Capacity maximal number of visible at once elements /// @param Amount total amount of elements, including not visible /// @param Value starting position - CSlider(Point position, int length, std::function Moved, int Capacity, int Amount, + CSlider(Point position, int length, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, EStyle style = BROWN); ~CSlider(); }; + +class SliderNonlinear : public CSlider +{ + /// If non-empty then slider has non-linear values, e.g. if slider is at position 5 out of 10 then actual "value" is not 5, but 5th value in this vector + std::vector scaledValues; + + using CSlider::setAmount; // make private +public: + void setValue(int to) override; + int getValue() const override; + + SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style); +}; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index e4e61a0c5..0bc99bab7 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -17,9 +17,12 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../windows/CMessage.h" +#include "../windows/InfoWindows.h" #include "../adventureMap/CInGameConsole.h" #include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../../lib/TextOperations.h" @@ -44,7 +47,7 @@ void CLabel::showAll(Canvas & to) } -CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const SDL_Color & Color, const std::string & Text) +CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : CTextContainer(Align, Font, Color), text(Text) { setRedrawParent(true); @@ -87,7 +90,7 @@ void CLabel::setText(const std::string & Txt) } } -void CLabel::setColor(const SDL_Color & Color) +void CLabel::setColor(const ColorRGBA & Color) { color = Color; if(autoRedraw) @@ -104,7 +107,7 @@ size_t CLabel::getWidth() return graphics->fonts[font]->getStringWidth(visibleText());; } -CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const SDL_Color & Color, const std::string & Text) : +CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : CLabel(position.x, position.y, Font, Align, Color, Text), visibleSize(0, 0, position.w, position.h) { @@ -150,6 +153,17 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) //We should count delimiters length from string to correct centering later. delimitersCount *= f->getStringWidth(delimeters)/2; + std::smatch match; + std::regex expr("\\{(.*?)\\|"); + std::string::const_iterator searchStart( what.cbegin() ); + while(std::regex_search(searchStart, what.cend(), match, expr)) + { + std::string colorText = match[1].str(); + if(auto c = Colors::parseColor(colorText)) + delimitersCount += f->getStringWidth(colorText + "|"); + searchStart = match.suffix().first; + } + // input is rect in which given text should be placed // calculate proper position for top-left corner of the text if(alignment == ETextAlignment::TOPLEFT) @@ -186,8 +200,25 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) { std::string toPrint = what.substr(begin, end - begin); - if(currDelimeter % 2) // Enclosed in {} text - set to yellow - to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); + if(currDelimeter % 2) // Enclosed in {} text - set to yellow or defined color + { + std::smatch match; + std::regex expr("^(.*?)\\|"); + if(std::regex_search(toPrint, match, expr)) + { + std::string colorText = match[1].str(); + + if(auto color = Colors::parseColor(colorText)) + { + toPrint = toPrint.substr(colorText.length() + 1, toPrint.length() - colorText.length()); + to.drawText(where, font, *color, ETextAlignment::TOPLEFT, toPrint); + } + else + to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); + } + else + to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); + } else // Non-enclosed text, use default color to.drawText(where, font, color, ETextAlignment::TOPLEFT, toPrint); @@ -199,7 +230,7 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) } while(begin++ != std::string::npos); } -CTextContainer::CTextContainer(ETextAlignment alignment, EFonts font, SDL_Color color) : +CTextContainer::CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color) : alignment(alignment), font(font), color(color) @@ -282,7 +313,7 @@ Rect CMultiLineLabel::getTextLocation() return Rect(); } -CLabelGroup::CLabelGroup(EFonts Font, ETextAlignment Align, const SDL_Color & Color) +CLabelGroup::CLabelGroup(EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : font(Font), align(Align), color(Color) { defActions = 255 - DISPOSE; @@ -299,7 +330,7 @@ size_t CLabelGroup::currentSize() const return labels.size(); } -CTextBox::CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font, ETextAlignment Align, const SDL_Color & Color) : +CTextBox::CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : sliderStyle(SliderStyle), slider(nullptr) { @@ -345,6 +376,7 @@ void CTextBox::setText(const std::string & text) { // decrease width again if slider still used label->pos.w = pos.w - 32; + assert(label->pos.w > 0); label->setText(text); slider->setAmount(label->textSize.y); } @@ -352,6 +384,7 @@ void CTextBox::setText(const std::string & text) { // create slider and update widget label->pos.w = pos.w - 32; + assert(label->pos.w > 0); label->setText(text); OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); @@ -410,7 +443,7 @@ void CGStatusBar::clear() write({}); } -CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const SDL_Color & Color) +CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "") , enteringText(false) { @@ -423,13 +456,22 @@ CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, E autoRedraw = false; } -CGStatusBar::CGStatusBar(int x, int y, std::string name, int maxw) +CGStatusBar::CGStatusBar(int x, int y) + : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) + , enteringText(false) +{ + // without background + addUsedEvents(LCLICK); +} + +CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) , enteringText(false) { addUsedEvents(LCLICK); OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + auto backgroundImage = std::make_shared(name); background = backgroundImage; pos = background->pos; @@ -492,7 +534,7 @@ Point CGStatusBar::getBorderSize() return Point(); } -CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) +CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput) : CLabel(Pos.x, Pos.y, font, ETextAlignment::CENTER), cb(CB), CFocusable(std::make_shared(this)) @@ -501,14 +543,15 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) +CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB) :cb(CB), CFocusable(std::make_shared(this)) { pos += Pos.topLeft(); @@ -517,7 +560,7 @@ CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const std::stri OBJ_CONSTRUCTION; background = std::make_shared(bgName, bgOffset.x, bgOffset.y); - addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT); + addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT); #if !defined(VCMI_MOBILE) giveFocus(); @@ -610,6 +653,13 @@ void CTextInput::keyPressed(EShortcut key) } } +void CTextInput::showPopupWindow(const Point & cursorPosition) +{ + if(!helpBox.empty()) //there is no point to show window with nothing inside... + CRClickPopup::createAndPush(helpBox); +} + + void CTextInput::setText(const std::string & nText) { setText(nText, false); @@ -622,6 +672,11 @@ void CTextInput::setText(const std::string & nText, bool callCb) cb(text); } +void CTextInput::setHelpText(const std::string & text) +{ + helpBox = text; +} + void CTextInput::textInputed(const std::string & enteredText) { if(!focus) @@ -753,3 +808,15 @@ void CFocusable::moveFocus() } } } + +void CFocusable::removeFocus() +{ + if(this == inputWithFocus) + { + focus = false; + focusListener->focusLost(); + redraw(); + + inputWithFocus = nullptr; + } +} diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index bf7cabcc3..35e21245c 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -12,10 +12,9 @@ #include "../gui/CIntObject.h" #include "../gui/TextAlignment.h" #include "../render/Colors.h" -#include "../render/Graphics.h" +#include "../render/EFont.h" #include "../../lib/FunctionList.h" - -#include +#include "../../lib/filesystem/ResourcePath.h" class IImage; class CSlider; @@ -30,12 +29,12 @@ protected: /// do actual blitting of line. Text "what" will be placed at "where" and aligned according to alignment void blitLine(Canvas & to, Rect where, std::string what); - CTextContainer(ETextAlignment alignment, EFonts font, SDL_Color color); + CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color); public: ETextAlignment alignment; EFonts font; - SDL_Color color; // default font color. Can be overridden by placing "{}" into the string + ColorRGBA color; // default font color. Can be overridden by placing "{}" into the string }; /// Label which shows text @@ -54,11 +53,11 @@ public: std::string getText(); virtual void setAutoRedraw(bool option); virtual void setText(const std::string & Txt); - virtual void setColor(const SDL_Color & Color); + virtual void setColor(const ColorRGBA & Color); size_t getWidth(); CLabel(int x = 0, int y = 0, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, - const SDL_Color & Color = Colors::WHITE, const std::string & Text = ""); + const ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); void showAll(Canvas & to) override; //shows statusbar (with current text) }; @@ -68,9 +67,9 @@ class CLabelGroup : public CIntObject std::vector> labels; EFonts font; ETextAlignment align; - SDL_Color color; + ColorRGBA color; public: - CLabelGroup(EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE); + CLabelGroup(EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE); void add(int x = 0, int y = 0, const std::string & text = ""); size_t currentSize() const; }; @@ -91,7 +90,7 @@ public: // total size of text, x = longest line of text, y = total height of lines Point textSize; - CMultiLineLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE, const std::string & Text = ""); + 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; void showAll(Canvas & to) override; @@ -111,7 +110,7 @@ public: std::shared_ptr label; std::shared_ptr slider; - CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE); + CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE); void resize(Point newSize); void setText(const std::string & Txt); @@ -125,8 +124,9 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const SDL_Color & Color = Colors::WHITE); - CGStatusBar(int x, int y, std::string name, int maxw = -1); + CGStatusBar(std::shared_ptr background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const ColorRGBA & Color = Colors::WHITE); + CGStatusBar(int x, int y, const ImagePath & name, int maxw = -1); + CGStatusBar(int x, int y); //make CLabel API private using CLabel::getText; @@ -184,6 +184,7 @@ public: void giveFocus(); //captures focus void moveFocus(); //moves focus to next active control (may be used for tab switching) + void removeFocus(); //remove focus bool hasFocus() const; static std::list focusables; //all existing objs @@ -211,21 +212,26 @@ public: class CTextInput : public CLabel, public CFocusable { std::string newText; + std::string helpBox; //for right-click help + protected: std::string visibleText() override; public: + CFunctionList cb; CFunctionList filters; void setText(const std::string & nText) override; void setText(const std::string & nText, bool callCb); + void setHelpText(const std::string &); - CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB); - CTextInput(const Rect & Pos, const Point & bgOffset, const std::string & bgName, const CFunctionList & CB); + CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput = true); + CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB); CTextInput(const Rect & Pos, std::shared_ptr srf); void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; + void showPopupWindow(const Point & cursorPosition) override; //bool captureThisKey(EShortcut key) override; diff --git a/client/windows/CAltarWindow.cpp b/client/windows/CAltarWindow.cpp new file mode 100644 index 000000000..a6a312b22 --- /dev/null +++ b/client/windows/CAltarWindow.cpp @@ -0,0 +1,141 @@ +/* + * CAltarWindow.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 "CAltarWindow.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" + +#include "../CGameInfo.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +CAltarWindow::CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin(mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp")) + , hero(hero) + , windowClosedCallback(onWindowClosed) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + assert(mode == EMarketMode::ARTIFACT_EXP || mode == EMarketMode::CREATURE_EXP); + if(mode == EMarketMode::ARTIFACT_EXP) + createAltarArtifacts(market, hero); + else if(mode == EMarketMode::CREATURE_EXP) + createAltarCreatures(market, hero); + + updateExpToLevel(); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CAltarWindow::updateExpToLevel() +{ + altar->expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(altar->hero->exp) + 1) - altar->hero->exp)); +} + +void CAltarWindow::updateGarrisons() +{ + if(auto altarCreatures = std::static_pointer_cast(altar)) + altarCreatures->updateGarrison(); +} + +bool CAltarWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + +const CGHeroInstance * CAltarWindow::getHero() const +{ + return hero; +} + +void CAltarWindow::close() +{ + if(windowClosedCallback) + windowClosedCallback(); + + CWindowObject::close(); +} + +void CAltarWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED); + + auto altarArtifacts = std::make_shared(market, hero); + altar = altarArtifacts; + artSets.clear(); + addSetAndCallbacks(altarArtifacts->getAOHset()); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), + CGI->generaltexth->zelp[572], std::bind(&CAltarWindow::createAltarCreatures, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::GOOD) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED); + + altar = std::make_shared(market, hero); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), + CGI->generaltexth->zelp[580], std::bind(&CAltarWindow::createAltarArtifacts, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::EVIL) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) +{ + if(!getState().has_value()) + return; + + if(auto altarArtifacts = std::static_pointer_cast(altar)) + { + if(const auto pickedArt = getPickedArtifact()) + altarArtifacts->setSelectedArtifact(pickedArt); + else + altarArtifacts->setSelectedArtifact(nullptr); + } + CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw); +} + +void CAltarWindow::showAll(Canvas & to) +{ + // This func is temporary workaround for compliance with CTradeWindow + CWindowObject::showAll(to); + + if(altar->hRight) + { + to.drawBorder(Rect::createAround(altar->hRight->pos, 1), Colors::BRIGHT_YELLOW, 2); + altar->hRight->showAllAt(altar->pos.topLeft() + Point(396, 423), "", to); + } + if(altar->hLeft) + { + to.drawBorder(Rect::createAround(altar->hLeft->pos, 1), Colors::BRIGHT_YELLOW, 2); + altar->hLeft->showAllAt(altar->pos.topLeft() + Point(150, 423), "", to); + } +} diff --git a/client/windows/CAltarWindow.h b/client/windows/CAltarWindow.h new file mode 100644 index 000000000..83ddea490 --- /dev/null +++ b/client/windows/CAltarWindow.h @@ -0,0 +1,39 @@ +/* + * CAltarWindow.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 "../widgets/CAltar.h" +#include "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +class CAltarWindow : public CWindowObject, public CWindowWithArtifacts, public IGarrisonHolder +{ +public: + CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode); + void updateExpToLevel(); + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + const CGHeroInstance * getHero() const; + void close() override; + + void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override; + void showAll(Canvas & to) override; + +private: + const CGHeroInstance * hero; + std::shared_ptr altar; + std::shared_ptr changeModeButton; + std::shared_ptr quitButton; + std::function windowClosedCallback; + std::shared_ptr statusBar; + + void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero); +}; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 2f6e20f58..d501464c2 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1,1880 +1,1972 @@ -/* - * CCastleInterface.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 "CCastleInterface.h" - -#include "CHeroWindow.h" -#include "CTradeWindow.h" -#include "InfoWindows.h" -#include "GUIClasses.h" -#include "QuickRecruitmentWindow.h" -#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 "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/Buttons.h" -#include "../widgets/TextControls.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/ColorFilter.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../adventureMap/CList.h" -#include "../adventureMap/CResDataBar.h" - -#include "../../CCallback.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CBuildingHandler.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" -#include "../../lib/GameSettings.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/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/CGTownInstance.h" - - -static bool useCompactCreatureBox() -{ - return settings["gameTweaks"]["compactTownCreatureInfo"].Bool(); -} - -static bool useAvailableAmountAsCreatureLabel() -{ - return settings["gameTweaks"]["availableCreaturesAsDwellingLabel"].Bool(); -} - -CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town, const CStructure * Str) - : CShowableAnim(0, 0, Str->defName, CShowableAnim::BASE, BUILDING_FRAME_TIME), - parent(Par), - town(Town), - str(Str), - border(nullptr), - area(nullptr), - stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT) -{ - addUsedEvents(LCLICK | SHOW_POPUP | MOVE | HOVER | TIME); - pos.x += str->pos.x; - pos.y += str->pos.y; - - // 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 && - (Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP)) - { - if(Town->hasBuilt(BuildingID::CITADEL)) - { - this->first = 1; - this->frame = 1; - } - else - this->last = 0; - } - - if(!str->borderName.empty()) - border = IImage::createFromFile(str->borderName, EImageBlitMode::ALPHA); - - if(!str->areaName.empty()) - area = IImage::createFromFile(str->areaName, EImageBlitMode::ALPHA); -} - -const CBuilding * CBuildingRect::getBuilding() -{ - if (!str->building) - return nullptr; - - if (str->hiddenUpgrade) // hidden upgrades, e.g. hordes - return base (dwelling for hordes) - return town->town->buildings.at(str->building->getBase()); - - return str->building; -} - -bool CBuildingRect::operator<(const CBuildingRect & p2) const -{ - return (str->pos.z) < (p2.str->pos.z); -} - -void CBuildingRect::hover(bool on) -{ - if (!area) - return; - - if(on) - { - if(! parent->selectedBuilding //no building hovered - || (*parent->selectedBuilding)<(*this)) //or we are on top - { - parent->selectedBuilding = this; - GH.statusbar()->write(getSubtitle()); - } - } - else - { - if(parent->selectedBuilding == this) - { - parent->selectedBuilding = nullptr; - GH.statusbar()->clear(); - } - } -} - -void CBuildingRect::clickPressed(const Point & cursorPosition) -{ - if(getBuilding() && area && (parent->selectedBuilding==this)) - { - auto building = getBuilding(); - parent->buildingClicked(building->bid, building->subId, building->upgrade); - } -} - -void CBuildingRect::showPopupWindow(const Point & cursorPosition) -{ - if((!area) || (this!=parent->selectedBuilding) || getBuilding() == nullptr) - return; - - BuildingID bid = getBuilding()->bid; - const CBuilding *bld = town->town->buildings.at(bid); - if (bid < BuildingID::DWELL_FIRST) - { - CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), - std::make_shared(CComponent::building, bld->town->faction->getIndex(), bld->bid)); - } - else - { - int level = ( bid - BuildingID::DWELL_FIRST ) % GameConstants::CREATURES_PER_TOWN; - GH.windows().createAndPushWindow(parent->pos.x+parent->pos.w / 2, parent->pos.y+parent->pos.h /2, town, level); - } -} - -void CBuildingRect::show(Canvas & to) -{ - uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT; - - if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT) - { - setAlpha(255 * stateTimeCounter / stageDelay); - CShowableAnim::show(to); - } - else - { - setAlpha(255); - CShowableAnim::show(to); - } - - if(border && stateTimeCounter > BUILDING_APPEAR_TIMEPOINT) - { - if(stateTimeCounter >= BUILD_ANIMATION_FINISHED_TIMEPOINT) - { - if(parent->selectedBuilding == this) - to.draw(border, pos.topLeft()); - return; - } - - auto darkBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 0.5f, 0.5f, 0.5f ); - auto lightBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 2.0f, 2.0f, 2.0f ); - auto baseBorder = ColorFilter::genEmptyShifter(); - - float progress = float(stateTimeCounter % stageDelay) / stageDelay; - - if (stateTimeCounter < BUILDING_WHITE_BORDER_TIMEPOINT) - border->adjustPalette(ColorFilter::genInterpolated(lightBorder, darkBorder, progress), 0); - else - if (stateTimeCounter < BUILDING_YELLOW_BORDER_TIMEPOINT) - border->adjustPalette(ColorFilter::genInterpolated(darkBorder, baseBorder, progress), 0); - else - border->adjustPalette(baseBorder, 0); - - to.draw(border, pos.topLeft()); - } -} - -void CBuildingRect::tick(uint32_t msPassed) -{ - CShowableAnim::tick(msPassed); - stateTimeCounter += msPassed; -} - -void CBuildingRect::showAll(Canvas & to) -{ - if (stateTimeCounter == 0) - return; - - CShowableAnim::showAll(to); - if(!isActive() && parent->selectedBuilding == this && border) - to.draw(border, pos.topLeft()); -} - -std::string CBuildingRect::getSubtitle()//hover text for building -{ - if (!getBuilding()) - return ""; - - int bid = getBuilding()->bid; - - if (bid<30)//non-dwellings - only buiding name - return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); - else//dwellings - recruit %creature% - { - auto & availableCreatures = town->creatures[(bid-30)%GameConstants::CREATURES_PER_TOWN].second; - if(availableCreatures.size()) - { - int creaID = availableCreatures.back();//taking last of available creatures - return CGI->generaltexth->allTexts[16] + " " + CGI->creh->objects.at(creaID)->getNamePluralTranslated(); - } - else - { - logGlobal->warn("Dwelling with id %d offers no creatures!", bid); - return "#ERROR#"; - } - } -} - -void CBuildingRect::mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) -{ - hover(true); -} - -bool CBuildingRect::receiveEvent(const Point & position, int eventType) const -{ - if (!pos.isInside(position.x, position.y)) - return false; - - if(area && area->isTransparent(position - pos.topLeft())) - return false; - - return CIntObject::receiveEvent(position, eventType); -} - -CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level) - : CWindowObject(RCLICK_POPUP, "CRTOINFO", Point(centerX, centerY)) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background->colorize(Town->tempOwner); - - const CCreature * creature = CGI->creh->objects.at(Town->creatures.at(level).second.back()); - - title = std::make_shared(80, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getNamePluralTranslated()); - animation = std::make_shared(30, 44, creature, true, true); - - std::string text = std::to_string(Town->creatures.at(level).first); - available = std::make_shared(80,190, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[217] + text); - costPerTroop = std::make_shared(80, 227, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[346]); - - for(int i = 0; i(i); - if(creature->getRecruitCost(res)) - { - resPicture.push_back(std::make_shared("RESOURCE", i, 0, 0, 0)); - resAmount.push_back(std::make_shared(0,0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(creature->getRecruitCost(res)))); - } - } - - int posY = 238; - int posX = pos.w/2 - (int)resAmount.size() * 25 + 5; - for (size_t i=0; imoveBy(Point(posX, posY)); - resAmount[i]->moveBy(Point(posX+16, posY+43)); - posX += 50; - } -} - -CDwellingInfoBox::~CDwellingInfoBox() = default; - -CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroSlots * Owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - owner = Owner; - pos.x += x; - pos.y += y; - pos.w = 58; - pos.h = 64; - upg = updown; - - portrait = std::make_shared("PortraitsLarge", 0, 0, 0, 0); - portrait->visible = false; - - flag = std::make_shared("CREST58", 0, 0, 0, 0); - flag->visible = false; - - selection = std::make_shared("TWCRPORT", 1, 0); - selection->visible = false; - - set(h); - - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); -} - -CHeroGSlot::~CHeroGSlot() = default; - -void CHeroGSlot::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - return; - } - std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; - std::string temp; - if(hero) - { - if(isSelected())//view NNN - { - temp = CGI->generaltexth->tcommands[4]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - else if(other->hero && other->isSelected())//exchange - { - temp = CGI->generaltexth->tcommands[7]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); - } - else// select NNN (in ZZZ) - { - if(upg)//down - visiting - { - temp = CGI->generaltexth->tcommands[32]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - else //up - garrison - { - temp = CGI->generaltexth->tcommands[12]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - } - } - else //we are empty slot - { - if(other->isSelected() && other->hero) //move NNNN - { - temp = CGI->generaltexth->tcommands[6]; - boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); - } - else //empty - { - temp = CGI->generaltexth->allTexts[507]; - } - } - if(temp.size()) - GH.statusbar()->write(temp); -} - -void CHeroGSlot::clickPressed(const Point & cursorPosition) -{ - std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; - - owner->garr->setSplittingMode(false); - owner->garr->selectSlot(nullptr); - - if(hero && isSelected()) - { - setHighlight(false); - LOCPLINT->openHeroWindow(hero); - } - else if(other->hero && other->isSelected()) - { - owner->swapArmies(); - } - else if(hero) - { - setHighlight(true); - owner->garr->selectSlot(nullptr); - redraw(); - } - - //refresh statusbar - hover(false); - hover(true); -} - -void CHeroGSlot::showPopupWindow(const Point & cursorPosition) -{ - if(hero) - { - GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); - } -} - -void CHeroGSlot::deactivate() -{ - selection->visible = false; - CIntObject::deactivate(); -} - -bool CHeroGSlot::isSelected() const -{ - return selection->visible; -} - -void CHeroGSlot::setHighlight(bool on) -{ - selection->visible = on; - - if(owner->garrisonedHero->hero && owner->visitingHero->hero) //two heroes in town - { - for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot higlighted - elem->block(!on); - } -} - -void CHeroGSlot::set(const CGHeroInstance * newHero) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - hero = newHero; - - selection->visible = false; - portrait->visible = false; - flag->visible = false; - - if(newHero) - { - portrait->visible = true; - portrait->setFrame(newHero->portrait); - } - else if(!upg && owner->showEmpty) //up garrison - { - flag->visible = true; - flag->setFrame(LOCPLINT->castleInt->town->getOwner().getNum()); - } -} - -HeroSlots::HeroSlots(const CGTownInstance * Town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty): - showEmpty(ShowEmpty), - town(Town), - garr(Garrison) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - garrisonedHero = std::make_shared(garrPos.x, garrPos.y, 0, town->garrisonHero, this); - visitingHero = std::make_shared(visitPos.x, visitPos.y, 1, town->visitingHero, this); -} - -HeroSlots::~HeroSlots() = default; - -void HeroSlots::update() -{ - garrisonedHero->set(town->garrisonHero); - visitingHero->set(town->visitingHero); -} - -void HeroSlots::splitClicked() -{ - if(!!town->visitingHero && town->garrisonHero && (visitingHero->isSelected() || garrisonedHero->isSelected())) - { - LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id); - } -} - -void HeroSlots::swapArmies() -{ - bool allow = true; - - //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)) - { - 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))); - - LOCPLINT->showInfoDialog(text, std::vector>(), soundBase::sound_todo); - allow = false; - } - else if (town->garrisonHero->stacksCount() == 0) - { - //This hero has no creatures. A hero must have creatures before he can brave the dangers of the countryside. - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.19"), {}, soundBase::sound_todo); - allow = false; - } - } - - if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army - { - if(!town->visitingHero->canBeMergedWith(*town)) - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[275], std::vector>(), soundBase::sound_todo); - allow = false; - } - } - - garrisonedHero->setHighlight(false); - visitingHero->setHighlight(false); - - if (allow) - LOCPLINT->cb->swapGarrisonHero(town); -} - -class SORTHELP -{ -public: - bool operator() (const CIntObject * a, const CIntObject * b) - { - auto b1 = dynamic_cast(a); - auto b2 = dynamic_cast(b); - - if(!b1 && !b2) - return intptr_t(a) < intptr_t(b); - if(b1 && !b2) - return false; - if(!b1 && b2) - return true; - - return (*b1)<(*b2); - } -}; - -SORTHELP buildSorter; - -CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): - town(Town), - selectedBuilding(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - background = std::make_shared(town->town->clientInfo.townBackground); - background->needRefresh = true; - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - pos.w = background->pos.w; - pos.h = background->pos.h; - - recreate(); -} - -CCastleBuildings::~CCastleBuildings() = default; - -void CCastleBuildings::recreate() -{ - selectedBuilding = nullptr; - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - buildings.clear(); - groups.clear(); - - //Generate buildings list - - auto buildingsCopy = town->builtBuildings;// a bit modified copy of built buildings - - if(vstd::contains(town->builtBuildings, BuildingID::SHIPYARD)) - { - auto bayPos = town->bestLocation(); - if(!bayPos.valid()) - logGlobal->warn("Shipyard in non-coastal town!"); - std::vector vobjs = LOCPLINT->cb->getVisitableObjs(bayPos, false); - //there is visitable obj at shipyard output tile and it's a boat or hero (on boat) - if(!vobjs.empty() && (vobjs.front()->ID == Obj::BOAT || vobjs.front()->ID == Obj::HERO)) - { - buildingsCopy.insert(BuildingID::SHIP); - } - } - - for(const CStructure * structure : town->town->clientInfo.structures) - { - if(!structure->building) - { - buildings.push_back(std::make_shared(this, town, structure)); - continue; - } - if(vstd::contains(buildingsCopy, structure->building->bid)) - { - groups[structure->building->getBase()].push_back(structure); - } - } - - for(auto & entry : groups) - { - const CBuilding * build = town->town->buildings.at(entry.first); - - const CStructure * toAdd = *boost::max_element(entry.second, [=](const CStructure * a, const CStructure * b) - { - return build->getDistance(a->building->bid) < build->getDistance(b->building->bid); - }); - - buildings.push_back(std::make_shared(this, town, toAdd)); - } - - boost::sort(children, buildSorter); //TODO: create building in blit order -} - -void CCastleBuildings::addBuilding(BuildingID building) -{ - //FIXME: implement faster method without complete recreation of town - BuildingID base = town->town->buildings.at(building)->getBase(); - - recreate(); - - auto & structures = groups.at(base); - - for(auto buildingRect : buildings) - { - if(vstd::contains(structures, buildingRect->str)) - { - //reset animation - if(structures.size() == 1) - buildingRect->stateTimeCounter = 0; // transparency -> fully visible stage - else - buildingRect->stateTimeCounter = CBuildingRect::BUILDING_APPEAR_TIMEPOINT; // already in fully visible stage - break; - } - } -} - -void CCastleBuildings::removeBuilding(BuildingID building) -{ - //FIXME: implement faster method without complete recreation of town - recreate(); -} - -const CGHeroInstance * CCastleBuildings::getHero() -{ - if(town->visitingHero) - return town->visitingHero; - else - return town->garrisonHero; -} - -void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades) -{ - logGlobal->trace("You've clicked on %d", (int)building.toEnum()); - const CBuilding *b = town->town->buildings.find(building)->second; - - if(building >= BuildingID::DWELL_FIRST) - { - enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN); - } - else - { - switch(building) - { - case BuildingID::MAGES_GUILD_1: - case BuildingID::MAGES_GUILD_2: - case BuildingID::MAGES_GUILD_3: - case BuildingID::MAGES_GUILD_4: - case BuildingID::MAGES_GUILD_5: - enterMagesGuild(); - break; - - case BuildingID::TAVERN: - LOCPLINT->showTavernWindow(town); - break; - - case BuildingID::SHIPYARD: - if(town->shipyardStatus() == IBoatGenerator::GOOD) - LOCPLINT->showShipyardDialog(town); - else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT) - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); - break; - - case BuildingID::FORT: - case BuildingID::CITADEL: - case BuildingID::CASTLE: - GH.windows().createAndPushWindow(town); - break; - - case BuildingID::VILLAGE_HALL: - case BuildingID::CITY_HALL: - case BuildingID::TOWN_HALL: - case BuildingID::CAPITOL: - enterTownHall(); - break; - - case BuildingID::MARKETPLACE: - GH.windows().createAndPushWindow(town, town->visitingHero); - break; - - case BuildingID::BLACKSMITH: - enterBlacksmith(town->town->warMachine); - break; - - case BuildingID::SHIP: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat - break; - - case BuildingID::SPECIAL_1: - case BuildingID::SPECIAL_2: - case BuildingID::SPECIAL_3: - switch(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, 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(), 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); - else - enterBuilding(building); - break; - - case BuildingSubID::BROTHERHOOD_OF_SWORD: - if(upgrades == BuildingID::TAVERN) - LOCPLINT->showTavernWindow(town); - else - enterBuilding(building); - break; - - case BuildingSubID::CASTLE_GATE: - enterCastleGate(); - break; - - case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer - GH.windows().createAndPushWindow(town, getHero()); - break; - - case BuildingSubID::PORTAL_OF_SUMMONING: - if (town->creatures[GameConstants::CREATURES_PER_TOWN].second.empty())//No creatures - LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); - else - enterDwelling(GameConstants::CREATURES_PER_TOWN); - break; - - case BuildingSubID::BALLISTA_YARD: - enterBlacksmith(ArtifactID::BALLISTA); - break; - - default: - enterBuilding(building); - break; - } - break; - - default: - enterBuilding(building); - break; - } - } -} - -void CCastleBuildings::enterBlacksmith(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())); - return; - } - auto art = artifactID.toArtifact(); - - int price = art->getPrice(); - bool possible = LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= price; - if(possible) - { - for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) - { - if(hero->getArt(slot) == nullptr) - { - possible = true; - break; - } - else - { - possible = false; - } - } - } - CreatureID cre = art->getWarMachine(); - GH.windows().createAndPushWindow(possible, cre, artifactID, hero->id); -} - -void CCastleBuildings::enterBuilding(BuildingID building) -{ - std::vector> comps(1, std::make_shared(CComponent::building, town->subID, building)); - LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); -} - -void CCastleBuildings::enterCastleGate() -{ - if (!town->visitingHero) - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[126]); - return;//only visiting hero can use castle gates - } - std::vector availableTowns; - std::vector Towns = LOCPLINT->cb->getTownsInfo(true); - for(auto & Town : Towns) - { - 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->hasBuilt(BuildingSubID::CASTLE_GATE)) //and the town has a castle gate - { - availableTowns.push_back(t->id.getNum());//add to the list - } - } - auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window - GH.windows().createAndPushWindow(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], - CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1)); -} - -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()); - return; - } - - auto recruitCb = [=](CreatureID id, int count) - { - LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); - }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, -87); -} - -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) - : town->creatures.cend(); - const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt, - [](const auto & creatureInfo) { return creatureInfo.first > 0; }); - if(hasSomeoneToRecruit) - GH.windows().createAndPushWindow(town, pos); - else - CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); -} - -void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades) -{ - std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); - std::string descr = town->town->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); - } - - bool isMysticPondOrItsUpgrade = subID == BuildingSubID::MYSTIC_POND - || (upgrades != BuildingID::NONE - && town->town->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); - - if(upgrades != BuildingID::NONE) - descr += "\n\n"+town->town->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); - - if(isMysticPondOrItsUpgrade) //for vanila Rampart like towns - { - if(town->bonusValue.first == 0) //Mystic Pond produced nothing; - descr += "\n\n" + hasNotProduced; - else //Mystic Pond produced something; - { - descr += "\n\n" + hasProduced; - boost::algorithm::replace_first(descr,"%s",CGI->generaltexth->restypes[town->bonusValue.first]); - boost::algorithm::replace_first(descr,"%d",std::to_string(town->bonusValue.second)); - } - } - LOCPLINT->showInfoDialog(descr, comps); -} - -void CCastleBuildings::enterMagesGuild() -{ - const CGHeroInstance *hero = getHero(); - - if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok - { - const StartInfo *si = LOCPLINT->cb->getStartInfo(); - // it would be nice to find a way to move this hack to config/mapOverrides.json - if(si && si->campState && si->campState && // We're in campaign, - (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", - (hero->subID == 45)) // and the hero is Yog (based on Solmyr) - { - // "Yog has given up magic in all its forms..." - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); - } - else if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < 500) //not enough gold to buy spellbook - { - openMagesGuild(); - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[213]); - } - else - { - CFunctionList onYes = [this](){ openMagesGuild(); }; - CFunctionList onNo = onYes; - onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; - std::vector> components(1, std::make_shared(CComponent::artifact,ArtifactID::SPELLBOOK,0)); - - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); - } - } - else - { - openMagesGuild(); - } -} - -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 - { - if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL)) - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[597], //Do you wish this to be the permanent home of the Grail? - [&](){ LOCPLINT->cb->buildBuilding(town, BuildingID::GRAIL); }, - [&](){ openTownHall(); }); - } - else - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[673]); - assert(GH.windows().topWindow() != nullptr); - GH.windows().topWindow()->buttons[0]->addCallback(std::bind(&CCastleBuildings::openTownHall, this)); - } - } - else - { - openTownHall(); - } -} - -void CCastleBuildings::openMagesGuild() -{ - std::string mageGuildBackground; - mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; - GH.windows().createAndPushWindow(LOCPLINT->castleInt,mageGuildBackground); -} - -void CCastleBuildings::openTownHall() -{ - GH.windows().createAndPushWindow(town); -} - -CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable): - town(Town), - level(Level), - showAvailable(_showAvailable) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos += position; - - if(town->creatures.size() <= level || town->creatures[level].second.empty()) - { - level = -1; - return;//No creature - } - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - - ui32 creatureID = town->creatures[level].second.back(); - creature = CGI->creh->objects[creatureID]; - - picture = std::make_shared("CPRSMALL", creature->getIconIndex(), 0, 8, 0); - - std::string value; - if(showAvailable) - value = std::to_string(town->creatures[level].first); - else - value = std::string("+") + std::to_string(town->creatureGrowth(level)); - - if(compact) - { - label = std::make_shared(40, 32, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, value); - pos.x += 8; - pos.w = 32; - pos.h = 32; - } - else - { - label = std::make_shared(24, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, value); - pos.w = 48; - pos.h = 48; - } -} - -void CCreaInfo::update() -{ - if(label) - { - std::string value; - if(showAvailable) - value = std::to_string(town->creatures[level].first); - else - value = std::string("+") + std::to_string(town->creatureGrowth(level)); - - if(value != label->getText()) - label->setText(value); - } -} - -void CCreaInfo::hover(bool on) -{ - std::string message = CGI->generaltexth->allTexts[588]; - boost::algorithm::replace_first(message, "%s", creature->getNamePluralTranslated()); - - if(on) - { - GH.statusbar()->write(message); - } - else - { - GH.statusbar()->clearIfMatching(message); - } -} - -void CCreaInfo::clickPressed(const Point & cursorPosition) -{ - int offset = LOCPLINT->castleInt? (-87) : 0; - auto recruitCb = [=](CreatureID id, int count) - { - LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); - }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, offset); -} - -std::string CCreaInfo::genGrowthText() -{ - GrowthInfo gi = town->getGrowthInfo(level); - std::string descr = boost::str(boost::format(CGI->generaltexth->allTexts[589]) % creature->getNameSingularTranslated() % gi.totalGrowth()); - - for(const GrowthInfo::Entry & entry : gi.entries) - descr +="\n" + entry.description; - - return descr; -} - -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(CComponent::creature, creature->getId())); -} - -bool CCreaInfo::getShowAvailable() -{ - return showAvailable; -} - -CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townHall) - : town(Town), - building(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - addUsedEvents(SHOW_POPUP | HOVER); - pos.x += posX; - pos.y += posY; - int buildID; - - if(townHall) - { - buildID = 10 + town->hallLevel(); - picture = std::make_shared("ITMTL.DEF", town->hallLevel()); - } - else - { - buildID = 6 + town->fortLevel(); - if(buildID == 6) - return;//FIXME: suspicious statement, fix or comment - picture = std::make_shared("ITMCL.DEF", town->fortLevel()-1); - } - building = town->town->buildings.at(BuildingID(buildID)); - pos = picture->pos; -} - -void CTownInfo::hover(bool on) -{ - if(on) - { - if(building ) - GH.statusbar()->write(building->getNameTranslated()); - } - else - { - GH.statusbar()->clear(); - } -} - -void CTownInfo::showPopupWindow(const Point & cursorPosition) -{ - if(building) - { - auto c = std::make_shared(CComponent::building, building->town->faction->getIndex(), building->bid); - CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); - } -} - -CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from): - CStatusbarWindow(PLAYER_COLORED | BORDERED), - town(Town) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - LOCPLINT->castleInt = this; - addUsedEvents(KEYBOARD); - - builds = std::make_shared(town); - panel = std::make_shared("TOWNSCRN", 0, builds->pos.h); - panel->colorize(LOCPLINT->playerID); - pos.w = panel->pos.w; - pos.h = builds->pos.h + panel->pos.h; - center(); - updateShadow(); - - garr = std::make_shared(Point(305, 387), 4, Point(0,96), town->getUpperArmy(), town->visitingHero); - garr->setRedrawParent(true); - - heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); - title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); - income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); - icon = std::make_shared("ITPT", 0, 0, 15, 387); - - exit = std::make_shared(Point(744, 544), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); - exit->setImageOrder(4, 5, 6, 7); - - auto split = std::make_shared(Point(744, 382), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() - { - garr->splitClick(); - heroes->splitClicked(); - }); - garr->addSplitBtn(split); - - Rect barRect(9, 182, 732, 18); - auto statusbarBackground = std::make_shared(panel->getSurface(), barRect, 9, 555); - statusbar = CGStatusBar::create(statusbarBackground); - resdatabar = std::make_shared("ARESBAR", 3, 575, 37, 3, 84, 78); - - townlist = std::make_shared(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() ); - townlist->setScrollUpButton( std::make_shared( Point(744, 414), "IAM014", CButton::tooltipLocalized("core.help.306"))); - townlist->setScrollDownButton( std::make_shared( Point(744, 526), "IAM015", CButton::tooltipLocalized("core.help.307"))); - - if(from) - townlist->select(from); - - townlist->select(town); //this will scroll list to select current town - townlist->onSelect = std::bind(&CCastleInterface::townChange, this); - - recreateIcons(); - if (!from) - adventureInt->onAudioPaused(); - CCS->musich->playMusic(town->town->clientInfo.musicTheme, true, false); -} - -CCastleInterface::~CCastleInterface() -{ - // resume map audio if: - // adventureInt exists (may happen on exiting client with open castle interface) - // castleInt has not been replaced (happens on switching between towns inside castle interface) - if (adventureInt && LOCPLINT->castleInt == this) - adventureInt->onAudioResumed(); - if(LOCPLINT->castleInt == this) - LOCPLINT->castleInt = nullptr; -} - -void CCastleInterface::updateGarrisons() -{ - garr->recreateSlots(); -} - -void CCastleInterface::close() -{ - if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town - { - if(town->visitingHero && town->visitingHero->tempOwner == LOCPLINT->playerID) - LOCPLINT->localState->setSelection(town->visitingHero); - else - LOCPLINT->localState->setSelection(town); - } - CWindowObject::close(); -} - -void CCastleInterface::castleTeleport(int where) -{ - const CGTownInstance * dest = LOCPLINT->cb->getTown(ObjectInstanceID(where)); - LOCPLINT->localState->setSelection(town->visitingHero);//according to assert(ho == adventureInt->selection) in the eraseCurrentPathOf - LOCPLINT->cb->teleportHero(town->visitingHero, dest); - LOCPLINT->localState->erasePath(town->visitingHero); -} - -void CCastleInterface::townChange() -{ - //TODO: do not recreate window - const CGTownInstance * dest = LOCPLINT->localState->getOwnedTown(townlist->getSelectedIndex()); - const CGTownInstance * town = this->town;// "this" is going to be deleted - if ( dest == town ) - return; - close(); - GH.windows().createAndPushWindow(dest, town); -} - -void CCastleInterface::addBuilding(BuildingID bid) -{ - deactivate(); - builds->addBuilding(bid); - recreateIcons(); - activate(); - redraw(); -} - -void CCastleInterface::removeBuilding(BuildingID bid) -{ - deactivate(); - builds->removeBuilding(bid); - recreateIcons(); - activate(); - redraw(); -} - -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)]; - - icon->setFrame(iconIndex); - TResources townIncome = town->dailyIncome(); - income->setText(std::to_string(townIncome[EGameResID::GOLD])); - - hall = std::make_shared(80, 413, town, true); - fort = std::make_shared(122, 413, town, false); - - fastArmyPurchase = std::make_shared(Point(122, 413), "itmcl.def", CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); - fastArmyPurchase->setAnimateLonelyFrame(true); - - creainfo.clear(); - - bool compactCreatureInfo = useCompactCreatureBox(); - bool useAvailableCreaturesForLabel = useAvailableAmountAsCreatureLabel(); - - for(size_t i=0; i<4; i++) - creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 459), town, (int)i, compactCreatureInfo, useAvailableCreaturesForLabel)); - - - for(size_t i=0; i<4; i++) - creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel)); - -} - -void CCastleInterface::keyPressed(EShortcut key) -{ - switch(key) - { - case EShortcut::MOVE_UP: - townlist->selectPrev(); - break; - case EShortcut::MOVE_DOWN: - townlist->selectNext(); - break; - case EShortcut::TOWN_SWAP_ARMIES: - heroes->swapArmies(); - break; - case EShortcut::TOWN_OPEN_TAVERN: - if(town->hasBuilt(BuildingID::TAVERN)) - LOCPLINT->showTavernWindow(town); - break; - default: - break; - } -} - -void CCastleInterface::creaturesChangedEventHandler() -{ - for(auto creatureInfoBox : creainfo) - { - if(creatureInfoBox->getShowAvailable()) - { - creatureInfoBox->update(); - } - } -} - -CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building): - town(Town), - building(Building) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos.x += x; - pos.y += y; - pos.w = 154; - pos.h = 92; - - state = LOCPLINT->cb->canBuildStructure(town, building->bid); - - static int panelIndex[12] = - { - 3, 3, 3, 0, 0, 2, 2, 1, 2, 2, 3, 3 - }; - static int iconIndex[12] = - { - -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); - header = std::make_shared("TPTHBAR", panelIndex[state], 0, 1, 73); - if(iconIndex[state] >=0) - mark = std::make_shared("TPTHCHK", iconIndex[state], 0, 136, 56); - name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); - - //todo: add support for all possible states - if(state >= EBuildingState::BUILDING_ERROR) - state = EBuildingState::FORBIDDEN; -} - -void CHallInterface::CBuildingBox::hover(bool on) -{ - if(on) - { - std::string toPrint; - if(state==EBuildingState::PREREQUIRES || state == EBuildingState::MISSING_BASE) - toPrint = CGI->generaltexth->hcommands[5]; - else if(state==EBuildingState::CANT_BUILD_TODAY) - toPrint = CGI->generaltexth->allTexts[223]; - else - toPrint = CGI->generaltexth->hcommands[state]; - boost::algorithm::replace_first(toPrint,"%s",building->getNameTranslated()); - GH.statusbar()->write(toPrint); - } - else - { - GH.statusbar()->clear(); - } -} - -void CHallInterface::CBuildingBox::clickPressed(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(town,building,state,0); -} - -void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(town,building,state,1); -} - -CHallInterface::CHallInterface(const CGTownInstance * Town): - CStatusbarWindow(PLAYER_COLORED | BORDERED, Town->town->clientInfo.hallBackground), - town(Town) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - Rect barRect(5, 556, 740, 18); - - 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()); - exit = std::make_shared(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); - - auto & boxList = town->town->clientInfo.hallSlots; - boxes.resize(boxList.size()); - for(size_t row=0; rowtown->buildings.at(buildingID); - if(vstd::contains(town->builtBuildings, buildingID)) - { - building = current; - } - else - { - if(current->mode == CBuilding::BUILD_NORMAL) - { - building = current; - break; - } - } - } - int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col, - posY = 35 + 104*(int)row; - - if(building) - boxes[row].push_back(std::make_shared(posX, posY, town, building)); - } - } -} - -CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, int state, bool rightClick): - CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), "TPUBUILD"), - town(Town), - building(Building) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - icon = std::make_shared(town->town->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); - - MetaString nameString; - nameString.appendTextID("core.hallinfo.7"); - nameString.replaceTextID(building->getNameTextID()); - - name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, nameString.toString()); - description = std::make_shared(building->getDescriptionTranslated(), Rect(33, 135, 329, 67), 0, FONT_MEDIUM, ETextAlignment::CENTER); - stateText = std::make_shared(getTextForState(state), Rect(33, 216, 329, 67), 0, FONT_SMALL, ETextAlignment::CENTER); - - //Create components for all required resources - std::vector> components; - - for(int i = 0; iresources[i]) - components.push_back(std::make_shared(CComponent::resource, i, building->resources[i], CComponent::small)); - } - - cost = std::make_shared(components, Rect(25, 300, pos.w - 50, 130)); - - if(!rightClick) - { //normal window - - MetaString tooltipYes; - tooltipYes.appendTextID("core.genrltxt.595"); - tooltipYes.replaceTextID(building->getNameTextID()); - - MetaString tooltipNo; - tooltipNo.appendTextID("core.genrltxt.596"); - tooltipNo.replaceTextID(building->getNameTextID()); - - buy = std::make_shared(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); - buy->setBorderColor(Colors::METALLIC_GOLD); - buy->block(state!=7 || LOCPLINT->playerID != town->tempOwner); - - cancel = std::make_shared(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); - cancel->setBorderColor(Colors::METALLIC_GOLD); - } -} - -void CBuildWindow::buyFunc() -{ - LOCPLINT->cb->buildBuilding(town,building->bid); - GH.windows().popWindows(2); //we - build window and hall screen -} - -std::string CBuildWindow::getTextForState(int state) -{ - std::string ret; - if(state < EBuildingState::ALLOWED) - ret = CGI->generaltexth->hcommands[state]; - switch (state) - { - case EBuildingState::ALREADY_PRESENT: - case EBuildingState::CANT_BUILD_TODAY: - case EBuildingState::NO_RESOURCES: - ret.replace(ret.find_first_of("%s"), 2, building->getNameTranslated()); - break; - case EBuildingState::ALLOWED: - return CGI->generaltexth->allTexts[219]; //all prereq. are met - case EBuildingState::PREREQUIRES: - { - auto toStr = [&](const BuildingID build) -> std::string - { - return town->town->buildings.at(build)->getNameTranslated(); - }; - - ret = CGI->generaltexth->allTexts[52]; - ret += "\n" + town->genBuildingRequirements(building->bid).toString(toStr); - break; - } - 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()); - break; - } - } - return ret; -} - -LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int min, int max) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x+=size.x; - pos.y+=size.y; - pos.w = size.w; - pos.h = size.h; - init(name, descr, min, max); -} - -LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int val) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x+=size.x; - pos.y+=size.y; - pos.w = size.w; - pos.h = size.h; - init(name, descr, val, val); -} - -void LabeledValue::init(std::string nameText, std::string descr, int min, int max) -{ - addUsedEvents(HOVER); - hoverText = descr; - std::string valueText; - if(min && max) - { - valueText = std::to_string(min); - if(min != max) - valueText += '-' + std::to_string(max); - } - name = std::make_shared(3, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, nameText); - value = std::make_shared(pos.w-3, pos.h-2, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, valueText); -} - -void LabeledValue::hover(bool on) -{ - if(on) - { - GH.statusbar()->write(hoverText); - } - else - { - GH.statusbar()->clear(); - } -} - -CFortScreen::CFortScreen(const CGTownInstance * town): - CStatusbarWindow(PLAYER_COLORED | BORDERED, getBgName(town)) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) - fortSize--; - - const CBuilding * fortBuilding = town->town->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()); - exit = std::make_shared(Point(748, 556), "TPMAGE1", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); - - std::vector positions = - { - Point(10, 22), Point(404, 22), - Point(10, 155), Point(404,155), - Point(10, 288), Point(404,288) - }; - - if(fortSize == GameConstants::CREATURES_PER_TOWN) - { - positions.push_back(Point(206,421)); - } - else - { - positions.push_back(Point(10, 421)); - positions.push_back(Point(404,421)); - } - - for(ui32 i=0; ibuiltBuildings, BuildingID::DWELL_UP_FIRST+i)) - buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); - else - buildingID = BuildingID(BuildingID::DWELL_FIRST+i); - } - else - { - buildingID = BuildingID::SPECIAL_3; - } - - recAreas.push_back(std::make_shared(positions[i].x, positions[i].y, town, i)); - } - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - Rect barRect(4, 554, 740, 18); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 4, 554); - statusbar = CGStatusBar::create(statusbarBackground); -} - -std::string CFortScreen::getBgName(const CGTownInstance * town) -{ - ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) - fortSize--; - - if(fortSize == GameConstants::CREATURES_PER_TOWN) - return "TPCASTL7"; - else - return "TPCASTL8"; -} - -void CFortScreen::creaturesChangedEventHandler() -{ - for(auto & elem : recAreas) - elem->creaturesChangedEventHandler(); - - LOCPLINT->castleInt->creaturesChangedEventHandler(); -} - -CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * Town, int Level): - town(Town), - level(Level), - availableCount(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x +=posX; - pos.y +=posY; - pos.w = 386; - pos.h = 126; - - if(!town->creatures[level].second.empty()) - addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present - - addUsedEvents(SHOW_POPUP); - - icons = std::make_shared("TPCAINFO", 261, 3); - - if(getMyBuilding() != nullptr) - { - buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); - buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); - - if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) - { - ui32 available = town->creatures[level].first; - std::string availableText = CGI->generaltexth->allTexts[217]+ std::to_string(available); - availableCount = std::make_shared(78, 119, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, availableText); - } - } - - if(getMyCreature() != nullptr) - { - hoverText = boost::str(boost::format(CGI->generaltexth->tcommands[21]) % getMyCreature()->getNamePluralTranslated()); - new CCreaturePic(159, 4, getMyCreature(), false); - new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated()); - - Rect sizes(287, 4, 96, 18); - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[191], CGI->generaltexth->fcommands[1], getMyCreature()->getDefense(false))); - sizes.y+=21; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[199], CGI->generaltexth->fcommands[2], getMyCreature()->getMinDamage(false), getMyCreature()->getMaxDamage(false))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[388], CGI->generaltexth->fcommands[3], getMyCreature()->getMaxHealth())); - sizes.y+=21; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[193], CGI->generaltexth->fcommands[4], getMyCreature()->valOfBonuses(BonusType::STACKS_SPEED))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[194], CGI->generaltexth->fcommands[5], town->creatureGrowth(level))); - } -} - -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 nullptr; -} - -const CBuilding * CFortScreen::RecruitArea::getMyBuilding() -{ - BuildingID myID = BuildingID(BuildingID::DWELL_FIRST).advance(level); - - if (level == GameConstants::CREATURES_PER_TOWN) - return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); - - if (!town->town->buildings.count(myID)) - return nullptr; - - const CBuilding * build = town->town->buildings.at(myID); - while (town->town->buildings.count(myID)) - { - if (town->hasBuilt(myID)) - build = town->town->buildings.at(myID); - myID.advance(GameConstants::CREATURES_PER_TOWN); - } - return build; -} - -void CFortScreen::RecruitArea::hover(bool on) -{ - if(on) - GH.statusbar()->write(hoverText); - else - GH.statusbar()->clear(); -} - -void CFortScreen::RecruitArea::creaturesChangedEventHandler() -{ - if(availableCount) - { - std::string availableText = CGI->generaltexth->allTexts[217] + std::to_string(town->creatures[level].first); - availableCount->setText(availableText); - } -} - -void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition) -{ - LOCPLINT->castleInt->builds->enterDwelling(level); -} - -void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) -{ - if (getMyCreature() != nullptr) - GH.windows().createAndPushWindow(getMyCreature(), true); -} - -CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem) - : CStatusbarWindow(BORDERED, imagem) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - window = std::make_shared(owner->town->town->clientInfo.guildWindow, 332, 76); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - Rect barRect(7, 556, 737, 18); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 7, 556); - statusbar = CGStatusBar::create(statusbarBackground); - - exit = std::make_shared(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); - - static const std::vector > positions = - { - {Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)}, - {Point(48,53), Point(48,147), Point(48,241), Point(48,335), Point(48,429)}, - {Point(570,82), Point(672,82), Point(570,157), Point(672,157)}, - {Point(183,42), Point(183,148), Point(183,253)}, - {Point(491,325), Point(591,325)} - }; - - for(size_t i=0; itown->town->mageLevel; i++) - { - size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm? - for(size_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]])); - else - emptyScrolls.push_back(std::make_shared("TPMAGES.DEF", 1, 0, positions[i][j].x, positions[i][j].y)); - } - } -} - -CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) - : spell(Spell) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos += position; - image = std::make_shared("SPELLSCR", spell->id); - pos = image->pos; -} - -void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) -{ - LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); -} - -void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); -} - -void CMageGuildScreen::Scroll::hover(bool on) -{ - if(on) - GH.statusbar()->write(spell->getNameTranslated()); - else - GH.statusbar()->clear(); - -} - -CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): - CStatusbarWindow(PLAYER_COLORED, "TPSMITH") -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - Rect barRect(8, pos.h - 26, pos.w - 16, 19); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 8, pos.h - 26); - statusbar = CGStatusBar::create(statusbarBackground); - - animBG = std::make_shared("TPSMITBK", 64, 50); - animBG->needRefresh = true; - - const CCreature * creature = CGI->creh->objects[creMachineID]; - anim = std::make_shared(64, 50, creature->animDefName); - anim->clipRect(113,125,200,150); - - MetaString titleString; - titleString.appendTextID("core.genrltxt.274"); - titleString.replaceTextID(creature->getNameSingularTextID()); - - MetaString buyText; - buyText.appendTextID("core.genrltxt.595"); - buyText.replaceTextID(creature->getNameSingularTextID()); - - MetaString cancelText; - cancelText.appendTextID("core.genrltxt.596"); - cancelText.replaceTextID(creature->getNameSingularTextID()); - - std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); - - title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); - costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); - costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); - buy = std::make_shared(Point(42, 312), "IBUY30.DEF", CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); - - if(possible) - buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); - else - buy->block(true); - - costIcon = std::make_shared("RESOURCE", GameResID(EGameResID::GOLD), 0, 148, 244); -} +/* + * CCastleInterface.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 "CCastleInterface.h" + +#include "CHeroWindow.h" +#include "CTradeWindow.h" +#include "InfoWindows.h" +#include "GUIClasses.h" +#include "QuickRecruitmentWindow.h" +#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 "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" +#include "../widgets/RadialMenu.h" +#include "../widgets/CExchangeController.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/ColorFilter.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../adventureMap/CList.h" +#include "../adventureMap/CResDataBar.h" + +#include "../../CCallback.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CBuildingHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/GameSettings.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/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" + + +static bool useCompactCreatureBox() +{ + return settings["gameTweaks"]["compactTownCreatureInfo"].Bool(); +} + +static bool useAvailableAmountAsCreatureLabel() +{ + return settings["gameTweaks"]["availableCreaturesAsDwellingLabel"].Bool(); +} + +CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town, const CStructure * Str) + : CShowableAnim(0, 0, Str->defName, CShowableAnim::BASE, BUILDING_FRAME_TIME), + parent(Par), + town(Town), + str(Str), + border(nullptr), + area(nullptr), + stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT) +{ + addUsedEvents(LCLICK | SHOW_POPUP | MOVE | HOVER | TIME); + pos.x += str->pos.x; + pos.y += str->pos.y; + + // 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 && + (Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP)) + { + if(Town->hasBuilt(BuildingID::CITADEL)) + { + this->first = 1; + this->frame = 1; + } + else + this->last = 0; + } + + if(!str->borderName.empty()) + border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::ALPHA); + + if(!str->areaName.empty()) + area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA); +} + +const CBuilding * CBuildingRect::getBuilding() +{ + if (!str->building) + return nullptr; + + if (str->hiddenUpgrade) // hidden upgrades, e.g. hordes - return base (dwelling for hordes) + return town->town->buildings.at(str->building->getBase()); + + return str->building; +} + +bool CBuildingRect::operator<(const CBuildingRect & p2) const +{ + return (str->pos.z) < (p2.str->pos.z); +} + +void CBuildingRect::hover(bool on) +{ + if (!area) + return; + + if(on) + { + if(! parent->selectedBuilding //no building hovered + || (*parent->selectedBuilding)<(*this)) //or we are on top + { + parent->selectedBuilding = this; + GH.statusbar()->write(getSubtitle()); + } + } + else + { + if(parent->selectedBuilding == this) + { + parent->selectedBuilding = nullptr; + GH.statusbar()->clear(); + } + } +} + +void CBuildingRect::clickPressed(const Point & cursorPosition) +{ + if(getBuilding() && area && (parent->selectedBuilding==this)) + { + auto building = getBuilding(); + parent->buildingClicked(building->bid, building->subId, building->upgrade); + } +} + +void CBuildingRect::showPopupWindow(const Point & cursorPosition) +{ + if((!area) || (this!=parent->selectedBuilding) || getBuilding() == nullptr) + return; + + BuildingID bid = getBuilding()->bid; + const CBuilding *bld = town->town->buildings.at(bid); + if (bid < BuildingID::DWELL_FIRST) + { + CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), + std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(bld->town->faction->getId(), bld->bid))); + } + else + { + int level = ( bid - BuildingID::DWELL_FIRST ) % GameConstants::CREATURES_PER_TOWN; + GH.windows().createAndPushWindow(parent->pos.x+parent->pos.w / 2, parent->pos.y+parent->pos.h /2, town, level); + } +} + +void CBuildingRect::show(Canvas & to) +{ + uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT; + + if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT) + { + setAlpha(255 * stateTimeCounter / stageDelay); + CShowableAnim::show(to); + } + else + { + setAlpha(255); + CShowableAnim::show(to); + } + + if(border && stateTimeCounter > BUILDING_APPEAR_TIMEPOINT) + { + if(stateTimeCounter >= BUILD_ANIMATION_FINISHED_TIMEPOINT) + { + if(parent->selectedBuilding == this) + to.draw(border, pos.topLeft()); + return; + } + + auto darkBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 0.5f, 0.5f, 0.5f ); + auto lightBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 2.0f, 2.0f, 2.0f ); + auto baseBorder = ColorFilter::genEmptyShifter(); + + float progress = float(stateTimeCounter % stageDelay) / stageDelay; + + if (stateTimeCounter < BUILDING_WHITE_BORDER_TIMEPOINT) + border->adjustPalette(ColorFilter::genInterpolated(lightBorder, darkBorder, progress), 0); + else + if (stateTimeCounter < BUILDING_YELLOW_BORDER_TIMEPOINT) + border->adjustPalette(ColorFilter::genInterpolated(darkBorder, baseBorder, progress), 0); + else + border->adjustPalette(baseBorder, 0); + + to.draw(border, pos.topLeft()); + } +} + +void CBuildingRect::tick(uint32_t msPassed) +{ + CShowableAnim::tick(msPassed); + stateTimeCounter += msPassed; +} + +void CBuildingRect::showAll(Canvas & to) +{ + if (stateTimeCounter == 0) + return; + + CShowableAnim::showAll(to); + if(!isActive() && parent->selectedBuilding == this && border) + to.draw(border, pos.topLeft()); +} + +std::string CBuildingRect::getSubtitle()//hover text for building +{ + if (!getBuilding()) + return ""; + + int bid = getBuilding()->bid; + + if (bid<30)//non-dwellings - only buiding name + return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); + else//dwellings - recruit %creature% + { + auto & availableCreatures = town->creatures[(bid-30)%GameConstants::CREATURES_PER_TOWN].second; + if(availableCreatures.size()) + { + int creaID = availableCreatures.back();//taking last of available creatures + return CGI->generaltexth->allTexts[16] + " " + CGI->creh->objects.at(creaID)->getNamePluralTranslated(); + } + else + { + logGlobal->warn("Dwelling with id %d offers no creatures!", bid); + return "#ERROR#"; + } + } +} + +void CBuildingRect::mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) +{ + hover(true); +} + +bool CBuildingRect::receiveEvent(const Point & position, int eventType) const +{ + if (!pos.isInside(position.x, position.y)) + return false; + + if(area && area->isTransparent(position - pos.topLeft())) + return false; + + return CIntObject::receiveEvent(position, eventType); +} + +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); + + const CCreature * creature = CGI->creh->objects.at(Town->creatures.at(level).second.back()); + + title = std::make_shared(80, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getNamePluralTranslated()); + animation = std::make_shared(30, 44, creature, true, true); + + std::string text = std::to_string(Town->creatures.at(level).first); + available = std::make_shared(80,190, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[217] + text); + costPerTroop = std::make_shared(80, 227, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[346]); + + for(int i = 0; i(i); + if(creature->getRecruitCost(res)) + { + resPicture.push_back(std::make_shared(AnimationPath::builtin("RESOURCE"), i, 0, 0, 0)); + resAmount.push_back(std::make_shared(0,0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(creature->getRecruitCost(res)))); + } + } + + int posY = 238; + int posX = pos.w/2 - (int)resAmount.size() * 25 + 5; + for (size_t i=0; imoveBy(Point(posX, posY)); + resAmount[i]->moveBy(Point(posX+16, posY+43)); + posX += 50; + } +} + +CDwellingInfoBox::~CDwellingInfoBox() = default; + +CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroSlots * Owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + owner = Owner; + pos.x += x; + pos.y += y; + pos.w = 58; + pos.h = 64; + upg = updown; + + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 0, 0); + portrait->visible = false; + + flag = std::make_shared(AnimationPath::builtin("CREST58"), 0, 0, 0, 0); + flag->visible = false; + + selection = std::make_shared(AnimationPath::builtin("TWCRPORT"), 1, 0); + selection->visible = false; + + set(h); + + addUsedEvents(LCLICK | SHOW_POPUP | GESTURE | HOVER); +} + +CHeroGSlot::~CHeroGSlot() = default; + +void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(!hero) + return; + + if (!settings["input"]["radialWheelGarrisonSwipe"].Bool()) + return; + + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + + bool twoHeroes = hero && other->hero; + + ObjectInstanceID heroId = hero->id; + ObjectInstanceID heroOtherId = twoHeroes ? other->hero->id : ObjectInstanceID::NONE; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_NW, twoHeroes, "moveTroops", "vcmi.radialWheel.heroGetArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArmy(false, std::nullopt);} }, + { RadialMenuConfig::ITEM_NE, twoHeroes, "stackSplitDialog", "vcmi.radialWheel.heroSwapArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArmy();} }, + { RadialMenuConfig::ITEM_EE, twoHeroes, "tradeHeroes", "vcmi.radialWheel.heroExchange", [heroId, heroOtherId](){LOCPLINT->showHeroExchange(heroId, heroOtherId);} }, + { RadialMenuConfig::ITEM_SW, twoHeroes, "moveArtifacts", "vcmi.radialWheel.heroGetArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArtifacts(false, true, true);} }, + { RadialMenuConfig::ITEM_SE, twoHeroes, "swapArtifacts", "vcmi.radialWheel.heroSwapArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArtifacts(true, true);} }, + { RadialMenuConfig::ITEM_WW, true, "dismissHero", "vcmi.radialWheel.heroDismiss", [this]() + { + CFunctionList ony = [=](){ }; + ony += [=](){ LOCPLINT->cb->dismissHero(hero); }; + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); + } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements); +} + +void CHeroGSlot::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + return; + } + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + std::string temp; + if(hero) + { + if(isSelected())//view NNN + { + temp = CGI->generaltexth->tcommands[4]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + else if(other->hero && other->isSelected())//exchange + { + temp = CGI->generaltexth->tcommands[7]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); + } + else// select NNN (in ZZZ) + { + if(upg)//down - visiting + { + temp = CGI->generaltexth->tcommands[32]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + else //up - garrison + { + temp = CGI->generaltexth->tcommands[12]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + } + } + else //we are empty slot + { + if(other->isSelected() && other->hero) //move NNNN + { + temp = CGI->generaltexth->tcommands[6]; + boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); + } + else //empty + { + temp = CGI->generaltexth->allTexts[507]; + } + } + if(temp.size()) + GH.statusbar()->write(temp); +} + +void CHeroGSlot::clickPressed(const Point & cursorPosition) +{ + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + + owner->garr->setSplittingMode(false); + owner->garr->selectSlot(nullptr); + + if(hero && isSelected()) + { + setHighlight(false); + LOCPLINT->openHeroWindow(hero); + } + else if(other->hero && other->isSelected()) + { + owner->swapArmies(); + } + else if(hero) + { + setHighlight(true); + owner->garr->selectSlot(nullptr); + redraw(); + } + + //refresh statusbar + hover(false); + hover(true); +} + +void CHeroGSlot::showPopupWindow(const Point & cursorPosition) +{ + if(hero) + { + GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); + } +} + +void CHeroGSlot::deactivate() +{ + selection->visible = false; + CIntObject::deactivate(); +} + +bool CHeroGSlot::isSelected() const +{ + return selection->visible; +} + +void CHeroGSlot::setHighlight(bool on) +{ + selection->visible = on; + + if(owner->garrisonedHero->hero && owner->visitingHero->hero) //two heroes in town + { + for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot higlighted + elem->block(!on); + } +} + +void CHeroGSlot::set(const CGHeroInstance * newHero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + hero = newHero; + + selection->visible = false; + portrait->visible = false; + flag->visible = false; + + if(newHero) + { + portrait->visible = true; + portrait->setFrame(newHero->getIconIndex()); + } + else if(!upg && owner->showEmpty) //up garrison + { + flag->visible = true; + flag->setFrame(LOCPLINT->castleInt->town->getOwner().getNum()); + } +} + +HeroSlots::HeroSlots(const CGTownInstance * Town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty): + showEmpty(ShowEmpty), + town(Town), + garr(Garrison) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + garrisonedHero = std::make_shared(garrPos.x, garrPos.y, 0, town->garrisonHero, this); + visitingHero = std::make_shared(visitPos.x, visitPos.y, 1, town->visitingHero, this); +} + +HeroSlots::~HeroSlots() = default; + +void HeroSlots::update() +{ + garrisonedHero->set(town->garrisonHero); + visitingHero->set(town->visitingHero); +} + +void HeroSlots::splitClicked() +{ + if(!!town->visitingHero && town->garrisonHero && (visitingHero->isSelected() || garrisonedHero->isSelected())) + { + LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id); + } +} + +void HeroSlots::swapArmies() +{ + bool allow = true; + + //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)) + { + 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))); + + LOCPLINT->showInfoDialog(text, std::vector>(), soundBase::sound_todo); + allow = false; + } + else if (town->garrisonHero->stacksCount() == 0) + { + //This hero has no creatures. A hero must have creatures before he can brave the dangers of the countryside. + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.19"), {}, soundBase::sound_todo); + allow = false; + } + } + + if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army + { + if(!town->visitingHero->canBeMergedWith(*town)) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[275], std::vector>(), soundBase::sound_todo); + allow = false; + } + } + + garrisonedHero->setHighlight(false); + visitingHero->setHighlight(false); + + if (allow) + LOCPLINT->cb->swapGarrisonHero(town); +} + +class SORTHELP +{ +public: + bool operator() (const CIntObject * a, const CIntObject * b) + { + auto b1 = dynamic_cast(a); + auto b2 = dynamic_cast(b); + + if(!b1 && !b2) + return intptr_t(a) < intptr_t(b); + if(b1 && !b2) + return false; + if(!b1 && b2) + return true; + + return (*b1)<(*b2); + } +}; + +SORTHELP buildSorter; + +CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): + town(Town), + selectedBuilding(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + background = std::make_shared(town->town->clientInfo.townBackground); + background->needRefresh = true; + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + pos.w = background->pos.w; + pos.h = background->pos.h; + + recreate(); +} + +CCastleBuildings::~CCastleBuildings() = default; + +void CCastleBuildings::recreate() +{ + selectedBuilding = nullptr; + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + buildings.clear(); + groups.clear(); + + //Generate buildings list + + auto buildingsCopy = town->builtBuildings;// a bit modified copy of built buildings + + if(vstd::contains(town->builtBuildings, BuildingID::SHIPYARD)) + { + auto bayPos = town->bestLocation(); + if(!bayPos.valid()) + logGlobal->warn("Shipyard in non-coastal town!"); + std::vector vobjs = LOCPLINT->cb->getVisitableObjs(bayPos, false); + //there is visitable obj at shipyard output tile and it's a boat or hero (on boat) + if(!vobjs.empty() && (vobjs.front()->ID == Obj::BOAT || vobjs.front()->ID == Obj::HERO)) + { + buildingsCopy.insert(BuildingID::SHIP); + } + } + + for(const CStructure * structure : town->town->clientInfo.structures) + { + if(!structure->building) + { + buildings.push_back(std::make_shared(this, town, structure)); + continue; + } + if(vstd::contains(buildingsCopy, structure->building->bid)) + { + groups[structure->building->getBase()].push_back(structure); + } + } + + for(auto & entry : groups) + { + const CBuilding * build = town->town->buildings.at(entry.first); + + const CStructure * toAdd = *boost::max_element(entry.second, [=](const CStructure * a, const CStructure * b) + { + return build->getDistance(a->building->bid) < build->getDistance(b->building->bid); + }); + + buildings.push_back(std::make_shared(this, town, toAdd)); + } + + boost::sort(children, buildSorter); //TODO: create building in blit order +} + +void CCastleBuildings::addBuilding(BuildingID building) +{ + //FIXME: implement faster method without complete recreation of town + BuildingID base = town->town->buildings.at(building)->getBase(); + + recreate(); + + auto & structures = groups.at(base); + + for(auto buildingRect : buildings) + { + if(vstd::contains(structures, buildingRect->str)) + { + //reset animation + if(structures.size() == 1) + buildingRect->stateTimeCounter = 0; // transparency -> fully visible stage + else + buildingRect->stateTimeCounter = CBuildingRect::BUILDING_APPEAR_TIMEPOINT; // already in fully visible stage + break; + } + } +} + +void CCastleBuildings::removeBuilding(BuildingID building) +{ + //FIXME: implement faster method without complete recreation of town + recreate(); +} + +const CGHeroInstance * CCastleBuildings::getHero() +{ + if(town->visitingHero) + return town->visitingHero; + else + return town->garrisonHero; +} + +void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) +{ + logGlobal->trace("You've clicked on %d", (int)building.toEnum()); + const CBuilding *b = town->town->buildings.find(building)->second; + + if(building >= BuildingID::DWELL_FIRST) + { + enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN); + } + else + { + switch(building) + { + case BuildingID::MAGES_GUILD_1: + case BuildingID::MAGES_GUILD_2: + case BuildingID::MAGES_GUILD_3: + case BuildingID::MAGES_GUILD_4: + case BuildingID::MAGES_GUILD_5: + enterMagesGuild(); + break; + + case BuildingID::TAVERN: + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + break; + + case BuildingID::SHIPYARD: + if(town->shipyardStatus() == IBoatGenerator::GOOD) + LOCPLINT->showShipyardDialog(town); + else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT) + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); + break; + + case BuildingID::FORT: + case BuildingID::CITADEL: + case BuildingID::CASTLE: + GH.windows().createAndPushWindow(town); + break; + + 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; + + case BuildingID::SHIP: + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat + break; + + case BuildingID::SPECIAL_1: + case BuildingID::SPECIAL_2: + case BuildingID::SPECIAL_3: + switch(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; + + case BuildingSubID::CASTLE_GATE: + enterCastleGate(); + break; + + case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer + GH.windows().createAndPushWindow(town, getHero(), nullptr); + break; + + case BuildingSubID::PORTAL_OF_SUMMONING: + if (town->creatures[GameConstants::CREATURES_PER_TOWN].second.empty())//No creatures + LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); + else + enterDwelling(GameConstants::CREATURES_PER_TOWN); + break; + + case BuildingSubID::BALLISTA_YARD: + enterBlacksmith(ArtifactID::BALLISTA); + break; + + default: + enterBuilding(building); + break; + } + break; + + default: + enterBuilding(building); + break; + } + } +} + +void CCastleBuildings::enterBlacksmith(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())); + return; + } + auto art = artifactID.toArtifact(); + + int price = art->getPrice(); + bool possible = LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= price; + if(possible) + { + for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) + { + if(hero->getArt(slot) == nullptr) + { + possible = true; + break; + } + else + { + possible = false; + } + } + } + CreatureID cre = art->getWarMachine(); + GH.windows().createAndPushWindow(possible, cre, 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); +} + +void CCastleBuildings::enterCastleGate() +{ + if (!town->visitingHero) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[126]); + return;//only visiting hero can use castle gates + } + std::vector availableTowns; + std::vector Towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & Town : Towns) + { + 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->hasBuilt(BuildingSubID::CASTLE_GATE)) //and the town has a castle gate + { + availableTowns.push_back(t->id.getNum());//add to the list + } + } + auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window + GH.windows().createAndPushWindow(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], + CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1)); +} + +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()); + return; + } + + auto recruitCb = [=](CreatureID id, int count) + { + LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); + }; + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, -87); +} + +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) + : town->creatures.cend(); + const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt, + [](const auto & creatureInfo) { return creatureInfo.first > 0; }); + if(hasSomeoneToRecruit) + GH.windows().createAndPushWindow(town, pos); + else + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); +} + +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::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); + } + + bool isMysticPondOrItsUpgrade = subID == BuildingSubID::MYSTIC_POND + || (upgrades != BuildingID::NONE + && town->town->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); + + if(upgrades != BuildingID::NONE) + descr += "\n\n"+town->town->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); + + if(isMysticPondOrItsUpgrade) //for vanila Rampart like towns + { + if(town->bonusValue.first == 0) //Mystic Pond produced nothing; + descr += "\n\n" + hasNotProduced; + else //Mystic Pond produced something; + { + descr += "\n\n" + hasProduced; + boost::algorithm::replace_first(descr,"%s",CGI->generaltexth->restypes[town->bonusValue.first]); + boost::algorithm::replace_first(descr,"%d",std::to_string(town->bonusValue.second)); + } + } + LOCPLINT->showInfoDialog(descr, comps); +} + +void CCastleBuildings::enterMagesGuild() +{ + const CGHeroInstance *hero = getHero(); + + if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok + { + const StartInfo *si = LOCPLINT->cb->getStartInfo(); + // it would be nice to find a way to move this hack to config/mapOverrides.json + if(si && si->campState && // We're in campaign, + (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", + (hero->getHeroType() == 45)) // and the hero is Yog (based on Solmyr) + { + // "Yog has given up magic in all its forms..." + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); + } + else if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < 500) //not enough gold to buy spellbook + { + openMagesGuild(); + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[213]); + } + else + { + CFunctionList onYes = [this](){ openMagesGuild(); }; + CFunctionList onNo = onYes; + onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; + std::vector> components(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::SPELLBOOK))); + + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); + } + } + else + { + openMagesGuild(); + } +} + +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 + { + if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL)) + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[597], //Do you wish this to be the permanent home of the Grail? + [&](){ LOCPLINT->cb->buildBuilding(town, BuildingID::GRAIL); }, + [&](){ openTownHall(); }); + } + else + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[673]); + assert(GH.windows().topWindow() != nullptr); + GH.windows().topWindow()->buttons[0]->addCallback(std::bind(&CCastleBuildings::openTownHall, this)); + } + } + else + { + openTownHall(); + } +} + +void CCastleBuildings::openMagesGuild() +{ + auto mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; + GH.windows().createAndPushWindow(LOCPLINT->castleInt, mageGuildBackground); +} + +void CCastleBuildings::openTownHall() +{ + GH.windows().createAndPushWindow(town); +} + +CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable): + town(Town), + level(Level), + showAvailable(_showAvailable) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos += position; + + if(town->creatures.size() <= level || town->creatures[level].second.empty()) + { + level = -1; + return;//No creature + } + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + + ui32 creatureID = town->creatures[level].second.back(); + creature = CGI->creh->objects[creatureID]; + + picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, 8, 0); + + std::string value; + if(showAvailable) + value = std::to_string(town->creatures[level].first); + else + value = std::string("+") + std::to_string(town->creatureGrowth(level)); + + if(compact) + { + label = std::make_shared(40, 32, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, value); + pos.x += 8; + pos.w = 32; + pos.h = 32; + } + else + { + label = std::make_shared(24, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, value); + pos.w = 48; + pos.h = 48; + } +} + +void CCreaInfo::update() +{ + if(label) + { + std::string value; + if(showAvailable) + value = std::to_string(town->creatures[level].first); + else + value = std::string("+") + std::to_string(town->creatureGrowth(level)); + + if(value != label->getText()) + label->setText(value); + } +} + +void CCreaInfo::hover(bool on) +{ + std::string message = CGI->generaltexth->allTexts[588]; + boost::algorithm::replace_first(message, "%s", creature->getNamePluralTranslated()); + + if(on) + { + GH.statusbar()->write(message); + } + else + { + GH.statusbar()->clearIfMatching(message); + } +} + +void CCreaInfo::clickPressed(const Point & cursorPosition) +{ + int offset = LOCPLINT->castleInt? (-87) : 0; + auto recruitCb = [=](CreatureID id, int count) + { + LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); + }; + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, offset); +} + +std::string CCreaInfo::genGrowthText() +{ + GrowthInfo gi = town->getGrowthInfo(level); + std::string descr = boost::str(boost::format(CGI->generaltexth->allTexts[589]) % creature->getNameSingularTranslated() % gi.totalGrowth()); + + for(const GrowthInfo::Entry & entry : gi.entries) + descr +="\n" + entry.description; + + return descr; +} + +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())); +} + +bool CCreaInfo::getShowAvailable() +{ + return showAvailable; +} + +CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townHall) + : town(Town), + building(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + addUsedEvents(SHOW_POPUP | HOVER); + pos.x += posX; + pos.y += posY; + int buildID; + + if(townHall) + { + buildID = 10 + town->hallLevel(); + picture = std::make_shared(AnimationPath::builtin("ITMTL.DEF"), town->hallLevel()); + } + else + { + buildID = 6 + town->fortLevel(); + if(buildID == 6) + 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)); + pos = picture->pos; +} + +void CTownInfo::hover(bool on) +{ + if(on) + { + if(building ) + GH.statusbar()->write(building->getNameTranslated()); + } + else + { + GH.statusbar()->clear(); + } +} + +void CTownInfo::showPopupWindow(const Point & cursorPosition) +{ + if(building) + { + auto c = std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(building->town->faction->getId(), building->bid)); + CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); + } +} + +CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from): + CStatusbarWindow(PLAYER_COLORED | BORDERED), + town(Town) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + 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); + pos.w = panel->pos.w; + pos.h = builds->pos.h + panel->pos.h; + center(); + updateShadow(); + + garr = std::make_shared(Point(305, 387), 4, Point(0,96), town->getUpperArmy(), town->visitingHero); + garr->setRedrawParent(true); + + heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); + title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); + income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); + icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); + + exit = std::make_shared(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); + exit->setImageOrder(4, 5, 6, 7); + + auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() + { + garr->splitClick(); + heroes->splitClicked(); + }); + garr->addSplitBtn(split); + + Rect barRect(9, 182, 732, 18); + auto statusbarBackground = std::make_shared(panel->getSurface(), barRect, 9, 555); + statusbar = CGStatusBar::create(statusbarBackground); + resdatabar = std::make_shared(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78); + + townlist = std::make_shared(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() ); + townlist->setScrollUpButton( std::make_shared( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"))); + townlist->setScrollDownButton( std::make_shared( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"))); + + if(from) + townlist->select(from); + + townlist->select(town); //this will scroll list to select current town + townlist->onSelect = std::bind(&CCastleInterface::townChange, this); + + recreateIcons(); + if (!from) + adventureInt->onAudioPaused(); + CCS->musich->playMusic(town->town->clientInfo.musicTheme, true, false); +} + +CCastleInterface::~CCastleInterface() +{ + // resume map audio if: + // adventureInt exists (may happen on exiting client with open castle interface) + // castleInt has not been replaced (happens on switching between towns inside castle interface) + if (adventureInt && LOCPLINT->castleInt == this) + adventureInt->onAudioResumed(); + if(LOCPLINT->castleInt == this) + LOCPLINT->castleInt = nullptr; +} + +void CCastleInterface::updateGarrisons() +{ + garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); + garr->setArmy(town->visitingHero, EGarrisonType::LOWER); + garr->recreateSlots(); + heroes->update(); + + redraw(); +} + +bool CCastleInterface::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + +void CCastleInterface::close() +{ + if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town + { + if(town->visitingHero && town->visitingHero->tempOwner == LOCPLINT->playerID) + LOCPLINT->localState->setSelection(town->visitingHero); + else + LOCPLINT->localState->setSelection(town); + } + CWindowObject::close(); +} + +void CCastleInterface::castleTeleport(int where) +{ + const CGTownInstance * dest = LOCPLINT->cb->getTown(ObjectInstanceID(where)); + LOCPLINT->localState->setSelection(town->visitingHero);//according to assert(ho == adventureInt->selection) in the eraseCurrentPathOf + LOCPLINT->cb->teleportHero(town->visitingHero, dest); + LOCPLINT->localState->erasePath(town->visitingHero); +} + +void CCastleInterface::townChange() +{ + //TODO: do not recreate window + const CGTownInstance * dest = LOCPLINT->localState->getOwnedTown(townlist->getSelectedIndex()); + const CGTownInstance * town = this->town;// "this" is going to be deleted + if ( dest == town ) + return; + close(); + GH.windows().createAndPushWindow(dest, town); +} + +void CCastleInterface::addBuilding(BuildingID bid) +{ + deactivate(); + builds->addBuilding(bid); + recreateIcons(); + activate(); + redraw(); +} + +void CCastleInterface::removeBuilding(BuildingID bid) +{ + deactivate(); + builds->removeBuilding(bid); + recreateIcons(); + activate(); + redraw(); +} + +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)]; + + icon->setFrame(iconIndex); + TResources townIncome = town->dailyIncome(); + income->setText(std::to_string(townIncome[EGameResID::GOLD])); + + hall = std::make_shared(80, 413, town, true); + fort = std::make_shared(122, 413, town, false); + + fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel(), town->hallLevel(), town->hallLevel(), town->hallLevel()); + fastTownHall->setAnimateLonelyFrame(true); + + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; + fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setImageOrder(imageIndex, imageIndex, imageIndex, imageIndex); + fastArmyPurchase->setAnimateLonelyFrame(true); + + fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); + + fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() + { + if(town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + }); + + creainfo.clear(); + + bool compactCreatureInfo = useCompactCreatureBox(); + bool useAvailableCreaturesForLabel = useAvailableAmountAsCreatureLabel(); + + for(size_t i=0; i<4; i++) + creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 459), town, (int)i, compactCreatureInfo, useAvailableCreaturesForLabel)); + + + for(size_t i=0; i<4; i++) + creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel)); + +} + +void CCastleInterface::keyPressed(EShortcut key) +{ + switch(key) + { + case EShortcut::MOVE_UP: + townlist->selectPrev(); + break; + case EShortcut::MOVE_DOWN: + townlist->selectNext(); + break; + case EShortcut::TOWN_SWAP_ARMIES: + heroes->swapArmies(); + break; + case EShortcut::TOWN_OPEN_TAVERN: + if(town->hasBuilt(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + break; + default: + break; + } +} + +void CCastleInterface::creaturesChangedEventHandler() +{ + for(auto creatureInfoBox : creainfo) + { + if(creatureInfoBox->getShowAvailable()) + { + creatureInfoBox->update(); + } + } +} + +CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building): + town(Town), + building(Building) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos.x += x; + pos.y += y; + pos.w = 154; + pos.h = 92; + + state = LOCPLINT->cb->canBuildStructure(town, building->bid); + + static int panelIndex[12] = + { + 3, 3, 3, 0, 0, 2, 2, 1, 2, 2, 3, 3 + }; + static int iconIndex[12] = + { + -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); + 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); + name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); + + //todo: add support for all possible states + if(state >= EBuildingState::BUILDING_ERROR) + state = EBuildingState::FORBIDDEN; +} + +void CHallInterface::CBuildingBox::hover(bool on) +{ + if(on) + { + std::string toPrint; + if(state==EBuildingState::PREREQUIRES || state == EBuildingState::MISSING_BASE) + toPrint = CGI->generaltexth->hcommands[5]; + else if(state==EBuildingState::CANT_BUILD_TODAY) + toPrint = CGI->generaltexth->allTexts[223]; + else + toPrint = CGI->generaltexth->hcommands[static_cast(state)]; + boost::algorithm::replace_first(toPrint,"%s",building->getNameTranslated()); + GH.statusbar()->write(toPrint); + } + else + { + GH.statusbar()->clear(); + } +} + +void CHallInterface::CBuildingBox::clickPressed(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(town,building,state,0); +} + +void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(town,building,state,1); +} + +CHallInterface::CHallInterface(const CGTownInstance * Town): + CStatusbarWindow(PLAYER_COLORED | BORDERED, Town->town->clientInfo.hallBackground), + town(Town) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + Rect barRect(5, 556, 740, 18); + + 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()); + 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; + boxes.resize(boxList.size()); + for(size_t row=0; rowtown->buildings.at(buildingID); + if(vstd::contains(town->builtBuildings, buildingID)) + { + building = current; + } + else + { + if(current->mode == CBuilding::BUILD_NORMAL) + { + building = current; + break; + } + } + } + int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col, + posY = 35 + 104*(int)row; + + if(building) + boxes[row].push_back(std::make_shared(posX, posY, town, building)); + } + } +} + +CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, EBuildingState state, bool rightClick): + CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), ImagePath::builtin("TPUBUILD")), + town(Town), + building(Building) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + icon = std::make_shared(town->town->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); + + MetaString nameString; + nameString.appendTextID("core.hallinfo.7"); + nameString.replaceTextID(building->getNameTextID()); + + name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, nameString.toString()); + description = std::make_shared(building->getDescriptionTranslated(), Rect(33, 135, 329, 67), 0, FONT_MEDIUM, ETextAlignment::CENTER); + stateText = std::make_shared(getTextForState(state), Rect(33, 216, 329, 67), 0, FONT_SMALL, ETextAlignment::CENTER); + + //Create components for all required resources + std::vector> components; + + for(GameResID i : GameResID::ALL_RESOURCES()) + { + if(building->resources[i]) + { + MetaString message; + int resourceAmount = LOCPLINT->cb->getResourceAmount(i); + bool canAfford = resourceAmount >= building->resources[i]; + + if(!canAfford && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + { + message.appendRawString("{H3Red|%d}/%d"); + message.replaceNumber(resourceAmount); + } + else + message.appendRawString("%d"); + + message.replaceNumber(building->resources[i]); + components.push_back(std::make_shared(ComponentType::RESOURCE, i, message.toString(), CComponent::small)); + } + } + + cost = std::make_shared(components, Rect(25, 300, pos.w - 50, 130)); + + if(!rightClick) + { //normal window + + MetaString tooltipYes; + tooltipYes.appendTextID("core.genrltxt.595"); + tooltipYes.replaceTextID(building->getNameTextID()); + + MetaString tooltipNo; + tooltipNo.appendTextID("core.genrltxt.596"); + tooltipNo.replaceTextID(building->getNameTextID()); + + buy = std::make_shared(Point(45, 446), AnimationPath::builtin("IBUY30"), CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); + buy->setBorderColor(Colors::METALLIC_GOLD); + buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner); + + cancel = std::make_shared(Point(290, 445), AnimationPath::builtin("ICANCEL"), CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); + cancel->setBorderColor(Colors::METALLIC_GOLD); + } +} + +void CBuildWindow::buyFunc() +{ + LOCPLINT->cb->buildBuilding(town,building->bid); + GH.windows().popWindows(2); //we - build window and hall screen +} + +std::string CBuildWindow::getTextForState(EBuildingState state) +{ + std::string ret; + if(state < EBuildingState::ALLOWED) + ret = CGI->generaltexth->hcommands[static_cast(state)]; + switch (state) + { + case EBuildingState::ALREADY_PRESENT: + case EBuildingState::CANT_BUILD_TODAY: + case EBuildingState::NO_RESOURCES: + ret.replace(ret.find_first_of("%s"), 2, building->getNameTranslated()); + break; + case EBuildingState::ALLOWED: + return CGI->generaltexth->allTexts[219]; //all prereq. are met + case EBuildingState::PREREQUIRES: + { + auto toStr = [&](const BuildingID build) -> std::string + { + return town->town->buildings.at(build)->getNameTranslated(); + }; + + ret = CGI->generaltexth->allTexts[52]; + ret += "\n" + town->genBuildingRequirements(building->bid).toString(toStr); + break; + } + 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()); + break; + } + } + return ret; +} + +LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int min, int max) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x+=size.x; + pos.y+=size.y; + pos.w = size.w; + pos.h = size.h; + init(name, descr, min, max); +} + +LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int val) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x+=size.x; + pos.y+=size.y; + pos.w = size.w; + pos.h = size.h; + init(name, descr, val, val); +} + +void LabeledValue::init(std::string nameText, std::string descr, int min, int max) +{ + addUsedEvents(HOVER); + hoverText = descr; + std::string valueText; + if(min && max) + { + valueText = std::to_string(min); + if(min != max) + valueText += '-' + std::to_string(max); + } + name = std::make_shared(3, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, nameText); + value = std::make_shared(pos.w-3, pos.h-2, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, valueText); +} + +void LabeledValue::hover(bool on) +{ + if(on) + { + GH.statusbar()->write(hoverText); + } + else + { + GH.statusbar()->clear(); + } +} + +CFortScreen::CFortScreen(const CGTownInstance * town): + CStatusbarWindow(PLAYER_COLORED | BORDERED, getBgName(town)) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + ui32 fortSize = static_cast(town->creatures.size()); + if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) + fortSize--; + + const CBuilding * fortBuilding = town->town->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()); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + + std::vector positions = + { + Point(10, 22), Point(404, 22), + Point(10, 155), Point(404,155), + Point(10, 288), Point(404,288) + }; + + if(fortSize == GameConstants::CREATURES_PER_TOWN) + { + positions.push_back(Point(206,421)); + } + else + { + positions.push_back(Point(10, 421)); + positions.push_back(Point(404,421)); + } + + for(ui32 i=0; ibuiltBuildings, dwelling)) + buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); + else + buildingID = BuildingID(BuildingID::DWELL_FIRST+i); + } + else + { + buildingID = BuildingID::SPECIAL_3; + } + + recAreas.push_back(std::make_shared(positions[i].x, positions[i].y, town, i)); + } + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + Rect barRect(4, 554, 740, 18); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 4, 554); + statusbar = CGStatusBar::create(statusbarBackground); +} + +ImagePath CFortScreen::getBgName(const CGTownInstance * town) +{ + ui32 fortSize = static_cast(town->creatures.size()); + if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) + fortSize--; + + if(fortSize == GameConstants::CREATURES_PER_TOWN) + return ImagePath::builtin("TPCASTL7"); + else + return ImagePath::builtin("TPCASTL8"); +} + +void CFortScreen::creaturesChangedEventHandler() +{ + for(auto & elem : recAreas) + elem->creaturesChangedEventHandler(); + + LOCPLINT->castleInt->creaturesChangedEventHandler(); +} + +CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * Town, int Level): + town(Town), + level(Level), + availableCount(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x +=posX; + pos.y +=posY; + pos.w = 386; + pos.h = 126; + + if(!town->creatures[level].second.empty()) + addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present + + addUsedEvents(SHOW_POPUP); + + icons = std::make_shared(ImagePath::builtin("TPCAINFO"), 261, 3); + + if(getMyBuilding() != nullptr) + { + buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); + buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); + + if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) + { + ui32 available = town->creatures[level].first; + std::string availableText = CGI->generaltexth->allTexts[217]+ std::to_string(available); + availableCount = std::make_shared(78, 119, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, availableText); + } + } + + if(getMyCreature() != nullptr) + { + hoverText = boost::str(boost::format(CGI->generaltexth->tcommands[21]) % getMyCreature()->getNamePluralTranslated()); + new CCreaturePic(159, 4, getMyCreature(), false); + new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated()); + + Rect sizes(287, 4, 96, 18); + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[191], CGI->generaltexth->fcommands[1], getMyCreature()->getDefense(false))); + sizes.y+=21; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[199], CGI->generaltexth->fcommands[2], getMyCreature()->getMinDamage(false), getMyCreature()->getMaxDamage(false))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[388], CGI->generaltexth->fcommands[3], getMyCreature()->getMaxHealth())); + sizes.y+=21; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[193], CGI->generaltexth->fcommands[4], getMyCreature()->valOfBonuses(BonusType::STACKS_SPEED))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[194], CGI->generaltexth->fcommands[5], town->creatureGrowth(level))); + } +} + +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 nullptr; +} + +const CBuilding * CFortScreen::RecruitArea::getMyBuilding() +{ + BuildingID myID = BuildingID(BuildingID::DWELL_FIRST + level); + + if (level == GameConstants::CREATURES_PER_TOWN) + return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); + + if (!town->town->buildings.count(myID)) + return nullptr; + + const CBuilding * build = town->town->buildings.at(myID); + while (town->town->buildings.count(myID)) + { + if (town->hasBuilt(myID)) + build = town->town->buildings.at(myID); + myID.advance(GameConstants::CREATURES_PER_TOWN); + } + return build; +} + +void CFortScreen::RecruitArea::hover(bool on) +{ + if(on) + GH.statusbar()->write(hoverText); + else + GH.statusbar()->clear(); +} + +void CFortScreen::RecruitArea::creaturesChangedEventHandler() +{ + if(availableCount) + { + std::string availableText = CGI->generaltexth->allTexts[217] + std::to_string(town->creatures[level].first); + availableCount->setText(availableText); + } +} + +void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition) +{ + LOCPLINT->castleInt->builds->enterDwelling(level); +} + +void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) +{ + if (getMyCreature() != nullptr) + GH.windows().createAndPushWindow(getMyCreature(), true); +} + +CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename) + : CStatusbarWindow(BORDERED, imagename) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + window = std::make_shared(owner->town->town->clientInfo.guildWindow, 332, 76); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + Rect barRect(7, 556, 737, 18); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 7, 556); + statusbar = CGStatusBar::create(statusbarBackground); + + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + + static const std::vector > positions = + { + {Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)}, + {Point(48,53), Point(48,147), Point(48,241), Point(48,335), Point(48,429)}, + {Point(570,82), Point(672,82), Point(570,157), Point(672,157)}, + {Point(183,42), Point(183,148), Point(183,253)}, + {Point(491,325), Point(591,325)} + }; + + for(size_t i=0; itown->town->mageLevel; i++) + { + size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm? + for(size_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]])); + else + emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); + } + } +} + +CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) + : spell(Spell) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos += position; + image = std::make_shared(AnimationPath::builtin("SPELLSCR"), spell->id); + pos = image->pos; +} + +void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) +{ + LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); +} + +void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); +} + +void CMageGuildScreen::Scroll::hover(bool on) +{ + if(on) + GH.statusbar()->write(spell->getNameTranslated()); + else + GH.statusbar()->clear(); + +} + +CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSMITH")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + Rect barRect(8, pos.h - 26, pos.w - 16, 19); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 8, pos.h - 26); + statusbar = CGStatusBar::create(statusbarBackground); + + animBG = std::make_shared(ImagePath::builtin("TPSMITBK"), 64, 50); + animBG->needRefresh = true; + + const CCreature * creature = CGI->creh->objects[creMachineID]; + anim = std::make_shared(64, 50, creature->animDefName); + anim->clipRect(113,125,200,150); + + MetaString titleString; + titleString.appendTextID("core.genrltxt.274"); + titleString.replaceTextID(creature->getNameSingularTextID()); + + MetaString buyText; + buyText.appendTextID("core.genrltxt.595"); + buyText.replaceTextID(creature->getNameSingularTextID()); + + MetaString cancelText; + cancelText.appendTextID("core.genrltxt.596"); + cancelText.replaceTextID(creature->getNameSingularTextID()); + + std::string costString = std::to_string(aid.toEntity(CGI)->getPrice()); + + title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); + costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); + costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); + buy = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30.DEF"), CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + + if(possible) + buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); + else + buy->block(true); + + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 148, 244); +} diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index d3bbc29ae..1b7884737 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -1,405 +1,411 @@ -/* - * CCastleInterface.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 "../widgets/Images.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBuilding; -class CGTownInstance; -class CSpell; -struct CStructure; -class CGHeroInstance; -class CCreature; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CCastleBuildings; -class CCreaturePic; -class CGStatusBar; -class CLabel; -class CMinorResDataBar; -class CPicture; -class CResDataBar; -class CTextBox; -class CTownList; -class CGarrisonInt; -class CComponent; -class CComponentBox; - -/// Building "button" -class CBuildingRect : public CShowableAnim -{ - std::string getSubtitle(); -public: - enum EBuildingCreationAnimationPhases : uint32_t - { - BUILDING_APPEAR_TIMEPOINT = 500, //500 msec building appears: 0->100% transparency - BUILDING_WHITE_BORDER_TIMEPOINT = 900, //400 msec border glows from white to yellow - BUILDING_YELLOW_BORDER_TIMEPOINT = 1100, //200 msec border glows from yellow to normal (dark orange) - BUILD_ANIMATION_FINISHED_TIMEPOINT = 2100, // 1000msec once border is back to yellow nothing happens (this stage is basically removed by HD Mod) - - BUILDING_FRAME_TIME = 150 // confirmed H3 timing: 150 ms for each building animation frame - }; - - /// returns building associated with this structure - const CBuilding * getBuilding(); - - CCastleBuildings * parent; - const CGTownInstance * town; - const CStructure* str; - std::shared_ptr border; - std::shared_ptr area; - - ui32 stateTimeCounter;//For building construction - current stage in animation - - CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str); - bool operator<(const CBuildingRect & p2) const; - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) override; - bool receiveEvent(const Point & position, int eventType) const override; - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; -}; - -/// Dwelling info box - right-click screen for dwellings -class CDwellingInfoBox : public CWindowObject -{ - std::shared_ptr title; - std::shared_ptr animation; - std::shared_ptr available; - std::shared_ptr costPerTroop; - - std::vector> resPicture; - std::vector> resAmount; -public: - CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level); - ~CDwellingInfoBox(); -}; - -class HeroSlots; -/// Hero icon slot -class CHeroGSlot : public CIntObject -{ - std::shared_ptr portrait; - std::shared_ptr flag; - std::shared_ptr selection; //selection border. nullptr if not selected - - HeroSlots * owner; - const CGHeroInstance * hero; - int upg; //0 - up garrison, 1 - down garrison - -public: - CHeroGSlot(int x, int y, int updown, const CGHeroInstance *h, HeroSlots * Owner); - ~CHeroGSlot(); - - bool isSelected() const; - - void setHighlight(bool on); - void set(const CGHeroInstance * newHero); - - void hover (bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void deactivate() override; -}; - -/// Two hero slots that can interact with each other -class HeroSlots : public CIntObject -{ -public: - bool showEmpty; - const CGTownInstance * town; - - std::shared_ptr garr; - std::shared_ptr garrisonedHero; - std::shared_ptr visitingHero; - - HeroSlots(const CGTownInstance * town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty); - ~HeroSlots(); - - void splitClicked(); //for hero meeting only (splitting stacks is handled by garrison int) - void update(); - void swapArmies(); //exchange garrisoned and visiting heroes or move hero to\from garrison -}; - -/// Class for town screen management (town background and structures) -class CCastleBuildings : public CIntObject -{ - std::shared_ptr background; - //List of buildings and structures that can represent them - std::map > groups; - // actual IntObject's visible on screen - std::vector> buildings; - - const CGTownInstance * town; - - const CGHeroInstance* getHero();//Select hero for buildings usage - - void enterBlacksmith(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::EBuildingID upgrades);//Rampart's fountains - void enterMagesGuild(); - void enterTownHall(); - - void openMagesGuild(); - void openTownHall(); - - void recreate(); -public: - CBuildingRect * selectedBuilding; - - CCastleBuildings(const CGTownInstance * town); - ~CCastleBuildings(); - - void enterDwelling(int level); - void enterToTheQuickRecruitmentWindow(); - - void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID::EBuildingID upgrades = BuildingID::NONE); - void addBuilding(BuildingID building); - void removeBuilding(BuildingID building);//FIXME: not tested!!! -}; - -/// Creature info window -class CCreaInfo : public CIntObject -{ - const CGTownInstance * town; - const CCreature * creature; - int level; - bool showAvailable; - - std::shared_ptr picture; - std::shared_ptr label; - - std::string genGrowthText(); - -public: - CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact=false, bool _showAvailable=false); - - void update(); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - bool getShowAvailable(); -}; - -/// Town hall and fort icons for town screen -class CTownInfo : public CIntObject -{ - const CGTownInstance * town; - const CBuilding * building; -public: - std::shared_ptr picture; - //if (townHall) hall-capital else fort - castle - CTownInfo(int posX, int posY, const CGTownInstance * town, bool townHall); - - void hover(bool on) override; - void showPopupWindow(const Point & cursorPosition) override; -}; - -/// Class which manages the castle window -class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder -{ - std::shared_ptr title; - std::shared_ptr income; - std::shared_ptr icon; - - std::shared_ptr panel; - std::shared_ptr resdatabar; - - std::shared_ptr hall; - std::shared_ptr fort; - - std::shared_ptr exit; - std::shared_ptr split; - std::shared_ptr fastArmyPurchase; - - std::vector> creainfo;//small icons of creatures (bottom-left corner); - -public: - std::shared_ptr townlist; - - //TODO: move to private - const CGTownInstance * town; - std::shared_ptr heroes; - std::shared_ptr builds; - - std::shared_ptr garr; - - //from - previously selected castle (if any) - CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); - ~CCastleInterface(); - - virtual void updateGarrisons() override; - - void castleTeleport(int where); - void townChange(); - void keyPressed(EShortcut key) override; - - void close(); - void addBuilding(BuildingID bid); - void removeBuilding(BuildingID bid); - void recreateIcons(); - void creaturesChangedEventHandler(); -}; - -/// Hall window where you can build things -class CHallInterface : public CStatusbarWindow -{ - class CBuildingBox : public CIntObject - { - const CGTownInstance * town; - const CBuilding * building; - - ui32 state;//Buildings::EBuildStructure enum - - std::shared_ptr header; - std::shared_ptr icon; - std::shared_ptr mark; - std::shared_ptr name; - public: - CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - }; - const CGTownInstance * town; - - std::vector>> boxes; - std::shared_ptr title; - std::shared_ptr resdatabar; - std::shared_ptr exit; - -public: - CHallInterface(const CGTownInstance * Town); -}; - -/// Window where you can decide to buy a building or not -class CBuildWindow: public CStatusbarWindow -{ - const CGTownInstance * town; - const CBuilding * building; - - std::shared_ptr icon; - std::shared_ptr name; - std::shared_ptr description; - std::shared_ptr stateText; - std::shared_ptr cost; - - std::shared_ptr buy; - std::shared_ptr cancel; - - std::string getTextForState(int state); - void buyFunc(); -public: - CBuildWindow(const CGTownInstance *Town, const CBuilding * building, int State, bool rightClick); -}; - -//Small class to display -class LabeledValue : public CIntObject -{ - std::string hoverText; - std::shared_ptr name; - std::shared_ptr value; - void init(std::string name, std::string descr, int min, int max); - -public: - LabeledValue(Rect size, std::string name, std::string descr, int min, int max); - LabeledValue(Rect size, std::string name, std::string descr, int val); - void hover(bool on) override; -}; - -/// The fort screen where you can afford units -class CFortScreen : public CStatusbarWindow -{ - class RecruitArea : public CIntObject - { - const CGTownInstance * town; - int level; - - std::string hoverText; - std::shared_ptr availableCount; - - std::vector> values; - std::shared_ptr icons; - std::shared_ptr buildingIcon; - std::shared_ptr buildingName; - - const CCreature * getMyCreature(); - const CBuilding * getMyBuilding(); - public: - RecruitArea(int posX, int posY, const CGTownInstance *town, int level); - - void creaturesChangedEventHandler(); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - - }; - std::shared_ptr title; - std::vector> recAreas; - std::shared_ptr resdatabar; - std::shared_ptr exit; - - std::string getBgName(const CGTownInstance * town); - -public: - CFortScreen(const CGTownInstance * town); - - void creaturesChangedEventHandler(); -}; - -/// The mage guild screen where you can see which spells you have -class CMageGuildScreen : public CStatusbarWindow -{ - class Scroll : public CIntObject - { - const CSpell * spell; - std::shared_ptr image; - - public: - Scroll(Point position, const CSpell *Spell); - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - }; - std::shared_ptr window; - std::shared_ptr exit; - std::vector> spells; - std::vector> emptyScrolls; - - std::shared_ptr resdatabar; - -public: - CMageGuildScreen(CCastleInterface * owner,std::string image); -}; - -/// The blacksmith window where you can buy available in town war machine -class CBlacksmithDialog : public CStatusbarWindow -{ - std::shared_ptr buy; - std::shared_ptr cancel; - std::shared_ptr animBG; - std::shared_ptr anim; - std::shared_ptr title; - std::shared_ptr costIcon; - std::shared_ptr costText; - std::shared_ptr costValue; - -public: - CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid); -}; +/* + * CCastleInterface.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 "../widgets/Images.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBuilding; +class CGTownInstance; +class CSpell; +struct CStructure; +class CGHeroInstance; +class CCreature; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CCastleBuildings; +class CCreaturePic; +class CGStatusBar; +class CLabel; +class CMinorResDataBar; +class CPicture; +class CResDataBar; +class CTextBox; +class CTownList; +class CGarrisonInt; +class CComponent; +class CComponentBox; +class LRClickableArea; + +/// Building "button" +class CBuildingRect : public CShowableAnim +{ + std::string getSubtitle(); +public: + enum EBuildingCreationAnimationPhases : uint32_t + { + BUILDING_APPEAR_TIMEPOINT = 500, //500 msec building appears: 0->100% transparency + BUILDING_WHITE_BORDER_TIMEPOINT = 900, //400 msec border glows from white to yellow + BUILDING_YELLOW_BORDER_TIMEPOINT = 1100, //200 msec border glows from yellow to normal (dark orange) + BUILD_ANIMATION_FINISHED_TIMEPOINT = 2100, // 1000msec once border is back to yellow nothing happens (this stage is basically removed by HD Mod) + + BUILDING_FRAME_TIME = 150 // confirmed H3 timing: 150 ms for each building animation frame + }; + + /// returns building associated with this structure + const CBuilding * getBuilding(); + + CCastleBuildings * parent; + const CGTownInstance * town; + const CStructure* str; + std::shared_ptr border; + std::shared_ptr area; + + ui32 stateTimeCounter;//For building construction - current stage in animation + + CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str); + bool operator<(const CBuildingRect & p2) const; + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) override; + bool receiveEvent(const Point & position, int eventType) const override; + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; +}; + +/// Dwelling info box - right-click screen for dwellings +class CDwellingInfoBox : public CWindowObject +{ + std::shared_ptr title; + std::shared_ptr animation; + std::shared_ptr available; + std::shared_ptr costPerTroop; + + std::vector> resPicture; + std::vector> resAmount; +public: + CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level); + ~CDwellingInfoBox(); +}; + +class HeroSlots; +/// Hero icon slot +class CHeroGSlot : public CIntObject +{ + std::shared_ptr portrait; + std::shared_ptr flag; + std::shared_ptr selection; //selection border. nullptr if not selected + + HeroSlots * owner; + const CGHeroInstance * hero; + int upg; //0 - up garrison, 1 - down garrison + +public: + CHeroGSlot(int x, int y, int updown, const CGHeroInstance *h, HeroSlots * Owner); + ~CHeroGSlot(); + + bool isSelected() const; + + void setHighlight(bool on); + void set(const CGHeroInstance * newHero); + + void hover (bool on) override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void deactivate() override; +}; + +/// Two hero slots that can interact with each other +class HeroSlots : public CIntObject +{ +public: + bool showEmpty; + const CGTownInstance * town; + + std::shared_ptr garr; + std::shared_ptr garrisonedHero; + std::shared_ptr visitingHero; + + HeroSlots(const CGTownInstance * town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty); + ~HeroSlots(); + + void splitClicked(); //for hero meeting only (splitting stacks is handled by garrison int) + void update(); + void swapArmies(); //exchange garrisoned and visiting heroes or move hero to\from garrison +}; + +/// Class for town screen management (town background and structures) +class CCastleBuildings : public CIntObject +{ + std::shared_ptr background; + //List of buildings and structures that can represent them + std::map > groups; + // actual IntObject's visible on screen + std::vector> buildings; + + const CGTownInstance * town; + + const CGHeroInstance* getHero();//Select hero for buildings usage + + void enterBlacksmith(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 + void enterMagesGuild(); + + void openMagesGuild(); + void openTownHall(); + + void recreate(); +public: + CBuildingRect * selectedBuilding; + + CCastleBuildings(const CGTownInstance * town); + ~CCastleBuildings(); + + void enterDwelling(int level); + void enterTownHall(); + void enterToTheQuickRecruitmentWindow(); + + void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); + void addBuilding(BuildingID building); + void removeBuilding(BuildingID building);//FIXME: not tested!!! +}; + +/// Creature info window +class CCreaInfo : public CIntObject +{ + const CGTownInstance * town; + const CCreature * creature; + int level; + bool showAvailable; + + std::shared_ptr picture; + std::shared_ptr label; + + std::string genGrowthText(); + +public: + CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact=false, bool _showAvailable=false); + + void update(); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + bool getShowAvailable(); +}; + +/// Town hall and fort icons for town screen +class CTownInfo : public CIntObject +{ + const CGTownInstance * town; + const CBuilding * building; +public: + std::shared_ptr picture; + //if (townHall) hall-capital else fort - castle + CTownInfo(int posX, int posY, const CGTownInstance * town, bool townHall); + + void hover(bool on) override; + void showPopupWindow(const Point & cursorPosition) override; +}; + +/// Class which manages the castle window +class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder +{ + std::shared_ptr title; + std::shared_ptr income; + std::shared_ptr icon; + + std::shared_ptr panel; + std::shared_ptr resdatabar; + + std::shared_ptr hall; + std::shared_ptr fort; + + std::shared_ptr exit; + std::shared_ptr split; + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; + + std::vector> creainfo;//small icons of creatures (bottom-left corner); + +public: + std::shared_ptr townlist; + + //TODO: move to private + const CGTownInstance * town; + std::shared_ptr heroes; + std::shared_ptr builds; + + std::shared_ptr garr; + + //from - previously selected castle (if any) + CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); + ~CCastleInterface(); + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + + void castleTeleport(int where); + void townChange(); + void keyPressed(EShortcut key) override; + + void close() override; + void addBuilding(BuildingID bid); + void removeBuilding(BuildingID bid); + void recreateIcons(); + void creaturesChangedEventHandler(); +}; + +/// Hall window where you can build things +class CHallInterface : public CStatusbarWindow +{ + class CBuildingBox : public CIntObject + { + const CGTownInstance * town; + const CBuilding * building; + + EBuildingState state; + + std::shared_ptr header; + std::shared_ptr icon; + std::shared_ptr mark; + std::shared_ptr name; + public: + CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + }; + const CGTownInstance * town; + + std::vector>> boxes; + std::shared_ptr title; + std::shared_ptr resdatabar; + std::shared_ptr exit; + +public: + CHallInterface(const CGTownInstance * Town); +}; + +/// Window where you can decide to buy a building or not +class CBuildWindow: public CStatusbarWindow +{ + const CGTownInstance * town; + const CBuilding * building; + + std::shared_ptr icon; + std::shared_ptr name; + std::shared_ptr description; + std::shared_ptr stateText; + std::shared_ptr cost; + + std::shared_ptr buy; + std::shared_ptr cancel; + + std::string getTextForState(EBuildingState state); + void buyFunc(); +public: + CBuildWindow(const CGTownInstance *Town, const CBuilding * building, EBuildingState State, bool rightClick); +}; + +//Small class to display +class LabeledValue : public CIntObject +{ + std::string hoverText; + std::shared_ptr name; + std::shared_ptr value; + void init(std::string name, std::string descr, int min, int max); + +public: + LabeledValue(Rect size, std::string name, std::string descr, int min, int max); + LabeledValue(Rect size, std::string name, std::string descr, int val); + void hover(bool on) override; +}; + +/// The fort screen where you can afford units +class CFortScreen : public CStatusbarWindow +{ + class RecruitArea : public CIntObject + { + const CGTownInstance * town; + int level; + + std::string hoverText; + std::shared_ptr availableCount; + + std::vector> values; + std::shared_ptr icons; + std::shared_ptr buildingIcon; + std::shared_ptr buildingName; + + const CCreature * getMyCreature(); + const CBuilding * getMyBuilding(); + public: + RecruitArea(int posX, int posY, const CGTownInstance *town, int level); + + void creaturesChangedEventHandler(); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + + }; + std::shared_ptr title; + std::vector> recAreas; + std::shared_ptr resdatabar; + std::shared_ptr exit; + + ImagePath getBgName(const CGTownInstance * town); + +public: + CFortScreen(const CGTownInstance * town); + + void creaturesChangedEventHandler(); +}; + +/// The mage guild screen where you can see which spells you have +class CMageGuildScreen : public CStatusbarWindow +{ + class Scroll : public CIntObject + { + const CSpell * spell; + std::shared_ptr image; + + public: + Scroll(Point position, const CSpell *Spell); + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + }; + std::shared_ptr window; + std::shared_ptr exit; + std::vector> spells; + std::vector> emptyScrolls; + + std::shared_ptr resdatabar; + +public: + CMageGuildScreen(CCastleInterface * owner, const ImagePath & image); +}; + +/// The blacksmith window where you can buy available in town war machine +class CBlacksmithDialog : public CStatusbarWindow +{ + std::shared_ptr buy; + std::shared_ptr cancel; + std::shared_ptr animBG; + std::shared_ptr anim; + std::shared_ptr title; + std::shared_ptr costIcon; + std::shared_ptr costText; + std::shared_ptr costValue; + +public: + CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid); +}; diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 1e43f7a0a..f6e073f2c 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -1,984 +1,985 @@ -/* - * CCreatureWindow.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 "CCreatureWindow.h" - -#include -#include - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../widgets/Buttons.h" -#include "../widgets/CArtifactHolder.h" -#include "../widgets/CComponent.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" -#include "../widgets/ObjectLists.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" - -#include "../../CCallback.h" -#include "../../lib/ArtifactUtils.h" -#include "../../lib/CStack.h" -#include "../../lib/CBonusTypeHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/gameState/CGameState.h" -#include "../../lib/TextOperations.h" - -class CCreatureArtifactInstance; -class CSelectableSkill; - -class UnitView -{ -public: - // helper structs - struct CommanderLevelInfo - { - std::vector skills; - std::function callback; - }; - struct StackDismissInfo - { - std::function callback; - }; - struct StackUpgradeInfo - { - UpgradeInfo info; - std::function callback; - }; - - // pointers to permament objects in game state - const CCreature * creature; - const CCommanderInstance * commander; - const CStackInstance * stackNode; - const CStack * stack; - const CGHeroInstance * owner; - - // temporary objects which should be kept as copy if needed - std::optional levelupInfo; - std::optional dismissInfo; - std::optional upgradeInfo; - - // misc fields - unsigned int creatureCount; - bool popupWindow; - - UnitView() - : creature(nullptr), - commander(nullptr), - stackNode(nullptr), - stack(nullptr), - owner(nullptr), - creatureCount(0), - popupWindow(false) - { - } - - std::string getName() const - { - if(commander) - return commander->type->getNameSingularTranslated(); - else - return creature->getNamePluralTranslated(); - } -private: - -}; - -CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback) - : object(), - isMasterAbility(isMasterAbility_), - isSelected(false), - callback(callback) -{ - pos = object_->pos; - this->isMasterAbility = isMasterAbility_; - setObject(object_); -} - -void CCommanderSkillIcon::setObject(std::shared_ptr newObject) -{ - if(object) - removeChild(object.get()); - object = newObject; - addChild(object.get()); - object->moveTo(pos.topLeft()); - redraw(); -} - -void CCommanderSkillIcon::clickPressed(const Point & cursorPosition) -{ - callback(); - isSelected = true; -} - -void CCommanderSkillIcon::deselect() -{ - isSelected = false; -} - -bool CCommanderSkillIcon::getIsMasterAbility() -{ - return isMasterAbility; -} - -void CCommanderSkillIcon::show(Canvas &to) -{ - CIntObject::show(to); - - if(isMasterAbility && isSelected) - to.drawBorder(pos, Colors::YELLOW, 2); -} - -static std::string skillToFile(int skill, int level, bool selected) -{ - // FIXME: is this a correct hadling? - // 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) - std::string file = "zvs/Lib1.res/_"; - switch (skill) - { - case ECommander::ATTACK: - file += "AT"; - break; - case ECommander::DEFENSE: - file += "DF"; - break; - case ECommander::HEALTH: - file += "HP"; - break; - case ECommander::DAMAGE: - file += "DM"; - break; - case ECommander::SPEED: - file += "SP"; - break; - case ECommander::SPELL_POWER: - file += "MP"; - break; - } - std::string sufix; - if (selected) - level++; // UI will display resulting level - if (level == 0) - sufix = "no"; //not avaliable - no number - else - sufix = std::to_string(level-1); - if (selected) - sufix += "="; //level-up highlight - - return file + sufix + ".bmp"; -} - -CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, std::string backgroundPath, int yOffset) - : parent(parent) -{ - pos.y += yOffset; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(!backgroundPath.empty()) - { - background = std::make_shared("stackWindow/" + backgroundPath); - pos.w = background->pos.w; - pos.h = background->pos.h; - } -} - -CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "spell-effects", yOffset) -{ - 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); - - const CStack * battleStack = parent->info->stack; - - assert(battleStack); // Section should be created only for battles - - //spell effects - int printed=0; //how many effect pics have been printed - std::vector spells = battleStack->activeSpells(); - for(si32 effect : spells) - { - const spells::Spell * spell = CGI->spells()->getByIndex(effect); - - std::string spellText; - - //not all effects have graphics (for eg. Acid Breath) - //for modded spells iconEffect is added to SpellInt.def - const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST); - - if (hasGraphics) - { - spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." - boost::replace_first(spellText, "%s", spell->getNameTranslated()); - //FIXME: support permanent duration - int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT,effect))->turnsRemain; - boost::replace_first(spellText, "%d", std::to_string(duration)); - - spellIcons.push_back(std::make_shared("SpellInt", effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); - clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); - if(++printed >= 8) // interface limit reached - break; - } - } -} - -CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) - : CWindowSection(owner, "bonus-effects", 0) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - static const std::array offset = - { - Point(6, 4), - Point(214, 4) - }; - - for(size_t leftRight : {0, 1}) - { - auto position = offset[leftRight]; - size_t bonusIndex = lineIndex * 2 + leftRight; - - if(parent->activeBonuses.size() > bonusIndex) - { - 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); - } - } -} - -CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): - CWindowSection(owner, "", yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - // size of single image for an item - static const int itemHeight = 59; - - size_t totalSize = (owner->activeBonuses.size() + 1) / 2; - size_t visibleSize = preferredSize.value_or(std::min(3, totalSize)); - - pos.w = owner->pos.w; - pos.h = itemHeight * (int)visibleSize; - - auto onCreate = [=](size_t index) -> std::shared_ptr - { - 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)); -} - -CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "button-panel", yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(parent->info->dismissInfo && parent->info->dismissInfo->callback) - { - auto onDismiss = [=]() - { - parent->info->dismissInfo->callback(); - parent->close(); - }; - auto onClick = [=] () - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr); - }; - dismiss = std::make_shared(Point(5, 5),"IVIEWCR2.DEF", CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); - } - - if(parent->info->upgradeInfo && !parent->info->commander) - { - // used space overlaps with commander switch button - // besides - should commander really be upgradeable? - - auto & upgradeInfo = parent->info->upgradeInfo.value(); - const size_t buttonsToCreate = std::min(upgradeInfo.info.newID.size(), upgrade.size()); - - for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++) - { - TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount; - - auto onUpgrade = [=]() - { - upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]); - parent->close(); - }; - auto onClick = [=]() - { - std::vector> resComps; - for(TResources::nziterator i(totalCost); i.valid(); i++) - { - resComps.push_back(std::make_shared(CComponent::resource, i->resType, (int)i->resVal)); - } - - if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[207], onUpgrade, nullptr, resComps); - } - else - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps); - } - }; - auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), "stackWindow/upgradeButton", CGI->generaltexth->zelp[446], onClick); - - upgradeBtn->addOverlay(std::make_shared("CPRSMALL", VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); - - if(buttonsToCreate == 1) // single upgrade avaialbe - upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; - - upgrade[buttonIndex] = upgradeBtn; - } - } - - if(parent->info->commander) - { - for(size_t buttonIndex = 0; buttonIndex < 2; buttonIndex++) - { - std::string btnIDs[2] = { "showSkills", "showBonuses" }; - auto onSwitch = [buttonIndex, this]() - { - logAnim->debug("Switch %d->%d", parent->activeTab, buttonIndex); - - parent->switchButtons[parent->activeTab]->enable(); - parent->commanderTab->setActive(buttonIndex); - parent->switchButtons[buttonIndex]->disable(); - parent->redraw(); // FIXME: enable/disable don't redraw screen themselves - }; - - std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; - parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltipLocalized(tooltipText), onSwitch); - parent->switchButtons[buttonIndex]->addOverlay(std::make_shared("stackWindow/switchModeIcons", buttonIndex)); - } - parent->switchButtons[parent->activeTab]->disable(); - } - - exit = std::make_shared(Point(382, 5), "hsbtns.def", CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); -} - -CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "commander-bg", yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - auto getSkillPos = [](int index) - { - return Point(10 + 80 * (index%3), 20 + 80 * (index/3)); - }; - - auto getSkillImage = [this](int skillIndex) -> std::string - { - bool selected = ((parent->selectedSkill == skillIndex) && parent->info->levelupInfo ); - return skillToFile(skillIndex, parent->info->commander->secondarySkills[skillIndex], selected); - }; - - auto getSkillDescription = [this](int skillIndex) -> std::string - { - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; - }; - - for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) - { - Point skillPos = getSkillPos(index); - - auto icon = std::make_shared(std::make_shared(getSkillImage(index), skillPos.x, skillPos.y), false, [=]() - { - LOCPLINT->showInfoDialog(getSkillDescription(index)); - }); - - icon->text = getSkillDescription(index); //used to handle right click description via LRClickableAreaWText::ClickRight() - - if(parent->selectedSkill == index) - parent->selectedIcon = icon; - - if(parent->info->levelupInfo && vstd::contains(parent->info->levelupInfo->skills, index)) // can be upgraded - enable selection switch - { - if(parent->selectedSkill == index) - parent->setSelection(index, icon); - - icon->callback = [=]() - { - parent->setSelection(index, icon); - }; - } - - skillIcons.push_back(icon); - } - - auto getArtifactPos = [](int index) - { - return Point(269 + 47 * (index % 3), 22 + 47 * (index / 3)); - }; - - 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); - artifacts.push_back(artPlace); - } - - if(parent->info->levelupInfo) - { - abilitiesBackground = std::make_shared("stackWindow/commander-abilities.png"); - abilitiesBackground->moveBy(Point(0, pos.h)); - - size_t abilitiesCount = boost::range::count_if(parent->info->levelupInfo->skills, [](ui32 skillID) - { - return skillID >= 100; - }); - - auto onCreate = [=](size_t index)->std::shared_ptr - { - for(auto skillID : parent->info->levelupInfo->skills) - { - if(index == 0 && skillID >= 100) - { - const auto bonus = CGI->creh->skillRequirements[skillID-100].first; - const CStackInstance * stack = parent->info->commander; - auto icon = std::make_shared(std::make_shared(stack->bonusToGraphics(bonus)), true, [](){}); - icon->callback = [=]() - { - parent->setSelection(skillID, icon); - }; - icon->text = stack->bonusToString(bonus, true); - icon->hoverText = stack->bonusToString(bonus, false); - - return icon; - } - if(skillID >= 100) - index--; - } - return nullptr; - }; - - abilities = std::make_shared(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount); - abilities->setRedrawParent(true); - - leftBtn = std::make_shared(Point(10, pos.h + 6), "hsbtns3.def", CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); - rightBtn = std::make_shared(Point(411, pos.h + 6), "hsbtns5.def", CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); - - if(abilitiesCount <= 6) - { - leftBtn->block(true); - rightBtn->block(true); - } - - pos.h += abilitiesBackground->pos.h; - } -} - -CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt) - : CWindowSection(owner, getBackgroundName(showExp, showArt), yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - statNames = - { - CGI->generaltexth->primarySkillNames[0], //ATTACK - CGI->generaltexth->primarySkillNames[1],//DEFENCE - CGI->generaltexth->allTexts[198],//SHOTS - CGI->generaltexth->allTexts[199],//DAMAGE - - CGI->generaltexth->allTexts[388],//HEALTH - CGI->generaltexth->allTexts[200],//HEALTH_LEFT - CGI->generaltexth->zelp[441].first,//SPEED - CGI->generaltexth->allTexts[399]//MANA - }; - - statFormats = - { - "%d (%d)", - "%d (%d)", - "%d (%d)", - "%d - %d", - - "%d (%d)", - "%d (%d)", - "%d (%d)", - "%d (%d)" - }; - - animation = std::make_shared(5, 41, parent->info->creature); - - 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()); - - int dmgMultiply = 1; - if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) - dmgMultiply += parent->info->owner->getPrimSkillLevel(PrimarySkill::ATTACK); - - icons = std::make_shared("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) )); - - if(battleStack != nullptr) // in battle - { - addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(battleStack->isShooter()), battleStack->getAttack(battleStack->isShooter())); - addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter())); - addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply); - addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed()); - - if(battleStack->isShooter()) - addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); - if(battleStack->isCaster()) - addStatLabel(EStat::MANA, battleStack->casts.total(), battleStack->casts.available()); - addStatLabel(EStat::HEALTH_LEFT, battleStack->getFirstHPleft()); - - morale->set(battleStack); - luck->set(battleStack); - } - else - { - const bool shooter = parent->info->stackNode->hasBonusOfType(BonusType::SHOOTER) && parent->info->stackNode->valOfBonuses(BonusType::SHOTS); - const bool caster = parent->info->stackNode->valOfBonuses(BonusType::CASTS); - - 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::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed()); - - if(shooter) - addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); - if(caster) - addStatLabel(EStat::MANA, parent->info->stackNode->valOfBonuses(BonusType::CASTS)); - - morale->set(parent->info->stackNode); - luck->set(parent->info->stackNode); - } - - if(showExp) - { - const CStackInstance * stack = parent->info->stackNode; - Point pos = showArt ? Point(321, 32) : Point(347, 32); - if(parent->info->commander) - { - const CCommanderInstance * commander = parent->info->commander; - expRankIcon = std::make_shared("PSKIL42", 4, 0, pos.x, pos.y); - - auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); - expArea = area; - area->text = CGI->generaltexth->allTexts[2]; - area->bonusValue = commander->getExpRank(); - boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); - boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); - boost::replace_first(area->text, "%d", std::to_string(commander->experience)); - } - else - { - expRankIcon = std::make_shared("stackWindow/levels", stack->getExpRank(), 0, pos.x, pos.y); - expArea = std::make_shared(Rect(pos.x, pos.y, 44, 44)); - expArea->text = parent->generateStackExpDescription(); - } - expLabel = std::make_shared( - pos.x + 21, pos.y + 52, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, - TextOperations::formatMetric(stack->experience, 6)); - } - - if(showArt) - { - Point pos = showExp ? Point(375, 32) : Point(347, 32); - // ALARMA: do not refactor this into a separate function - // otherwise, artifact icon is drawn near the hero's portrait - // this is really strange - auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); - if(art) - { - parent->stackArtifactIcon = std::make_shared("ARTIFACT", art->artType->getIconIndex(), 0, pos.x, pos.y); - parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); - parent->stackArtifactHelp->type = art->artType->getId(); - - if(parent->info->owner) - { - parent->stackArtifactButton = std::make_shared( - Point(pos.x - 2 , pos.y + 46), "stackWindow/cancelButton", - CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() - { - parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); - }); - } - } - } -} - - -std::string CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) -{ - if(showExp && showArt) - return "info-panel-2"; - else if(showExp || showArt) - return "info-panel-1"; - else - return "info-panel-0"; -} - -void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value1, int64_t value2) -{ - const auto title = statNames.at(static_cast(index)); - stats.push_back(std::make_shared(145, 32 + (int)index*19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, title)); - - const bool useRange = value1 != value2; - std::string formatStr = useRange ? statFormats.at(static_cast(index)) : "%d"; - - boost::format fmt(formatStr); - fmt % value1; - if(useRange) - fmt % value2; - - stats.push_back(std::make_shared(307, 48 + (int)index*19, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, fmt.str())); -} - -void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value) -{ - addStatLabel(index, value, value); -} - -CStackWindow::CStackWindow(const CStack * stack, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stack = stack; - info->stackNode = stack->base; - info->creature = stack->unitType(); - info->creatureCount = stack->getCount(); - info->popupWindow = popup; - init(); -} - -CStackWindow::CStackWindow(const CCreature * creature, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->creature = creature; - info->popupWindow = popup; - init(); -} - -CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stackNode = stack; - info->creature = stack->type; - info->creatureCount = stack->count; - info->popupWindow = popup; - info->owner = dynamic_cast (stack->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & upgradeInfo, std::function callback) - : CWindowObject(BORDERED), - info(new UnitView()) -{ - info->stackNode = stack; - info->creature = stack->type; - info->creatureCount = stack->count; - - info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); - info->dismissInfo = std::make_optional(UnitView::StackDismissInfo()); - info->upgradeInfo->info = upgradeInfo; - info->upgradeInfo->callback = callback; - info->dismissInfo->callback = dismiss; - info->owner = dynamic_cast (stack->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CCommanderInstance * commander, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stackNode = commander; - info->creature = commander->type; - info->commander = commander; - info->creatureCount = 1; - info->popupWindow = popup; - info->owner = dynamic_cast (commander->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback) - : CWindowObject(BORDERED), - info(new UnitView()) -{ - info->stackNode = commander; - info->creature = commander->type; - info->commander = commander; - info->creatureCount = 1; - info->levelupInfo = std::make_optional(UnitView::CommanderLevelInfo()); - info->levelupInfo->skills = skills; - info->levelupInfo->callback = callback; - info->owner = dynamic_cast (commander->armyObj); - init(); -} - -CStackWindow::~CStackWindow() -{ - if(info->levelupInfo && !info->levelupInfo->skills.empty()) - info->levelupInfo->callback(vstd::find_pos(info->levelupInfo->skills, selectedSkill)); -} - -void CStackWindow::init() -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(!info->stackNode) - info->stackNode = new CStackInstance(info->creature, 1, true);// FIXME: free data - - selectedIcon = nullptr; - selectedSkill = -1; - if(info->levelupInfo && !info->levelupInfo->skills.empty()) - selectedSkill = info->levelupInfo->skills.front(); - - activeTab = 0; - - initBonusesList(); - initSections(); -} - -void CStackWindow::initBonusesList() -{ - BonusList output, input; - input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); - - while(!input.empty()) - { - auto b = input.front(); - output.push_back(std::make_shared(*b)); - output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one - input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses - } - - BonusInfo bonusInfo; - for(auto b : output) - { - bonusInfo.name = info->stackNode->bonusToString(b, false); - bonusInfo.description = info->stackNode->bonusToString(b, true); - bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); - - //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 - if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty()) - activeBonuses.push_back(bonusInfo); - } -} - -void CStackWindow::initSections() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - 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; - - mainSection = std::make_shared(this, pos.h, showExp, showArt); - - pos.w = mainSection->pos.w; - pos.h += mainSection->pos.h; - - if(info->stack) // in battle - { - activeSpellsSection = std::make_shared(this, pos.h); - pos.h += activeSpellsSection->pos.h; - } - - if(info->commander) - { - auto onCreate = [=](size_t index) -> std::shared_ptr - { - auto obj = switchTab(index); - - if(obj) - { - obj->activate(); - obj->recActions |= (UPDATE | SHOWALL); - } - return obj; - }; - - auto deactivateObj = [=](std::shared_ptr obj) - { - obj->deactivate(); - obj->recActions &= ~(UPDATE | SHOWALL); - }; - - commanderMainSection = std::make_shared(this, 0); - - auto size = std::make_optional((info->levelupInfo) ? 4 : 3); - commanderBonusesSection = std::make_shared(this, 0, size); - deactivateObj(commanderBonusesSection); - - commanderTab = std::make_shared(onCreate, Point(0, pos.h), 0); - - pos.h += commanderMainSection->pos.h; - } - - if(!info->commander && !activeBonuses.empty()) - { - bonusesSection = std::make_shared(this, pos.h); - pos.h += bonusesSection->pos.h; - } - - if(!info->popupWindow) - { - buttonsSection = std::make_shared(this, pos.h); - pos.h += buttonsSection->pos.h; - //FIXME: add status bar to image? - } - updateShadow(); - pos = center(pos); -} - -std::string CStackWindow::generateStackExpDescription() -{ - const CStackInstance * stack = info->stackNode; - const CCreature * creature = info->creature; - - int tier = stack->type->getLevel(); - int rank = stack->getExpRank(); - if (!vstd::iswithin(tier, 1, 7)) - tier = 0; - int number; - std::string expText = CGI->generaltexth->translate("vcmi.stackExperience.description"); - boost::replace_first(expText, "%s", creature->getNamePluralTranslated()); - boost::replace_first(expText, "%s", CGI->generaltexth->translate("vcmi.stackExperience.rank", rank)); - boost::replace_first(expText, "%i", std::to_string(rank)); - boost::replace_first(expText, "%i", std::to_string(stack->experience)); - number = static_cast(CGI->creh->expRanks[tier][rank] - stack->experience); - boost::replace_first(expText, "%i", std::to_string(number)); - - number = CGI->creh->maxExpPerBattle[tier]; //percent - boost::replace_first(expText, "%i%", std::to_string(number)); - number *= CGI->creh->expRanks[tier].back() / 100; //actual amount - boost::replace_first(expText, "%i", std::to_string(number)); - - boost::replace_first(expText, "%i", std::to_string(stack->count)); //Number of Creatures in stack - - int expmin = std::max(CGI->creh->expRanks[tier][std::max(rank-1, 0)], (ui32)1); - number = static_cast((stack->count * (stack->experience - expmin)) / expmin); //Maximum New Recruits without losing current Rank - boost::replace_first(expText, "%i", std::to_string(number)); //TODO - - boost::replace_first(expText, "%.2f", std::to_string(1)); //TODO Experience Multiplier - number = CGI->creh->expAfterUpgrade; - boost::replace_first(expText, "%.2f", std::to_string(number) + "%"); //Upgrade Multiplier - - expmin = CGI->creh->expRanks[tier][9]; - int expmax = CGI->creh->expRanks[tier][10]; - number = expmax - expmin; - boost::replace_first(expText, "%i", std::to_string(number)); //Experience after Rank 10 - number = (stack->count * (expmax - expmin)) / expmin; - boost::replace_first(expText, "%i", std::to_string(number)); //Maximum New Recruits to remain at Rank 10 if at Maximum Experience - - return expText; -} - -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 - else - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; - }; - - auto getSkillImage = [this](int skillIndex) -> std::string - { - bool selected = ((selectedSkill == skillIndex) && info->levelupInfo ); - return skillToFile(skillIndex, info->commander->secondarySkills[skillIndex], selected); - }; - - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - int oldSelection = selectedSkill; // update selection - selectedSkill = newSkill; - - if(selectedIcon && oldSelection < 100) // recreate image on old selection, only for skills - selectedIcon->setObject(std::make_shared(getSkillImage(oldSelection))); - - if(selectedIcon) - { - if(!selectedIcon->getIsMasterAbility()) //unlike WoG, in VCMI master skill descriptions are taken from bonus descriptions - { - selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level - } - selectedIcon->deselect(); - } - - selectedIcon = newIcon; // update new selection - if(newSkill < 100) - { - newIcon->setObject(std::make_shared(getSkillImage(newSkill))); - if(!newIcon->getIsMasterAbility()) - { - newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description - } - } -} - -std::shared_ptr CStackWindow::switchTab(size_t index) -{ - std::shared_ptr ret; - switch(index) - { - case 0: - { - activeTab = 0; - ret = commanderMainSection; - } - break; - case 1: - { - activeTab = 1; - ret = commanderBonusesSection; - } - break; - default: - break; - } - - return ret; -} - -void CStackWindow::removeStackArtifact(ArtifactPosition pos) -{ - auto art = info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); - if(!art) - { - logGlobal->error("Attempt to remove missing artifact"); - return; - } - const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot)); - stackArtifactButton.reset(); - stackArtifactHelp.reset(); - stackArtifactIcon.reset(); - redraw(); - } -} - +/* + * CCreatureWindow.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 "CCreatureWindow.h" + +#include +#include + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../widgets/Buttons.h" +#include "../widgets/CArtifactHolder.h" +#include "../widgets/CComponent.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.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/gameState/CGameState.h" +#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/TextOperations.h" + +class CCreatureArtifactInstance; +class CSelectableSkill; + +class UnitView +{ +public: + // helper structs + struct CommanderLevelInfo + { + std::vector skills; + std::function callback; + }; + struct StackDismissInfo + { + std::function callback; + }; + struct StackUpgradeInfo + { + UpgradeInfo info; + std::function callback; + }; + + // pointers to permament objects in game state + const CCreature * creature; + const CCommanderInstance * commander; + const CStackInstance * stackNode; + const CStack * stack; + const CGHeroInstance * owner; + + // temporary objects which should be kept as copy if needed + std::optional levelupInfo; + std::optional dismissInfo; + std::optional upgradeInfo; + + // misc fields + unsigned int creatureCount; + bool popupWindow; + + UnitView() + : creature(nullptr), + commander(nullptr), + stackNode(nullptr), + stack(nullptr), + owner(nullptr), + creatureCount(0), + popupWindow(false) + { + } + + std::string getName() const + { + if(commander) + return commander->type->getNameSingularTranslated(); + else + return creature->getNamePluralTranslated(); + } +private: + +}; + +CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback) + : object(), + isMasterAbility(isMasterAbility_), + isSelected(false), + callback(callback) +{ + pos = object_->pos; + this->isMasterAbility = isMasterAbility_; + setObject(object_); +} + +void CCommanderSkillIcon::setObject(std::shared_ptr newObject) +{ + if(object) + removeChild(object.get()); + object = newObject; + addChild(object.get()); + object->moveTo(pos.topLeft()); + redraw(); +} + +void CCommanderSkillIcon::clickPressed(const Point & cursorPosition) +{ + callback(); + isSelected = true; +} + +void CCommanderSkillIcon::deselect() +{ + isSelected = false; +} + +bool CCommanderSkillIcon::getIsMasterAbility() +{ + return isMasterAbility; +} + +void CCommanderSkillIcon::show(Canvas &to) +{ + CIntObject::show(to); + + if(isMasterAbility && isSelected) + to.drawBorder(pos, Colors::YELLOW, 2); +} + +static ImagePath skillToFile(int skill, int level, bool selected) +{ + // FIXME: is this a correct hadling? + // 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) + std::string file = "zvs/Lib1.res/_"; + switch (skill) + { + case ECommander::ATTACK: + file += "AT"; + break; + case ECommander::DEFENSE: + file += "DF"; + break; + case ECommander::HEALTH: + file += "HP"; + break; + case ECommander::DAMAGE: + file += "DM"; + break; + case ECommander::SPEED: + file += "SP"; + break; + case ECommander::SPELL_POWER: + file += "MP"; + break; + } + std::string sufix; + if (selected) + level++; // UI will display resulting level + if (level == 0) + sufix = "no"; //not avaliable - no number + else + sufix = std::to_string(level-1); + if (selected) + sufix += "="; //level-up highlight + + return ImagePath::builtin(file + sufix + ".bmp"); +} + +CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset) + : parent(parent) +{ + pos.y += yOffset; + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(!backgroundPath.empty()) + { + background = std::make_shared(backgroundPath); + pos.w = background->pos.w; + pos.h = background->pos.h; + } +} + +CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/spell-effects"), yOffset) +{ + 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); + + const CStack * battleStack = parent->info->stack; + + assert(battleStack); // Section should be created only for battles + + //spell effects + int printed=0; //how many effect pics have been printed + std::vector spells = battleStack->activeSpells(); + for(SpellID effect : spells) + { + const spells::Spell * spell = CGI->spells()->getById(effect); + + std::string spellText; + + //not all effects have graphics (for eg. Acid Breath) + //for modded spells iconEffect is added to SpellInt.def + const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST); + + if (hasGraphics) + { + spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." + boost::replace_first(spellText, "%s", spell->getNameTranslated()); + //FIXME: support permanent duration + int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; + boost::replace_first(spellText, "%d", std::to_string(duration)); + + spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); + if(++printed >= 8) // interface limit reached + break; + } + } +} + +CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) + : CWindowSection(owner, ImagePath::builtin("stackWindow/bonus-effects"), 0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + static const std::array offset = + { + Point(6, 4), + Point(214, 4) + }; + + for(size_t leftRight : {0, 1}) + { + auto position = offset[leftRight]; + size_t bonusIndex = lineIndex * 2 + leftRight; + + if(parent->activeBonuses.size() > bonusIndex) + { + 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); + } + } +} + +CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): + CWindowSection(owner, {}, yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + // size of single image for an item + static const int itemHeight = 59; + + size_t totalSize = (owner->activeBonuses.size() + 1) / 2; + size_t visibleSize = preferredSize.value_or(std::min(3, totalSize)); + + pos.w = owner->pos.w; + pos.h = itemHeight * (int)visibleSize; + + auto onCreate = [=](size_t index) -> std::shared_ptr + { + 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)); +} + +CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/button-panel"), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(parent->info->dismissInfo && parent->info->dismissInfo->callback) + { + auto onDismiss = [=]() + { + parent->info->dismissInfo->callback(); + parent->close(); + }; + auto onClick = [=] () + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr); + }; + dismiss = std::make_shared(Point(5, 5),AnimationPath::builtin("IVIEWCR2.DEF"), CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); + } + + if(parent->info->upgradeInfo && !parent->info->commander) + { + // used space overlaps with commander switch button + // besides - should commander really be upgradeable? + + auto & upgradeInfo = parent->info->upgradeInfo.value(); + const size_t buttonsToCreate = std::min(upgradeInfo.info.newID.size(), upgrade.size()); + + for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++) + { + TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount; + + auto onUpgrade = [=]() + { + upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]); + parent->close(); + }; + auto onClick = [=]() + { + std::vector> resComps; + for(TResources::nziterator i(totalCost); i.valid(); i++) + { + resComps.push_back(std::make_shared(ComponentType::RESOURCE, i->resType, i->resVal)); + } + + if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[207], onUpgrade, nullptr, resComps); + } + else + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps); + } + }; + auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); + + upgradeBtn->addOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); + + if(buttonsToCreate == 1) // single upgrade avaialbe + upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; + + upgrade[buttonIndex] = upgradeBtn; + } + } + + if(parent->info->commander) + { + for(size_t buttonIndex = 0; buttonIndex < 2; buttonIndex++) + { + std::string btnIDs[2] = { "showSkills", "showBonuses" }; + auto onSwitch = [buttonIndex, this]() + { + logAnim->debug("Switch %d->%d", parent->activeTab, buttonIndex); + + parent->switchButtons[parent->activeTab]->enable(); + parent->commanderTab->setActive(buttonIndex); + parent->switchButtons[buttonIndex]->disable(); + parent->redraw(); // FIXME: enable/disable don't redraw screen themselves + }; + + std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; + parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CButton::tooltipLocalized(tooltipText), onSwitch); + parent->switchButtons[buttonIndex]->addOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); + } + parent->switchButtons[parent->activeTab]->disable(); + } + + exit = std::make_shared(Point(382, 5), AnimationPath::builtin("hsbtns.def"), CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); +} + +CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/commander-bg"), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + auto getSkillPos = [](int index) + { + return Point(10 + 80 * (index%3), 20 + 80 * (index/3)); + }; + + auto getSkillImage = [this](int skillIndex) + { + bool selected = ((parent->selectedSkill == skillIndex) && parent->info->levelupInfo ); + return skillToFile(skillIndex, parent->info->commander->secondarySkills[skillIndex], selected); + }; + + auto getSkillDescription = [this](int skillIndex) -> std::string + { + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; + }; + + for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) + { + Point skillPos = getSkillPos(index); + + auto icon = std::make_shared(std::make_shared(getSkillImage(index), skillPos.x, skillPos.y), false, [=]() + { + LOCPLINT->showInfoDialog(getSkillDescription(index)); + }); + + icon->text = getSkillDescription(index); //used to handle right click description via LRClickableAreaWText::ClickRight() + + if(parent->selectedSkill == index) + parent->selectedIcon = icon; + + if(parent->info->levelupInfo && vstd::contains(parent->info->levelupInfo->skills, index)) // can be upgraded - enable selection switch + { + if(parent->selectedSkill == index) + parent->setSelection(index, icon); + + icon->callback = [=]() + { + parent->setSelection(index, icon); + }; + } + + skillIcons.push_back(icon); + } + + auto getArtifactPos = [](int index) + { + return Point(269 + 47 * (index % 3), 22 + 47 * (index / 3)); + }; + + 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); + artifacts.push_back(artPlace); + } + + if(parent->info->levelupInfo) + { + abilitiesBackground = std::make_shared(ImagePath::builtin("stackWindow/commander-abilities.png")); + abilitiesBackground->moveBy(Point(0, pos.h)); + + size_t abilitiesCount = boost::range::count_if(parent->info->levelupInfo->skills, [](ui32 skillID) + { + return skillID >= 100; + }); + + auto onCreate = [=](size_t index)->std::shared_ptr + { + for(auto skillID : parent->info->levelupInfo->skills) + { + if(index == 0 && skillID >= 100) + { + const auto bonus = CGI->creh->skillRequirements[skillID-100].first; + const CStackInstance * stack = parent->info->commander; + auto icon = std::make_shared(std::make_shared(stack->bonusToGraphics(bonus)), true, [](){}); + icon->callback = [=]() + { + parent->setSelection(skillID, icon); + }; + icon->text = stack->bonusToString(bonus, true); + icon->hoverText = stack->bonusToString(bonus, false); + + return icon; + } + if(skillID >= 100) + index--; + } + return nullptr; + }; + + abilities = std::make_shared(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount); + abilities->setRedrawParent(true); + + leftBtn = std::make_shared(Point(10, pos.h + 6), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); + rightBtn = std::make_shared(Point(411, pos.h + 6), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); + + if(abilitiesCount <= 6) + { + leftBtn->block(true); + rightBtn->block(true); + } + + pos.h += abilitiesBackground->pos.h; + } +} + +CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt) + : CWindowSection(owner, getBackgroundName(showExp, showArt), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + statNames = + { + CGI->generaltexth->primarySkillNames[0], //ATTACK + CGI->generaltexth->primarySkillNames[1],//DEFENCE + CGI->generaltexth->allTexts[198],//SHOTS + CGI->generaltexth->allTexts[199],//DAMAGE + + CGI->generaltexth->allTexts[388],//HEALTH + CGI->generaltexth->allTexts[200],//HEALTH_LEFT + CGI->generaltexth->zelp[441].first,//SPEED + CGI->generaltexth->allTexts[399]//MANA + }; + + statFormats = + { + "%d (%d)", + "%d (%d)", + "%d (%d)", + "%d - %d", + + "%d (%d)", + "%d (%d)", + "%d (%d)", + "%d (%d)" + }; + + animation = std::make_shared(5, 41, parent->info->creature); + + 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()); + + int dmgMultiply = 1; + if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) + dmgMultiply += parent->info->owner->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) )); + + if(battleStack != nullptr) // in battle + { + addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(battleStack->isShooter()), battleStack->getAttack(battleStack->isShooter())); + addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter())); + addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply); + addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth()); + addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed()); + + if(battleStack->isShooter()) + addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); + if(battleStack->isCaster()) + addStatLabel(EStat::MANA, battleStack->casts.total(), battleStack->casts.available()); + addStatLabel(EStat::HEALTH_LEFT, battleStack->getFirstHPleft()); + + morale->set(battleStack); + luck->set(battleStack); + } + else + { + const bool shooter = parent->info->stackNode->hasBonusOfType(BonusType::SHOOTER) && parent->info->stackNode->valOfBonuses(BonusType::SHOTS); + const bool caster = parent->info->stackNode->valOfBonuses(BonusType::CASTS); + + 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::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); + addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed()); + + if(shooter) + addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); + if(caster) + addStatLabel(EStat::MANA, parent->info->stackNode->valOfBonuses(BonusType::CASTS)); + + morale->set(parent->info->stackNode); + luck->set(parent->info->stackNode); + } + + if(showExp) + { + const CStackInstance * stack = parent->info->stackNode; + Point pos = showArt ? Point(321, 32) : Point(347, 32); + if(parent->info->commander) + { + const CCommanderInstance * commander = parent->info->commander; + expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); + + auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), ComponentType::EXPERIENCE); + expArea = area; + area->text = CGI->generaltexth->allTexts[2]; + area->component.value = commander->getExpRank(); + boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); + boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); + boost::replace_first(area->text, "%d", std::to_string(commander->experience)); + } + else + { + expRankIcon = std::make_shared(AnimationPath::builtin("stackWindow/levels"), stack->getExpRank(), 0, pos.x, pos.y); + expArea = std::make_shared(Rect(pos.x, pos.y, 44, 44)); + expArea->text = parent->generateStackExpDescription(); + } + expLabel = std::make_shared( + pos.x + 21, pos.y + 52, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, + TextOperations::formatMetric(stack->experience, 6)); + } + + if(showArt) + { + Point pos = showExp ? Point(375, 32) : Point(347, 32); + // ALARMA: do not refactor this into a separate function + // otherwise, artifact icon is drawn near the hero's portrait + // this is really strange + 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(); + + if(parent->info->owner) + { + parent->stackArtifactButton = std::make_shared( + Point(pos.x - 2 , pos.y + 46), AnimationPath::builtin("stackWindow/cancelButton"), + CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() + { + parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); + }); + } + } + } +} + + +ImagePath CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) +{ + if(showExp && showArt) + return ImagePath::builtin("stackWindow/info-panel-2"); + else if(showExp || showArt) + return ImagePath::builtin("stackWindow/info-panel-1"); + else + return ImagePath::builtin("stackWindow/info-panel-0"); +} + +void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value1, int64_t value2) +{ + const auto title = statNames.at(static_cast(index)); + stats.push_back(std::make_shared(145, 32 + (int)index*19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, title)); + + const bool useRange = value1 != value2; + std::string formatStr = useRange ? statFormats.at(static_cast(index)) : "%d"; + + boost::format fmt(formatStr); + fmt % value1; + if(useRange) + fmt % value2; + + stats.push_back(std::make_shared(307, 48 + (int)index*19, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, fmt.str())); +} + +void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value) +{ + addStatLabel(index, value, value); +} + +CStackWindow::CStackWindow(const CStack * stack, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stack = stack; + info->stackNode = stack->base; + info->creature = stack->unitType(); + info->creatureCount = stack->getCount(); + info->popupWindow = popup; + init(); +} + +CStackWindow::CStackWindow(const CCreature * creature, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->creature = creature; + info->popupWindow = popup; + init(); +} + +CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stackNode = stack; + info->creature = stack->type; + info->creatureCount = stack->count; + info->popupWindow = popup; + info->owner = dynamic_cast (stack->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & upgradeInfo, std::function callback) + : CWindowObject(BORDERED), + info(new UnitView()) +{ + info->stackNode = stack; + info->creature = stack->type; + info->creatureCount = stack->count; + + info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); + info->dismissInfo = std::make_optional(UnitView::StackDismissInfo()); + info->upgradeInfo->info = upgradeInfo; + info->upgradeInfo->callback = callback; + info->dismissInfo->callback = dismiss; + info->owner = dynamic_cast (stack->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CCommanderInstance * commander, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stackNode = commander; + info->creature = commander->type; + info->commander = commander; + info->creatureCount = 1; + info->popupWindow = popup; + info->owner = dynamic_cast (commander->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback) + : CWindowObject(BORDERED), + info(new UnitView()) +{ + info->stackNode = commander; + info->creature = commander->type; + info->commander = commander; + info->creatureCount = 1; + info->levelupInfo = std::make_optional(UnitView::CommanderLevelInfo()); + info->levelupInfo->skills = skills; + info->levelupInfo->callback = callback; + info->owner = dynamic_cast (commander->armyObj); + init(); +} + +CStackWindow::~CStackWindow() +{ + if(info->levelupInfo && !info->levelupInfo->skills.empty()) + info->levelupInfo->callback(vstd::find_pos(info->levelupInfo->skills, selectedSkill)); +} + +void CStackWindow::init() +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(!info->stackNode) + info->stackNode = new CStackInstance(info->creature, 1, true);// FIXME: free data + + selectedIcon = nullptr; + selectedSkill = -1; + if(info->levelupInfo && !info->levelupInfo->skills.empty()) + selectedSkill = info->levelupInfo->skills.front(); + + activeTab = 0; + + initBonusesList(); + initSections(); +} + +void CStackWindow::initBonusesList() +{ + BonusList output, input; + input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); + + while(!input.empty()) + { + auto b = input.front(); + output.push_back(std::make_shared(*b)); + output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one + input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses + } + + BonusInfo bonusInfo; + for(auto b : output) + { + bonusInfo.name = info->stackNode->bonusToString(b, false); + bonusInfo.description = info->stackNode->bonusToString(b, true); + bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); + + //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 + if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty()) + activeBonuses.push_back(bonusInfo); + } +} + +void CStackWindow::initSections() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + 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; + + mainSection = std::make_shared(this, pos.h, showExp, showArt); + + pos.w = mainSection->pos.w; + pos.h += mainSection->pos.h; + + if(info->stack) // in battle + { + activeSpellsSection = std::make_shared(this, pos.h); + pos.h += activeSpellsSection->pos.h; + } + + if(info->commander) + { + auto onCreate = [=](size_t index) -> std::shared_ptr + { + auto obj = switchTab(index); + + if(obj) + { + obj->activate(); + obj->recActions |= (UPDATE | SHOWALL); + } + return obj; + }; + + auto deactivateObj = [=](std::shared_ptr obj) + { + obj->deactivate(); + obj->recActions &= ~(UPDATE | SHOWALL); + }; + + commanderMainSection = std::make_shared(this, 0); + + auto size = std::make_optional((info->levelupInfo) ? 4 : 3); + commanderBonusesSection = std::make_shared(this, 0, size); + deactivateObj(commanderBonusesSection); + + commanderTab = std::make_shared(onCreate, Point(0, pos.h), 0); + + pos.h += commanderMainSection->pos.h; + } + + if(!info->commander && !activeBonuses.empty()) + { + bonusesSection = std::make_shared(this, pos.h); + pos.h += bonusesSection->pos.h; + } + + if(!info->popupWindow) + { + buttonsSection = std::make_shared(this, pos.h); + pos.h += buttonsSection->pos.h; + //FIXME: add status bar to image? + } + updateShadow(); + pos = center(pos); +} + +std::string CStackWindow::generateStackExpDescription() +{ + const CStackInstance * stack = info->stackNode; + const CCreature * creature = info->creature; + + int tier = stack->type->getLevel(); + int rank = stack->getExpRank(); + if (!vstd::iswithin(tier, 1, 7)) + tier = 0; + int number; + std::string expText = CGI->generaltexth->translate("vcmi.stackExperience.description"); + boost::replace_first(expText, "%s", creature->getNamePluralTranslated()); + boost::replace_first(expText, "%s", CGI->generaltexth->translate("vcmi.stackExperience.rank", rank)); + boost::replace_first(expText, "%i", std::to_string(rank)); + boost::replace_first(expText, "%i", std::to_string(stack->experience)); + number = static_cast(CGI->creh->expRanks[tier][rank] - stack->experience); + boost::replace_first(expText, "%i", std::to_string(number)); + + number = CGI->creh->maxExpPerBattle[tier]; //percent + boost::replace_first(expText, "%i%", std::to_string(number)); + number *= CGI->creh->expRanks[tier].back() / 100; //actual amount + boost::replace_first(expText, "%i", std::to_string(number)); + + boost::replace_first(expText, "%i", std::to_string(stack->count)); //Number of Creatures in stack + + int expmin = std::max(CGI->creh->expRanks[tier][std::max(rank-1, 0)], (ui32)1); + number = static_cast((stack->count * (stack->experience - expmin)) / expmin); //Maximum New Recruits without losing current Rank + boost::replace_first(expText, "%i", std::to_string(number)); //TODO + + boost::replace_first(expText, "%.2f", std::to_string(1)); //TODO Experience Multiplier + number = CGI->creh->expAfterUpgrade; + boost::replace_first(expText, "%.2f", std::to_string(number) + "%"); //Upgrade Multiplier + + expmin = CGI->creh->expRanks[tier][9]; + int expmax = CGI->creh->expRanks[tier][10]; + number = expmax - expmin; + boost::replace_first(expText, "%i", std::to_string(number)); //Experience after Rank 10 + number = (stack->count * (expmax - expmin)) / expmin; + boost::replace_first(expText, "%i", std::to_string(number)); //Maximum New Recruits to remain at Rank 10 if at Maximum Experience + + return expText; +} + +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 + else + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; + }; + + auto getSkillImage = [this](int skillIndex) + { + bool selected = ((selectedSkill == skillIndex) && info->levelupInfo ); + return skillToFile(skillIndex, info->commander->secondarySkills[skillIndex], selected); + }; + + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + int oldSelection = selectedSkill; // update selection + selectedSkill = newSkill; + + if(selectedIcon && oldSelection < 100) // recreate image on old selection, only for skills + selectedIcon->setObject(std::make_shared(getSkillImage(oldSelection))); + + if(selectedIcon) + { + if(!selectedIcon->getIsMasterAbility()) //unlike WoG, in VCMI master skill descriptions are taken from bonus descriptions + { + selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level + } + selectedIcon->deselect(); + } + + selectedIcon = newIcon; // update new selection + if(newSkill < 100) + { + newIcon->setObject(std::make_shared(getSkillImage(newSkill))); + if(!newIcon->getIsMasterAbility()) + { + newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description + } + } +} + +std::shared_ptr CStackWindow::switchTab(size_t index) +{ + std::shared_ptr ret; + switch(index) + { + case 0: + { + activeTab = 0; + ret = commanderMainSection; + } + break; + case 1: + { + activeTab = 1; + ret = commanderBonusesSection; + } + break; + default: + break; + } + + return ret; +} + +void CStackWindow::removeStackArtifact(ArtifactPosition pos) +{ + auto art = info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); + if(!art) + { + logGlobal->error("Attempt to remove missing artifact"); + return; + } + const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); + if(slot != ArtifactPosition::PRE_FIRST) + { + auto artLoc = ArtifactLocation(info->owner->id, pos); + artLoc.creature = info->stackNode->armyObj->findStack(info->stackNode); + LOCPLINT->cb->swapArtifacts(artLoc, ArtifactLocation(info->owner->id, slot)); + stackArtifactButton.reset(); + stackArtifactHelp.reset(); + stackArtifactIcon.reset(); + redraw(); + } +} diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index 4b904cb22..b20a1ef39 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -1,205 +1,206 @@ -/* - * CCreatureWindow.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/bonuses/Bonus.h" -#include "../widgets/MiscWidgets.h" -#include "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCommanderInstance; -class CStackInstance; -class CStack; -struct UpgradeInfo; - -VCMI_LIB_NAMESPACE_END - -class UnitView; -class CTabbedInt; -class CButton; -class CMultiLineLabel; -class CListBox; -class CCommanderArtPlace; - -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" -public: - CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback); - - std::function callback; - - void clickPressed(const Point & cursorPosition) override; - - void setObject(std::shared_ptr object); - void deselect(); //TODO: consider using observer pattern instead? - bool getIsMasterAbility(); - - void show(Canvas &to) override; -}; - -class CStackWindow : public CWindowObject -{ - struct BonusInfo - { - std::string name; - std::string description; - std::string imagePath; - }; - - class CWindowSection : public CIntObject - { - private: - std::shared_ptr background; - protected: - CStackWindow * parent; - public: - CWindowSection(CStackWindow * parent, std::string backgroundPath, int yOffset); - }; - - class ActiveSpellsSection : public CWindowSection - { - std::vector> spellIcons; - std::vector> clickableAreas; - public: - ActiveSpellsSection(CStackWindow * owner, int yOffset); - }; - - class BonusLineSection : public CWindowSection - { - std::array, 2> icon; - std::array, 2> name; - std::array, 2> description; - public: - BonusLineSection(CStackWindow * owner, size_t lineIndex); - }; - - class BonusesSection : public CWindowSection - { - std::shared_ptr lines; - public: - BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize = std::optional()); - }; - - class ButtonsSection : public CWindowSection - { - std::shared_ptr dismiss; - std::array, 3> upgrade;// no more than 3 buttons - space limit - std::shared_ptr exit; - public: - ButtonsSection(CStackWindow * owner, int yOffset); - }; - - class CommanderMainSection : public CWindowSection - { - std::vector> skillIcons; - std::vector> artifacts; - - std::shared_ptr abilitiesBackground; - std::shared_ptr abilities; - - std::shared_ptr leftBtn; - std::shared_ptr rightBtn; - public: - CommanderMainSection(CStackWindow * owner, int yOffset); - }; - - class MainSection : public CWindowSection - { - enum class EStat : size_t - { - ATTACK, - DEFENCE, - SHOTS, - DAMAGE, - HEALTH, - HEALTH_LEFT, - SPEED, - MANA, - AFTER_LAST - }; - - std::shared_ptr animation; - std::shared_ptr name; - std::shared_ptr icons; - std::shared_ptr morale; - std::shared_ptr luck; - - std::vector> stats; - - std::shared_ptr expRankIcon; - std::shared_ptr expArea; - std::shared_ptr expLabel; - - void addStatLabel(EStat index, int64_t value1, int64_t value2); - void addStatLabel(EStat index, int64_t value); - - static std::string getBackgroundName(bool showExp, bool showArt); - - std::array statNames; - std::array statFormats; - public: - MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt); - }; - - std::shared_ptr stackArtifactIcon; - std::shared_ptr stackArtifactHelp; - std::shared_ptr stackArtifactButton; - - - std::shared_ptr info; - std::vector activeBonuses; - size_t activeTab; - std::shared_ptr commanderTab; - - std::map> switchButtons; - - std::shared_ptr mainSection; - std::shared_ptr activeSpellsSection; - std::shared_ptr commanderMainSection; - std::shared_ptr commanderBonusesSection; - std::shared_ptr bonusesSection; - std::shared_ptr buttonsSection; - - std::shared_ptr selectedIcon; - si32 selectedSkill; - - void setSelection(si32 newSkill, std::shared_ptr newIcon); - std::shared_ptr switchTab(size_t index); - - void removeStackArtifact(ArtifactPosition pos); - - void initSections(); - void initBonusesList(); - - void init(); - - std::string generateStackExpDescription(); - -public: - // for battles - CStackWindow(const CStack * stack, bool popup); - - // for non-existing stacks, e.g. recruit screen - CStackWindow(const CCreature * creature, bool popup); - - // for normal stacks in armies - CStackWindow(const CStackInstance * stack, bool popup); - CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & info, std::function callback); - - // for commanders & commander level-up dialog - CStackWindow(const CCommanderInstance * commander, bool popup); - CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback); - - ~CStackWindow(); -}; +/* + * CCreatureWindow.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/bonuses/Bonus.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../widgets/MiscWidgets.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCommanderInstance; +class CStackInstance; +class CStack; +struct UpgradeInfo; + +VCMI_LIB_NAMESPACE_END + +class UnitView; +class CTabbedInt; +class CButton; +class CMultiLineLabel; +class CListBox; +class CCommanderArtPlace; + +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" +public: + CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback); + + std::function callback; + + void clickPressed(const Point & cursorPosition) override; + + void setObject(std::shared_ptr object); + void deselect(); //TODO: consider using observer pattern instead? + bool getIsMasterAbility(); + + void show(Canvas &to) override; +}; + +class CStackWindow : public CWindowObject +{ + struct BonusInfo + { + std::string name; + std::string description; + ImagePath imagePath; + }; + + class CWindowSection : public CIntObject + { + private: + std::shared_ptr background; + protected: + CStackWindow * parent; + public: + CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset); + }; + + class ActiveSpellsSection : public CWindowSection + { + std::vector> spellIcons; + std::vector> clickableAreas; + public: + ActiveSpellsSection(CStackWindow * owner, int yOffset); + }; + + class BonusLineSection : public CWindowSection + { + std::array, 2> icon; + std::array, 2> name; + std::array, 2> description; + public: + BonusLineSection(CStackWindow * owner, size_t lineIndex); + }; + + class BonusesSection : public CWindowSection + { + std::shared_ptr lines; + public: + BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize = std::optional()); + }; + + class ButtonsSection : public CWindowSection + { + std::shared_ptr dismiss; + std::array, 3> upgrade;// no more than 3 buttons - space limit + std::shared_ptr exit; + public: + ButtonsSection(CStackWindow * owner, int yOffset); + }; + + class CommanderMainSection : public CWindowSection + { + std::vector> skillIcons; + std::vector> artifacts; + + std::shared_ptr abilitiesBackground; + std::shared_ptr abilities; + + std::shared_ptr leftBtn; + std::shared_ptr rightBtn; + public: + CommanderMainSection(CStackWindow * owner, int yOffset); + }; + + class MainSection : public CWindowSection + { + enum class EStat : size_t + { + ATTACK, + DEFENCE, + SHOTS, + DAMAGE, + HEALTH, + HEALTH_LEFT, + SPEED, + MANA, + AFTER_LAST + }; + + std::shared_ptr animation; + std::shared_ptr name; + std::shared_ptr icons; + std::shared_ptr morale; + std::shared_ptr luck; + + std::vector> stats; + + std::shared_ptr expRankIcon; + std::shared_ptr expArea; + std::shared_ptr expLabel; + + void addStatLabel(EStat index, int64_t value1, int64_t value2); + void addStatLabel(EStat index, int64_t value); + + static ImagePath getBackgroundName(bool showExp, bool showArt); + + std::array statNames; + std::array statFormats; + public: + MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt); + }; + + std::shared_ptr stackArtifactIcon; + std::shared_ptr stackArtifactHelp; + std::shared_ptr stackArtifactButton; + + + std::shared_ptr info; + std::vector activeBonuses; + size_t activeTab; + std::shared_ptr commanderTab; + + std::map> switchButtons; + + std::shared_ptr mainSection; + std::shared_ptr activeSpellsSection; + std::shared_ptr commanderMainSection; + std::shared_ptr commanderBonusesSection; + std::shared_ptr bonusesSection; + std::shared_ptr buttonsSection; + + std::shared_ptr selectedIcon; + si32 selectedSkill; + + void setSelection(si32 newSkill, std::shared_ptr newIcon); + std::shared_ptr switchTab(size_t index); + + void removeStackArtifact(ArtifactPosition pos); + + void initSections(); + void initBonusesList(); + + void init(); + + std::string generateStackExpDescription(); + +public: + // for battles + CStackWindow(const CStack * stack, bool popup); + + // for non-existing stacks, e.g. recruit screen + CStackWindow(const CCreature * creature, bool popup); + + // for normal stacks in armies + CStackWindow(const CStackInstance * stack, bool popup); + CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & info, std::function callback); + + // for commanders & commander level-up dialog + CStackWindow(const CCommanderInstance * commander, bool popup); + CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback); + + ~CStackWindow(); +}; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index 3455b2b0b..a4ff6d1c8 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -24,15 +24,15 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - stretchedBackground = std::make_shared("DIBOXBCK", Rect()); + stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 410, 425)); arts = std::make_shared(Point(windowMargin, windowMargin)); arts->setHero(hero); - addSet(arts); + addSetAndCallbacks(arts); addCloseCallback(std::bind(&CHeroBackpackWindow::close, this)); - quitButton = std::make_shared(Point(), "IOKAY32.def", CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN); + quitButton = std::make_shared(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN); stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin; stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp new file mode 100644 index 000000000..00b522cc2 --- /dev/null +++ b/client/windows/CHeroOverview.cpp @@ -0,0 +1,237 @@ +/* + * CHeroOverview.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 "CHeroOverview.h" + +#include "../CGameInfo.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/Images.h" +#include "../widgets/TextControls.h" +#include "../widgets/MiscWidgets.h" + +#include "../../lib/GameSettings.h" +#include "../../lib/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; + + heroIdx = hero.getNum(); + + pos = Rect(0, 0, 600, 485); + + genBackground(); + genControls(); + + center(); +} + +void CHeroOverview::genBackground() +{ + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); + updateShadow(); +} + +void CHeroOverview::genControls() +{ + Rect r = Rect(); + + labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); + + // hero image + r = Rect(borderOffset, borderOffset + yOffset, 58, 64); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageHero = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIdx]->imageIndex, 0, r.x, r.y); + + // hero name + r = Rect(64 + borderOffset, borderOffset + yOffset, 220, 64); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroName = std::make_shared(r.x + 110, r.y + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIdx]->getNameTranslated()); + labelHeroClass = std::make_shared(r.x + 110, r.y + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIdx]->heroClass->getNameTranslated()); + + // vertical line + backgroundLines.push_back(std::make_shared(Point(295, borderOffset + yOffset - 1), Point(295, borderOffset + yOffset - 2 + 439), borderColor)); + + // skills header + r = Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + for(int i = 0; i < 4; i++) + labelSkillHeader.push_back(std::make_shared((r.w / 4) * i + 42, r.y + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); + + // skill + const int tmp[] = {0, 1, 2, 5}; + for(int i = 0; i < 4; i++) + { + r = Rect((284 / 4) * i + 21, 3 * borderOffset + yOffset + 85, 42, 42); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), tmp[i], 0, r.x, r.y)); + } + + // skills footer + r = Rect(borderOffset, 4 * borderOffset + yOffset + 128, 284, 20); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + for(int i = 0; i < 4; i++) + { + r = Rect((284 / 4) * i + 42, r.y, r.w, r.h); + labelSkillFooter.push_back(std::make_shared(r.x, r.y + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIdx]->heroClass->primarySkillInitial[i]))); + } + + // hero biography + r = Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroBiography = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getBiographyTranslated()); + + // speciality name + r = Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroSpeciality = std::make_shared(r.x + borderOffset, r.y + borderOffset, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + labelSpecialityName = std::make_shared(r.x + borderOffset, r.y + borderOffset + 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyNameTranslated()); + + // speciality image + r = Rect(borderOffset, 6 * borderOffset + yOffset + 278, 44, 44); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIdx]->imageIndex, 0, r.x, r.y); + + // speciality description + r = Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSpecialityDescription = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated()); + + // army title + r = Rect(302, borderOffset + yOffset, 292, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelArmyTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); + + // army numbers + 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); + + // army + int space = (260 - 7 * 32) / 6; + for(int i = 0; i < 7; i++) + { + r = Rect(318 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } + int i = 0; + int iStack = 0; + for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) + { + if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) + { + imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30)); + labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 72, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); + if(iStack(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 86, FONT_SMALL, ETextAlignment::CENTER, grayedColor, std::to_string(stacksCountChances[iStack]) + "%")); + i++; + } + iStack++; + } + + // war machine title + r = Rect(302, 4 * borderOffset + yOffset + 94, 292, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelWarMachineTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); + + // war machine + space = (260 - 4 * 32) / 3; + for(int i = 0; i < 4; i++) + { + r = Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 124, 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } + i = 0; + iStack = 0; + for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) + { + if(i == 0) + { + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 124)); + labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 51, 5 * borderOffset + yOffset + 144, FONT_SMALL, ETextAlignment::TOPLEFT, grayedColor, "100%")); + i++; + } + if((*CGI->creh)[army.creature]->warMachine != ArtifactID::NONE) + { + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 124)); + if(iStack(302 + i * (32 + space) + 51, 5 * borderOffset + yOffset + 144, FONT_SMALL, ETextAlignment::TOPLEFT, grayedColor, std::to_string(stacksCountChances[iStack]) + "%")); + i++; + } + iStack++; + } + + // secskill title + r = Rect(302, 6 * borderOffset + yOffset + 156, (292 / 2) - 2 * borderOffset, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSecSkillTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); + + // vertical line + backgroundLines.push_back(std::make_shared(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 156 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 156 - 2 + 254), borderColor)); + + // spell title + r = Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 156, (292 / 2) - 2 * borderOffset, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSpellTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); + + // secskill + for(int i = 0; i < 6; i++) + { + r = Rect(302, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } + 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))); + 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++; + } + + // spell + for(int i = 0; i < 6; i++) + { + r = Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } + i = 0; + for(auto & spell : (*CGI->heroh)[heroIdx]->spells) + { + if(i == 0) + { + 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)); + } + 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)); + 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++; + } +} \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h new file mode 100644 index 000000000..64c9b16e9 --- /dev/null +++ b/client/windows/CHeroOverview.h @@ -0,0 +1,71 @@ +/* + * CHeroOverview.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" + +class CLabel; +class CMultiLineLabel; +class CFilledTexture; +class CAnimImage; +class CComponentBox; +class CTextBox; +class TransparentFilledRectangle; +class SimpleLine; + +class CHeroOverview : public CWindowObject +{ + const HeroTypeID & hero; + int heroIdx; + + const int yOffset = 35; + const int borderOffset = 5; + const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); + const ColorRGBA borderColor = ColorRGBA(128, 100, 75); + const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); + + std::shared_ptr backgroundTexture; + std::vector> backgroundRectangles; + std::vector> backgroundLines; + + std::shared_ptr labelTitle; + std::shared_ptr imageHero; + std::shared_ptr labelHeroName; + std::shared_ptr labelHeroBiography; + std::shared_ptr labelHeroClass; + std::shared_ptr labelHeroSpeciality; + std::shared_ptr imageSpeciality; + std::vector> labelSkillHeader; + std::vector> imageSkill; + std::vector> labelSkillFooter; + std::shared_ptr labelSpecialityName; + std::shared_ptr labelSpecialityDescription; + + std::shared_ptr labelArmyTitle; + std::vector> imageArmy; + std::vector> labelArmyCount; + + std::shared_ptr labelWarMachineTitle; + std::vector> imageWarMachine; + + std::shared_ptr labelSpellTitle; + std::vector> imageSpells; + std::vector> labelSpellsNames; + + std::shared_ptr labelSecSkillTitle; + std::vector> imageSecSkills; + std::vector> labelSecSkillsNames; + + void genBackground(); + void genControls(); + +public: + CHeroOverview(const HeroTypeID & h); +}; \ No newline at end of file diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 55f26c829..9d5d66636 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -1,362 +1,360 @@ -/* - * CHeroWindow.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 "CHeroWindow.h" - -#include "CCreatureWindow.h" -#include "CHeroBackpackWindow.h" -#include "CKingdomInterface.h" -#include "GUIClasses.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/TextAlignment.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/TextControls.h" -#include "../widgets/Buttons.h" -#include "../render/CAnimation.h" - -#include "../../CCallback.h" - -#include "../lib/ArtifactUtils.h" -#include "../lib/CArtHandler.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CSkillHandler.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/NetPacksBase.h" - -void CHeroSwitcher::clickPressed(const Point & cursorPosition) -{ - //TODO: do not recreate window - if (false) - { - owner->update(hero, true); - } - else - { - const CGHeroInstance * buf = hero; - GH.windows().popWindows(1); - GH.windows().createAndPushWindow(buf); - } -} - -CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_) - : CIntObject(LCLICK), - owner(owner_), - hero(hero_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos += pos_; - - image = std::make_shared("PortraitsSmall", hero->portrait); - pos.w = image->pos.w; - pos.h = image->pos.h; -} - -CHeroWindow::CHeroWindow(const CGHeroInstance * hero) - : CStatusbarWindow(PLAYER_COLORED, "HeroScr4") -{ - auto & heroscrn = CGI->generaltexth->heroscrn; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - curHero = hero; - - banner = std::make_shared("CREST58", LOCPLINT->playerID.getNum(), 0, 606, 8); - name = std::make_shared(190, 38, EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); - title = std::make_shared(190, 65, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - - statusbar = CGStatusBar::create(7, 559, "ADROLLVR.bmp", 660); - - quitButton = std::make_shared(Point(609, 516), "hsbtns.def", CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); - - if(settings["general"]["enableUiEnhancements"].Bool()) - { - questlogButton = std::make_shared(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - backpackButton = std::make_shared(Point(424, 429), "buttons/backpack", CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); - backpackButton->addOverlay(std::make_shared("buttons/backpackButtonIcon")); - dismissButton = std::make_shared(Point(534, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); - } - else - { - dismissLabel = std::make_shared(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - questlogLabel = std::make_shared(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - dismissButton = std::make_shared(Point(454, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); - questlogButton = std::make_shared(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - } - - formations = std::make_shared(0); - formations->addToggle(0, std::make_shared(Point(481, 483), "hsbtns6.def", std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); - formations->addToggle(1, std::make_shared(Point(481, 519), "hsbtns7.def", std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); - - if(hero->commander) - { - commanderButton = std::make_shared(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); - } - - //right list of heroes - for(int i=0; i < std::min(LOCPLINT->cb->howManyHeroes(false), 8); i++) - heroList.push_back(std::make_shared(this, Point(612, 87 + i * 54), LOCPLINT->cb->getHeroBySerial(i, false))); - - //areas - portraitArea = std::make_shared(Rect(18, 18, 58, 64)); - portraitImage = std::make_shared("PortraitsLarge", 0, 0, 19, 19); - - for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) - { - auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill); - area->text = CGI->generaltexth->arraytxt[2+v]; - area->type = v; - area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); - primSkillAreas.push_back(area); - - auto value = std::make_shared(53 + 70 * v, 166, FONT_SMALL, ETextAlignment::CENTER); - primSkillValues.push_back(value); - } - - auto primSkills = std::make_shared("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)); - - specImage = std::make_shared("UN44", 0, 0, 18, 180); - specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); - specName = std::make_shared(69, 205); - - expArea = std::make_shared(Rect(18, 228, 136, 42), CGI->generaltexth->heroscrn[9]); - morale = std::make_shared(true, Rect(175, 179, 53, 45)); - luck = std::make_shared(false, Rect(233, 179, 53, 45)); - spellPointsArea = std::make_shared(Rect(162,228, 136, 42), CGI->generaltexth->heroscrn[22]); - - expValue = std::make_shared(68, 252); - manaValue = std::make_shared(211, 252); - - auto secSkills = std::make_shared("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, CComponent::secskill)); - secSkillImages.push_back(std::make_shared(secSkills, 0, 0, r.x, r.y)); - - int x = (i % 2) ? 212 : 68; - int y = 280 + 48 * (i/2); - - secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT)); - secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT)); - } - - // various texts - labels.push_back(std::make_shared(52, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[1])); - labels.push_back(std::make_shared(123, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[2])); - labels.push_back(std::make_shared(193, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[3])); - labels.push_back(std::make_shared(262, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[4])); - - labels.push_back(std::make_shared(69, 183, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[5])); - 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); -} - -void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) -{ - 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); - - name->setText(curHero->getNameTranslated()); - title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str()); - - specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); - specImage->setFrame(curHero->type->imageIndex); - specName->setText(curHero->type->getSpecialtyNameTranslated()); - - tacticsButton = std::make_shared(Point(539, 483), "hsbtns8.def", std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); - tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); - - dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); - portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); - portraitArea->text = curHero->getBiographyTranslated(); - portraitImage->setFrame(curHero->portrait); - - { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - if(!garr) - { - 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), "hsbtns9.def", CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }); - garr->addSplitBtn(split); - } - if(!arts) - { - arts = std::make_shared(Point(-65, -8)); - arts->setHero(curHero); - addSet(arts); - } - - int serial = LOCPLINT->cb->getHeroSerial(curHero, false); - - listSelection.reset(); - if(serial >= 0) - listSelection = std::make_shared("HPSYYY", 612, 33 + serial * 54); - } - - //primary skills support - for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); - primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); - } - - //secondary skills support - for(size_t g=0; g< secSkillAreas.size(); ++g) - { - int skill = curHero->secSkills[g].first; - int level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first)); - std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); - std::string skillValue = CGI->generaltexth->levels[level-1]; - - secSkillAreas[g]->type = skill; - secSkillAreas[g]->bonusValue = 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); - } - - std::ostringstream expstr; - expstr << curHero->exp; - expValue->setText(expstr.str()); - - std::ostringstream manastr; - manastr << curHero->mana << '/' << curHero->manaLimit(); - manaValue->setText(manastr.str()); - - //printing experience - original format does not support ui64 - expArea->text = CGI->generaltexth->allTexts[2]; - boost::replace_first(expArea->text, "%d", std::to_string(curHero->level)); - boost::replace_first(expArea->text, "%d", std::to_string(CGI->heroh->reqExp(curHero->level+1))); - boost::replace_first(expArea->text, "%d", std::to_string(curHero->exp)); - - //printing spell points, boost::format can't be used due to locale issues - spellPointsArea->text = CGI->generaltexth->allTexts[205]; - boost::replace_first(spellPointsArea->text, "%s", curHero->getNameTranslated()); - boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->mana)); - boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->manaLimit())); - - //if we have exchange window with this curHero open - bool noDismiss=false; - - for(auto cew : GH.windows().findWindows()) - { - for(int g=0; g < cew->heroInst.size(); ++g) - if(cew->heroInst[g] == curHero) - noDismiss = true; - } - - //if player only have one hero and no towns - if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1) - noDismiss = true; - - if(curHero->isMissionCritical()) - noDismiss = true; - - dismissButton->block(noDismiss); - - if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0) - { - tacticsButton->block(true); - } - else - { - tacticsButton->block(false); - tacticsButton->addCallback([&](bool on){curHero->tacticFormationEnabled = on;}); - } - - formations->resetCallback(); - //setting formations - formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); - formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, value);}); - - morale->set(curHero); - luck->set(curHero); - - if(redrawNeeded) - redraw(); -} - -void CHeroWindow::dismissCurrent() -{ - CFunctionList ony = [=](){ close(); }; - ony += [=](){ LOCPLINT->cb->dismissHero(curHero); }; - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); -} - -void CHeroWindow::createBackpackWindow() -{ - GH.windows().createAndPushWindow(curHero); -} - -void CHeroWindow::commanderWindow() -{ - const auto pickedArtInst = getPickedArtifact(); - const auto hero = getHeroPickedArtifact(); - - if(pickedArtInst) - { - const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); - if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack! - { - ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(curHero->commander.get(), freeSlot); - - if(pickedArtInst->canBePutAt(dst, true)) - { //equip clicked stack - if(dst.getArt()) - { - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero, - ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); - } - LOCPLINT->cb->swapArtifacts(src, dst); - } - } - } - else - { - GH.windows().createAndPushWindow(curHero->commander, false); - } -} - -void CHeroWindow::updateGarrisons() -{ - garr->recreateSlots(); - morale->set(curHero); -} +/* + * CHeroWindow.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 "CHeroWindow.h" + +#include "CCreatureWindow.h" +#include "CHeroBackpackWindow.h" +#include "CKingdomInterface.h" +#include "GUIClasses.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" + +#include "../lib/ArtifactUtils.h" +#include "../lib/CArtHandler.h" +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CSkillHandler.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/ArtifactLocation.h" + +void CHeroSwitcher::clickPressed(const Point & cursorPosition) +{ + //TODO: do not recreate window + if (false) + { + owner->update(hero, true); + } + else + { + const CGHeroInstance * buf = hero; + GH.windows().popWindows(1); + GH.windows().createAndPushWindow(buf); + } +} + +CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_) + : CIntObject(LCLICK), + owner(owner_), + hero(hero_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos += pos_; + + image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex()); + pos.w = image->pos.w; + pos.h = image->pos.h; +} + +CHeroWindow::CHeroWindow(const CGHeroInstance * hero) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("HeroScr4")) +{ + auto & heroscrn = CGI->generaltexth->heroscrn; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + curHero = hero; + + banner = std::make_shared(AnimationPath::builtin("CREST58"), LOCPLINT->playerID.getNum(), 0, 606, 8); + name = std::make_shared(190, 38, EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); + title = std::make_shared(190, 65, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + + statusbar = CGStatusBar::create(7, 559, ImagePath::builtin("ADROLLVR.bmp"), 660); + + quitButton = std::make_shared(Point(609, 516), AnimationPath::builtin("hsbtns.def"), CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); + + if(settings["general"]["enableUiEnhancements"].Bool()) + { + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); + backpackButton->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + dismissButton = std::make_shared(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + } + else + { + dismissLabel = std::make_shared(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + questlogLabel = std::make_shared(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + dismissButton = std::make_shared(Point(454, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + } + + formations = std::make_shared(0); + formations->addToggle(0, std::make_shared(Point(481, 483), AnimationPath::builtin("hsbtns6.def"), std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); + formations->addToggle(1, std::make_shared(Point(481, 519), AnimationPath::builtin("hsbtns7.def"), std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); + + if(hero->commander) + { + commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("buttons/commander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); + } + + //right list of heroes + for(int i=0; i < std::min(LOCPLINT->cb->howManyHeroes(false), 8); i++) + heroList.push_back(std::make_shared(this, Point(612, 87 + i * 54), LOCPLINT->cb->getHeroBySerial(i, false))); + + //areas + portraitArea = std::make_shared(Rect(18, 18, 58, 64)); + portraitImage = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 19, 19); + + for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) + { + auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), ComponentType::PRIM_SKILL); + area->text = CGI->generaltexth->arraytxt[2+v]; + area->component.subType = PrimarySkill(v); + area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); + primSkillAreas.push_back(area); + + auto value = std::make_shared(53 + 70 * v, 166, FONT_SMALL, ETextAlignment::CENTER); + 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)); + + specImage = std::make_shared(AnimationPath::builtin("UN44"), 0, 0, 18, 180); + specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); + specName = std::make_shared(69, 205); + + expArea = std::make_shared(Rect(18, 228, 136, 42), CGI->generaltexth->heroscrn[9]); + morale = std::make_shared(true, Rect(175, 179, 53, 45)); + luck = std::make_shared(false, Rect(233, 179, 53, 45)); + spellPointsArea = std::make_shared(Rect(162,228, 136, 42), CGI->generaltexth->heroscrn[22]); + + 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)); + + int x = (i % 2) ? 212 : 68; + int y = 280 + 48 * (i/2); + + secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT)); + secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT)); + } + + // various texts + labels.push_back(std::make_shared(52, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[1])); + labels.push_back(std::make_shared(123, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[2])); + labels.push_back(std::make_shared(193, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[3])); + labels.push_back(std::make_shared(262, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[4])); + + labels.push_back(std::make_shared(69, 183, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[5])); + 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); +} + +void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) +{ + 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); + + name->setText(curHero->getNameTranslated()); + title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str()); + + specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); + specImage->setFrame(curHero->type->imageIndex); + specName->setText(curHero->type->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(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); + + dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); + portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); + portraitArea->text = curHero->getBiographyTranslated(); + portraitImage->setFrame(curHero->getIconIndex()); + + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + if(!garr) + { + 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(); }); + garr->addSplitBtn(split); + } + if(!arts) + { + arts = std::make_shared(Point(-65, -8)); + arts->setHero(curHero); + addSetAndCallbacks(arts); + } + + int serial = LOCPLINT->cb->getHeroSerial(curHero, false); + + listSelection.reset(); + if(serial >= 0) + listSelection = std::make_shared(ImagePath::builtin("HPSYYY"), 612, 33 + serial * 54); + } + + //primary skills support + for(size_t g=0; ggetPrimSkillLevel(static_cast(g)); + primSkillAreas[g]->component.value = value; + primSkillValues[g]->setText(std::to_string(value)); + } + + //secondary skills support + for(size_t g=0; g< secSkillAreas.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); + } + + std::ostringstream expstr; + expstr << curHero->exp; + expValue->setText(expstr.str()); + + std::ostringstream manastr; + manastr << curHero->mana << '/' << curHero->manaLimit(); + manaValue->setText(manastr.str()); + + //printing experience - original format does not support ui64 + expArea->text = CGI->generaltexth->allTexts[2]; + boost::replace_first(expArea->text, "%d", std::to_string(curHero->level)); + boost::replace_first(expArea->text, "%d", std::to_string(CGI->heroh->reqExp(curHero->level+1))); + boost::replace_first(expArea->text, "%d", std::to_string(curHero->exp)); + + //printing spell points, boost::format can't be used due to locale issues + spellPointsArea->text = CGI->generaltexth->allTexts[205]; + boost::replace_first(spellPointsArea->text, "%s", curHero->getNameTranslated()); + boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->mana)); + boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->manaLimit())); + + //if we have exchange window with this curHero open + bool noDismiss=false; + + for(auto cew : GH.windows().findWindows()) + { + for(int g=0; g < cew->heroInst.size(); ++g) + if(cew->heroInst[g] == curHero) + noDismiss = true; + } + + //if player only have one hero and no towns + if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1) + noDismiss = true; + + if(curHero->isMissionCritical()) + noDismiss = true; + + dismissButton->block(noDismiss); + + if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0) + { + tacticsButton->block(true); + } + else + { + tacticsButton->block(false); + tacticsButton->addCallback([&](bool on){curHero->tacticFormationEnabled = on;}); + } + + formations->resetCallback(); + //setting formations + formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); + formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, static_cast(value));}); + + morale->set(curHero); + luck->set(curHero); + + if(redrawNeeded) + redraw(); +} + +void CHeroWindow::dismissCurrent() +{ + CFunctionList ony = [=](){ close(); }; + ony += [=](){ LOCPLINT->cb->dismissHero(curHero); }; + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); +} + +void CHeroWindow::createBackpackWindow() +{ + GH.windows().createAndPushWindow(curHero); +} + +void CHeroWindow::commanderWindow() +{ + const auto pickedArtInst = getPickedArtifact(); + const auto hero = getHeroPickedArtifact(); + + if(pickedArtInst) + { + const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); + if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack! + { + ArtifactLocation dst(curHero->id, freeSlot); + dst.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS), dst); + } + } + else + { + GH.windows().createAndPushWindow(curHero->commander, false); + } +} + +void CHeroWindow::updateGarrisons() +{ + garr->recreateSlots(); + morale->set(curHero); +} + +bool CHeroWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == curHero; +} diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index 8f87ac7e9..94e91ad33 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -1,115 +1,116 @@ -/* - * CHeroWindow.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 "../widgets/CWindowWithArtifacts.h" -#include "CWindowObject.h" - -#include "../../lib/bonuses/IBonusBearer.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CHeroWindow; -class LClickableAreaHero; -class LRClickableAreaWText; -class LRClickableAreaWTextComp; -class CArtifactsOfHeroMain; -class MoraleLuckBox; -class CToggleButton; -class CToggleGroup; -class CGStatusBar; -class CTextBox; -class CGarrisonInt; - -/// Button which switches hero selection -class CHeroSwitcher : public CIntObject -{ - const CGHeroInstance * hero; - std::shared_ptr image; - CHeroWindow * owner; -public: - void clickPressed(const Point & cursorPosition) override; - - CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_); -}; - -class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts -{ - std::shared_ptr name; - std::shared_ptr title; - - std::shared_ptr banner; - - std::vector> heroList; - std::shared_ptr listSelection; - - std::shared_ptr portraitArea; - std::shared_ptr portraitImage; - - std::vector> primSkillAreas; - std::vector> primSkillImages; - std::vector> primSkillValues; - - std::shared_ptr expValue; - std::shared_ptr expArea; - - std::shared_ptr manaValue; - std::shared_ptr spellPointsArea; - - std::shared_ptr specArea; - std::shared_ptr specImage; - std::shared_ptr specName; - std::shared_ptr morale; - std::shared_ptr luck; - std::vector> secSkillAreas; - std::vector> secSkillImages; - std::vector> secSkillNames; - std::vector> secSkillValues; - - std::shared_ptr quitButton; - std::shared_ptr dismissLabel; - std::shared_ptr dismissButton; - std::shared_ptr questlogLabel; - std::shared_ptr questlogButton; - std::shared_ptr commanderButton; - std::shared_ptr backpackButton; - - std::shared_ptr tacticsButton; - std::shared_ptr formations; - - std::shared_ptr garr; - std::shared_ptr arts; - - std::vector> labels; - -public: - const CGHeroInstance * curHero; - - CHeroWindow(const CGHeroInstance * hero); - - void update(const CGHeroInstance * hero, bool redrawNeeded = false); //sets main displayed hero - - void dismissCurrent(); //dissmissed currently displayed hero (curHero) - void commanderWindow(); - void switchHero(); //changes displayed hero - void updateGarrisons() override; - void createBackpackWindow(); - - //friends - friend void CHeroArtPlace::clickPressed(const Point & cursorPosition); - friend class CPlayerInterface; -}; +/* + * CHeroWindow.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 "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +#include "../../lib/bonuses/IBonusBearer.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CHeroWindow; +class LClickableAreaHero; +class LRClickableAreaWText; +class LRClickableAreaWTextComp; +class CArtifactsOfHeroMain; +class MoraleLuckBox; +class CToggleButton; +class CToggleGroup; +class CGStatusBar; +class CTextBox; +class CGarrisonInt; + +/// Button which switches hero selection +class CHeroSwitcher : public CIntObject +{ + const CGHeroInstance * hero; + std::shared_ptr image; + CHeroWindow * owner; +public: + void clickPressed(const Point & cursorPosition) override; + + CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_); +}; + +class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts +{ + std::shared_ptr name; + std::shared_ptr title; + + std::shared_ptr banner; + + std::vector> heroList; + std::shared_ptr listSelection; + + std::shared_ptr portraitArea; + std::shared_ptr portraitImage; + + std::vector> primSkillAreas; + std::vector> primSkillImages; + std::vector> primSkillValues; + + std::shared_ptr expValue; + std::shared_ptr expArea; + + std::shared_ptr manaValue; + std::shared_ptr spellPointsArea; + + std::shared_ptr specArea; + std::shared_ptr specImage; + std::shared_ptr specName; + std::shared_ptr morale; + std::shared_ptr luck; + std::vector> secSkillAreas; + std::vector> secSkillImages; + std::vector> secSkillNames; + std::vector> secSkillValues; + + std::shared_ptr quitButton; + std::shared_ptr dismissLabel; + std::shared_ptr dismissButton; + std::shared_ptr questlogLabel; + std::shared_ptr questlogButton; + std::shared_ptr commanderButton; + std::shared_ptr backpackButton; + + std::shared_ptr tacticsButton; + std::shared_ptr formations; + + std::shared_ptr garr; + std::shared_ptr arts; + + std::vector> labels; + +public: + const CGHeroInstance * curHero; + + CHeroWindow(const CGHeroInstance * hero); + + void update(const CGHeroInstance * hero, bool redrawNeeded = false); //sets main displayed hero + + void dismissCurrent(); //dissmissed currently displayed hero (curHero) + void commanderWindow(); + void switchHero(); //changes displayed hero + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + void createBackpackWindow(); + + //friends + friend void CHeroArtPlace::clickPressed(const Point & cursorPosition); + friend class CPlayerInterface; +}; diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index d81acf5a9..8c661de75 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -19,12 +19,14 @@ #include "../adventureMap/CResDataBar.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" #include "../widgets/TextControls.h" #include "../widgets/MiscWidgets.h" #include "../widgets/Buttons.h" #include "../widgets/ObjectLists.h" +#include "../windows/CTradeWindow.h" #include "../../CCallback.h" @@ -32,7 +34,6 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/CSkillHandler.h" #include "../../lib/CTownHandler.h" @@ -170,7 +171,7 @@ std::string InfoBoxAbstractHeroData::getNameText() return ""; } -std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) +AnimationPath InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) { //TODO: sizes switch(size) @@ -182,11 +183,11 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) case HERO_PRIMARY_SKILL: case HERO_MANA: case HERO_EXPERIENCE: - return "PSKIL32"; + return AnimationPath::builtin("PSKIL32"); case HERO_SPECIAL: - return "UN32"; + return AnimationPath::builtin("UN32"); case HERO_SECONDARY_SKILL: - return "SECSK32"; + return AnimationPath::builtin("SECSK32"); default: assert(0); } @@ -198,11 +199,11 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) case HERO_PRIMARY_SKILL: case HERO_MANA: case HERO_EXPERIENCE: - return "PSKIL42"; + return AnimationPath::builtin("PSKIL42"); case HERO_SPECIAL: - return "UN44"; + return AnimationPath::builtin("UN44"); case HERO_SECONDARY_SKILL: - return "SECSKILL"; + return AnimationPath::builtin("SECSKILL"); default: assert(0); } @@ -210,7 +211,7 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) default: assert(0); } - return ""; + return {}; } std::string InfoBoxAbstractHeroData::getHoverText() @@ -255,7 +256,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr break; case HERO_PRIMARY_SKILL: text = CGI->generaltexth->arraytxt[2+getSubID()]; - comp = std::make_shared(CComponent::primskill, getSubID(), (int)getValue()); + comp = std::make_shared(ComponentType::PRIM_SKILL, PrimarySkill(getSubID()), getValue()); break; case HERO_MANA: text = CGI->generaltexth->allTexts[149]; @@ -270,7 +271,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr if(value) { text = CGI->skillh->getByIndex(subID)->getDescriptionTranslated((int)value); - comp = std::make_shared(CComponent::secskill, subID, (int)value); + comp = std::make_shared(ComponentType::SEC_SKILL, SecondarySkill(subID), (int)value); } break; } @@ -316,7 +317,7 @@ si64 InfoBoxHeroData::getValue() switch(type) { case HERO_PRIMARY_SKILL: - return hero->getPrimSkillLevel(static_cast(index)); + return hero->getPrimSkillLevel(static_cast(index)); case HERO_MANA: return hero->mana; case HERO_EXPERIENCE: @@ -418,7 +419,7 @@ si64 InfoBoxCustomHeroData::getValue() return value; } -InfoBoxCustom::InfoBoxCustom(std::string ValueText, std::string NameText, std::string ImageName, size_t ImageIndex, std::string HoverText): +InfoBoxCustom::InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText): IInfoBoxData(CUSTOM), valueText(ValueText), nameText(NameText), @@ -438,7 +439,7 @@ size_t InfoBoxCustom::getImageIndex() return imageIndex; } -std::string InfoBoxCustom::getImageName(InfoBox::InfoSize size) +AnimationPath InfoBoxCustom::getImageName(InfoBox::InfoSize size) { return imageName; } @@ -458,7 +459,7 @@ void InfoBoxCustom::prepareMessage(std::string & text, std::shared_ptr("KSTATBAR", 10,pos.h - 45)); - resdatabar = std::make_shared("KRESBAR", 7, 111+footerPos, 29, 5, 76, 81); + 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); + + activateTab(persistentStorage["gui"]["lastKindomInterface"].Integer()); } void CKingdomInterface::generateObjectsList(const std::vector &ownedObjects) @@ -532,7 +535,7 @@ std::shared_ptr CKingdomInterface::createOwnedObject(size_t index) { OwnedObjectInfo & obj = objects[index]; std::string value = std::to_string(obj.count); - auto data = std::make_shared(value, "", "FLAGPORT", obj.imageID, obj.hoverText); + auto data = std::make_shared(value, "", AnimationPath::builtin("FLAGPORT"), obj.imageID, obj.hoverText); return std::make_shared(Point(), InfoBox::POS_CORNER, InfoBox::SIZE_SMALL, data); } return std::shared_ptr(); @@ -576,7 +579,7 @@ void CKingdomInterface::generateMinesList(const std::vector heroes = LOCPLINT->cb->getHeroesInfo(true); for(auto & heroe : heroes) { - totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, GameResID(EGameResID::GOLD))); + totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))); } //Add town income of all towns @@ -588,7 +591,7 @@ void CKingdomInterface::generateMinesList(const std::vector(value, "", "OVMINES", i, CGI->generaltexth->translate("core.minename", i)); + auto data = std::make_shared(value, "", AnimationPath::builtin("OVMINES"), i, CGI->generaltexth->translate("core.minename", i)); minesBox[i] = std::make_shared(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data); minesBox[i]->removeUsedEvents(LCLICK|SHOW_POPUP); //fixes #890 - mines boxes ignore clicks } @@ -603,37 +606,45 @@ void CKingdomInterface::generateButtons() ui32 footerPos = OVERVIEW_SIZE * 116; //Main control buttons - btnHeroes = std::make_shared(Point(748, 28+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->overview[11], CGI->generaltexth->overview[6]), + btnHeroes = std::make_shared(Point(748, 28+footerPos), AnimationPath::builtin("OVBUTN1.DEF"), CButton::tooltip(CGI->generaltexth->overview[11], CGI->generaltexth->overview[6]), std::bind(&CKingdomInterface::activateTab, this, 0), EShortcut::KINGDOM_HEROES_TAB); btnHeroes->block(true); - btnTowns = std::make_shared(Point(748, 64+footerPos), "OVBUTN6.DEF", CButton::tooltip(CGI->generaltexth->overview[12], CGI->generaltexth->overview[7]), + btnTowns = std::make_shared(Point(748, 64+footerPos), AnimationPath::builtin("OVBUTN6.DEF"), CButton::tooltip(CGI->generaltexth->overview[12], CGI->generaltexth->overview[7]), std::bind(&CKingdomInterface::activateTab, this, 1), EShortcut::KINGDOM_TOWNS_TAB); - btnExit = std::make_shared(Point(748,99+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[600]), + btnExit = std::make_shared(Point(748,99+footerPos), AnimationPath::builtin("OVBUTN1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[600]), std::bind(&CKingdomInterface::close, this), EShortcut::GLOBAL_RETURN); btnExit->setImageOrder(3, 4, 5, 6); //Object list control buttons - dwellTop = std::make_shared(Point(733, 4), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPos(0);}); + dwellTop = std::make_shared(Point(733, 4), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPos(0);}); - dwellBottom = std::make_shared(Point(733, footerPos+2), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPos(-1); }); + dwellBottom = std::make_shared(Point(733, footerPos+2), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPos(-1); }); dwellBottom->setImageOrder(2, 3, 4, 5); - dwellUp = std::make_shared(Point(733, 24), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPrev(); }); + dwellUp = std::make_shared(Point(733, 24), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPrev(); }); dwellUp->setImageOrder(4, 5, 6, 7); - dwellDown = std::make_shared(Point(733, footerPos-18), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToNext(); }); + dwellDown = std::make_shared(Point(733, footerPos-18), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToNext(); }); dwellDown->setImageOrder(6, 7, 8, 9); } void CKingdomInterface::activateTab(size_t which) { + Settings s = persistentStorage.write["gui"]["lastKindomInterface"]; + s->Integer() = which; + btnHeroes->block(which == 0); btnTowns->block(which == 1); tabArea->setActive(which); } +void CKingdomInterface::buildChanged() +{ + tabArea->reset(); +} + void CKingdomInterface::townChanged(const CGTownInstance *town) { if(auto townList = std::dynamic_pointer_cast(tabArea->getItem())) @@ -651,6 +662,11 @@ void CKingdomInterface::updateGarrisons() garrison->updateGarrisons(); } +bool CKingdomInterface::holdsGarrison(const CArmedInstance * army) +{ + return army->getOwner() == LOCPLINT->playerID; +} + void CKingdomInterface::artifactAssembled(const ArtifactLocation& artLoc) { if(auto arts = std::dynamic_pointer_cast(tabArea->getItem())) @@ -678,7 +694,7 @@ void CKingdomInterface::artifactRemoved(const ArtifactLocation& artLoc) CKingdHeroList::CKingdHeroList(size_t maxSize) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - title = std::make_shared("OVTITLE",16,0); + title = std::make_shared(ImagePath::builtin("OVTITLE"),16,0); title->colorize(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]); @@ -698,6 +714,15 @@ void CKingdHeroList::updateGarrisons() } } +bool CKingdHeroList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : heroes->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdHeroList::createHeroItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -707,19 +732,19 @@ std::shared_ptr CKingdHeroList::createHeroItem(size_t index) if(index < heroesList.size()) { auto hero = std::make_shared(heroesList[index]); - addSet(hero->heroArts); + addSetAndCallbacks(hero->heroArts); return hero; } else { - return std::make_shared("OVSLOT", (index-2) % picCount ); + return std::make_shared(AnimationPath::builtin("OVSLOT"), (index-2) % picCount ); } } CKingdTownList::CKingdTownList(size_t maxSize) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - title = std::make_shared("OVTITLE", 16, 0); + title = std::make_shared(ImagePath::builtin("OVTITLE"), 16, 0); title->colorize(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]); @@ -750,6 +775,15 @@ void CKingdTownList::updateGarrisons() } } +bool CKingdTownList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : towns->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdTownList::createTownItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -759,14 +793,14 @@ std::shared_ptr CKingdTownList::createTownItem(size_t index) if(index < townsList.size()) return std::make_shared(townsList[index]); else - return std::make_shared("OVSLOT", (index-2) % picCount ); + return std::make_shared(AnimationPath::builtin("OVSLOT"), (index-2) % picCount ); } CTownItem::CTownItem(const CGTownInstance * Town) : town(Town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 6); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 6); name = std::make_shared(74, 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); income = std::make_shared( 190, 60, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(town->dailyIncome()[EGameResID::GOLD])); @@ -778,7 +812,7 @@ CTownItem::CTownItem(const CGTownInstance * Town) size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - picture = std::make_shared("ITPT", iconIndex, 0, 5, 6); + 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++) @@ -786,6 +820,36 @@ CTownItem::CTownItem(const CGTownInstance * Town) growth.push_back(std::make_shared(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)); } + + fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel(), town->hallLevel(), town->hallLevel(), town->hallLevel()); + fastTownHall->setAnimateLonelyFrame(true); + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; + fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setImageOrder(imageIndex, imageIndex, imageIndex, imageIndex); + fastArmyPurchase->setAnimateLonelyFrame(true); + fastTavern = std::make_shared(Rect(5, 6, 58, 64), [&]() + { + if(town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + }); + 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)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); + fastTown = std::make_shared(Rect(67, 6, 165, 20), [&]() + { + GH.windows().createAndPushWindow(town); + }); } void CTownItem::updateGarrisons() @@ -796,6 +860,11 @@ void CTownItem::updateGarrisons() garr->recreateSlots(); } +bool CTownItem::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + void CTownItem::update() { std::string incomeVal = std::to_string(town->dailyIncome()[EGameResID::GOLD]); @@ -820,7 +889,7 @@ public: ArtSlotsTab() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 4); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 4); pos = background->pos; for(int i=0; i<9; i++) arts.push_back(std::make_shared(Point(269+i*48, 66))); @@ -838,10 +907,10 @@ public: BackpackTab() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 5); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 5); pos = background->pos; - btnLeft = std::make_shared(Point(269, 66), "HSBTNS3", CButton::tooltip(), 0); - btnRight = std::make_shared(Point(675, 66), "HSBTNS5", CButton::tooltip(), 0); + btnLeft = std::make_shared(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0); + btnRight = std::make_shared(Point(675, 66), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), 0); for(int i=0; i<8; i++) arts.push_back(std::make_shared(Point(294+i*48, 66))); } @@ -906,7 +975,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) std::string hover = CGI->generaltexth->overview[13+it]; std::string overlay = CGI->generaltexth->overview[8+it]; - auto button = std::make_shared(Point(364+(int)it*112, 46), "OVBUTN3", CButton::tooltip(hover, overlay), 0); + auto button = std::make_shared(Point(364+(int)it*112, 46), AnimationPath::builtin("OVBUTN3"), CButton::tooltip(hover, overlay), 0); button->addTextOverlay(CGI->generaltexth->allTexts[stringID[it]], FONT_SMALL, Colors::YELLOW); artButtons->addToggle((int)it, button); } @@ -916,7 +985,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) garr = std::make_shared(Point(6, 78), 4, Point(), hero, nullptr, true, true); - portrait = std::make_shared("PortraitsLarge", hero->portrait, 0, 5, 6); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 5, 6); heroArea = std::make_shared(5, 6, hero); name = std::make_shared(73, 7, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero->getNameTranslated()); @@ -958,6 +1027,11 @@ void CHeroItem::updateGarrisons() garr->recreateSlots(); } +bool CHeroItem::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + std::shared_ptr CHeroItem::onTabSelected(size_t index) { return artTabs.at(index); diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 6a6b51301..26dd395a0 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -1,350 +1,366 @@ -/* - * CKingdomInterface.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 "../widgets/CWindowWithArtifacts.h" -#include "CWindowObject.h" - -class CButton; -class CAnimImage; -class CToggleGroup; -class CResDataBar; -class CSlider; -class CTownInfo; -class CCreaInfo; -class HeroSlots; -class LRClickableAreaOpenTown; -class CComponent; -class CHeroArea; -class MoraleLuckBox; -class CListBox; -class CTabbedInt; -class CGStatusBar; -class CGarrisonInt; - -class CKingdHeroList; -class CKingdTownList; -class IInfoBoxData; - -/* - * Several classes to display basically any data. - * Main part - class InfoBox which controls how data will be formatted\positioned - * InfoBox have image and 0-2 labels - * In constructor it should receive object that implements IInfoBoxData interface - * - * interface IInfoBoxData defines way to get data for use in InfoBox - * have several implementations: - * InfoBoxHeroData - to display one of fields from hero (e.g. absolute value of primary skills) - * InfoBoxCustomHeroData - to display one of hero fields without hero (e.g. bonuses from objects) - * InfoBoxTownData - data from town - * InfoBoxCustom - user-defined data - */ - -/// Displays one of object propertries with image and optional labels -class InfoBox : public CIntObject -{ -public: - enum InfoPos - { - POS_UP_DOWN, POS_DOWN, POS_RIGHT, POS_INSIDE, POS_CORNER, POS_NONE - }; - enum InfoSize - { - SIZE_TINY, SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG, SIZE_HUGE - }; - -private: - InfoSize size; - InfoPos infoPos; - std::shared_ptr data; - - std::shared_ptr value; - std::shared_ptr name; - std::shared_ptr image; - std::shared_ptr hover; - -public: - InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr Data); - ~InfoBox(); - - void showPopupWindow(const Point & cursorPosition) override; - void clickPressed(const Point & cursorPosition) override; -}; - -class IInfoBoxData -{ -public: - enum InfoType - { - HERO_PRIMARY_SKILL, HERO_MANA, HERO_EXPERIENCE, HERO_SPECIAL, HERO_SECONDARY_SKILL, - //TODO: Luck? Morale? Artifact? - ARMY_SLOT,//TODO - TOWN_GROWTH, TOWN_AVAILABLE, TOWN_BUILDING,//TODO - CUSTOM - }; - -protected: - InfoType type; - - IInfoBoxData(InfoType Type); - -public: - //methods that generate values for displaying - virtual std::string getValueText()=0; - virtual std::string getNameText()=0; - virtual std::string getImageName(InfoBox::InfoSize size)=0; - virtual std::string getHoverText()=0; - virtual size_t getImageIndex()=0; - - //TODO: replace with something better - virtual void prepareMessage(std::string & text, std::shared_ptr & comp)=0; - - virtual ~IInfoBoxData(){}; -}; - -class InfoBoxAbstractHeroData : public IInfoBoxData -{ -protected: - virtual int getSubID()=0; - virtual si64 getValue()=0; - -public: - InfoBoxAbstractHeroData(InfoType Type); - - std::string getValueText() override; - std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -class InfoBoxHeroData : public InfoBoxAbstractHeroData -{ - const CGHeroInstance * hero; - int index;//index of data in hero (0-7 for sec. skill, 0-3 for pr. skill) - - int getSubID() override; - si64 getValue() override; - -public: - InfoBoxHeroData(InfoType Type, const CGHeroInstance *Hero, int Index=0); - - //To get a bit different texts for hero window - std::string getHoverText() override; - std::string getValueText() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -class InfoBoxCustomHeroData : public InfoBoxAbstractHeroData -{ - int subID;//subID of data (0=attack...) - si64 value;//actual value of data, 64-bit to fit experience and negative values - - int getSubID() override; - si64 getValue() override; - -public: - InfoBoxCustomHeroData(InfoType Type, int subID, si64 value); -}; - -class InfoBoxCustom : public IInfoBoxData -{ -public: - std::string valueText; - std::string nameText; - std::string imageName; - std::string hoverText; - size_t imageIndex; - - InfoBoxCustom(std::string ValueText, std::string NameText, std::string ImageName, size_t ImageIndex, std::string HoverText=""); - - std::string getValueText() override; - std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -//TODO!!! -class InfoBoxTownData : public IInfoBoxData -{ - const CGTownInstance * town; - int index;//index of data in town - int value;//actual value of data - -public: - InfoBoxTownData(InfoType Type, const CGTownInstance * Town, int Index); - InfoBoxTownData(InfoType Type, int SubID, int Value); - - std::string getValueText() override; - std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; -}; - -/// Class which holds all parts of kingdom overview window -class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder -{ -private: - struct OwnedObjectInfo - { - int imageID; - ui32 count; - std::string hoverText; - OwnedObjectInfo(): - imageID(0), - count(0) - {} - }; - std::vector objects; - - std::shared_ptr dwellingsList; - std::shared_ptr tabArea; - - //Main buttons - std::shared_ptr btnTowns; - std::shared_ptr btnHeroes; - std::shared_ptr btnExit; - - //Buttons for scrolling dwellings list - std::shared_ptr dwellUp; - std::shared_ptr dwellDown; - std::shared_ptr dwellTop; - std::shared_ptr dwellBottom; - - std::array, 7> minesBox; - - std::shared_ptr incomeArea; - std::shared_ptr incomeAmount; - - std::shared_ptr statusbar; - std::shared_ptr resdatabar; - - void activateTab(size_t which); - - //Internal functions used during construction - void generateButtons(); - void generateObjectsList(const std::vector &ownedObjects); - void generateMinesList(const std::vector &ownedObjects); - - std::shared_ptr createOwnedObject(size_t index); - std::shared_ptr createMainTab(size_t index); - -public: - CKingdomInterface(); - - void townChanged(const CGTownInstance *town); - void heroRemoved(); - void updateGarrisons() override; - void artifactRemoved(const ArtifactLocation &artLoc) override; - void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; - void artifactDisassembled(const ArtifactLocation &artLoc) override; - void artifactAssembled(const ArtifactLocation &artLoc) override; -}; - -/// List item with town -class CTownItem : public CIntObject, public IGarrisonHolder -{ - std::shared_ptr background; - std::shared_ptr picture; - std::shared_ptr name; - std::shared_ptr income; - std::shared_ptr garr; - - std::shared_ptr heroes; - std::shared_ptr hall; - std::shared_ptr fort; - - std::vector> available; - std::vector> growth; - - std::shared_ptr openTown; - -public: - const CGTownInstance * town; - - CTownItem(const CGTownInstance * Town); - - void updateGarrisons() override; - void update(); -}; - -/// List item with hero -class CHeroItem : public CIntObject, public IGarrisonHolder -{ - const CGHeroInstance * hero; - - std::vector> artTabs; - - std::shared_ptr portrait; - std::shared_ptr name; - std::shared_ptr heroArea; - - std::shared_ptr artsText; - std::shared_ptr artsTabs; - - std::shared_ptr artButtons; - std::vector> heroInfo; - std::shared_ptr morale; - std::shared_ptr luck; - - std::shared_ptr garr; - - void onArtChange(int tabIndex); - - std::shared_ptr onTabSelected(size_t index); - -public: - std::shared_ptr heroArts; - - void updateGarrisons() override; - - CHeroItem(const CGHeroInstance * hero); -}; - -/// Tab with all hero-specific data -class CKingdHeroList : public CIntObject, public IGarrisonHolder, public CWindowWithArtifacts -{ -private: - std::shared_ptr heroes; - std::shared_ptr title; - std::shared_ptr heroLabel; - std::shared_ptr skillsLabel; - - std::shared_ptr createHeroItem(size_t index); -public: - CKingdHeroList(size_t maxSize); - - void updateGarrisons() override; -}; - -/// Tab with all town-specific data -class CKingdTownList : public CIntObject, public IGarrisonHolder -{ -private: - std::shared_ptr towns; - std::shared_ptr title; - std::shared_ptr townLabel; - std::shared_ptr garrHeroLabel; - std::shared_ptr visitHeroLabel; - - std::shared_ptr createTownItem(size_t index); -public: - CKingdTownList(size_t maxSize); - - void townChanged(const CGTownInstance * town); - void updateGarrisons() override; -}; +/* + * CKingdomInterface.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 "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class CButton; +class CAnimImage; +class CToggleGroup; +class CResDataBar; +class CSlider; +class CTownInfo; +class CCreaInfo; +class HeroSlots; +class LRClickableAreaOpenTown; +class CComponent; +class CHeroArea; +class MoraleLuckBox; +class CListBox; +class CTabbedInt; +class CGStatusBar; +class CGarrisonInt; + +class CKingdHeroList; +class CKingdTownList; +class IInfoBoxData; + +/* + * Several classes to display basically any data. + * Main part - class InfoBox which controls how data will be formatted\positioned + * InfoBox have image and 0-2 labels + * In constructor it should receive object that implements IInfoBoxData interface + * + * interface IInfoBoxData defines way to get data for use in InfoBox + * have several implementations: + * InfoBoxHeroData - to display one of fields from hero (e.g. absolute value of primary skills) + * InfoBoxCustomHeroData - to display one of hero fields without hero (e.g. bonuses from objects) + * InfoBoxTownData - data from town + * InfoBoxCustom - user-defined data + */ + +/// Displays one of object propertries with image and optional labels +class InfoBox : public CIntObject +{ +public: + enum InfoPos + { + POS_UP_DOWN, POS_DOWN, POS_RIGHT, POS_INSIDE, POS_CORNER, POS_NONE + }; + enum InfoSize + { + SIZE_TINY, SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG, SIZE_HUGE + }; + +private: + InfoSize size; + InfoPos infoPos; + std::shared_ptr data; + + std::shared_ptr value; + std::shared_ptr name; + std::shared_ptr image; + std::shared_ptr hover; + +public: + InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr Data); + ~InfoBox(); + + void showPopupWindow(const Point & cursorPosition) override; + void clickPressed(const Point & cursorPosition) override; +}; + +class IInfoBoxData +{ +public: + enum InfoType + { + HERO_PRIMARY_SKILL, HERO_MANA, HERO_EXPERIENCE, HERO_SPECIAL, HERO_SECONDARY_SKILL, + //TODO: Luck? Morale? Artifact? + ARMY_SLOT,//TODO + TOWN_GROWTH, TOWN_AVAILABLE, TOWN_BUILDING,//TODO + CUSTOM + }; + +protected: + InfoType type; + + IInfoBoxData(InfoType Type); + +public: + //methods that generate values for displaying + virtual std::string getValueText()=0; + virtual std::string getNameText()=0; + virtual AnimationPath getImageName(InfoBox::InfoSize size)=0; + virtual std::string getHoverText()=0; + virtual size_t getImageIndex()=0; + + //TODO: replace with something better + virtual void prepareMessage(std::string & text, std::shared_ptr & comp)=0; + + virtual ~IInfoBoxData(){}; +}; + +class InfoBoxAbstractHeroData : public IInfoBoxData +{ +protected: + virtual int getSubID()=0; + virtual si64 getValue()=0; + +public: + InfoBoxAbstractHeroData(InfoType Type); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +class InfoBoxHeroData : public InfoBoxAbstractHeroData +{ + const CGHeroInstance * hero; + int index;//index of data in hero (0-7 for sec. skill, 0-3 for pr. skill) + + int getSubID() override; + si64 getValue() override; + +public: + InfoBoxHeroData(InfoType Type, const CGHeroInstance *Hero, int Index=0); + + //To get a bit different texts for hero window + std::string getHoverText() override; + std::string getValueText() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +class InfoBoxCustomHeroData : public InfoBoxAbstractHeroData +{ + int subID;//subID of data (0=attack...) + si64 value;//actual value of data, 64-bit to fit experience and negative values + + int getSubID() override; + si64 getValue() override; + +public: + InfoBoxCustomHeroData(InfoType Type, int subID, si64 value); +}; + +class InfoBoxCustom : public IInfoBoxData +{ +public: + std::string valueText; + std::string nameText; + AnimationPath imageName; + std::string hoverText; + size_t imageIndex; + + InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText=""); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +//TODO!!! +class InfoBoxTownData : public IInfoBoxData +{ + const CGTownInstance * town; + int index;//index of data in town + int value;//actual value of data + +public: + InfoBoxTownData(InfoType Type, const CGTownInstance * Town, int Index); + InfoBoxTownData(InfoType Type, int SubID, int Value); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; +}; + +/// Class which holds all parts of kingdom overview window +class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder, public ITownHolder +{ +private: + struct OwnedObjectInfo + { + int imageID; + ui32 count; + std::string hoverText; + OwnedObjectInfo(): + imageID(0), + count(0) + {} + }; + std::vector objects; + + std::shared_ptr dwellingsList; + std::shared_ptr tabArea; + + //Main buttons + std::shared_ptr btnTowns; + std::shared_ptr btnHeroes; + std::shared_ptr btnExit; + + //Buttons for scrolling dwellings list + std::shared_ptr dwellUp; + std::shared_ptr dwellDown; + std::shared_ptr dwellTop; + std::shared_ptr dwellBottom; + + std::array, 7> minesBox; + + std::shared_ptr incomeArea; + std::shared_ptr incomeAmount; + + std::shared_ptr statusbar; + std::shared_ptr resdatabar; + + void activateTab(size_t which); + + //Internal functions used during construction + void generateButtons(); + void generateObjectsList(const std::vector &ownedObjects); + void generateMinesList(const std::vector &ownedObjects); + + std::shared_ptr createOwnedObject(size_t index); + std::shared_ptr createMainTab(size_t index); + +public: + CKingdomInterface(); + + void townChanged(const CGTownInstance *town); + void heroRemoved(); + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + void artifactRemoved(const ArtifactLocation &artLoc) override; + void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; + void artifactDisassembled(const ArtifactLocation &artLoc) override; + void artifactAssembled(const ArtifactLocation &artLoc) override; + void buildChanged() override; +}; + +/// List item with town +class CTownItem : public CIntObject, public IGarrisonHolder +{ + std::shared_ptr background; + std::shared_ptr picture; + std::shared_ptr name; + std::shared_ptr income; + std::shared_ptr garr; + + std::shared_ptr heroes; + std::shared_ptr hall; + std::shared_ptr fort; + + std::vector> available; + std::vector> growth; + + std::shared_ptr openTown; + + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; + std::shared_ptr fastTown; + +public: + const CGTownInstance * town; + + CTownItem(const CGTownInstance * Town); + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + void update(); +}; + +/// List item with hero +class CHeroItem : public CIntObject, public IGarrisonHolder +{ + const CGHeroInstance * hero; + + std::vector> artTabs; + + std::shared_ptr portrait; + std::shared_ptr name; + std::shared_ptr heroArea; + + std::shared_ptr artsText; + std::shared_ptr artsTabs; + + std::shared_ptr artButtons; + std::vector> heroInfo; + std::shared_ptr morale; + std::shared_ptr luck; + + std::shared_ptr garr; + + void onArtChange(int tabIndex); + + std::shared_ptr onTabSelected(size_t index); + +public: + std::shared_ptr heroArts; + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + + CHeroItem(const CGHeroInstance * hero); +}; + +/// Tab with all hero-specific data +class CKingdHeroList : public CIntObject, public IGarrisonHolder, public CWindowWithArtifacts +{ +private: + std::shared_ptr heroes; + std::shared_ptr title; + std::shared_ptr heroLabel; + std::shared_ptr skillsLabel; + + std::shared_ptr createHeroItem(size_t index); +public: + CKingdHeroList(size_t maxSize); + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; +}; + +/// Tab with all town-specific data +class CKingdTownList : public CIntObject, public IGarrisonHolder +{ +private: + std::shared_ptr towns; + std::shared_ptr title; + std::shared_ptr townLabel; + std::shared_ptr garrHeroLabel; + std::shared_ptr visitHeroLabel; + + std::shared_ptr createTownItem(size_t index); +public: + CKingdTownList(size_t maxSize); + + void townChanged(const CGTownInstance * town); + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; +}; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp new file mode 100644 index 000000000..b78d07b84 --- /dev/null +++ b/client/windows/CMapOverview.cpp @@ -0,0 +1,212 @@ +/* + * CMapOverview.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 "CMapOverview.h" + +#include "../lobby/SelectionTab.h" + +#include + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/MiscWidgets.h" +#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/CConfigHandler.h" +#include "../../lib/campaign/CampaignState.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/mapping/CMapHeader.h" +#include "../../lib/mapping/MapFormat.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/filesystem/Filesystem.h" + +#include "../../lib/serializer/CLoadFile.h" +#include "../../lib/StartInfo.h" +#include "../../lib/rmg/CMapGenOptions.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) +{ + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + widget = std::make_shared(*this); + + updateShadow(); + + center(GH.getCursorPosition()); //center on mouse +#ifdef VCMI_MOBILE + moveBy({0, -pos.h / 2}); +#endif + fitToScreen(10); +} + +Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const +{ + Canvas canvas = Canvas(Point(map->width, map->height)); + + 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; + + if(drawPlayerElements) + // if object at tile is owned - it will be colored as its owner + for (const CGObjectInstance *obj : tile.blockingObjects) + { + PlayerColor player = obj->getOwner(); + if(player == PlayerColor::NEUTRAL) + { + color = graphics->neutralColor; + break; + } + if (player.isValidPlayer()) + { + color = graphics->playerColors[player.getNum()]; + break; + } + } + + canvas.drawPoint(Point(x, y), color); + } + + return canvas; +} + +std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) const +{ + std::vector ret = std::vector(); + + CMapService mapService; + std::unique_ptr map; + try + { + map = mapService.loadMap(resource); + } + catch (const std::exception & e) + { + logGlobal->warn("Failed to generate map preview! %s", e.what()); + return ret; + } + + return createMinimaps(map); +} + +std::vector CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const +{ + std::vector ret = std::vector(); + + for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) + ret.push_back(createMinimapForLayer(map, i)); + + return ret; +} + +std::shared_ptr CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const +{ + logGlobal->debug("Building widget drawMinimap"); + + auto rect = readRect(config["rect"]); + auto id = config["id"].Integer(); + + if(id >= minimaps.size()) + 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; + Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize); + + Canvas canvasScaled = Canvas(Point(rect.w, rect.h)); + 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()); + + return std::make_shared(img, Point(rect.x, rect.y)); +} + +CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): + InterfaceObjectConfigurable(), p(parent) +{ + drawPlayerElements = p.tabType == ESelectionScreen::newGame; + + const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); + + if(settings["lobby"]["mapPreview"].Bool() && p.tabType != ESelectionScreen::campaignList) + { + ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); + std::unique_ptr campaignMap = nullptr; + if(p.tabType != ESelectionScreen::newGame && config["variables"]["mapPreviewForSaves"].Bool()) + { + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + std::unique_ptr mapHeader = std::make_unique(); + StartInfo * startInfo; + lf >> *(mapHeader) >> startInfo; + + if(startInfo->campState) + campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); + res = ResourcePath(startInfo->fileURI, EResType::MAP); + } + if(!campaignMap) + minimaps = createMinimaps(res); + else + minimaps = createMinimaps(campaignMap); + } + + REGISTER_BUILDER("drawMinimap", &CMapOverviewWidget::buildDrawMinimap); + + build(config); + + if(auto w = widget("background")) + { + p.pos = w->pos; + } + if(auto w = widget("fileName")) + { + w->setText(p.fileName); + } + if(auto w = widget("mapName")) + { + w->setText(p.mapName); + } + if(auto w = widget("date")) + { + if(p.date.empty()) + { + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), p.tabType == ESelectionScreen::campaignList ? EResType::CAMPAIGN : EResType::MAP))); + w->setText(vstd::getFormattedDateTime(time)); + } + else + w->setText(p.date); + } + if(auto w = widget("noUnderground")) + { + if(minimaps.size() == 0) + w->setText(""); + } +} diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h new file mode 100644 index 000000000..1602e80c3 --- /dev/null +++ b/client/windows/CMapOverview.h @@ -0,0 +1,59 @@ +/* + * CMapOverview.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 CMap; +VCMI_LIB_NAMESPACE_END +#include "CWindowObject.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../gui/InterfaceObjectConfigurable.h" + +class CSlider; +class CLabel; +class CPicture; +class CFilledTexture; +class CTextBox; +class IImage; +class Canvas; +class TransparentFilledRectangle; +enum ESelectionScreen : ui8; + +class CMapOverview; + +class CMapOverviewWidget : public InterfaceObjectConfigurable +{ + CMapOverview& p; + + bool drawPlayerElements; + std::vector minimaps; + + Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; + std::vector createMinimaps(ResourcePath resource) const; + std::vector createMinimaps(std::unique_ptr & map) const; + + std::shared_ptr buildDrawMinimap(const JsonNode & config) const; +public: + CMapOverviewWidget(CMapOverview& p); +}; + +class CMapOverview : public CWindowObject +{ + std::shared_ptr widget; + +public: + const ResourcePath resource; + const std::string mapName; + const std::string fileName; + const std::string date; + const ESelectionScreen tabType; + + CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType); +}; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index bf897be96..5484a9425 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -1,527 +1,553 @@ -/* - * CMessage.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 "CMessage.h" - -#include "../CGameInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/TextOperations.h" - -#include "../windows/InfoWindows.h" -#include "../widgets/Buttons.h" -#include "../widgets/CComponent.h" -#include "../widgets/Slider.h" -#include "../widgets/TextControls.h" -#include "../gui/CGuiHandler.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/Canvas.h" -#include "../renderSDL/SDL_Extensions.h" - -#include - -const int BETWEEN_COMPS_ROWS = 10; -const int BEFORE_COMPONENTS = 30; -const int BETWEEN_COMPS = 30; -const int SIDE_MARGIN = 30; - -template std::pair max(const std::pair &x, const std::pair &y) -{ - std::pair ret; - ret.first = std::max(x.first,y.first); - ret.second = std::max(x.second,y.second); - return ret; -} - -//One image component + subtitles below it -class ComponentResolved : public CIntObject -{ -public: - std::shared_ptr comp; - - //blit component with image centered at this position - void showAll(Canvas & to) override; - - //ComponentResolved(); - ComponentResolved(std::shared_ptr Comp); - ~ComponentResolved(); -}; -// Full set of components for blitting on dialog box -struct ComponentsToBlit -{ - std::vector< std::vector>> comps; - int w, h; - - void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); - ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); - ~ComponentsToBlit(); -}; - -namespace -{ - std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; - std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; - - std::shared_ptr background;//todo: should be CFilledTexture -} - -void CMessage::init() -{ - for(int i=0; i("DIALGBOX"); - dialogBorders[i]->preload(); - - for(int j=0; j < dialogBorders[i]->size(0); j++) - { - auto image = dialogBorders[i]->getImage(j, 0); - //assume blue color initially - if(i != 1) - image->playerColored(PlayerColor(i)); - piecesOfBox[i].push_back(image); - } - } - - background = IImage::createFromFile("DIBOXBCK.BMP", EImageBlitMode::OPAQUE); -} - -void CMessage::dispose() -{ - for(auto & item : dialogBorders) - item.reset(); -} - -SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) -{ - //prepare surface - SDL_Surface * ret = CSDL_Ext::newSurface(w,h); - for (int i=0; iwidth())//background - { - for (int j=0; jheight()) - { - background->draw(ret, i, j); - } - } - - drawBorder(playerColor, ret, w, h); - return ret; -} - -std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) -{ - std::vector ret; - - boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); - - // 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 - - size_t symbolSize = 0; // width of character, in bytes - size_t glyphWidth = 0; // width of printable glyph, pixels - - // loops till line is full or end of text reached - while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) - { - symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); - glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); - - // candidate for line break - if (ui8(text[currPos]) <= ui8(' ')) - wordBreak = currPos; - - /* We don't count braces in string length. */ - if (text[currPos] == '{') - opened=true; - else if (text[currPos]=='}') - opened=false; - else - lineWidth += (ui32)glyphWidth; - currPos += (ui32)symbolSize; - } - - // long line, create line break - if (currPos < text.length() && (text[currPos] != 0x0a)) - { - if (wordBreak != ui32(-1)) - currPos = wordBreak; - else - currPos -= (ui32)symbolSize; - } - - //non-blank line - if(currPos != 0) - { - ret.push_back(text.substr(0, currPos)); - - if (opened) - /* Close the brace for the current line. */ - ret.back() += '}'; - - text.erase(0, currPos); - } - else if(text[currPos] == 0x0a) - { - ret.push_back(""); //add empty string, no extra actions needed - } - - if (text.length() != 0 && text[0] == 0x0a) - { - /* Remove LF */ - text.erase(0, 1); - } - else - { - // trim only if line does not starts with LF - // FIXME: necessary? All lines will be trimmed before returning anyway - boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); - } - - if (opened) - { - /* Add an opening brace for the next line. */ - if (text.length() != 0) - text.insert(0, "{"); - } - } - - /* Trim whitespaces of every line. */ - for (auto & elem : ret) - boost::algorithm::trim(elem); - - return ret; -} - -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); - if(start > msg.size() || end > msg.size()) - return ""; - return msg.substr(begin, end); -} - -int CMessage::guessHeight(const std::string & txt, int width, EFonts font) -{ - const auto f = graphics->fonts[font]; - auto lines = CMessage::breakText(txt, width, font); - int lineHeight = static_cast(f->getLineHeight()); - return lineHeight * (int)lines.size(); -} - -int CMessage::getEstimatedComponentHeight(int numComps) -{ - if (numComps > 8) //Bigger than 8 components - return invalid value - return std::numeric_limits::max(); - else if (numComps > 2) - return 160; // 32px * 1 row + 20 to offset - else if (numComps) - return 118; // 118 px to offset - return 0; -} - -void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) -{ - bool blitOr = false; - if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components - blitOr = true; - - const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; - - assert(ret && ret->text); - for(int i = 0; - i < std::size(sizes) - && sizes[i][0] < GH.screenDimensions().x - 150 - && sizes[i][1] < GH.screenDimensions().y - 150 - && ret->text->slider; - i++) - { - ret->text->resize(Point(sizes[i][0], sizes[i][1])); - } - - if(ret->text->slider) - { - ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); - } - else - { - ret->text->resize(ret->text->label->textSize + Point(10, 10)); - } - - std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size - - ComponentsToBlit comps(ret->components,500, blitOr); - if (ret->components.size()) - winSize.second += 10 + comps.h; //space to first component - - int bw = 0; - if (ret->buttons.size()) - { - int bh = 0; - // Compute total width of buttons - bw = 20*((int)ret->buttons.size()-1); // space between all buttons - for(auto & elem : ret->buttons) //and add buttons width - { - bw+=elem->pos.w; - vstd::amax(bh, elem->pos.h); - } - winSize.second += 20 + bh;//before button + button - } - - // Clip window size - vstd::amax(winSize.second, 50); - vstd::amax(winSize.first, 80); - vstd::amax(winSize.first, comps.w); - vstd::amax(winSize.first, bw); - - vstd::amin(winSize.first, GH.screenDimensions().x - 150); - - ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); - ret->pos.h=ret->bitmap->h; - ret->pos.w=ret->bitmap->w; - ret->center(); - - int curh = SIDE_MARGIN; - int xOffset = (ret->pos.w - ret->text->pos.w)/2; - - if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically - { - if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) - curh = (ret->bitmap->h - ret->text->pos.h)/2; - } - - ret->text->moveBy(Point(xOffset, curh)); - - curh += ret->text->pos.h; - - if (ret->components.size()) - { - curh += BEFORE_COMPONENTS; - comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); - } - if(ret->buttons.size()) - { - // Position the buttons at the bottom of the window - bw = (ret->bitmap->w/2) - (bw/2); - curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; - - for(auto & elem : ret->buttons) - { - elem->moveBy(Point(bw, curh)); - bw += elem->pos.w + 20; - } - } - for(size_t i=0; icomponents.size(); i++) - ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); -} - -void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) -{ - if(playerColor.isSpectator()) - playerColor = PlayerColor(1); - auto & box = piecesOfBox.at(playerColor.getNum()); - - // Note: this code assumes that the corner dimensions are all the same. - - // Horizontal borders - int start_x = x + box[0]->width(); - const int stop_x = x + w - box[1]->width(); - const int bottom_y = y+h-box[7]->height()+1; - while (start_x < stop_x) { - int cur_w = stop_x - start_x; - if (cur_w > box[6]->width()) - cur_w = box[6]->width(); - - // Top border - Rect srcR(0, 0, cur_w, box[6]->height()); - Rect dstR(start_x, y, 0, 0); - box[6]->draw(ret, &dstR, &srcR); - - // Bottom border - dstR.y = bottom_y; - box[7]->draw(ret, &dstR, &srcR); - - start_x += cur_w; - } - - // Vertical borders - int start_y = y + box[0]->height(); - const int stop_y = y + h - box[2]->height()+1; - const int right_x = x+w-box[5]->width(); - while (start_y < stop_y) { - int cur_h = stop_y - start_y; - if (cur_h > box[4]->height()) - cur_h = box[4]->height(); - - // Left border - Rect srcR(0, 0, box[4]->width(), cur_h); - Rect dstR(x, start_y, 0, 0); - box[4]->draw(ret, &dstR, &srcR); - - // Right border - dstR.x = right_x; - box[5]->draw(ret, &dstR, &srcR); - - start_y += cur_h; - } - - //corners - Rect dstR(x, y, box[0]->width(), box[0]->height()); - box[0]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); - box[1]->draw(ret, &dstR, nullptr); - - dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); - box[2]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); - box[3]->draw(ret, &dstR, nullptr); -} - -ComponentResolved::ComponentResolved(std::shared_ptr Comp): - comp(Comp) -{ - //Temporary assign ownership on comp - if (parent) - parent->removeChild(this); - if (comp->parent) - { - comp->parent->addChild(this); - comp->parent->removeChild(comp.get()); - } - - addChild(comp.get()); - defActions = 255 - DISPOSE; - pos.x = pos.y = 0; - - pos.w = comp->pos.w; - pos.h = comp->pos.h; -} - -ComponentResolved::~ComponentResolved() -{ - if (parent) - { - removeChild(comp.get()); - parent->addChild(comp.get()); - } -} - -void ComponentResolved::showAll(Canvas & to) -{ - CIntObject::showAll(to); - comp->showAll(to); -} - -ComponentsToBlit::~ComponentsToBlit() = default; - -ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - w = h = 0; - if(SComps.empty()) - return; - - comps.resize(1); - int curw = 0; - int curr = 0; //current row - - for(auto & SComp : SComps) - { - auto cur = std::make_shared(SComp); - - int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); - if (curw + toadd > maxw) - { - curr++; - vstd::amax(w,curw); - curw = cur->pos.w; - comps.resize(curr+1); - } - else - { - curw += toadd; - vstd::amax(w,curw); - } - - comps[curr].push_back(cur); - } - - for(auto & elem : comps) - { - int maxHeight = 0; - for(size_t j=0;jpos.h); - - h += maxHeight + BETWEEN_COMPS_ROWS; - } -} - -void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - for (auto & elem : comps)//for each row - { - int totalw=0, maxHeight=0; - for(size_t j=0;jpos.w; - vstd::amax(maxHeight, cur->pos.h); - } - - //add space between comps in this row - if(blitOr) - totalw += (inter*2+orWidth) * ((int)elem.size() - 1); - else - totalw += (inter) * ((int)elem.size() - 1); - - int middleh = curh + maxHeight/2;//axis for image aligment - int curw = ret->w/2 - totalw/2; - - for(size_t j=0;jmoveTo(Point(curw, curh)); - - //blit component - Canvas canvas = Canvas::createFromSurface(ret); - - cur->showAll(canvas); - curw += cur->pos.w; - - //if there is subsequent component blit "or" - if(j<(elem.size()-1)) - { - if(blitOr) - { - curw+=inter; - - graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, - Point(curw,middleh-((int)graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); - - curw+=orWidth; - } - curw+=inter; - } - } - curh += maxHeight + BETWEEN_COMPS_ROWS; - } -} +/* + * CMessage.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 "CMessage.h" + +#include "../CGameInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/TextOperations.h" + +#include "../windows/InfoWindows.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../renderSDL/SDL_Extensions.h" + +#include + +const int BETWEEN_COMPS_ROWS = 10; +const int BEFORE_COMPONENTS = 30; +const int BETWEEN_COMPS = 30; +const int SIDE_MARGIN = 30; + +template std::pair max(const std::pair &x, const std::pair &y) +{ + std::pair ret; + ret.first = std::max(x.first,y.first); + ret.second = std::max(x.second,y.second); + return ret; +} + +//One image component + subtitles below it +class ComponentResolved : public CIntObject +{ +public: + std::shared_ptr comp; + + //blit component with image centered at this position + void showAll(Canvas & to) override; + + //ComponentResolved(); + ComponentResolved(std::shared_ptr Comp); + ~ComponentResolved(); +}; +// Full set of components for blitting on dialog box +struct ComponentsToBlit +{ + std::vector< std::vector>> comps; + int w, h; + + void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); + ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); + ~ComponentsToBlit(); +}; + +namespace +{ + std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; + std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; + + std::shared_ptr background;//todo: should be CFilledTexture +} + +void CMessage::init() +{ + for(int i=0; ipreload(); + + for(int j=0; j < dialogBorders[i]->size(0); j++) + { + auto image = dialogBorders[i]->getImage(j, 0); + //assume blue color initially + if(i != 1) + image->playerColored(PlayerColor(i)); + piecesOfBox[i].push_back(image); + } + } + + background = GH.renderHandler().loadImage(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); +} + +void CMessage::dispose() +{ + for(auto & item : dialogBorders) + item.reset(); +} + +SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) +{ + //prepare surface + SDL_Surface * ret = CSDL_Ext::newSurface(w,h); + for (int i=0; iwidth())//background + { + for (int j=0; jheight()) + { + background->draw(ret, i, j); + } + } + + drawBorder(playerColor, ret, w, h); + return ret; +} + +std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) +{ + assert(maxLineWidth != 0); + if (maxLineWidth == 0) + return { text }; + + std::vector ret; + + boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); + + // 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 + + // loops till line is full or end of text reached + while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) + { + symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); + glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); + + // candidate for line break + if (ui8(text[currPos]) <= ui8(' ')) + wordBreak = currPos; + + /* We don't count braces in string length. */ + if (text[currPos] == '{') + { + opened=true; + + std::smatch match; + std::regex expr("^\\{(.*?)\\|"); + std::string tmp = text.substr(currPos); + if(std::regex_search(tmp, match, expr)) + { + std::string colorText = match[1].str(); + if(auto c = Colors::parseColor(colorText)) + { + color = colorText + "|"; + currPos += colorText.length() + 1; + } + } + } + else if (text[currPos]=='}') + { + opened=false; + color = ""; + } + else + lineWidth += (ui32)glyphWidth; + currPos += (ui32)symbolSize; + } + + // long line, create line break + if (currPos < text.length() && (text[currPos] != 0x0a)) + { + if (wordBreak != ui32(-1)) + currPos = wordBreak; + else + currPos -= (ui32)symbolSize; + } + + //non-blank line + if(currPos != 0) + { + ret.push_back(text.substr(0, currPos)); + + if (opened) + /* Close the brace for the current line. */ + ret.back() += '}'; + + text.erase(0, currPos); + } + else if(text[currPos] == 0x0a) + { + ret.push_back(""); //add empty string, no extra actions needed + } + + if (text.length() != 0 && text[0] == 0x0a) + { + /* Remove LF */ + text.erase(0, 1); + } + else + { + // trim only if line does not starts with LF + // FIXME: necessary? All lines will be trimmed before returning anyway + boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); + } + + if (opened) + { + /* Add an opening brace for the next line. */ + if (text.length() != 0) + text.insert(0, "{" + color); + } + } + + /* Trim whitespaces of every line. */ + for (auto & elem : ret) + boost::algorithm::trim(elem); + + return ret; +} + +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); + if(start > msg.size() || end > msg.size()) + return ""; + return msg.substr(begin, end); +} + +int CMessage::guessHeight(const std::string & txt, int width, EFonts font) +{ + const auto f = graphics->fonts[font]; + auto lines = CMessage::breakText(txt, width, font); + int lineHeight = static_cast(f->getLineHeight()); + return lineHeight * (int)lines.size(); +} + +int CMessage::getEstimatedComponentHeight(int numComps) +{ + if (numComps > 8) //Bigger than 8 components - return invalid value + return std::numeric_limits::max(); + else if (numComps > 2) + return 160; // 32px * 1 row + 20 to offset + else if (numComps) + return 118; // 118 px to offset + return 0; +} + +void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) +{ + bool blitOr = false; + if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components + blitOr = true; + + const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; + + assert(ret && ret->text); + for(int i = 0; + i < std::size(sizes) + && sizes[i][0] < GH.screenDimensions().x - 150 + && sizes[i][1] < GH.screenDimensions().y - 150 + && ret->text->slider; + i++) + { + ret->text->resize(Point(sizes[i][0], sizes[i][1])); + } + + if(ret->text->slider) + { + ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); + } + else + { + ret->text->resize(ret->text->label->textSize + Point(10, 10)); + } + + std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size + + ComponentsToBlit comps(ret->components,500, blitOr); + if (ret->components.size()) + winSize.second += 10 + comps.h; //space to first component + + int bw = 0; + if (ret->buttons.size()) + { + int bh = 0; + // Compute total width of buttons + bw = 20*((int)ret->buttons.size()-1); // space between all buttons + for(auto & elem : ret->buttons) //and add buttons width + { + bw+=elem->pos.w; + vstd::amax(bh, elem->pos.h); + } + winSize.second += 20 + bh;//before button + button + } + + // Clip window size + vstd::amax(winSize.second, 50); + vstd::amax(winSize.first, 80); + vstd::amax(winSize.first, comps.w); + vstd::amax(winSize.first, bw); + + vstd::amin(winSize.first, GH.screenDimensions().x - 150); + + ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); + ret->pos.h=ret->bitmap->h; + ret->pos.w=ret->bitmap->w; + ret->center(); + + int curh = SIDE_MARGIN; + int xOffset = (ret->pos.w - ret->text->pos.w)/2; + + if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically + { + if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) + curh = (ret->bitmap->h - ret->text->pos.h)/2; + } + + ret->text->moveBy(Point(xOffset, curh)); + + curh += ret->text->pos.h; + + if (ret->components.size()) + { + curh += BEFORE_COMPONENTS; + comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); + } + if(ret->buttons.size()) + { + // Position the buttons at the bottom of the window + bw = (ret->bitmap->w/2) - (bw/2); + curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; + + for(auto & elem : ret->buttons) + { + elem->moveBy(Point(bw, curh)); + bw += elem->pos.w + 20; + } + } + for(size_t i=0; icomponents.size(); i++) + ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); +} + +void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) +{ + if(playerColor.isSpectator()) + playerColor = PlayerColor(1); + auto & box = piecesOfBox.at(playerColor.getNum()); + + // Note: this code assumes that the corner dimensions are all the same. + + // Horizontal borders + int start_x = x + box[0]->width(); + const int stop_x = x + w - box[1]->width(); + const int bottom_y = y+h-box[7]->height()+1; + while (start_x < stop_x) { + int cur_w = stop_x - start_x; + if (cur_w > box[6]->width()) + cur_w = box[6]->width(); + + // Top border + Rect srcR(0, 0, cur_w, box[6]->height()); + Rect dstR(start_x, y, 0, 0); + box[6]->draw(ret, &dstR, &srcR); + + // Bottom border + dstR.y = bottom_y; + box[7]->draw(ret, &dstR, &srcR); + + start_x += cur_w; + } + + // Vertical borders + int start_y = y + box[0]->height(); + const int stop_y = y + h - box[2]->height()+1; + const int right_x = x+w-box[5]->width(); + while (start_y < stop_y) { + int cur_h = stop_y - start_y; + if (cur_h > box[4]->height()) + cur_h = box[4]->height(); + + // Left border + Rect srcR(0, 0, box[4]->width(), cur_h); + Rect dstR(x, start_y, 0, 0); + box[4]->draw(ret, &dstR, &srcR); + + // Right border + dstR.x = right_x; + box[5]->draw(ret, &dstR, &srcR); + + start_y += cur_h; + } + + //corners + Rect dstR(x, y, box[0]->width(), box[0]->height()); + box[0]->draw(ret, &dstR, nullptr); + + dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); + box[1]->draw(ret, &dstR, nullptr); + + dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); + box[2]->draw(ret, &dstR, nullptr); + + dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); + box[3]->draw(ret, &dstR, nullptr); +} + +ComponentResolved::ComponentResolved(std::shared_ptr Comp): + comp(Comp) +{ + //Temporary assign ownership on comp + if (parent) + parent->removeChild(this); + if (comp->parent) + { + comp->parent->addChild(this); + comp->parent->removeChild(comp.get()); + } + + addChild(comp.get()); + defActions = 255 - DISPOSE; + pos.x = pos.y = 0; + + pos.w = comp->pos.w; + pos.h = comp->pos.h; +} + +ComponentResolved::~ComponentResolved() +{ + if (parent) + { + removeChild(comp.get()); + parent->addChild(comp.get()); + } +} + +void ComponentResolved::showAll(Canvas & to) +{ + CIntObject::showAll(to); + comp->showAll(to); +} + +ComponentsToBlit::~ComponentsToBlit() = default; + +ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) +{ + int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); + + w = h = 0; + if(SComps.empty()) + return; + + comps.resize(1); + int curw = 0; + int curr = 0; //current row + + for(auto & SComp : SComps) + { + auto cur = std::make_shared(SComp); + + int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); + if (curw + toadd > maxw) + { + curr++; + vstd::amax(w,curw); + curw = cur->pos.w; + comps.resize(curr+1); + } + else + { + curw += toadd; + vstd::amax(w,curw); + } + + comps[curr].push_back(cur); + } + + for(auto & elem : comps) + { + int maxHeight = 0; + for(size_t j=0;jpos.h); + + h += maxHeight + BETWEEN_COMPS_ROWS; + } +} + +void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) +{ + int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); + + for (auto & elem : comps)//for each row + { + int totalw=0, maxHeight=0; + for(size_t j=0;jpos.w; + vstd::amax(maxHeight, cur->pos.h); + } + + //add space between comps in this row + if(blitOr) + totalw += (inter*2+orWidth) * ((int)elem.size() - 1); + else + totalw += (inter) * ((int)elem.size() - 1); + + int middleh = curh + maxHeight/2;//axis for image aligment + int curw = ret->w/2 - totalw/2; + + for(size_t j=0;jmoveTo(Point(curw, curh)); + + //blit component + Canvas canvas = Canvas::createFromSurface(ret); + + cur->showAll(canvas); + curw += cur->pos.w; + + //if there is subsequent component blit "or" + if(j<(elem.size()-1)) + { + if(blitOr) + { + curw+=inter; + + graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, + Point(curw,middleh-((int)graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); + + curw+=orWidth; + } + curw+=inter; + } + } + curh += maxHeight + BETWEEN_COMPS_ROWS; + } +} diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 84c6fa29d..84bbfada8 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -1,46 +1,50 @@ -/* - * CMessage.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/Graphics.h" -#include "../../lib/GameConstants.h" - -struct SDL_Surface; -class CInfoWindow; -class CComponent; - - -/// Class which draws formatted text messages and generates chat windows -class CMessage -{ - /// Draw simple dialog box (borders and background only) - static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); - -public: - /// Draw border on exiting surface - static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); - - static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); - - /// split text in lines - static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); - - /// Try to guess a header of a message - static std::string guessHeader(const std::string & msg); - - /// For convenience - static int guessHeight(const std::string & string, int width, EFonts fnt); - - static int getEstimatedComponentHeight(int numComps); - /// constructor - static void init(); - /// destructor - static void dispose(); -}; +/* + * CMessage.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/EFont.h" +#include "../../lib/GameConstants.h" + +struct SDL_Surface; +class CInfoWindow; +class CComponent; + +VCMI_LIB_NAMESPACE_BEGIN +class ColorRGBA; +VCMI_LIB_NAMESPACE_END + + +/// Class which draws formatted text messages and generates chat windows +class CMessage +{ + /// Draw simple dialog box (borders and background only) + static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); + +public: + /// Draw border on exiting surface + static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); + + static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); + + /// split text in lines + static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); + + /// Try to guess a header of a message + static std::string guessHeader(const std::string & msg); + + /// For convenience + static int guessHeight(const std::string & string, int width, EFonts fnt); + + static int getEstimatedComponentHeight(int numComps); + /// constructor + static void init(); + /// destructor + static void dispose(); +}; diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 363b85b01..79edef9cb 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -28,22 +28,22 @@ #include "../../lib/StartInfo.h" CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) - : CWindowObject(PLAYER_COLORED | BORDERED, "PUZZLE"), + : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("PUZZLE")), grailPos(GrailPos), - currentAlpha(SDL_ALPHA_OPAQUE) + currentAlpha(ColorRGBA::ALPHA_OPAQUE) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); CCS->soundh->playSound(soundBase::OBELISK); - quitb = std::make_shared(Point(670, 538), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN); + quitb = std::make_shared(Point(670, 538), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN); quitb->setBorderColor(Colors::METALLIC_GOLD); mapView = std::make_shared(Point(8,9), Point(591, 544), grailPos); - logo = std::make_shared("PUZZLOGO", 607, 3); + logo = std::make_shared(ImagePath::builtin("PUZZLOGO"), 607, 3); title = std::make_shared(700, 95, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[463]); - resDataBar = std::make_shared("ARESBAR.bmp", 3, 575, 32, 2, 85, 85); + resDataBar = std::make_shared(ImagePath::builtin("ARESBAR.bmp"), 3, 575, 32, 2, 85, 85); int faction = LOCPLINT->cb->getStartInfo()->playerInfos.find(LOCPLINT->playerID)->second.castle; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 560f407ad..cd6480d8e 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -1,319 +1,325 @@ -/* - * CQuestLog.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 "CQuestLog.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../widgets/Buttons.h" -#include "../widgets/CComponent.h" -#include "../widgets/Slider.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../adventureMap/CMinimap.h" -#include "../render/Canvas.h" -#include "../renderSDL/SDL_Extensions.h" - -#include "../../CCallback.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/gameState/QuestInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.h" -#include "../../lib/mapObjects/CQuest.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class CAdvmapInterface; - -void CQuestLabel::clickPressed(const Point & cursorPosition) -{ - callback(); -} - -void CQuestLabel::showAll(Canvas & to) -{ - CMultiLineLabel::showAll (to); -} - -CQuestIcon::CQuestIcon (const std::string &defname, int index, int x, int y) : - CAnimImage(defname, index, 0, x, y) -{ - addUsedEvents(LCLICK); -} - -void CQuestIcon::clickPressed(const Point & cursorPosition) -{ - callback(); -} - -void CQuestIcon::showAll(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos); - CAnimImage::showAll(to); -} - -CQuestMinimap::CQuestMinimap(const Rect & position) - : CMinimap(position), - currentQuest(nullptr) -{ -} - -void CQuestMinimap::addQuestMarks (const QuestInfo * q) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - icons.clear(); - - int3 tile; - if (q->obj) - tile = q->obj->pos; - else - tile = q->tile; - - Point offset = tileToPixels(tile); - - onMapViewMoved(Rect(), tile.z); - - auto pic = std::make_shared("VwSymbol.def", 3, offset.x, offset.y); - - pic->moveBy (Point ( -pic->pos.w/2, -pic->pos.h/2)); - pic->callback = std::bind (&CQuestMinimap::iconClicked, this); - - icons.push_back(pic); -} - -void CQuestMinimap::update() -{ - CMinimap::update(); - if(currentQuest) - addQuestMarks(currentQuest); -} - -void CQuestMinimap::iconClicked() -{ - if(currentQuest->obj) - adventureInt->centerOnTile(currentQuest->obj->pos); - //moveAdvMapSelection(); -} - -void CQuestMinimap::showAll(Canvas & to) -{ - CIntObject::showAll(to); // blitting IntObject directly to hide radar -// for (auto pic : icons) -// pic->showAll(to); -} - -CQuestLog::CQuestLog (const std::vector & Quests) - : CWindowObject(PLAYER_COLORED | BORDERED, "questDialog"), - questIndex(0), - currentQuest(nullptr), - hideComplete(false), - quests(Quests) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - 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), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT); - // 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 - hideCompleteButton = std::make_shared(Point(10, 396), "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); - slider->setPanningStep(32); - - recreateLabelList(); - recreateQuestList(0); -} - -void CQuestLog::recreateLabelList() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - labels.clear(); - - bool completeMissing = true; - int currentLabel = 0; - for (int i = 0; i < quests.size(); ++i) - { - // Quests with MISSION_NONE type don't have text for them and can't be displayed - if (quests[i].quest->missionType == CQuest::MISSION_NONE) - continue; - - if (quests[i].quest->progress == CQuest::COMPLETE) - { - completeMissing = false; - if (hideComplete) - continue; - } - - MetaString text; - quests[i].quest->getRolloverText (text, false); - if (quests[i].obj) - { - if (auto seersHut = dynamic_cast(quests[i].obj)) - { - MetaString toSeer; - toSeer.appendRawString(VLC->generaltexth->allTexts[347]); - toSeer.replaceRawString(seersHut->seerName); - text.replaceRawString(toSeer.toString()); - } - else - text.replaceRawString(quests[i].obj->getObjectName()); //get name of the object - } - auto label = std::make_shared(Rect(13, 195, 149,31), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, text.toString()); - label->disable(); - - label->callback = std::bind(&CQuestLog::selectQuest, this, i, currentLabel); - labels.push_back(label); - - // Select latest active quest - if (quests[i].quest->progress != CQuest::COMPLETE) - selectQuest(i, currentLabel); - - currentLabel = static_cast(labels.size()); - } - - if (completeMissing) // We can't use block(completeMissing) because if false button state reset to NORMAL - hideCompleteButton->block(true); - - slider->setAmount(currentLabel); - if (currentLabel > QUEST_COUNT) - { - slider->block(false); - slider->scrollToMax(); - } - else - { - slider->block(true); - slider->scrollToMin(); - } -} - -void CQuestLog::showAll(Canvas & to) -{ - CWindowObject::showAll(to); - if(questIndex >= 0 && questIndex < labels.size()) - { - //TODO: use child object to selection rect - Rect rect = Rect::createAround(labels[questIndex]->pos, 1); - rect.x -= 2; // Adjustment needed as we want selection box on top of border in graphics - rect.w += 2; - to.drawBorder(rect, Colors::METALLIC_GOLD); - } -} - -void CQuestLog::recreateQuestList (int newpos) -{ - for (int i = 0; i < labels.size(); ++i) - { - labels[i]->pos = Rect (pos.x + 14, pos.y + 195 + (i-newpos) * 32, 151, 31); - if (i >= newpos && i < newpos + QUEST_COUNT) - labels[i]->enable(); - else - labels[i]->disable(); - } - minimap->update(); -} - -void CQuestLog::selectQuest(int which, int labelId) -{ - questIndex = labelId; - currentQuest = &quests[which]; - minimap->currentQuest = currentQuest; - - MetaString text; - std::vector components; - currentQuest->quest->getVisitText (text, components, currentQuest->quest->isCustomFirst, true); - if(description->slider) - description->slider->scrollToMin(); // scroll text to start position - description->setText(text.toString()); //TODO: use special log entry text - - componentsBox.reset(); - - int componentsSize = static_cast(components.size()); - int descriptionHeight = DESCRIPTION_HEIGHT_MAX; - if(componentsSize) - { - descriptionHeight -= 15; - CComponent::ESize imageSize = CComponent::large; - switch (currentQuest->quest->missionType) - { - case CQuest::MISSION_ARMY: - { - if (componentsSize > 4) - descriptionHeight -= 195; - else - descriptionHeight -= 100; - - break; - } - case CQuest::MISSION_ART: - { - if (componentsSize > 4) - descriptionHeight -= 190; - else - descriptionHeight -= 90; - - break; - } - case CQuest::MISSION_PRIMARY_STAT: - case CQuest::MISSION_RESOURCES: - { - if (componentsSize > 4) - { - imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space - descriptionHeight -= 140; - } - else - descriptionHeight -= 125; - - break; - } - default: - descriptionHeight -= 115; - break; - } - - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - std::vector> comps; - for(auto & component : components) - { - auto c = std::make_shared(component, imageSize); - comps.push_back(c); - } - - componentsBox = std::make_shared(comps, Rect(202, 20+descriptionHeight+15, 391, DESCRIPTION_HEIGHT_MAX-(20+descriptionHeight))); - } - description->resize(Point(385, descriptionHeight)); - - minimap->update(); - redraw(); -} - -void CQuestLog::sliderMoved(int newpos) -{ - recreateQuestList(newpos); //move components - redraw(); -} - -void CQuestLog::toggleComplete(bool on) -{ - hideComplete = on; - recreateLabelList(); - recreateQuestList(0); - redraw(); -} +/* + * CQuestLog.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 "CQuestLog.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Slider.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../adventureMap/CMinimap.h" +#include "../render/Canvas.h" +#include "../renderSDL/SDL_Extensions.h" + +#include "../../CCallback.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/gameState/QuestInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" +#include "../../lib/mapObjects/CQuest.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class CAdvmapInterface; + +void CQuestLabel::clickPressed(const Point & cursorPosition) +{ + callback(); +} + +void CQuestLabel::showAll(Canvas & to) +{ + CMultiLineLabel::showAll (to); +} + +CQuestIcon::CQuestIcon (const AnimationPath &defname, int index, int x, int y) : + CAnimImage(defname, index, 0, x, y) +{ + addUsedEvents(LCLICK); +} + +void CQuestIcon::clickPressed(const Point & cursorPosition) +{ + callback(); +} + +void CQuestIcon::showAll(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos); + CAnimImage::showAll(to); +} + +CQuestMinimap::CQuestMinimap(const Rect & position) + : CMinimap(position), + currentQuest(nullptr) +{ +} + +void CQuestMinimap::addQuestMarks (const QuestInfo * q) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + icons.clear(); + + int3 tile; + if (q->obj) + tile = q->obj->pos; + else + tile = q->tile; + + Point offset = tileToPixels(tile); + + onMapViewMoved(Rect(), tile.z); + + auto pic = std::make_shared(AnimationPath::builtin("VwSymbol.def"), 3, offset.x, offset.y); + + pic->moveBy (Point ( -pic->pos.w/2, -pic->pos.h/2)); + pic->callback = std::bind (&CQuestMinimap::iconClicked, this); + + icons.push_back(pic); +} + +void CQuestMinimap::update() +{ + CMinimap::update(); + if(currentQuest) + addQuestMarks(currentQuest); +} + +void CQuestMinimap::iconClicked() +{ + if(currentQuest->obj) + adventureInt->centerOnTile(currentQuest->obj->pos); + //moveAdvMapSelection(); +} + +void CQuestMinimap::showAll(Canvas & to) +{ + CIntObject::showAll(to); // blitting IntObject directly to hide radar +// for (auto pic : icons) +// pic->showAll(to); +} + +CQuestLog::CQuestLog (const std::vector & Quests) + : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("questDialog")), + questIndex(0), + currentQuest(nullptr), + hideComplete(false), + quests(Quests) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + 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_ACCEPT); + // 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 + 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); + slider->setPanningStep(32); + + recreateLabelList(); + recreateQuestList(0); +} + +void CQuestLog::recreateLabelList() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + labels.clear(); + + bool completeMissing = true; + int currentLabel = 0; + for (int i = 0; i < quests.size(); ++i) + { + // Quests without mision don't have text for them and can't be displayed + if (quests[i].quest->mission == Rewardable::Limiter{}) + continue; + + if (quests[i].quest->isCompleted) + { + completeMissing = false; + if (hideComplete) + continue; + } + + MetaString text; + quests[i].quest->getRolloverText (text, false); + if (quests[i].obj) + { + if (auto seersHut = dynamic_cast(quests[i].obj)) + { + MetaString toSeer; + toSeer.appendRawString(VLC->generaltexth->allTexts[347]); + toSeer.replaceRawString(seersHut->seerName); + text.replaceRawString(toSeer.toString()); + } + else + text.replaceRawString(quests[i].obj->getObjectName()); //get name of the object + } + auto label = std::make_shared(Rect(13, 195, 149,31), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, text.toString()); + label->disable(); + + label->callback = std::bind(&CQuestLog::selectQuest, this, i, currentLabel); + labels.push_back(label); + + // Select latest active quest + if(!quests[i].quest->isCompleted) + selectQuest(i, currentLabel); + + currentLabel = static_cast(labels.size()); + } + + if (completeMissing) // We can't use block(completeMissing) because if false button state reset to NORMAL + hideCompleteButton->block(true); + + slider->setAmount(currentLabel); + if (currentLabel > QUEST_COUNT) + { + slider->block(false); + slider->scrollToMax(); + } + else + { + slider->block(true); + slider->scrollToMin(); + } +} + +void CQuestLog::showAll(Canvas & to) +{ + CWindowObject::showAll(to); + if(questIndex >= 0 && questIndex < labels.size()) + { + //TODO: use child object to selection rect + Rect rect = Rect::createAround(labels[questIndex]->pos, 1); + rect.x -= 2; // Adjustment needed as we want selection box on top of border in graphics + rect.w += 2; + to.drawBorder(rect, Colors::METALLIC_GOLD); + } +} + +void CQuestLog::recreateQuestList (int newpos) +{ + for (int i = 0; i < labels.size(); ++i) + { + labels[i]->pos = Rect (pos.x + 14, pos.y + 195 + (i-newpos) * 32, 151, 31); + if (i >= newpos && i < newpos + QUEST_COUNT) + labels[i]->enable(); + else + labels[i]->disable(); + } + minimap->update(); +} + +void CQuestLog::selectQuest(int which, int labelId) +{ + questIndex = labelId; + currentQuest = &quests[which]; + minimap->currentQuest = currentQuest; + + MetaString text; + std::vector components; + currentQuest->quest->getVisitText(text, components, true); + if(description->slider) + description->slider->scrollToMin(); // scroll text to start position + description->setText(text.toString()); //TODO: use special log entry text + + componentsBox.reset(); + + int componentsSize = static_cast(components.size()); + int descriptionHeight = DESCRIPTION_HEIGHT_MAX; + if(componentsSize) + { + CComponent::ESize imageSize = CComponent::large; + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 155; + } + else + descriptionHeight -= 130; + /*switch (currentQuest->quest->missionType) + { + case CQuest::MISSION_ARMY: + { + if (componentsSize > 4) + descriptionHeight -= 195; + else + descriptionHeight -= 100; + + break; + } + case CQuest::MISSION_ART: + { + if (componentsSize > 4) + descriptionHeight -= 190; + else + descriptionHeight -= 90; + + break; + } + case CQuest::MISSION_PRIMARY_STAT: + case CQuest::MISSION_RESOURCES: + { + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 140; + } + else + descriptionHeight -= 125; + + break; + } + default: + descriptionHeight -= 115; + break; + }*/ + + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + std::vector> comps; + for(auto & component : components) + { + auto c = std::make_shared(component, imageSize); + comps.push_back(c); + } + + componentsBox = std::make_shared(comps, Rect(202, 20+descriptionHeight+15, 391, DESCRIPTION_HEIGHT_MAX-(20+descriptionHeight))); + } + description->resize(Point(385, descriptionHeight)); + + minimap->update(); + redraw(); +} + +void CQuestLog::sliderMoved(int newpos) +{ + recreateQuestList(newpos); //move components + redraw(); +} + +void CQuestLog::toggleComplete(bool on) +{ + hideComplete = on; + recreateLabelList(); + recreateQuestList(0); + redraw(); +} diff --git a/client/windows/CQuestLog.h b/client/windows/CQuestLog.h index b8e9c597d..c9c4d7983 100644 --- a/client/windows/CQuestLog.h +++ b/client/windows/CQuestLog.h @@ -1,111 +1,111 @@ -/* - * CQuestLog.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 "../widgets/TextControls.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/Images.h" -#include "../adventureMap/CMinimap.h" -#include "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCreature; -class CStackInstance; -class CGHeroInstance; -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CToggleButton; -class CComponentBox; -class LRClickableAreaWText; -class CButton; -class CPicture; -class CCreaturePic; -class LRClickableAreaWTextComp; -class CSlider; -class CLabel; - -const int QUEST_COUNT = 6; -const int DESCRIPTION_HEIGHT_MAX = 355; - -class CQuestLabel : public LRClickableAreaWText, public CMultiLineLabel -{ -public: - std::function callback; - - CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color &Color = Colors::WHITE, const std::string &Text = "") - : CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){}; - void clickPressed(const Point & cursorPosition) override; - void showAll(Canvas & to) override; -}; - -class CQuestIcon : public CAnimImage -{ -public: - std::function callback; //TODO: merge with other similar classes? - - CQuestIcon(const std::string &defname, int index, int x=0, int y=0); - - void clickPressed(const Point & cursorPosition) override; - void showAll(Canvas & to) override; -}; - -class CQuestMinimap : public CMinimap -{ - std::vector> icons; - - void clickPressed(const Point & cursorPosition) override{}; //minimap ignores clicking on its surface - void iconClicked(); - void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override{}; - -public: - const QuestInfo * currentQuest; - - CQuestMinimap(const Rect & position); - //should be called to invalidate whole map - different player or level - void update(); - void addQuestMarks (const QuestInfo * q); - - void showAll(Canvas & to) override; -}; - -class CQuestLog : public CWindowObject -{ - int questIndex; - const QuestInfo * currentQuest; - std::shared_ptr componentsBox; - bool hideComplete; - std::shared_ptr hideCompleteButton; - std::shared_ptr hideCompleteLabel; - - const std::vector quests; - std::vector> labels; - std::shared_ptr description; - std::shared_ptr minimap; - std::shared_ptr slider; //scrolls quests - std::shared_ptr ok; - -public: - CQuestLog(const std::vector & Quests); - - ~CQuestLog(){}; - - void selectQuest (int which, int labelId); - void updateMinimap (int which){}; - void printDescription (int which){}; - void sliderMoved (int newpos); - void recreateLabelList(); - void recreateQuestList (int pos); - void toggleComplete(bool on); - void showAll (Canvas & to) override; -}; +/* + * CQuestLog.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 "../widgets/TextControls.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/Images.h" +#include "../adventureMap/CMinimap.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCreature; +class CStackInstance; +class CGHeroInstance; +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CToggleButton; +class CComponentBox; +class LRClickableAreaWText; +class CButton; +class CPicture; +class CCreaturePic; +class LRClickableAreaWTextComp; +class CSlider; +class CLabel; + +const int QUEST_COUNT = 6; +const int DESCRIPTION_HEIGHT_MAX = 355; + +class CQuestLabel : public LRClickableAreaWText, public CMultiLineLabel +{ +public: + std::function callback; + + CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA &Color = Colors::WHITE, const std::string &Text = "") + : CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){}; + void clickPressed(const Point & cursorPosition) override; + void showAll(Canvas & to) override; +}; + +class CQuestIcon : public CAnimImage +{ +public: + std::function callback; //TODO: merge with other similar classes? + + CQuestIcon(const AnimationPath & defname, int index, int x=0, int y=0); + + void clickPressed(const Point & cursorPosition) override; + void showAll(Canvas & to) override; +}; + +class CQuestMinimap : public CMinimap +{ + std::vector> icons; + + void clickPressed(const Point & cursorPosition) override{}; //minimap ignores clicking on its surface + void iconClicked(); + void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override{}; + +public: + const QuestInfo * currentQuest; + + CQuestMinimap(const Rect & position); + //should be called to invalidate whole map - different player or level + void update(); + void addQuestMarks (const QuestInfo * q); + + void showAll(Canvas & to) override; +}; + +class CQuestLog : public CWindowObject +{ + int questIndex; + const QuestInfo * currentQuest; + std::shared_ptr componentsBox; + bool hideComplete; + std::shared_ptr hideCompleteButton; + std::shared_ptr hideCompleteLabel; + + const std::vector quests; + std::vector> labels; + std::shared_ptr description; + std::shared_ptr minimap; + std::shared_ptr slider; //scrolls quests + std::shared_ptr ok; + +public: + CQuestLog(const std::vector & Quests); + + ~CQuestLog(){}; + + void selectQuest (int which, int labelId); + void updateMinimap (int which){}; + void printDescription (int which){}; + void sliderMoved (int newpos); + void recreateLabelList(); + void recreateQuestList (int pos); + void toggleComplete(bool on); + void showAll (Canvas & to) override; +}; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 25265aaea..bbd104fd4 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -1,624 +1,700 @@ -/* - * CSpellWindow.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 "CSpellWindow.h" - -#include "../../lib/ScopeGuard.h" - -#include "GUIClasses.h" -#include "InfoWindows.h" -#include "CCastleInterface.h" - -#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 "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/TextControls.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../render/CAnimation.h" - -#include "../../CCallback.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/Problem.h" -#include "../../lib/GameConstants.h" - -#include "../../lib/mapObjects/CGHeroInstance.h" - -CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function funcL, int helpTextId, CSpellWindow * _owner) -{ - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos = myRect; - onLeft = funcL; - hoverText = CGI->generaltexth->zelp[helpTextId].first; - helpText = CGI->generaltexth->zelp[helpTextId].second; - owner = _owner; -} - -void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition) -{ - onLeft(); -} - -void CSpellWindow::InteractiveArea::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(helpText); -} - -void CSpellWindow::InteractiveArea::hover(bool on) -{ - if(on) - owner->statusBar->write(hoverText); - else - owner->statusBar->clear(); -} - -class SpellbookSpellSorter -{ -public: - bool operator()(const CSpell * A, const CSpell * B) - { - if(A->level < B->level) - return true; - if(A->level > B->level) - return false; - - - for(auto schoolId = 0; schoolId < GameConstants::DEFAULT_SCHOOLS; schoolId++) - { - if(A->school.at(SpellSchool(schoolId)) && !B->school.at(SpellSchool(schoolId))) - return true; - if(!A->school.at(SpellSchool(schoolId)) && B->school.at(SpellSchool(schoolId))) - return false; - } - - return A->getNameTranslated() < B->getNameTranslated(); - } -} spellsorter; - -CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED, "SpelBack"), - battleSpellsOnly(openOnBattleSpells), - selectedTab(4), - currentPage(0), - myHero(_myHero), - myInt(_myInt) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - //initializing castable spells - mySpells.reserve(CGI->spellh->objects.size()); - for(const CSpell * spell : CGI->spellh->objects) - { - if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell)) - mySpells.push_back(spell); - } - std::sort(mySpells.begin(), mySpells.end(), spellsorter); - - //initializing sizes of spellbook's parts - for(auto & elem : sitesPerTabAdv) - elem = 0; - for(auto & elem : sitesPerTabBattle) - elem = 0; - - for(const auto spell : mySpells) - { - int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; - - ++sitesPerOurTab[4]; - - spell->forEachSchool([&sitesPerOurTab](const spells::SchoolInfo & school, bool & stop) - { - ++sitesPerOurTab[(ui8)school.id]; - }); - } - if(sitesPerTabAdv[4] % 12 == 0) - sitesPerTabAdv[4]/=12; - else - sitesPerTabAdv[4] = sitesPerTabAdv[4]/12 + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabAdv[v] <= 10) - sitesPerTabAdv[v] = 1; - else - { - if((sitesPerTabAdv[v] - 10) % 12 == 0) - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - 10) / 12 + 1; - else - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - 10) / 12 + 2; - } - } - - if(sitesPerTabBattle[4] % 12 == 0) - sitesPerTabBattle[4]/=12; - else - sitesPerTabBattle[4] = sitesPerTabBattle[4]/12 + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabBattle[v] <= 10) - sitesPerTabBattle[v] = 1; - else - { - if((sitesPerTabBattle[v] - 10) % 12 == 0) - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - 10) / 12 + 1; - else - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - 10) / 12 + 2; - } - } - - - //numbers of spell pages computed - - leftCorner = std::make_shared("SpelTrnL.bmp", 97, 77); - rightCorner = std::make_shared("SpelTrnR.bmp", 487, 72); - - spellIcons = std::make_shared("Spells"); - - schoolTab = std::make_shared("SpelTab", selectedTab, 0, 524, 88); - schoolPicture = std::make_shared("Schools", 0, 0, 117, 74); - - schoolBorders[0] = std::make_shared("SplevA.def"); - schoolBorders[1] = std::make_shared("SplevF.def"); - schoolBorders[2] = std::make_shared("SplevW.def"); - schoolBorders[3] = std::make_shared("SplevE.def"); - - for(auto item : schoolBorders) - item->preload(); - mana = std::make_shared(435, 426, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); - statusBar = CGStatusBar::create(7, 569, "Spelroll.bmp"); - - interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); - interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); - interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); - interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 94 + pos.y, 36, 56), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 151 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 210 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 270 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 330 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); - - interactiveAreas.push_back(std::make_shared( Rect( 97 + pos.x, 77 + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); - interactiveAreas.push_back(std::make_shared( Rect( 487 + pos.x, 72 + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); - - //areas for spells - int xpos = 117 + pos.x, ypos = 90 + pos.y; - - for(int v=0; v<12; ++v) - { - spellAreas[v] = std::make_shared( Rect(xpos, ypos, 65, 78), this); - - if(v == 5) //to right page - { - xpos = 336 + pos.x; ypos = 90 + pos.y; - } - else - { - if(v%2 == 0) - { - xpos+=85; - } - else - { - xpos -= 85; ypos+=97; - } - } - } - - selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; - schoolTab->setFrame(selectedTab, 0); - int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap; - // 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); - computeSpellsPerArea(); - addUsedEvents(KEYBOARD); -} - -CSpellWindow::~CSpellWindow() -{ -} - -void CSpellWindow::fexitb() -{ - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage; - - close(); -} - -void CSpellWindow::fadvSpellsb() -{ - if(battleSpellsOnly == true) - { - turnPageRight(); - battleSpellsOnly = false; - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fbattleSpellsb() -{ - if(battleSpellsOnly == false) - { - turnPageLeft(); - battleSpellsOnly = true; - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fmanaPtsb() -{ -} - -void CSpellWindow::selectSchool(int school) -{ - if(selectedTab != school) - { - if(selectedTab < school) - turnPageLeft(); - else - turnPageRight(); - selectedTab = school; - schoolTab->setFrame(selectedTab, 0); - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fLcornerb() -{ - if(currentPage>0) - { - turnPageLeft(); - setCurrentPage(currentPage - 1); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fRcornerb() -{ - if((currentPage + 1) < (pagesWithinCurrentTab())) - { - turnPageRight(); - setCurrentPage(currentPage + 1); - } - computeSpellsPerArea(); -} - -void CSpellWindow::show(Canvas & to) -{ - statusBar->show(to); -} - -void CSpellWindow::computeSpellsPerArea() -{ - std::vector spellsCurSite; - spellsCurSite.reserve(mySpells.size()); - for(const CSpell * spell : mySpells) - { - if(spell->isCombat() ^ !battleSpellsOnly - && ((selectedTab == 4) || spell->school.at(SpellSchool(selectedTab))) - ) - { - spellsCurSite.push_back(spell); - } - } - - if(selectedTab == 4) - { - if(spellsCurSite.size() > 12) - { - spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*12, spellsCurSite.end()); - if(spellsCurSite.size() > 12) - { - spellsCurSite.erase(spellsCurSite.begin()+12, spellsCurSite.end()); - } - } - } - else //selectedTab == 0, 1, 2 or 3 - { - if(spellsCurSite.size() > 10) - { - if(currentPage == 0) - { - spellsCurSite.erase(spellsCurSite.begin()+10, spellsCurSite.end()); - } - else - { - spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*12 + 10, spellsCurSite.end()); - if(spellsCurSite.size() > 12) - { - spellsCurSite.erase(spellsCurSite.begin()+12, spellsCurSite.end()); - } - } - } - } - //applying - if(selectedTab == 4 || currentPage != 0) - { - for(size_t c=0; c<12; ++c) - { - if(c < spellsCurSite.size()) - { - spellAreas[c]->setSpell(spellsCurSite[c]); - } - else - { - spellAreas[c]->setSpell(nullptr); - } - } - } - else - { - spellAreas[0]->setSpell(nullptr); - spellAreas[1]->setSpell(nullptr); - for(size_t c=0; c<10; ++c) - { - if(c < spellsCurSite.size()) - spellAreas[c+2]->setSpell(spellsCurSite[c]); - else - spellAreas[c+2]->setSpell(nullptr); - } - } - redraw(); -} - -void CSpellWindow::setCurrentPage(int value) -{ - currentPage = value; - schoolPicture->visible = selectedTab!=4 && currentPage == 0; - if(selectedTab != 4) - schoolPicture->setFrame(selectedTab, 0); - leftCorner->visible = currentPage != 0; - rightCorner->visible = (currentPage+1) < pagesWithinCurrentTab(); - - mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book -} - -void CSpellWindow::turnPageLeft() -{ - if(settings["video"]["spellbookAnimation"].Bool()) - CCS->videoh->openAndPlayVideo("PGTRNLFT.SMK", pos.x+13, pos.y+15); -} - -void CSpellWindow::turnPageRight() -{ - if(settings["video"]["spellbookAnimation"].Bool()) - CCS->videoh->openAndPlayVideo("PGTRNRGH.SMK", pos.x+13, pos.y+15); -} - -void CSpellWindow::keyPressed(EShortcut key) -{ - switch(key) - { - case EShortcut::GLOBAL_RETURN: - fexitb(); - break; - - case EShortcut::MOVE_LEFT: - fLcornerb(); - break; - case EShortcut::MOVE_RIGHT: - fRcornerb(); - break; - case EShortcut::MOVE_UP: - case EShortcut::MOVE_DOWN: - { - bool down = key == EShortcut::MOVE_DOWN; - static const int schoolsOrder[] = { 0, 3, 1, 2, 4 }; - int index = -1; - while(schoolsOrder[++index] != selectedTab); - index += (down ? 1 : -1); - vstd::abetween(index, 0, std::size(schoolsOrder) - 1); - if(selectedTab != schoolsOrder[index]) - selectSchool(schoolsOrder[index]); - break; - } - case EShortcut::SPELLBOOK_TAB_COMBAT: - fbattleSpellsb(); - break; - case EShortcut::SPELLBOOK_TAB_ADVENTURE: - fadvSpellsb(); - break; - } -} - -int CSpellWindow::pagesWithinCurrentTab() -{ - return battleSpellsOnly ? sitesPerTabBattle[selectedTab] : sitesPerTabAdv[selectedTab]; -} - -CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner) -{ - this->pos = pos; - this->owner = owner; - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - - schoolLevel = -1; - mySpell = nullptr; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - image = std::make_shared(owner->spellIcons, 0, 0); - image->visible = false; - - name = std::make_shared(39, 70, FONT_TINY, ETextAlignment::CENTER); - level = std::make_shared(39, 82, FONT_TINY, ETextAlignment::CENTER); - cost = std::make_shared(39, 94, FONT_TINY, ETextAlignment::CENTER); - - for(auto l : {name, level, cost}) - l->setAutoRedraw(false); -} - -CSpellWindow::SpellArea::~SpellArea() = default; - -void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) -{ - if(mySpell) - { - auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); - if(spellCost > owner->myHero->mana) //insufficient mana - { - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana)); - return; - } - - //anything that is not combat spell is adventure spell - //this not an error in general to cast even creature ability with hero - const bool combatSpell = mySpell->isCombat(); - if(combatSpell == mySpell->isAdventure()) - { - logGlobal->error("Spell have invalid flags"); - return; - } - - const bool inCombat = owner->myInt->battleInt != nullptr; - const bool inCastle = owner->myInt->castleInt != nullptr; - - //battle spell on adv map or adventure map spell during combat => display infowindow, not cast - if((combatSpell ^ inCombat) || inCastle) - { - std::vector> hlp(1, std::make_shared(CComponent::spell, mySpell->id, 0)); - LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); - } - else if(combatSpell) - { - spells::detail::ProblemImpl problem; - if(mySpell->canBeCast(problem, owner->myInt->cb.get(), spells::Mode::HERO, owner->myHero)) - { - owner->myInt->battleInt->castThisSpell(mySpell->id); - owner->fexitb(); - } - else - { - std::vector texts; - problem.getAll(texts); - if(!texts.empty()) - LOCPLINT->showInfoDialog(texts.front()); - else - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); - } - } - else //adventure spell - { - const CGHeroInstance * h = owner->myHero; - GH.windows().popWindows(1); - - auto guard = vstd::makeScopeGuard([this]() - { - owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; - owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage; - }); - - if(mySpell->getTargetType() == spells::AimType::LOCATION) - adventureInt->enterCastingMode(mySpell); - else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) - owner->myInt->cb->castSpell(h, mySpell->id); - else - logGlobal->error("Invalid spell target type"); - } - } -} - -void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) -{ - if(mySpell) - { - std::string dmgInfo; - auto causedDmg = owner->myInt->cb->estimateSpellDamage(mySpell, owner->myHero); - if(causedDmg == 0 || mySpell->id == SpellID::TITANS_LIGHTNING_BOLT) //Titan's Lightning Bolt already has damage info included - dmgInfo.clear(); - else - { - dmgInfo = CGI->generaltexth->allTexts[343]; - boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); - } - - CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(CComponent::spell, mySpell->id)); - } -} - -void CSpellWindow::SpellArea::hover(bool on) -{ - if(mySpell) - { - if(on) - owner->statusBar->write(boost::to_string(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->level])); - else - owner->statusBar->clear(); - } -} - -void CSpellWindow::SpellArea::setSpell(const CSpell * spell) -{ - schoolBorder.reset(); - image->visible = false; - name->setText(""); - level->setText(""); - cost->setText(""); - mySpell = spell; - if(mySpell) - { - int32_t whichSchool = 0; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, - schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); - auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); - - image->setFrame(mySpell->id); - image->visible = true; - - { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); - } - - SDL_Color firstLineColor, secondLineColor; - if(spellCost > owner->myHero->mana) //hero cannot cast this spell - { - firstLineColor = Colors::WHITE; - secondLineColor = Colors::ORANGE; - } - else - { - firstLineColor = Colors::YELLOW; - secondLineColor = Colors::WHITE; - } - - name->color = firstLineColor; - name->setText(mySpell->getNameTranslated()); - - level->color = secondLineColor; - if(schoolLevel > 0) - { - boost::format fmt("%s/%s"); - fmt % CGI->generaltexth->allTexts[171 + mySpell->level]; - fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 - level->setText(fmt.str()); - } - else - level->setText(CGI->generaltexth->allTexts[171 + mySpell->level]); - - cost->color = secondLineColor; - boost::format costfmt("%s: %d"); - costfmt % CGI->generaltexth->allTexts[387] % spellCost; - cost->setText(costfmt.str()); - } -} +/* + * CSpellWindow.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 "CSpellWindow.h" + +#include "../../lib/ScopeGuard.h" + +#include "GUIClasses.h" +#include "InfoWindows.h" +#include "CCastleInterface.h" + +#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 "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/TextControls.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 "../../CCallback.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/Problem.h" +#include "../../lib/GameConstants.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" + +CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function funcL, int helpTextId, CSpellWindow * _owner) +{ + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos = myRect; + onLeft = funcL; + hoverText = CGI->generaltexth->zelp[helpTextId].first; + helpText = CGI->generaltexth->zelp[helpTextId].second; + owner = _owner; +} + +void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition) +{ + onLeft(); +} + +void CSpellWindow::InteractiveArea::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(helpText); +} + +void CSpellWindow::InteractiveArea::hover(bool on) +{ + if(on) + owner->statusBar->write(hoverText); + else + owner->statusBar->clear(); +} + +class SpellbookSpellSorter +{ +public: + bool operator()(const CSpell * A, const CSpell * B) + { + if(A->getLevel() < B->getLevel()) + return true; + if(A->getLevel() > B->getLevel()) + return false; + + + for(auto schoolId = 0; schoolId < GameConstants::DEFAULT_SCHOOLS; schoolId++) + { + if(A->school.at(SpellSchool(schoolId)) && !B->school.at(SpellSchool(schoolId))) + return true; + if(!A->school.at(SpellSchool(schoolId)) && B->school.at(SpellSchool(schoolId))) + return false; + } + + return A->getNameTranslated() < B->getNameTranslated(); + } +} spellsorter; + +CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): + CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), + battleSpellsOnly(openOnBattleSpells), + selectedTab(4), + currentPage(0), + myHero(_myHero), + myInt(_myInt), + isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()), + spellsPerPage(24), + offL(-11), + offR(195), + offRM(110), + offT(-37), + offB(56) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(isBigSpellbook) + { + background = std::make_shared(createBigSpellBook(), Point(0, 0)); + updateShadow(); + } + else + { + background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); + offL = offR = offT = offB = offRM = 0; + spellsPerPage = 12; + } + pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); + + //initializing castable spells + mySpells.reserve(CGI->spellh->objects.size()); + for(const CSpell * spell : CGI->spellh->objects) + { + if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell)) + mySpells.push_back(spell); + } + std::sort(mySpells.begin(), mySpells.end(), spellsorter); + + //initializing sizes of spellbook's parts + for(auto & elem : sitesPerTabAdv) + elem = 0; + for(auto & elem : sitesPerTabBattle) + elem = 0; + + for(const auto spell : mySpells) + { + int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; + + ++sitesPerOurTab[4]; + + spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) + { + ++sitesPerOurTab[school]; + }); + } + if(sitesPerTabAdv[4] % spellsPerPage == 0) + sitesPerTabAdv[4]/=spellsPerPage; + else + sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabAdv[v] <= spellsPerPage - 2) + sitesPerTabAdv[v] = 1; + else + { + if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; + else + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; + } + } + + if(sitesPerTabBattle[4] % spellsPerPage == 0) + sitesPerTabBattle[4]/=spellsPerPage; + else + sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabBattle[v] <= spellsPerPage - 2) + sitesPerTabBattle[v] = 1; + else + { + if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; + else + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; + } + } + + + //numbers of spell pages computed + + 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) + statusBar = CGStatusBar::create(400, 587); + else + statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); + + Rect schoolRect( 549 + pos.x + offR, 94 + pos.y, 45, 35); + interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); + interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); + interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); + interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 0), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 57), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); + + interactiveAreas.push_back(std::make_shared( Rect( 97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); + interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); + + //areas for spells + int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; + + for(int v=0; v( Rect(xpos, ypos, 65, 78), this); + + if(v == (spellsPerPage / 2) - 1) //to right page + { + xpos = offRM + 336 + pos.x; ypos = 90 + offT + pos.y; + } + else + { + if(v%(isBigSpellbook ? 3 : 2) == 0 || (v%3 == 1 && isBigSpellbook)) + { + xpos+=85; + } + else + { + xpos -= (isBigSpellbook ? 2 : 1)*85; ypos+=97; + } + } + } + + selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; + schoolTab->setFrame(selectedTab, 0); + int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap; + // 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); + computeSpellsPerArea(); + addUsedEvents(KEYBOARD); +} + +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)); + + return GH.renderHandler().createImage(canvas.getInternalSurface()); +} + +void CSpellWindow::fexitb() +{ + (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; + (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage; + + close(); +} + +void CSpellWindow::fadvSpellsb() +{ + if(battleSpellsOnly == true) + { + turnPageRight(); + battleSpellsOnly = false; + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fbattleSpellsb() +{ + if(battleSpellsOnly == false) + { + turnPageLeft(); + battleSpellsOnly = true; + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fmanaPtsb() +{ +} + +void CSpellWindow::selectSchool(int school) +{ + if(selectedTab != school) + { + if(selectedTab < school) + turnPageLeft(); + else + turnPageRight(); + selectedTab = school; + schoolTab->setFrame(selectedTab, 0); + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fLcornerb() +{ + if(currentPage>0) + { + turnPageLeft(); + setCurrentPage(currentPage - 1); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fRcornerb() +{ + if((currentPage + 1) < (pagesWithinCurrentTab())) + { + turnPageRight(); + setCurrentPage(currentPage + 1); + } + computeSpellsPerArea(); +} + +void CSpellWindow::show(Canvas & to) +{ + statusBar->show(to); +} + +void CSpellWindow::computeSpellsPerArea() +{ + std::vector spellsCurSite; + spellsCurSite.reserve(mySpells.size()); + for(const CSpell * spell : mySpells) + { + if(spell->isCombat() ^ !battleSpellsOnly + && ((selectedTab == 4) || spell->school.at(SpellSchool(selectedTab))) + ) + { + spellsCurSite.push_back(spell); + } + } + + if(selectedTab == 4) + { + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*spellsPerPage, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); + } + } + } + else //selectedTab == 0, 1, 2 or 3 + { + if(spellsCurSite.size() > spellsPerPage - 2) + { + if(currentPage == 0) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage-2, spellsCurSite.end()); + } + else + { + spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*spellsPerPage + spellsPerPage-2, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); + } + } + } + } + //applying + if(selectedTab == 4 || currentPage != 0) + { + for(size_t c=0; csetSpell(spellsCurSite[c]); + } + else + { + spellAreas[c]->setSpell(nullptr); + } + } + } + else + { + spellAreas[0]->setSpell(nullptr); + spellAreas[1]->setSpell(nullptr); + for(size_t c=0; csetSpell(spellsCurSite[c]); + else + spellAreas[c+2]->setSpell(nullptr); + } + } + redraw(); +} + +void CSpellWindow::setCurrentPage(int value) +{ + currentPage = value; + schoolPicture->visible = selectedTab!=4 && currentPage == 0; + if(selectedTab != 4) + schoolPicture->setFrame(selectedTab, 0); + + if (currentPage != 0) + leftCorner->enable(); + else + leftCorner->disable(); + + if (currentPage + 1 < pagesWithinCurrentTab()) + rightCorner->enable(); + else + rightCorner->disable(); + + mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book +} + +void CSpellWindow::turnPageLeft() +{ + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); +} + +void CSpellWindow::turnPageRight() +{ + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); +} + +void CSpellWindow::keyPressed(EShortcut key) +{ + switch(key) + { + case EShortcut::GLOBAL_RETURN: + fexitb(); + break; + + case EShortcut::MOVE_LEFT: + fLcornerb(); + break; + case EShortcut::MOVE_RIGHT: + fRcornerb(); + break; + case EShortcut::MOVE_UP: + case EShortcut::MOVE_DOWN: + { + bool down = key == EShortcut::MOVE_DOWN; + static const int schoolsOrder[] = { 0, 3, 1, 2, 4 }; + int index = -1; + while(schoolsOrder[++index] != selectedTab); + index += (down ? 1 : -1); + vstd::abetween(index, 0, std::size(schoolsOrder) - 1); + if(selectedTab != schoolsOrder[index]) + selectSchool(schoolsOrder[index]); + break; + } + case EShortcut::SPELLBOOK_TAB_COMBAT: + fbattleSpellsb(); + break; + case EShortcut::SPELLBOOK_TAB_ADVENTURE: + fadvSpellsb(); + break; + } +} + +int CSpellWindow::pagesWithinCurrentTab() +{ + return battleSpellsOnly ? sitesPerTabBattle[selectedTab] : sitesPerTabAdv[selectedTab]; +} + +CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner) +{ + this->pos = pos; + this->owner = owner; + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + + schoolLevel = -1; + mySpell = nullptr; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + image = std::make_shared(owner->spellIcons, 0, 0); + image->visible = false; + + name = std::make_shared(39, 70, FONT_TINY, ETextAlignment::CENTER); + level = std::make_shared(39, 82, FONT_TINY, ETextAlignment::CENTER); + cost = std::make_shared(39, 94, FONT_TINY, ETextAlignment::CENTER); + + for(auto l : {name, level, cost}) + l->setAutoRedraw(false); +} + +CSpellWindow::SpellArea::~SpellArea() = default; + +void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) +{ + if(mySpell) + { + auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); + if(spellCost > owner->myHero->mana) //insufficient mana + { + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana)); + return; + } + + //anything that is not combat spell is adventure spell + //this not an error in general to cast even creature ability with hero + const bool combatSpell = mySpell->isCombat(); + if(combatSpell == mySpell->isAdventure()) + { + logGlobal->error("Spell have invalid flags"); + return; + } + + const bool inCombat = owner->myInt->battleInt != nullptr; + const bool inCastle = owner->myInt->castleInt != nullptr; + + //battle spell on adv map or adventure map spell during combat => display infowindow, not cast + if((combatSpell ^ inCombat) || inCastle) + { + std::vector> hlp(1, std::make_shared(ComponentType::SPELL, mySpell->id)); + LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); + } + else if(combatSpell) + { + spells::detail::ProblemImpl problem; + if(mySpell->canBeCast(problem, owner->myInt->battleInt->getBattle().get(), spells::Mode::HERO, owner->myHero)) + { + owner->myInt->battleInt->castThisSpell(mySpell->id); + owner->fexitb(); + } + else + { + std::vector texts; + problem.getAll(texts); + if(!texts.empty()) + LOCPLINT->showInfoDialog(texts.front()); + else + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); + } + } + else //adventure spell + { + const CGHeroInstance * h = owner->myHero; + GH.windows().popWindows(1); + + auto guard = vstd::makeScopeGuard([this]() + { + owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; + owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage; + }); + + if(mySpell->getTargetType() == spells::AimType::LOCATION) + adventureInt->enterCastingMode(mySpell); + else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) + owner->myInt->cb->castSpell(h, mySpell->id); + else + logGlobal->error("Invalid spell target type"); + } + } +} + +void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) +{ + if(mySpell) + { + std::string dmgInfo; + auto causedDmg = owner->myInt->cb->estimateSpellDamage(mySpell, owner->myHero); + if(causedDmg == 0 || mySpell->id == SpellID::TITANS_LIGHTNING_BOLT) //Titan's Lightning Bolt already has damage info included + dmgInfo.clear(); + else + { + dmgInfo = CGI->generaltexth->allTexts[343]; + boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); + } + + CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(ComponentType::SPELL, mySpell->id)); + } +} + +void CSpellWindow::SpellArea::hover(bool on) +{ + if(mySpell) + { + if(on) + owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->getLevel()])); + else + owner->statusBar->clear(); + } +} + +void CSpellWindow::SpellArea::setSpell(const CSpell * spell) +{ + schoolBorder.reset(); + image->visible = false; + name->setText(""); + level->setText(""); + cost->setText(""); + mySpell = spell; + if(mySpell) + { + SpellSchool whichSchool; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, + schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); + auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); + + image->setFrame(mySpell->id); + image->visible = true; + + { + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + schoolBorder.reset(); + if (owner->selectedTab >= 4) + { + if (whichSchool.getNum() != SpellSchool()) + schoolBorder = std::make_shared(owner->schoolBorders.at(whichSchool.getNum()), schoolLevel); + } + else + schoolBorder = std::make_shared(owner->schoolBorders.at(owner->selectedTab), schoolLevel); + } + + ColorRGBA firstLineColor, secondLineColor; + if(spellCost > owner->myHero->mana) //hero cannot cast this spell + { + firstLineColor = Colors::WHITE; + secondLineColor = Colors::ORANGE; + } + else + { + firstLineColor = Colors::YELLOW; + secondLineColor = Colors::WHITE; + } + + name->color = firstLineColor; + name->setText(mySpell->getNameTranslated()); + + level->color = secondLineColor; + if(schoolLevel > 0) + { + boost::format fmt("%s/%s"); + fmt % CGI->generaltexth->allTexts[171 + mySpell->getLevel()]; + fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 + level->setText(fmt.str()); + } + else + level->setText(CGI->generaltexth->allTexts[171 + mySpell->getLevel()]); + + cost->color = secondLineColor; + boost::format costfmt("%s: %d"); + costfmt % CGI->generaltexth->allTexts[387] % spellCost; + cost->setText(costfmt.str()); + } +} diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index abd7f95a8..941de6428 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -1,117 +1,128 @@ -/* - * CSpellWindow.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 "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CSpell; - -VCMI_LIB_NAMESPACE_END - -class IImage; -class CAnimImage; -class CPicture; -class CLabel; -class CGStatusBar; -class CPlayerInterface; -class CSpellWindow; - -/// The spell window -class CSpellWindow : public CWindowObject -{ - class SpellArea : public CIntObject - { - const CSpell * mySpell; - int schoolLevel; //range: 0 none, 3 - expert - CSpellWindow * owner; - std::shared_ptr image; - std::shared_ptr schoolBorder; - std::shared_ptr name; - std::shared_ptr level; - std::shared_ptr cost; - public: - SpellArea(Rect pos, CSpellWindow * owner); - ~SpellArea(); - void setSpell(const CSpell * spell); - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - }; - - class InteractiveArea : public CIntObject - { - std::function onLeft; - CSpellWindow * owner; - - std::string hoverText; - std::string helpText; - public: - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - - 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; - - std::shared_ptr schoolTab; - std::shared_ptr schoolPicture; - - std::array, 12> spellAreas; - std::shared_ptr mana; - std::shared_ptr statusBar; - - std::vector> interactiveAreas; - - int sitesPerTabAdv[5]; - int sitesPerTabBattle[5]; - - bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed - uint8_t selectedTab; // 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools - int currentPage; //changes when corners are clicked - std::vector mySpells; //all spels in this spellbook - - const CGHeroInstance * myHero; //hero whose spells are presented - CPlayerInterface * myInt; - - void computeSpellsPerArea(); //recalculates spellAreas::mySpell - - void setCurrentPage(int value); - void turnPageLeft(); - void turnPageRight(); - -public: - CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); - ~CSpellWindow(); - - void fexitb(); - void fadvSpellsb(); - void fbattleSpellsb(); - void fmanaPtsb(); - - void fLcornerb(); - void fRcornerb(); - - void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools - int pagesWithinCurrentTab(); - - void keyPressed(EShortcut key) override; - - void show(Canvas & to) override; -}; +/* + * CSpellWindow.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 "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class IImage; +class CAnimation; +class CAnimImage; +class CPicture; +class CLabel; +class CGStatusBar; +class CPlayerInterface; +class CSpellWindow; + +/// The spell window +class CSpellWindow : public CWindowObject +{ + class SpellArea : public CIntObject + { + const CSpell * mySpell; + int schoolLevel; //range: 0 none, 3 - expert + CSpellWindow * owner; + std::shared_ptr image; + std::shared_ptr schoolBorder; + std::shared_ptr name; + std::shared_ptr level; + std::shared_ptr cost; + public: + SpellArea(Rect pos, CSpellWindow * owner); + ~SpellArea(); + void setSpell(const CSpell * spell); + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + }; + + class InteractiveArea : public CIntObject + { + std::function onLeft; + CSpellWindow * owner; + + std::string hoverText; + std::string helpText; + public: + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + + 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; + + std::shared_ptr schoolTab; + std::shared_ptr schoolPicture; + + std::array, 24> spellAreas; + std::shared_ptr mana; + std::shared_ptr statusBar; + + std::vector> interactiveAreas; + + bool isBigSpellbook; + int spellsPerPage; + int offL; + int offR; + int offRM; + int offT; + int offB; + + int sitesPerTabAdv[5]; + int sitesPerTabBattle[5]; + + bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed + uint8_t selectedTab; // 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools + int currentPage; //changes when corners are clicked + std::vector mySpells; //all spels in this spellbook + + const CGHeroInstance * myHero; //hero whose spells are presented + CPlayerInterface * myInt; + + void computeSpellsPerArea(); //recalculates spellAreas::mySpell + + void setCurrentPage(int value); + void turnPageLeft(); + void turnPageRight(); + + std::shared_ptr createBigSpellBook(); + +public: + CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); + ~CSpellWindow(); + + void fexitb(); + void fadvSpellsb(); + void fbattleSpellsb(); + void fmanaPtsb(); + + void fLcornerb(); + void fRcornerb(); + + void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools + int pagesWithinCurrentTab(); + + void keyPressed(EShortcut key) override; + + void show(Canvas & to) override; +}; diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 7787ec4de..d5a728dcb 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -12,315 +12,28 @@ #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" -#include "../widgets/Images.h" #include "../render/Canvas.h" -#include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/Buttons.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" -#include "../windows/InfoWindows.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/VCMI_Lib.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" -CTradeWindow::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) - : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos), - type(EType(-1)),// set to invalid, will be corrected in setType - id(ID), - serial(Serial), - left(Left) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - downSelection = false; - hlp = nullptr; - setType(Type); -} - -void CTradeWindow::CTradeableItem::setType(EType newType) -{ - if(type != newType) - { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - type = newType; - - if(getIndex() < 0) - { - image = std::make_shared(getFilename(), 0); - image->disable(); - } - else - { - image = std::make_shared(getFilename(), getIndex()); - } - } -} - -void CTradeWindow::CTradeableItem::setID(int newID) -{ - if (id != newID) - { - id = newID; - if (image) - { - int index = getIndex(); - if (index < 0) - image->disable(); - else - { - image->enable(); - image->setFrame(index); - } - } - } -} - -std::string CTradeWindow::CTradeableItem::getFilename() -{ - switch(type) - { - case RESOURCE: - return "RESOURCE"; - case PLAYER: - return "CREST58"; - case ARTIFACT_TYPE: - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return "artifact"; - case CREATURE: - return "TWCRPORT"; - default: - return ""; - } -} - -int CTradeWindow::CTradeableItem::getIndex() -{ - if (id < 0) - return -1; - - switch(type) - { - case RESOURCE: - case PLAYER: - return id; - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - case ARTIFACT_PLACEHOLDER: - return CGI->artifacts()->getByIndex(id)->getIconIndex(); - case CREATURE: - return CGI->creatures()->getByIndex(id)->getIconIndex(); - default: - return -1; - } -} - -void CTradeWindow::CTradeableItem::showAll(Canvas & to) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - - Point posToBitmap; - Point posToSubCenter; - - switch(type) - { - case RESOURCE: - posToBitmap = Point(19,9); - posToSubCenter = Point(36, 59); - break; - case CREATURE_PLACEHOLDER: - case CREATURE: - posToSubCenter = Point(29, 76); - // Positing of unit count is different in Altar of Sacrifice and Freelancer's Guild - if(mw->mode == EMarketMode::CREATURE_EXP && downSelection) - posToSubCenter.y += 5; - break; - case PLAYER: - posToSubCenter = Point(31, 76); - break; - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - posToSubCenter = Point(19, 55); - if(downSelection) - posToSubCenter.y += 8; - break; - case ARTIFACT_TYPE: - posToSubCenter = Point(19, 58); - break; - } - - if (image) - { - image->moveTo(pos.topLeft() + posToBitmap); - CIntObject::showAll(to); - } - - to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); -} - -void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - if(type == ARTIFACT_PLACEHOLDER) - { - CAltarWindow *aw = static_cast(mw); - const auto pickedArtInst = aw->getPickedArtifact(); - - if(pickedArtInst) - { - aw->arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); - aw->moveArtToAltar(this->shared_from_this(), pickedArtInst); - } - else if(const CArtifactInstance *art = getArtInstance()) - { - const auto hero = aw->arts->getHero(); - const auto slot = hero->getSlotByInstance(art); - assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slot), - ArtifactLocation(hero, ArtifactPosition::TRANSITION_POS)); - aw->arts->pickedArtFromSlot = slot; - aw->arts->artifactsOnAltar.erase(art); - setID(-1); - subtitle.clear(); - aw->deal->block(!aw->arts->artifactsOnAltar.size()); - } - - aw->calcTotalExp(); - return; - } - if(left) - { - if(mw->hLeft != this->shared_from_this()) - mw->hLeft = this->shared_from_this(); - else - return; - } - else - { - if(mw->hRight != this->shared_from_this()) - mw->hRight = this->shared_from_this(); - else - return; - } - mw->selectionChanged(left); -} - -void CTradeWindow::CTradeableItem::showAllAt(const Point &dstPos, const std::string &customSub, Canvas & to) -{ - Rect oldPos = pos; - std::string oldSub = subtitle; - downSelection = true; - - moveTo(dstPos); - subtitle = customSub; - showAll(to); - - downSelection = false; - moveTo(oldPos.topLeft()); - subtitle = oldSub; -} - -void CTradeWindow::CTradeableItem::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - return; - } - - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); - break; - case ARTIFACT_PLACEHOLDER: - if(id < 0) - GH.statusbar()->write(CGI->generaltexth->zelp[582].first); - else - GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); - break; - } -} - -void CTradeWindow::CTradeableItem::showPopupWindow(const Point & cursorPosition) -{ - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - //GH.statusbar->print(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->namePl)); - break; - case ARTIFACT_TYPE: - case 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. - if(id >= 0) - CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); - break; - } -} - -std::string CTradeWindow::CTradeableItem::getName(int number) const -{ - switch(type) - { - case PLAYER: - return CGI->generaltexth->capColors[id]; - case RESOURCE: - return CGI->generaltexth->restypes[id]; - case CREATURE: - if(number == 1) - return CGI->creh->objects[id]->getNameSingularTranslated(); - else - return CGI->creh->objects[id]->getNamePluralTranslated(); - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - return CGI->artifacts()->getByIndex(id)->getNameTranslated(); - } - logGlobal->error("Invalid trade item type: %d", (int)type); - return ""; -} - -const CArtifactInstance * CTradeWindow::CTradeableItem::getArtInstance() const -{ - switch(type) - { - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return hlp; - default: - return nullptr; - } -} - -void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) -{ - assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE); - hlp = art; - if(art) - setID(art->artType->getId()); - else - setID(-1); -} - -CTradeWindow::CTradeWindow(std::string bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode::EMarketMode Mode): +CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, const std::function & onWindowClosed, EMarketMode Mode): + CTradeBase(Market, Hero), CWindowObject(PLAYER_COLORED, bgName), - market(Market), - hero(Hero), + onWindowClosed(onWindowClosed), readyToTrade(false) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -353,14 +66,6 @@ void CTradeWindow::initTypes() itemsType[1] = ARTIFACT_INSTANCE; itemsType[0] = RESOURCE; break; - case EMarketMode::CREATURE_EXP: - itemsType[1] = CREATURE; - itemsType[0] = CREATURE_PLACEHOLDER; - break; - case EMarketMode::ARTIFACT_EXP: - itemsType[1] = ARTIFACT_TYPE; - itemsType[0] = ARTIFACT_PLACEHOLDER; - break; } } @@ -401,6 +106,26 @@ void CTradeWindow::initItems(bool Left) auto item = std::make_shared(pos[j].topLeft(), itemsType[Left], id, Left, j); item->pos = pos[j] + this->pos.topLeft(); + if(mode != EMarketMode::ARTIFACT_EXP) + item->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void + { + if(altarSlot->left) + { + if(hLeft != altarSlot) + hLeft = altarSlot; + else + return; + } + else + { + if(hRight != altarSlot) + hRight = altarSlot; + else + return; + } + selectionChanged(altarSlot->left); + }; + items[Left].push_back(item); } vstd::clear_pointer(ids); @@ -412,9 +137,6 @@ std::vector *CTradeWindow::getItemsIds(bool Left) { std::vector *ids = nullptr; - if(mode == EMarketMode::ARTIFACT_EXP) - return new std::vector(22, -1); - if(Left) { switch(itemsType[1]) @@ -443,7 +165,9 @@ std::vector *CTradeWindow::getItemsIds(bool Left) break; case ARTIFACT_TYPE: - ids = new std::vector(market->availableItemsIds(mode)); + ids = new std::vector; + for (auto const & item : market->availableItemsIds(mode)) + ids->push_back(item.getNum()); break; } } @@ -453,50 +177,68 @@ std::vector *CTradeWindow::getItemsIds(bool Left) void CTradeWindow::getPositionsFor(std::vector &poss, bool Left, EType type) const { - if(mode == EMarketMode::ARTIFACT_EXP && !Left) - { - //22 boxes, 5 in row, last row: two boxes centered - int h, w, x, y, dx, dy; - h = w = 44; - x = 317; - y = 53; - dx = 54; - dy = 70; - for (int i = 0; i < 4 ; i++) - for (int j = 0; j < 5 ; j++) - poss.push_back(Rect(x + dx*j, y + dy*i, w, h)); + //seven boxes: + // X X X + // X X X + // X + int h = 0, w = 0, x = 0, y = 0, dx = 0, dy = 0; - poss.push_back(Rect((int)(x + dx * 1.5), (y + dy * 4), w, h)); - poss.push_back(Rect((int)(x + dx * 2.5), (y + dy * 4), w, h)); + switch(type) + { + case RESOURCE: + dx = 82; + dy = 79; + x = 39; + y = 180; + h = 68; + w = 70; + break; + case PLAYER: + dx = 83; + dy = 118; + h = 64; + w = 58; + x = 44; + y = 83; + assert(!Left); + break; + case CREATURE://45,123 + x = 45; + y = 123; + w = 58; + h = 64; + dx = 83; + dy = 98; + assert(Left); + break; + case ARTIFACT_TYPE://45,123 + x = 340 - 289; + y = 180; + w = 44; + h = 44; + dx = 83; + dy = 79; + break; } - else + int leftToRightOffset = 289; + + const std::vector tmp = { - //seven boxes: - // X X X - // X X X - // X - int h, w, x, y, dx, dy; - int leftToRightOffset; - getBaseForPositions(type, dx, dy, x, y, h, w, !Left, leftToRightOffset); + Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) ) + }; - const std::vector tmp = - { - Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) ) - }; + vstd::concatenate(poss, tmp); - vstd::concatenate(poss, tmp); - - if(!Left) - { - for(Rect &r : poss) - r.x += leftToRightOffset; - } + if(!Left) + { + for(Rect &r : poss) + r.x += leftToRightOffset; } } @@ -555,51 +297,36 @@ void CTradeWindow::showAll(Canvas & to) if(readyToTrade) { if(hLeft) - hLeft->showAllAt(pos.topLeft() + selectionOffset(true), selectionSubtitle(true), to); + hLeft->showAllAt(pos.topLeft() + selectionOffset(true), updateSlotSubtitle(true), to); if(hRight) - hRight->showAllAt(pos.topLeft() + selectionOffset(false), selectionSubtitle(false), to); + hRight->showAllAt(pos.topLeft() + selectionOffset(false), updateSlotSubtitle(false), to); } } -void CTradeWindow::removeItems(const std::set> & toRemove) +void CTradeWindow::close() { - for(auto item : toRemove) - removeItem(item); + if (onWindowClosed) + onWindowClosed(); + + CWindowObject::close(); } -void CTradeWindow::removeItem(std::shared_ptr item) -{ - items[item->left] -= item; - - if(hRight == item) - { - hRight.reset(); - selectionChanged(false); - } -} - -void CTradeWindow::getEmptySlots(std::set> & toRemove) -{ - for(auto item : items[1]) - if(!hero->getStackCount(SlotID(item->serial))) - toRemove.insert(item); -} - -void CTradeWindow::setMode(EMarketMode::EMarketMode Mode) +void CTradeWindow::setMode(EMarketMode Mode) { const IMarket *m = market; const CGHeroInstance *h = hero; + const auto functor = onWindowClosed; + onWindowClosed = nullptr; // don't call on closing of this window - pass it to next window close(); switch(Mode) { case EMarketMode::CREATURE_EXP: case EMarketMode::ARTIFACT_EXP: - GH.windows().createAndPushWindow(m, h, Mode); break; default: - GH.windows().createAndPushWindow(m, h, Mode); + GH.windows().createAndPushWindow(m, h, functor, Mode); break; } } @@ -616,27 +343,27 @@ void CTradeWindow::artifactSelected(CHeroArtPlace *slot) selectionChanged(true); } -std::string CMarketplaceWindow::getBackgroundForMode(EMarketMode::EMarketMode mode) +ImagePath CMarketplaceWindow::getBackgroundForMode(EMarketMode mode) { switch(mode) { case EMarketMode::RESOURCE_RESOURCE: - return "TPMRKRES.bmp"; + return ImagePath::builtin("TPMRKRES.bmp"); case EMarketMode::RESOURCE_PLAYER: - return "TPMRKPTS.bmp"; + return ImagePath::builtin("TPMRKPTS.bmp"); case EMarketMode::CREATURE_RESOURCE: - return "TPMRKCRS.bmp"; + return ImagePath::builtin("TPMRKCRS.bmp"); case EMarketMode::RESOURCE_ARTIFACT: - return "TPMRKABS.bmp"; + return ImagePath::builtin("TPMRKABS.bmp"); case EMarketMode::ARTIFACT_RESOURCE: - return "TPMRKASS.bmp"; + return ImagePath::builtin("TPMRKASS.bmp"); } assert(0); - return ""; + return {}; } -CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode) - : CTradeWindow(getBackgroundForMode(Mode), Market, Hero, Mode) +CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode) + : CTradeWindow(getBackgroundForMode(Mode), Market, Hero, onWindowClosed, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -655,10 +382,10 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta title = (*CGI->townh)[ETownType::STRONGHOLD]->town->buildings[BuildingID::FREELANCERS_GUILD]->getNameTranslated(); break; case EMarketMode::RESOURCE_ARTIFACT: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); break; case EMarketMode::ARTIFACT_RESOURCE: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); // create image that copies part of background containing slot MISC_1 into position of slot MISC_5 // this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing @@ -680,19 +407,19 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta arts = std::make_shared(Point(-361, 46)); arts->selectArtCallback = std::bind(&CTradeWindow::artifactSelected, this, _1); arts->setHero(hero); - addSet(arts); + addSetAndCallbacks(arts); } initItems(false); initItems(true); - ok = std::make_shared(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN); - deal = std::make_shared(Point(307, 520), "TPMRKB.DEF", CGI->generaltexth->zelp[595], [&](){ makeDeal(); } ); + ok = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN); + deal = std::make_shared(Point(307, 520), AnimationPath::builtin("TPMRKB.DEF"), CGI->generaltexth->zelp[595], [&](){ makeDeal(); } ); deal->block(true); if(sliderNeeded) { slider = std::make_shared(Point(231, 490), 137, std::bind(&CMarketplaceWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(229, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[596], [&](){ setMax(); }); + max = std::make_shared(Point(229, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[596], [&](){ setMax(); }); max->block(true); } else @@ -740,15 +467,15 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta int specialOffset = mode == EMarketMode::ARTIFACT_RESOURCE ? 35 : 0; //in selling artifacts mode we need to move res-res and art-res buttons down if(printButtonFor(EMarketMode::RESOURCE_PLAYER)) - buttons.push_back(std::make_shared(Point(18, 520),"TPMRKBU1.DEF", CGI->generaltexth->zelp[612], [&](){ setMode(EMarketMode::RESOURCE_PLAYER);})); + buttons.push_back(std::make_shared(Point(18, 520),AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], [&](){ setMode(EMarketMode::RESOURCE_PLAYER);})); if(printButtonFor(EMarketMode::RESOURCE_RESOURCE)) - buttons.push_back(std::make_shared(Point(516, 450 + specialOffset),"TPMRKBU5.DEF", CGI->generaltexth->zelp[605], [&](){ setMode(EMarketMode::RESOURCE_RESOURCE);})); + buttons.push_back(std::make_shared(Point(516, 450 + specialOffset),AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], [&](){ setMode(EMarketMode::RESOURCE_RESOURCE);})); if(printButtonFor(EMarketMode::CREATURE_RESOURCE)) - buttons.push_back(std::make_shared(Point(516, 485),"TPMRKBU4.DEF", CGI->generaltexth->zelp[599], [&](){ setMode(EMarketMode::CREATURE_RESOURCE);})); + buttons.push_back(std::make_shared(Point(516, 485),AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], [&](){ setMode(EMarketMode::CREATURE_RESOURCE);})); if(printButtonFor(EMarketMode::RESOURCE_ARTIFACT)) - buttons.push_back(std::make_shared(Point(18, 450 + specialOffset),"TPMRKBU2.DEF", CGI->generaltexth->zelp[598], [&](){ setMode(EMarketMode::RESOURCE_ARTIFACT);})); + buttons.push_back(std::make_shared(Point(18, 450 + specialOffset),AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], [&](){ setMode(EMarketMode::RESOURCE_ARTIFACT);})); if(printButtonFor(EMarketMode::ARTIFACT_RESOURCE)) - buttons.push_back(std::make_shared(Point(18, 485),"TPMRKBU3.DEF", CGI->generaltexth->zelp[613], [&](){ setMode(EMarketMode::ARTIFACT_RESOURCE);})); + buttons.push_back(std::make_shared(Point(18, 485),AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], [&](){ setMode(EMarketMode::ARTIFACT_RESOURCE);})); updateTraderText(); } @@ -794,14 +521,27 @@ void CMarketplaceWindow::makeDeal() if(allowDeal) { - if(slider) + switch(mode) { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, slider->getValue() * r1, hero); + case EMarketMode::RESOURCE_RESOURCE: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); slider->scrollTo(0); - } - else - { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, r2, hero); + break; + case EMarketMode::CREATURE_RESOURCE: + LOCPLINT->cb->trade(market, mode, SlotID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + case EMarketMode::RESOURCE_PLAYER: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), PlayerColor(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + + case EMarketMode::RESOURCE_ARTIFACT: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), ArtifactID(hRight->id), r2, hero); + break; + case EMarketMode::ARTIFACT_RESOURCE: + LOCPLINT->cb->trade(market, mode, ArtifactInstanceID(leftIdToSend), GameResID(hRight->id), r2, hero); + break; } } @@ -870,12 +610,30 @@ void CMarketplaceWindow::selectionChanged(bool side) redraw(); } -bool CMarketplaceWindow::printButtonFor(EMarketMode::EMarketMode M) const +bool CMarketplaceWindow::printButtonFor(EMarketMode M) const { - return market->allowsTrade(M) && M != mode && (hero || ( M != EMarketMode::CREATURE_RESOURCE && M != EMarketMode::RESOURCE_ARTIFACT && M != EMarketMode::ARTIFACT_RESOURCE )); + if (!market->allowsTrade(M)) + return false; + + if (M == mode) + return false; + + if ( M == EMarketMode::RESOURCE_RESOURCE || M == EMarketMode::RESOURCE_PLAYER) + { + auto * town = dynamic_cast(market); + + if (town) + return town->getOwner() == LOCPLINT->playerID; + else + return true; + } + else + { + return hero != nullptr; + } } -void CMarketplaceWindow::garrisonChanged() +void CMarketplaceWindow::updateGarrison() { if(mode != EMarketMode::CREATURE_RESOURCE) return; @@ -892,10 +650,10 @@ void CMarketplaceWindow::artifactsChanged(bool Left) if(mode != EMarketMode::RESOURCE_ARTIFACT) return; - std::vector available = market->availableItemsIds(mode); + std::vector available = market->availableItemsIds(mode); std::set> toRemove; for(auto item : items[0]) - if(!vstd::contains(available, item->id)) + if(!vstd::contains(available, ArtifactID(item->id))) toRemove.insert(item); removeItems(toRemove); @@ -905,7 +663,7 @@ void CMarketplaceWindow::artifactsChanged(bool Left) redraw(); } -std::string CMarketplaceWindow::selectionSubtitle(bool Left) const +std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const { if(Left) { @@ -982,49 +740,6 @@ void CMarketplaceWindow::resourceChanged() initSubs(true); } -void CMarketplaceWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - switch(type) - { - case RESOURCE: - dx = 82; - dy = 79; - x = 39; - y = 180; - h = 68; - w = 70; - break; - case PLAYER: - dx = 83; - dy = 118; - h = 64; - w = 58; - x = 44; - y = 83; - assert(Right); - break; - case CREATURE://45,123 - x = 45; - y = 123; - w = 58; - h = 64; - dx = 83; - dy = 98; - assert(!Right); - break; - case ARTIFACT_TYPE://45,123 - x = 340-289; - y = 180; - w = 44; - h = 44; - dx = 83; - dy = 79; - break; - } - - leftToRightOffset = 289; -} - void CMarketplaceWindow::updateTraderText() { if(readyToTrade) @@ -1074,431 +789,3 @@ void CMarketplaceWindow::updateTraderText() } traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]); } - -CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode) - : CTradeWindow((Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, Mode) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(Mode == EMarketMode::CREATURE_EXP) - { - //%s's Creatures - labels.push_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, - boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); - - //Altar of Sacrifice - labels.push_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); - - //To sacrifice creatures, move them from your army on to the Altar and click Sacrifice - new CTextBox(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW); - - slider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(147, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider)); - - sacrificedUnits.resize(GameConstants::ARMY_SIZE, 0); - sacrificeAll = std::make_shared(Point(393, 520), "ALTARMY.DEF", CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this)); - - initItems(true); - mimicCres(); - } - else - { - //Sacrifice artifacts for experience - labels.push_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); - //%s's Creatures - labels.push_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); - - sacrificeAll = std::make_shared(Point(393, 520), "ALTFILL.DEF", CGI->generaltexth->zelp[571], std::bind(&CAltarWindow::SacrificeAll,this)); - sacrificeAll->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); - sacrificeBackpack = std::make_shared(Point(147, 520), "ALTEMBK.DEF", CGI->generaltexth->zelp[570], std::bind(&CAltarWindow::SacrificeBackpack,this)); - sacrificeBackpack->block(hero->artifactsInBackpack.empty()); - - arts = std::make_shared(Point(-365, -12)); - arts->setHero(hero); - addSet(arts); - - initItems(true); - initItems(false); - artIcon = std::make_shared("ARTIFACT", 0, 0, 281, 442); - artIcon->disable(); - } - - //Experience needed to reach next level - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - //Total experience on the Altar - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - - statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - ok = std::make_shared(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN); - - deal = std::make_shared(Point(269, 520), "ALTSACR.DEF", CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this)); - - if(Mode == EMarketMode::CREATURE_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), "ALTART.DEF", CGI->generaltexth->zelp[580], std::bind(&CTradeWindow::setMode,this, EMarketMode::ARTIFACT_EXP)); - if(Hero->getAlignment() == ::EAlignment::EVIL) - changeMode->block(true); - buttons.push_back(changeMode); - } - else if(Mode == EMarketMode::ARTIFACT_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), "ALTSACC.DEF", CGI->generaltexth->zelp[572], std::bind(&CTradeWindow::setMode,this, EMarketMode::CREATURE_EXP)); - if(Hero->getAlignment() == ::EAlignment::GOOD) - changeMode->block(true); - buttons.push_back(changeMode); - } - - expPerUnit.resize(GameConstants::ARMY_SIZE, 0); - getExpValues(); - - expToLevel = std::make_shared(73, 475, FONT_SMALL, ETextAlignment::CENTER); - expOnAltar = std::make_shared(73, 543, FONT_SMALL, ETextAlignment::CENTER); - - setExpToLevel(); - calcTotalExp(); - blockTrade(); -} - -CAltarWindow::~CAltarWindow() = default; - -void CAltarWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - leftToRightOffset = 289; - x = 45; - y = 110; - w = 58; - h = 64; - dx = 83; - dy = 98; -} - -void CAltarWindow::sliderMoved(int to) -{ - if(hLeft) - sacrificedUnits[hLeft->serial] = to; - if(hRight) - updateRight(hRight); - - deal->block(!to); - calcTotalExp(); - redraw(); -} - -void CAltarWindow::makeDeal() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - blockTrade(); - slider->scrollTo(0); - - std::vector ids; - std::vector toSacrifice; - - for(int i = 0; i < sacrificedUnits.size(); i++) - { - if(sacrificedUnits[i]) - { - ids.push_back(i); - toSacrifice.push_back(sacrificedUnits[i]); - } - } - - LOCPLINT->cb->trade(market, mode, ids, {}, toSacrifice, hero); - - for(int& val : sacrificedUnits) - val = 0; - - for(auto item : items[0]) - { - item->setType(CREATURE_PLACEHOLDER); - item->subtitle = ""; - } - } - else - { - std::vector positions; - for(const CArtifactInstance * art : arts->artifactsOnAltar) - { - positions.push_back(hero->getSlotByInstance(art)); - } - std::sort(positions.begin(), positions.end(), std::greater<>()); - - LOCPLINT->cb->trade(market, mode, positions, {}, {}, hero); - arts->artifactsOnAltar.clear(); - - for(auto item : items[0]) - { - item->setID(-1); - item->subtitle = ""; - } - - //arts->scrollBackpack(0); - deal->block(true); - } - - calcTotalExp(); -} - -void CAltarWindow::SacrificeAll() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - bool movedAnything = false; - for(auto item : items[1]) - sacrificedUnits[item->serial] = hero->getStackCount(SlotID(item->serial)); - - sacrificedUnits[items[1].front()->serial]--; - - for(auto item : items[0]) - { - updateRight(item); - if(item->type == CREATURE) - movedAnything = true; - } - - deal->block(!movedAnything); - calcTotalExp(); - } - else - { - std::vector> artsForMove; - for(const auto& slotInfo : arts->visibleArtSet.artifactsWorn) - { - if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) - artsForMove.push_back(slotInfo.second.artifact); - } - for(auto artInst : artsForMove) - moveArtToAltar(nullptr, artInst); - arts->updateWornSlots(); - SacrificeBackpack(); - } - redraw(); -} - -void CAltarWindow::selectionChanged(bool side) -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - int stackCount = 0; - for (int i = 0; i < GameConstants::ARMY_SIZE; i++) - if(hero->getStackCount(SlotID(i)) > sacrificedUnits[i]) - stackCount++; - - slider->setAmount(hero->getStackCount(SlotID(hLeft->serial)) - (stackCount == 1)); - slider->block(!slider->getAmount()); - slider->scrollTo(sacrificedUnits[hLeft->serial]); - max->block(!slider->getAmount()); - selectOppositeItem(side); - readyToTrade = true; - redraw(); -} - -void CAltarWindow::selectOppositeItem(bool side) -{ - bool oppositeSide = !side; - int pos = vstd::find_pos(items[side], side ? hLeft : hRight); - int oppositePos = vstd::find_pos(items[oppositeSide], oppositeSide ? hLeft : hRight); - - if(pos >= 0 && pos != oppositePos) - { - if(oppositeSide) - hLeft = items[oppositeSide][pos]; - else - hRight = items[oppositeSide][pos]; - - selectionChanged(oppositeSide); - } -} - -void CAltarWindow::mimicCres() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - std::vector positions; - getPositionsFor(positions, false, CREATURE); - - for(auto item : items[1]) - { - auto hlp = std::make_shared(positions[item->serial].topLeft(), CREATURE_PLACEHOLDER, item->id, false, item->serial); - hlp->pos = positions[item->serial] + this->pos.topLeft(); - items[0].push_back(hlp); - } -} - -Point CAltarWindow::selectionOffset(bool Left) const -{ - if(Left) - return Point(150, 421); - else - return Point(396, 421); -} - -std::string CAltarWindow::selectionSubtitle(bool Left) const -{ - if(Left && slider && hLeft) - return std::to_string(slider->getValue()); - else if(!Left && hRight) - return hRight->subtitle; - else - return ""; -} - -void CAltarWindow::artifactsChanged(bool left) -{ - -} - -void CAltarWindow::garrisonChanged() -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - std::set> empty; - getEmptySlots(empty); - - removeItems(empty); - - initSubs(true); - getExpValues(); -} - -void CAltarWindow::getExpValues() -{ - int dump; - for(auto item : items[1]) - { - if(item->id >= 0) - market->getOffer(item->id, 0, dump, expPerUnit[item->serial], EMarketMode::CREATURE_EXP); - } -} - -void CAltarWindow::calcTotalExp() -{ - int val = 0; - if(mode == EMarketMode::CREATURE_EXP) - { - for (int i = 0; i < sacrificedUnits.size(); i++) - { - val += expPerUnit[i] * sacrificedUnits[i]; - } - } - else - { - auto artifactsOfHero = std::dynamic_pointer_cast(arts); - for(const CArtifactInstance * art : artifactsOfHero->artifactsOnAltar) - { - int dmp, valOfArt; - market->getOffer(art->artType->getId(), 0, dmp, valOfArt, mode); - val += valOfArt; //WAS val += valOfArt * arts->artifactsOnAltar.count(*i); - } - } - val = static_cast(hero->calculateXp(val)); - expOnAltar->setText(std::to_string(val)); -} - -void CAltarWindow::setExpToLevel() -{ - expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(hero->exp)+1) - hero->exp)); -} - -void CAltarWindow::blockTrade() -{ - hLeft = hRight = nullptr; - readyToTrade = false; - if(slider) - { - slider->block(true); - max->block(true); - } - deal->block(true); -} - -void CAltarWindow::updateRight(std::shared_ptr toUpdate) -{ - int val = sacrificedUnits[toUpdate->serial]; - toUpdate->setType(val ? CREATURE : CREATURE_PLACEHOLDER); - toUpdate->subtitle = val ? boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(val * expPerUnit[toUpdate->serial]))) : ""; //%s exp -} - -int CAltarWindow::firstFreeSlot() -{ - int ret = -1; - while(items[0][++ret]->id >= 0 && ret + 1 < items[0].size()); - return items[0][ret]->id == -1 ? ret : -1; -} - -void CAltarWindow::SacrificeBackpack() -{ - while(!arts->visibleArtSet.artifactsInBackpack.empty()) - { - if(!putOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact)) - break; - }; - calcTotalExp(); -} - -void CAltarWindow::artifactPicked() -{ - redraw(); -} - -void CAltarWindow::showAll(Canvas & to) -{ - CTradeWindow::showAll(to); - if(mode == EMarketMode::ARTIFACT_EXP && arts) - { - if(auto pickedArt = arts->getPickedArtifact()) - { - artIcon->setFrame(pickedArt->artType->getIconIndex()); - artIcon->showAll(to); - - int dmp, val; - market->getOffer(pickedArt->getTypeId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - to.drawText(Point(304, 498), FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, std::to_string(val)); - } - } -} - -bool CAltarWindow::putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(!art->artType->isTradable()) //special art - { - logGlobal->warn("Cannot put special artifact on altar!"); - return false; - } - - if(!altarSlot || altarSlot->id != -1) - { - int slotIndex = firstFreeSlot(); - if(slotIndex < 0) - { - logGlobal->warn("No free slots on altar!"); - return false; - } - altarSlot = items[0][slotIndex]; - } - - int dmp, val; - market->getOffer(art->artType->getId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - arts->artifactsOnAltar.insert(art); - arts->deleteFromVisible(art); - altarSlot->setArtInstance(art); - altarSlot->subtitle = std::to_string(val); - - deal->block(false); - return true; -} - -void CAltarWindow::moveArtToAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(putOnAltar(altarSlot, art)) - { - CCS->curh->dragAndDropCursor(nullptr); - arts->unmarkSlots(); - } -} diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 396b6e435..04ea7529c 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -9,108 +9,47 @@ */ #pragma once +#include "../widgets/CTradeBase.h" #include "../widgets/CWindowWithArtifacts.h" #include "CWindowObject.h" -#include "../../lib/FunctionList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class IMarket; - -VCMI_LIB_NAMESPACE_END class CSlider; -class CTextBox; -class CPicture; class CGStatusBar; -class CTradeWindow : public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice +class CTradeWindow : public CTradeBase, public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice { public: - enum EType - { - RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE - }; - - class CTradeableItem : public CIntObject, public std::enable_shared_from_this - { - std::shared_ptr image; - std::string getFilename(); - int getIndex(); - public: - const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact - EType type; - int id; - const int serial; - const bool left; - std::string subtitle; //empty if default - - void setType(EType newType); - void setID(int newID); - - const CArtifactInstance * getArtInstance() const; - void setArtInstance(const CArtifactInstance * art); - - CFunctionList callback; - bool downSelection; - - void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); - - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - void showAll(Canvas & to) override; - void clickPressed(const Point & cursorPosition) override; - std::string getName(int number = -1) const; - CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial); - }; - - const IMarket * market; - const CGHeroInstance * hero; - - //all indexes: 1 = left, 0 = right - std::array>, 2> items; - - //highlighted items (nullptr if no highlight) - std::shared_ptr hLeft; - std::shared_ptr hRight; EType itemsType[2]; - EMarketMode::EMarketMode mode; + EMarketMode mode; std::shared_ptr ok; std::shared_ptr max; - std::shared_ptr deal; std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; - CTradeWindow(std::string bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode); //c + CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); //c void showAll(Canvas & to) override; + void close() override; void initSubs(bool Left); void initTypes(); void initItems(bool Left); std::vector *getItemsIds(bool Left); //nullptr if default void getPositionsFor(std::vector &poss, bool Left, EType type) const; - void removeItems(const std::set> & toRemove); - void removeItem(std::shared_ptr item); - void getEmptySlots(std::set> & toRemove); - void setMode(EMarketMode::EMarketMode Mode); //mode setter + void setMode(EMarketMode Mode); //mode setter void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot - - virtual void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const = 0; virtual void selectionChanged(bool side) = 0; //true == left virtual Point selectionOffset(bool Left) const = 0; - virtual std::string selectionSubtitle(bool Left) const = 0; - virtual void garrisonChanged() = 0; + virtual std::string updateSlotSubtitle(bool Left) const = 0; + virtual void updateGarrison() = 0; virtual void artifactsChanged(bool left) = 0; protected: + std::function onWindowClosed; std::shared_ptr statusBar; - std::vector> labels; std::vector> images; - std::vector> buttons; - std::vector> texts; }; class CMarketplaceWindow : public CTradeWindow @@ -118,9 +57,9 @@ class CMarketplaceWindow : public CTradeWindow std::shared_ptr titleLabel; std::shared_ptr arts; - bool printButtonFor(EMarketMode::EMarketMode M) const; + bool printButtonFor(EMarketMode M) const; - std::string getBackgroundForMode(EMarketMode::EMarketMode mode); + ImagePath getBackgroundForMode(EMarketMode mode); public: int r1, r2; //suggested amounts of traded resources bool madeTransaction; //if player made at least one transaction @@ -128,64 +67,16 @@ public: void setMax(); void sliderMoved(int to); - void makeDeal(); + void makeDeal() override; void selectionChanged(bool side) override; //true == left - CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero = nullptr, EMarketMode::EMarketMode Mode = EMarketMode::RESOURCE_RESOURCE); + CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); ~CMarketplaceWindow(); Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; + std::string updateSlotSubtitle(bool Left) const override; - void garrisonChanged() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) + void updateGarrison() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) void artifactsChanged(bool left) override; void resourceChanged(); - - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; void updateTraderText(); }; - -class CAltarWindow : public CTradeWindow -{ - std::shared_ptr artIcon; -public: - std::vector sacrificedUnits; //[slot_nr] -> how many creatures from that slot will be sacrificed - std::vector expPerUnit; - - std::shared_ptr sacrificeAll; - std::shared_ptr sacrificeBackpack; - std::shared_ptr expToLevel; - std::shared_ptr expOnAltar; - std::shared_ptr arts; - - CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode); - ~CAltarWindow(); - - void getExpValues(); - - void selectionChanged(bool side) override; //true == left - void selectOppositeItem(bool side); - void SacrificeAll(); - void SacrificeBackpack(); - - void putOnAltar(int backpackIndex); - bool putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); - void makeDeal(); - void showAll(Canvas & to) override; - - void blockTrade(); - void sliderMoved(int to); - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; - void mimicCres(); - - Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; - void garrisonChanged() override; - void artifactsChanged(bool left) override; - void calcTotalExp(); - void setExpToLevel(); - void updateRight(std::shared_ptr toUpdate); - - void artifactPicked(); - int firstFreeSlot(); - void moveArtToAltar(std::shared_ptr, const CArtifactInstance * art); -}; diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp new file mode 100644 index 000000000..72f945547 --- /dev/null +++ b/client/windows/CTutorialWindow.cpp @@ -0,0 +1,123 @@ +/* + * CTutorialWindow.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 "CTutorialWindow.h" + +#include "../eventsSDL/InputHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CondSh.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../CPlayerInterface.h" +#include "../CGameInfo.h" +#include "../CVideoHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Images.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.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; + + pos = Rect(pos.x, pos.y, 380, 400); //video: 320x240 + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + + updateShadow(); + + center(); + + addUsedEvents(LCLICK); + + if(mode == TutorialMode::TOUCH_ADVENTUREMAP) videos = { "RightClick", "MapPanning", "MapZooming", "RadialWheel" }; + else if(mode == TutorialMode::TOUCH_BATTLE) videos = { "BattleDirection", "BattleDirectionAbort", "AbortSpell" }; + + labelTitle = std::make_shared(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title")); + labelInformation = std::make_shared(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, ""); + buttonOk = std::make_shared(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_ACCEPT); //62x28 + buttonLeft = std::make_shared(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 + buttonRight = std::make_shared(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 + + setContent(); +} + +void CTutorialWindow::setContent() +{ + video = "tutorial/" + videos[page]; + + buttonLeft->block(page<1); + buttonRight->block(page>videos.size() - 2); + + labelInformation->setText(CGI->generaltexth->translate("vcmi.tutorialWindow.decription." + videos[page])); +} + +void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) +{ + if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) + { + if(LOCPLINT) + LOCPLINT->showingDialog->set(true); + GH.windows().pushWindow(std::make_shared(m)); + + Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)]; + s->Bool() = true; + } +} + +void CTutorialWindow::exit() +{ + if(LOCPLINT) + LOCPLINT->showingDialog->setn(false); + + close(); +} + +void CTutorialWindow::next() +{ + page++; + setContent(); + deactivate(); + activate(); +} + +void CTutorialWindow::previous() +{ + page--; + setContent(); + 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 new file mode 100644 index 000000000..d002f9aed --- /dev/null +++ b/client/windows/CTutorialWindow.h @@ -0,0 +1,54 @@ +/* + * CTutorialWindow.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" + +class CFilledTexture; +class CButton; +class CLabel; +class CMultiLineLabel; + +enum TutorialMode +{ + TOUCH_ADVENTUREMAP, + TOUCH_BATTLE +}; + +class CTutorialWindow : public CWindowObject +{ + TutorialMode mode; + std::shared_ptr background; + + std::shared_ptr buttonOk; + std::shared_ptr buttonLeft; + std::shared_ptr buttonRight; + + std::shared_ptr labelTitle; + std::shared_ptr labelInformation; + + std::string video; + std::vector videos; + + int page; + + void exit(); + void next(); + void previous(); + void setContent(); + +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 e164090f4..231213219 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -20,6 +20,7 @@ #include "../windows/CMessage.h" #include "../renderSDL/SDL_PixelAccess.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/Canvas.h" #include "../CGameInfo.h" @@ -33,12 +34,13 @@ #include -CWindowObject::CWindowObject(int options_, std::string imageName, Point centerAt): +CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point centerAt): WindowBase(0, Point()), options(options_), background(createBg(imageName, options & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + 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; @@ -54,12 +56,13 @@ CWindowObject::CWindowObject(int options_, std::string imageName, Point centerAt setShadow(true); } -CWindowObject::CWindowObject(int options_, std::string imageName): +CWindowObject::CWindowObject(int options_, const ImagePath & imageName): WindowBase(0, Point()), options(options_), background(createBg(imageName, options_ & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + 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; @@ -81,7 +84,7 @@ CWindowObject::~CWindowObject() CCS->curh->show(); } -std::shared_ptr CWindowObject::createBg(std::string imageName, bool playerColored) +std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, bool playerColored) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); @@ -95,7 +98,7 @@ std::shared_ptr CWindowObject::createBg(std::string imageName, bool pl return image; } -void CWindowObject::setBackground(std::string filename) +void CWindowObject::setBackground(const ImagePath & filename) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); @@ -210,9 +213,9 @@ void CWindowObject::setShadow(bool on) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowCorner), Point(shadowPos.x, shadowPos.y))); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowRight ), Point(shadowPos.x, shadowStart.y))); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowBottom), Point(shadowStart.x, shadowPos.y))); + 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))); + shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowBottom), Point(shadowStart.x, shadowPos.y))); } SDL_FreeSurface(shadowCorner); @@ -237,10 +240,10 @@ bool CWindowObject::isPopupWindow() const return options & RCLICK_POPUP; } -CStatusbarWindow::CStatusbarWindow(int options, std::string imageName, Point centerAt) : CWindowObject(options, imageName, centerAt) +CStatusbarWindow::CStatusbarWindow(int options, const ImagePath & imageName, Point centerAt) : CWindowObject(options, imageName, centerAt) { } -CStatusbarWindow::CStatusbarWindow(int options, std::string imageName) : CWindowObject(options, imageName) +CStatusbarWindow::CStatusbarWindow(int options, const ImagePath & imageName) : CWindowObject(options, imageName) { } diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index 66e7ca27a..52111aee6 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -10,13 +10,12 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/filesystem/ResourcePath.h" class CGStatusBar; class CWindowObject : public WindowBase { - std::shared_ptr createBg(std::string imageName, bool playerColored); - std::vector> shadowParts; void setShadow(bool on); @@ -30,14 +29,16 @@ protected: bool isPopupWindow() const override; //To display border void updateShadow(); - void setBackground(std::string filename); + void setBackground(const ImagePath & filename); + std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); public: enum EOptions { PLAYER_COLORED=1, //background will be player-colored RCLICK_POPUP=2, // window will behave as right-click popup BORDERED=4, // window will have border if current resolution is bigger than size of window - SHADOW_DISABLED=8 //this window won't display any shadow + SHADOW_DISABLED=8, //this window won't display any shadow + NEEDS_ANIMATED_BACKGROUND=16 //there are videos in the background that have to be played }; /* @@ -45,8 +46,8 @@ public: * imageName - name for background image, can be empty * centerAt - position of window center. Default - center of the screen */ - CWindowObject(int options, std::string imageName, Point centerAt); - CWindowObject(int options, std::string imageName = ""); + CWindowObject(int options, const ImagePath & imageName, Point centerAt); + CWindowObject(int options, const ImagePath & imageName = {}); ~CWindowObject(); void showAll(Canvas & to) override; @@ -55,8 +56,8 @@ public: class CStatusbarWindow : public CWindowObject { public: - CStatusbarWindow(int options, std::string imageName, Point centerAt); - CStatusbarWindow(int options, std::string imageName = ""); + CStatusbarWindow(int options, const ImagePath & imageName, Point centerAt); + CStatusbarWindow(int options, const ImagePath & imageName = {}); protected: std::shared_ptr statusbar; }; diff --git a/client/windows/CreaturePurchaseCard.cpp b/client/windows/CreaturePurchaseCard.cpp index 18be16e94..879f0e588 100644 --- a/client/windows/CreaturePurchaseCard.cpp +++ b/client/windows/CreaturePurchaseCard.cpp @@ -35,17 +35,17 @@ void CreaturePurchaseCard::initButtons() void CreaturePurchaseCard::initMaxButton() { - maxButton = std::make_shared(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMax,slider), EShortcut::RECRUITMENT_MAX); + maxButton = std::make_shared(Point(pos.x + 52, pos.y + 180), AnimationPath::builtin("QuickRecruitmentWindow/QuickRecruitmentAllButton.def"), CButton::tooltip(), std::bind(&CSlider::scrollToMax,slider), EShortcut::RECRUITMENT_MAX); } void CreaturePurchaseCard::initMinButton() { - minButton = std::make_shared(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMin,slider), EShortcut::RECRUITMENT_MIN); + minButton = std::make_shared(Point(pos.x, pos.y + 180), AnimationPath::builtin("QuickRecruitmentWindow/QuickRecruitmentNoneButton.def"), CButton::tooltip(), std::bind(&CSlider::scrollToMin,slider), EShortcut::RECRUITMENT_MIN); } void CreaturePurchaseCard::initCreatureSwitcherButton() { - creatureSwitcher = std::make_shared(Point(pos.x + 18, pos.y-37), "iDv6432.def", CButton::tooltip(), [&](){ switchCreatureLevel(); }); + creatureSwitcher = std::make_shared(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); }); } void CreaturePurchaseCard::switchCreatureLevel() @@ -104,7 +104,7 @@ CreaturePurchaseCard::CreaturePurchaseCard(const std::vector & creat void CreaturePurchaseCard::initView() { picture = std::make_shared(pos.x, pos.y, creatureOnTheCard); - background = std::make_shared("QuickRecruitmentWindow/CreaturePurchaseCard.png", pos.x-4, pos.y-50); + background = std::make_shared(ImagePath::builtin("QuickRecruitmentWindow/CreaturePurchaseCard.png"), pos.x-4, pos.y-50); creatureClickArea = std::make_shared(Point(pos.x, pos.y), picture, creatureOnTheCard); initAmountInfo(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index bba9a5d5d..d808480d9 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1,1890 +1,1770 @@ -/* - * GUIClasses.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 "GUIClasses.h" - -#include "CCastleInterface.h" -#include "CCreatureWindow.h" -#include "CHeroWindow.h" -#include "InfoWindows.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../CVideoHandler.h" -#include "../CServerHandler.h" - -#include "../battle/BattleInterfaceClasses.h" -#include "../battle/BattleInterface.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/TextAlignment.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" - -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/CreatureCostBox.h" -#include "../widgets/Buttons.h" -#include "../widgets/Slider.h" -#include "../widgets/TextControls.h" -#include "../widgets/ObjectLists.h" - -#include "../lobby/CSavingScreen.h" -#include "../render/Canvas.h" -#include "../render/CAnimation.h" -#include "../CMT.h" - -#include "../../CCallback.h" - -#include "../lib/mapObjectConstructors/AObjectTypeHandler.h" -#include "../lib/mapObjectConstructors/CObjectClassesHandler.h" -#include "../lib/mapObjectConstructors/CommonConstructors.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGMarket.h" -#include "../lib/ArtifactUtils.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/mapObjects/ObjectTemplate.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/gameState/InfoAboutArmy.h" -#include "../lib/gameState/SThievesGuildInfo.h" -#include "../lib/CArtHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CCreatureHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" -#include "../lib/GameSettings.h" -#include "../lib/CondSh.h" -#include "../lib/CSkillHandler.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/CStopWatch.h" -#include "../lib/CTownHandler.h" -#include "../lib/GameConstants.h" -#include "../lib/bonuses/Bonus.h" -#include "../lib/NetPacksBase.h" -#include "../lib/StartInfo.h" -#include "../lib/TextOperations.h" - -CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) - : CIntObject(LCLICK | SHOW_POPUP), - parent(window), - selected(false), - creature(crea), - amount(totalAmount) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - animation = std::make_shared(1, 1, creature, true, true); - // 1 + 1 px for borders - pos.w = animation->pos.w + 2; - pos.h = animation->pos.h + 2; -} - -void CRecruitmentWindow::CCreatureCard::select(bool on) -{ - selected = on; - redraw(); -} - -void CRecruitmentWindow::CCreatureCard::clickPressed(const Point & cursorPosition) -{ - parent->select(this->shared_from_this()); -} - -void CRecruitmentWindow::CCreatureCard::showPopupWindow(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(creature, true); -} - -void CRecruitmentWindow::CCreatureCard::showAll(Canvas & to) -{ - CIntObject::showAll(to); - if(selected) - to.drawBorder(pos, Colors::RED); - else - to.drawBorder(pos, Colors::YELLOW); -} - -void CRecruitmentWindow::select(std::shared_ptr card) -{ - if(card == selected) - return; - - if(selected) - selected->select(false); - - selected = card; - - if(selected) - selected->select(true); - - if(card) - { - si32 maxAmount = card->creature->maxAmount(LOCPLINT->cb->getResourceAmount()); - - vstd::amin(maxAmount, card->amount); - - slider->setAmount(maxAmount); - - if(slider->getValue() != maxAmount) - slider->scrollTo(maxAmount); - else // if slider already at 0 - emulate call to sliderMoved() - sliderMoved(maxAmount); - - costPerTroopValue->createItems(card->creature->getFullRecruitCost()); - totalCostValue->createItems(card->creature->getFullRecruitCost()); - - costPerTroopValue->set(card->creature->getFullRecruitCost()); - totalCostValue->set(card->creature->getFullRecruitCost() * maxAmount); - - //Recruit %s - title->setText(boost::str(boost::format(CGI->generaltexth->tcommands[21]) % card->creature->getNamePluralTranslated())); - - maxButton->block(maxAmount == 0); - slider->block(maxAmount == 0); - } -} - -void CRecruitmentWindow::buy() -{ - CreatureID crid = selected->creature->getId(); - SlotID dstslot = dst->getSlotFor(crid); - - if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot - { - std::pair toMerge; - bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); - - if (allowMerge && dst->mergableStacks(toMerge)) - { - LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); - } - else - { - std::string txt; - if(dst->ID == Obj::HERO) - { - txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. - boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); - } - else - { - txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. - } - - LOCPLINT->showInfoDialog(txt); - return; - } - } - - onRecruit(crid, slider->getValue()); - if(level >= 0) - close(); -} - -void CRecruitmentWindow::showAll(Canvas & to) -{ - CWindowObject::showAll(to); - - Rect(172, 222, 67, 42) + pos.topLeft(); - - // recruit\total values - to.drawBorder(Rect(172, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); - to.drawBorder(Rect(246, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); - - //cost boxes - to.drawBorder(Rect( 64, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); - to.drawBorder(Rect(322, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); - - //buttons borders - to.drawBorder(Rect(133, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); - to.drawBorder(Rect(211, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); - to.drawBorder(Rect(289, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); -} - -CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, int y_offset): - CStatusbarWindow(PLAYER_COLORED, "TPRCRT"), - onRecruit(Recruit), - level(Level), - dst(Dst), - selected(nullptr), - dwelling(Dwelling) -{ - moveBy(Point(0, y_offset)); - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - slider = std::make_shared(Point(176, 279), 135, std::bind(&CRecruitmentWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - - maxButton = std::make_shared(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); - buyButton = std::make_shared(Point(212, 313), "IBY6432.DEF", CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); - cancelButton = std::make_shared(Point(290, 313), "ICN6432.DEF", CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); - - title = std::make_shared(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); - availableValue = std::make_shared(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - toRecruitValue = std::make_shared(279, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - costPerTroopValue = std::make_shared(Rect(65, 222, 97, 74), CGI->generaltexth->allTexts[346]); - totalCostValue = std::make_shared(Rect(323, 222, 97, 74), CGI->generaltexth->allTexts[466]); - - availableTitle = std::make_shared(205, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[465]); - toRecruitTitle = std::make_shared(279, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[16]); - - availableCreaturesChanged(); -} - -void CRecruitmentWindow::availableCreaturesChanged() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - size_t selectedIndex = 0; - - if(!cards.empty() && selected) // find position of selected item - selectedIndex = std::find(cards.begin(), cards.end(), selected) - cards.begin(); - - select(nullptr); - - cards.clear(); - - for(int i=0; icreatures.size(); i++) - { - //find appropriate level - if(level >= 0 && i != level) - continue; - - int amount = dwelling->creatures[i].first; - - //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)); - } - - const int creatureWidth = 102; - - //normal distance between cards - 18px - int requiredSpace = 18; - //maximum distance we can use without reaching window borders - int availableSpace = pos.w - 50 - creatureWidth * (int)cards.size(); - - if (cards.size() > 1) // avoid division by zero - availableSpace /= (int)cards.size() - 1; - else - availableSpace = 0; - - assert(availableSpace >= 0); - - const int spaceBetween = std::min(requiredSpace, availableSpace); - const int totalCreatureWidth = spaceBetween + creatureWidth; - - //now we know total amount of cards and can move them to correct position - int curx = pos.w / 2 - (creatureWidth*(int)cards.size()/2) - (spaceBetween*((int)cards.size()-1)/2); - for(auto & card : cards) - { - card->moveBy(Point(curx, 64)); - curx += totalCreatureWidth; - } - - //restore selection - select(cards[selectedIndex]); - - if(slider->getValue() == slider->getAmount()) - slider->scrollToMax(); - else // if slider already at 0 - emulate call to sliderMoved() - sliderMoved(slider->getAmount()); -} - -void CRecruitmentWindow::sliderMoved(int to) -{ - if(!selected) - return; - - buyButton->block(!to); - availableValue->setText(std::to_string(selected->amount - to)); - toRecruitValue->setText(std::to_string(to)); - - totalCostValue->set(selected->creature->getFullRecruitCost() * to); -} - -CSplitWindow::CSplitWindow(const CCreature * creature, std::function callback_, int leftMin_, int rightMin_, int leftAmount_, int rightAmount_) - : CWindowObject(PLAYER_COLORED, "GPUCRDIV"), - callback(callback_), - leftAmount(leftAmount_), - rightAmount(rightAmount_), - leftMin(leftMin_), - rightMin(rightMin_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - int total = leftAmount + rightAmount; - int leftMax = total - rightMin; - int rightMax = total - leftMin; - - ok = std::make_shared(Point(20, 263), "IOK6432", CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(214, 263), "ICN6432", CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); - - int sliderPosition = total - leftMin - rightMin; - - leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); - rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); - - //add filters to allow only number input - leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); - rightInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, rightMin, rightMax); - - leftInput->setText(std::to_string(leftAmount), false); - rightInput->setText(std::to_string(rightAmount), false); - - animLeft = std::make_shared(20, 54, creature, true, false); - animRight = std::make_shared(177, 54,creature, true, false); - - slider = std::make_shared(Point(21, 194), 257, std::bind(&CSplitWindow::sliderMoved, this, _1), 0, sliderPosition, rightAmount - rightMin, Orientation::HORIZONTAL); - - std::string titleStr = CGI->generaltexth->allTexts[256]; - boost::algorithm::replace_first(titleStr,"%s", creature->getNamePluralTranslated()); - title = std::make_shared(150, 34, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleStr); -} - -void CSplitWindow::setAmountText(std::string text, bool left) -{ - int amount = 0; - if(text.length()) - { - try - { - amount = boost::lexical_cast(text); - } - catch(boost::bad_lexical_cast &) - { - amount = left ? leftAmount : rightAmount; - } - - int total = leftAmount + rightAmount; - if(amount > total) - amount = total; - } - - setAmount(amount, left); - slider->scrollTo(rightAmount - rightMin); -} - -void CSplitWindow::setAmount(int value, bool left) -{ - int total = leftAmount + rightAmount; - leftAmount = left ? value : total - value; - rightAmount = left ? total - value : value; - - leftInput->setText(std::to_string(leftAmount)); - rightInput->setText(std::to_string(rightAmount)); -} - -void CSplitWindow::apply() -{ - callback(leftAmount, rightAmount); - close(); -} - -void CSplitWindow::sliderMoved(int to) -{ - setAmount(rightMin + to, false); -} - -CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, std::function callback) - : CWindowObject(PLAYER_COLORED, "LVLUPBKG"), - cb(callback) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - LOCPLINT->showingDialog->setn(true); - - if(!skills.empty()) - { - std::vector> comps; - for(auto & skill : skills) - { - auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); - comps.push_back(comp); - } - - box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); - } - - portrait = std::make_shared("PortraitsLarge", hero->portrait, 0, 170, 66); - ok = std::make_shared(Point(297, 413), "IOKAY", CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); - - //%s has gained a level. - mainTitle = std::make_shared(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated())); - - //%s is now a level %d %s. - std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445"); - boost::replace_first(levelTitleText, "%s", hero->getNameTranslated()); - boost::replace_first(levelTitleText, "%d", std::to_string(hero->level)); - boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated()); - - levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); - - skillIcon = std::make_shared("PSKIL42", pskill, 0, 174, 190); - - skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[pskill] + " +1"); -} - - -CLevelWindow::~CLevelWindow() -{ - //FIXME: call callback if there was nothing to select? - if (box && box->selectedIndex() != -1) - cb(box->selectedIndex()); - - LOCPLINT->showingDialog->setn(false); -} - -CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) - : CStatusbarWindow(PLAYER_COLORED, "TPTAVERN"), - tavernObj(TavernObj) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - std::vector h = LOCPLINT->cb->getAvailableHeroes(TavernObj); - if(h.size() < 2) - h.resize(2, nullptr); - - selected = 0; - if(!h[0]) - selected = 1; - if(!h[0] && !h[1]) - selected = -1; - - oldSelected = -1; - - h1 = std::make_shared(selected, 0, 72, 299, h[0]); - h2 = std::make_shared(selected, 1, 162, 299, h[1]); - - title = std::make_shared(197, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[37]); - cost = std::make_shared(320, 328, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(GameConstants::HERO_GOLD_COST)); - heroDescription = std::make_shared("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - heroesForHire = std::make_shared(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]); - - auto rumorText = boost::str(boost::format(CGI->generaltexth->allTexts[216]) % LOCPLINT->cb->getTavernRumor(tavernObj)); - rumor = std::make_shared(rumorText, Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - cancel = std::make_shared(Point(310, 428), "ICANCEL.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); - recruit = std::make_shared(Point(272, 355), "TPTAV01.DEF", CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); - thiefGuild = std::make_shared(Point(22, 428), "TPTAV02.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); - - if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold - { - recruit->addHoverText(CButton::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)) - { - //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); - recruit->block(true); - } - else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) - { - //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); - recruit->block(true); - } - else if(LOCPLINT->castleInt && LOCPLINT->castleInt->town->visitingHero) - { - recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. - recruit->block(true); - } - else - { - if(selected == -1) - recruit->block(true); - } - if(LOCPLINT->castleInt) - CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); - else - CCS->videoh->open("TAVERN.BIK"); -} - -void CTavernWindow::recruitb() -{ - const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; - const CGObjectInstance *obj = tavernObj; - close(); - LOCPLINT->cb->recruitHero(obj, toBuy); -} - -void CTavernWindow::thievesguildb() -{ - GH.windows().createAndPushWindow(tavernObj); -} - -CTavernWindow::~CTavernWindow() -{ - CCS->videoh->close(); -} - -void CTavernWindow::show(Canvas & to) -{ - CWindowObject::show(to); - - if(selected >= 0) - { - auto sel = selected ? h2 : h1; - - if(selected != oldSelected) - { - // Selected hero just changed. Update RECRUIT button hover text if recruitment is allowed. - oldSelected = selected; - - heroDescription->setText(sel->description); - - //Recruit %s the %s - if (!recruit->isBlocked()) - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated())); - - } - - 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) -{ - if(h) - *_sel = _id; -} - -void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition) -{ - if(h) - GH.windows().createAndPushWindow(std::make_shared(h)); -} - -CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H) - : CIntObject(LCLICK | SHOW_POPUP | HOVER), - h(H), _sel(&sel), _id(id) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - h = H; - pos.x += x; - pos.y += y; - pos.w = 58; - pos.h = 64; - - if(H) - { - hoverName = CGI->generaltexth->tavernInfo[4]; - boost::algorithm::replace_first(hoverName,"%s",H->getNameTranslated()); - - int artifs = (int)h->artifactsWorn.size() + (int)h->artifactsInBackpack.size(); - for(int i=13; i<=17; i++) //war machines and spellbook don't count - if(vstd::contains(h->artifactsWorn, ArtifactPosition(i))) - artifs--; - - description = CGI->generaltexth->allTexts[215]; - boost::algorithm::replace_first(description, "%s", h->getNameTranslated()); - boost::algorithm::replace_first(description, "%d", std::to_string(h->level)); - boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); - boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); - - portrait = std::make_shared("portraitsLarge", h->portrait); - } -} - -void CTavernWindow::HeroPortrait::hover(bool on) -{ - //Hoverable::hover(on); - if(on) - GH.statusbar()->write(hoverName); - else - GH.statusbar()->clear(); -} - -static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange"; -static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE"; - -static bool isQuickExchangeLayoutAvailable() -{ - return CResourceHandler::get()->existsResource(ResourceID(std::string("SPRITES/") + QUICK_EXCHANGE_BG, EResType::IMAGE)); -} - -CExchangeController::CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2) - :left(LOCPLINT->cb->getHero(hero1)), right(LOCPLINT->cb->getHero(hero2)), cb(LOCPLINT->cb), view(view) -{ -} - -std::function CExchangeController::onMoveArmyToLeft() -{ - return [&]() { moveArmy(false); }; -} - -std::function CExchangeController::onMoveArmyToRight() -{ - return [&]() { moveArmy(true); }; -} - -std::vector getBackpackArts(const CGHeroInstance * hero) -{ - std::vector result; - - for(auto slot : hero->artifactsInBackpack) - { - result.push_back(slot.artifact); - } - - return result; -} - -std::function CExchangeController::onSwapArtifacts() -{ - return [&]() - { - cb->bulkMoveArtifacts(left->id, right->id, true); - }; -} - -std::function CExchangeController::onMoveArtifactsToLeft() -{ - return [&]() { moveArtifacts(false); }; -} - -std::function CExchangeController::onMoveArtifactsToRight() -{ - return [&]() { moveArtifacts(true); }; -} - -std::vector> getStacks(const CArmedInstance * source) -{ - auto slots = source->Slots(); - - return std::vector>(slots.begin(), slots.end()); -} - -std::function CExchangeController::onSwapArmy() -{ - return [&]() - { - if(left->tempOwner != cb->getMyColor() - || right->tempOwner != cb->getMyColor()) - { - return; - } - - auto leftSlots = getStacks(left); - auto rightSlots = getStacks(right); - - auto i = leftSlots.begin(), j = rightSlots.begin(); - - for(; i != leftSlots.end() && j != rightSlots.end(); i++, j++) - { - cb->swapCreatures(left, right, i->first, j->first); - } - - if(i != leftSlots.end()) - { - auto freeSlots = right->getFreeSlots(); - auto slot = freeSlots.begin(); - - for(; i != leftSlots.end() && slot != freeSlots.end(); i++, slot++) - { - cb->swapCreatures(left, right, i->first, *slot); - } - } - else if(j != rightSlots.end()) - { - auto freeSlots = left->getFreeSlots(); - auto slot = freeSlots.begin(); - - for(; j != rightSlots.end() && slot != freeSlots.end(); j++, slot++) - { - cb->swapCreatures(left, right, *slot, j->first); - } - } - }; -} - -std::function CExchangeController::onMoveStackToLeft(SlotID slotID) -{ - return [=]() - { - if(right->tempOwner != cb->getMyColor()) - { - return; - } - - moveStack(right, left, slotID); - }; -} - -std::function CExchangeController::onMoveStackToRight(SlotID slotID) -{ - return [=]() - { - if(left->tempOwner != cb->getMyColor()) - { - return; - } - - moveStack(left, right, slotID); - }; -} - -void CExchangeController::moveStack( - const CGHeroInstance * source, - const CGHeroInstance * target, - SlotID sourceSlot) -{ - auto creature = source->getCreature(sourceSlot); - if(creature == nullptr) - return; - - SlotID targetSlot = target->getSlotFor(creature); - - if(targetSlot.validSlot()) - { - if(source->stacksCount() == 1 && source->needsLastStack()) - { - cb->splitStack( - source, - target, - sourceSlot, - targetSlot, - target->getStackCount(targetSlot) + source->getStackCount(sourceSlot) - 1); - } - else - { - cb->mergeOrSwapStacks(source, target, sourceSlot, targetSlot); - } - } -} - -void CExchangeController::moveArmy(bool leftToRight) -{ - const CGHeroInstance * source = leftToRight ? left : right; - const CGHeroInstance * target = leftToRight ? right : left; - const CGarrisonSlot * selection = this->view->getSelectedSlotID(); - SlotID slot; - - if(source->tempOwner != cb->getMyColor()) - { - return; - } - - if(selection && selection->our() && selection->getObj() == source) - { - slot = selection->getSlot(); - } - else - { - auto weakestSlot = vstd::minElementByFun( - source->Slots(), - [](const std::pair & s) -> int - { - return s.second->getCreatureID().toCreature()->getAIValue(); - }); - - slot = weakestSlot->first; - } - - cb->bulkMoveArmy(source->id, target->id, slot); -} - -void CExchangeController::moveArtifacts(bool leftToRight) -{ - const CGHeroInstance * source = leftToRight ? left : right; - const CGHeroInstance * target = leftToRight ? right : left; - - if(source->tempOwner != cb->getMyColor()) - { - return; - } - - cb->bulkMoveArtifacts(source->id, target->id, false); -} - -void CExchangeController::moveArtifact( - const CGHeroInstance * source, - const CGHeroInstance * target, - ArtifactPosition srcPosition) -{ - auto srcLocation = ArtifactLocation(source, srcPosition); - auto dstLocation = ArtifactLocation(target, - ArtifactUtils::getArtAnyPosition(target, source->getArt(srcPosition)->getTypeId())); - - cb->swapArtifacts(srcLocation, dstLocation); -} - -CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) - : CStatusbarWindow(PLAYER_COLORED | BORDERED, isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2"), - controller(this, hero1, hero2), - moveStackLeftButtons(), - moveStackRightButtons() -{ - const bool qeLayout = isQuickExchangeLayoutAvailable(); - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - heroInst[0] = LOCPLINT->cb->getHero(hero1); - heroInst[1] = LOCPLINT->cb->getHero(hero2); - - auto genTitle = [](const CGHeroInstance * h) - { - boost::format fmt(CGI->generaltexth->allTexts[138]); - fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated(); - return boost::str(fmt); - }; - - 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 = std::make_shared("PSKIL32"); - PSKIL32->preload(); - - auto SECSK32 = std::make_shared("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))); - else - primSkillImages.push_back(std::make_shared(PSKIL32, g, 0, 385, 19 + 36 * g)); - } - - for(int leftRight : {0, 1}) - { - const CGHeroInstance * hero = heroInst.at(leftRight); - - for(int m=0; m(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE)); - - - 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)); - - specImages[leftRight] = std::make_shared("UN32", hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); - - expImages[leftRight] = std::make_shared(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); - manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - - portraits[0] = std::make_shared("PortraitsLarge", heroInst[0]->portrait, 0, 257, 13); - portraits[1] = std::make_shared("PortraitsLarge", heroInst[1]->portrait, 0, 485, 13); - - artifs[0] = std::make_shared(Point(-334, 150)); - artifs[0]->setHero(heroInst[0]); - artifs[1] = std::make_shared(Point(98, 150)); - artifs[1]->setHero(heroInst[1]); - - addSet(artifs[0]); - addSet(artifs[1]); - - for(int g=0; g<4; ++g) - { - primSkillAreas.push_back(std::make_shared()); - if (qeLayout) - primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22)); - else - primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); - primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; - primSkillAreas[g]->type = g; - primSkillAreas[g]->bonusValue = 0; - primSkillAreas[g]->baseType = 0; - primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; - boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); - } - - //heroes related thing - for(int b=0; b < heroInst.size(); b++) - { - const CGHeroInstance * hero = heroInst.at(b); - - //secondary skill's clickable areas - for(int g=0; gsecSkills.size(); ++g) - { - int skill = hero->secSkills[g].first, - 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]->baseType = 1; - - secSkillAreas[b][g]->type = skill; - secSkillAreas[b][g]->bonusValue = 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); - - 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(); - - experienceAreas[b] = std::make_shared(); - experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); - experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9]; - experienceAreas[b]->text = CGI->generaltexth->allTexts[2]; - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level)); - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1))); - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp)); - - spellPointsAreas[b] = std::make_shared(); - spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); - spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22]; - spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205]; - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated()); - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana)); - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit())); - - morale[b] = std::make_shared(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true); - luck[b] = std::make_shared(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true); - } - - quit = std::make_shared(Point(732, 567), "IOKAY.DEF", CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); - if(queryID.getNum() > 0) - quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); }); - - questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), "hsbtns4.def", CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); - questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), "hsbtns4.def", CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); - - Rect barRect(5, 578, 725, 18); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), barRect, 5, 578)); - - //garrison interface - - garr = std::make_shared(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true); - auto splitButtonCallback = [&](){ garr->splitClick(); }; - garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - - if(qeLayout) - { - moveAllGarrButtonLeft = std::make_shared(Point(325, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight()); - echangeGarrButton = std::make_shared(Point(377, 118), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy()); - moveAllGarrButtonRight = std::make_shared(Point(425, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToLeft()); - moveArtifactsButtonLeft = std::make_shared(Point(325, 154), QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToRight()); - echangeArtifactsButton = std::make_shared(Point(377, 154), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[4]), controller.onSwapArtifacts()); - moveArtifactsButtonRight = std::make_shared(Point(425, 154), QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToLeft()); - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - moveStackLeftButtons.push_back( - std::make_shared( - Point(484 + 35 * i, 154), - QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF", - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - controller.onMoveStackToLeft(SlotID(i)))); - - moveStackRightButtons.push_back( - std::make_shared( - Point(66 + 35 * i, 154), - QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF", - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - controller.onMoveStackToRight(SlotID(i)))); - } - } - - updateWidgets(); -} - -const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const -{ - return garr->getSelection(); -} - -void CExchangeWindow::updateGarrisons() -{ - garr->recreateSlots(); - - updateWidgets(); -} - -void CExchangeWindow::questlog(int whichHero) -{ - CCS->curh->dragAndDropCursor(nullptr); - LOCPLINT->showQuestLog(); -} - -void CExchangeWindow::updateWidgets() -{ - for(size_t leftRight : {0, 1}) - { - const CGHeroInstance * hero = heroInst.at(leftRight); - - for(int m=0; mgetPrimSkillLevel(static_cast(m)); - primSkillValues[leftRight][m]->setText(std::to_string(value)); - } - - for(int m=0; m < hero->secSkills.size(); ++m) - { - int id = hero->secSkills[m].first; - int level = hero->secSkills[m].second; - - secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level); - } - - expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3)); - manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3)); - - morale[leftRight]->set(hero); - luck[leftRight]->set(hero); - } -} - -CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) - : CStatusbarWindow(PLAYER_COLORED, "TPSHIP") -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - bgWater = std::make_shared("TPSHIPBK", 100, 69); - - auto handler = CGI->objtypeh->getHandlerFor(Obj::BOAT, boatType); - - auto boatConstructor = std::dynamic_pointer_cast(handler); - - assert(boatConstructor); - - if (boatConstructor) - { - std::string 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->center(waterCenter); - } - - // Create resource icons and costs. - std::string goldValue = std::to_string(cost[EGameResID::GOLD]); - std::string woodValue = std::to_string(cost[EGameResID::WOOD]); - - goldCost = std::make_shared(118, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, goldValue); - woodCost = std::make_shared(212, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, woodValue); - - goldPic = std::make_shared("RESOURCE",GameResID(EGameResID::GOLD), 0, 100, 244); - woodPic = std::make_shared("RESOURCE", GameResID(EGameResID::WOOD), 0, 196, 244); - - quit = std::make_shared(Point(224, 312), "ICANCEL", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); - build = std::make_shared(Point(42, 312), "IBUY30", CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); - build->addCallback(onBuy); - - for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) - { - if(cost[i] > LOCPLINT->cb->getResourceAmount(i)) - { - build->block(true); - break; - } - } - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - title = std::make_shared(164, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[13]); - costLabel = std::make_shared(164, 220, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[14]); -} - -void CTransformerWindow::CItem::move() -{ - if(left) - moveBy(Point(289, 0)); - else - moveBy(Point(-289, 0)); - left = !left; -} - -void CTransformerWindow::CItem::clickPressed(const Point & cursorPosition) -{ - move(); - parent->redraw(); -} - -void CTransformerWindow::CItem::update() -{ - icon->setFrame(parent->army->getCreature(SlotID(id))->getId() + 2); -} - -CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id_) - : CIntObject(LCLICK), - id(id_), - size(size_), - parent(parent_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - left = true; - pos.w = 58; - pos.h = 64; - - pos.x += 45 + (id%3)*83 + id/6*83; - pos.y += 109 + (id/3)*98; - icon = std::make_shared("TWCRPORT", parent->army->getCreature(SlotID(id))->getId() + 2); - count = std::make_shared(28, 76,FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(size)); -} - -void CTransformerWindow::makeDeal() -{ - for(auto & elem : items) - { - if(!elem->left) - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, elem->id, 0, 0, hero); - } -} - -void CTransformerWindow::addAll() -{ - for(auto & elem : items) - { - if(elem->left) - elem->move(); - } - redraw(); -} - -void CTransformerWindow::updateGarrisons() -{ - for(auto & item : items) - item->update(); -} - -CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero) - : CStatusbarWindow(PLAYER_COLORED, "SKTRNBK"), - hero(_hero), - market(_market) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(hero) - army = hero; - else - army = dynamic_cast(market); //for town only - - if(army) - { - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - if(army->getCreature(SlotID(i))) - items.push_back(std::make_shared(this, army->getStackCount(SlotID(i)), i)); - } - } - - all = std::make_shared(Point(146, 416), "ALTARMY.DEF", CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); - convert = std::make_shared(Point(269, 416), "ALTSACR.DEF", CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(392, 416), "ICANCEL.DEF", CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - titleLeft = std::make_shared(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area - titleRight = std::make_shared(153+295, 29, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[486]);//transformer - helpLeft = std::make_shared(CGI->generaltexth->allTexts[487], Rect(26, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//move creatures to create skeletons - helpRight = std::make_shared(CGI->generaltexth->allTexts[488], Rect(320, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//creatures here will become skeletons -} - -CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int Y) - : CIntObject(LCLICK | SHOW_POPUP | HOVER), - ID(_ID), - parent(_parent) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - 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); - - icon = std::make_shared("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; -} - -void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) -{ - if(state() == 2) - GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); -} - -void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(CComponent::secskill, ID, 1)); -} - -void CUniversityWindow::CItem::hover(bool on) -{ - if(on) - GH.statusbar()->write(CGI->skillh->getByIndex(ID)->getNameTranslated()); - else - GH.statusbar()->clear(); -} - -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) - : CStatusbarWindow(PLAYER_COLORED, "UNIVERS1"), - hero(_hero), - market(_market) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - bars = std::make_shared(); - 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); - } - else if(auto uni = dynamic_cast(_market); uni->appearance) - { - titlePic = std::make_shared(uni->appearance->animationFile, 0); - titleStr = uni->title; - speechStr = uni->speech; - } - else - { - titlePic = std::make_shared("UNIVBLDG"); - } - - titlePic->center(Point(232 + pos.x, 76 + pos.y)); - - clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); - - std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); - - for(int i=0; i(this, goods[i], 54+i*104, 234)); - - cancel = std::make_shared(Point(200, 313), "IOKAY.DEF", CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); -} - -void CUniversityWindow::makeDeal(int skill) -{ - LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); -} - - -CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) - : CStatusbarWindow(PLAYER_COLORED, "UNIVERS2.PCX"), - owner(owner_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - std::string text = CGI->generaltexth->allTexts[608]; - boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); - boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - boost::replace_first(text, "%d", "2000"); - - clerkSpeech = std::make_shared(text, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - name = std::make_shared(230, 37, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - icon = std::make_shared("SECSKILL", SKILL*3+3, 0, 211, 51); - level = std::make_shared(230, 107, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[1]); - - costIcon = std::make_shared("RESOURCE", GameResID(EGameResID::GOLD), 0, 210, 210); - cost = std::make_shared(230, 267, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "2000"); - - std::string hoverText = CGI->generaltexth->allTexts[609]; - boost::replace_first(hoverText, "%s", CGI->generaltexth->levels[0]+ " " + CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - - text = CGI->generaltexth->zelp[633].second; - boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); - boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - boost::replace_first(text, "%d", "2000"); - - confirm = std::make_shared(Point(148, 299), "IBY6432.DEF", CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); - confirm->block(!available); - - cancel = std::make_shared(Point(252,299), "ICANCEL.DEF", CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); -} - -void CUnivConfirmWindow::makeDeal(int skill) -{ - owner->makeDeal(skill); - close(); -} - -CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) - : CWindowObject(PLAYER_COLORED, "GARRISON") -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - garr = std::make_shared(Point(92, 127), 4, Point(0,96), up, down, removableUnits); - { - auto split = std::make_shared(Point(88, 314), "IDV6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } ); - garr->addSplitBtn(split); - } - quit = std::make_shared(Point(399, 314), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - - std::string titleText; - if(down->tempOwner == up->tempOwner) - { - titleText = CGI->generaltexth->allTexts[709]; - } - else - { - //assume that this is joining monsters dialog - if(up->Slots().size() > 0) - { - titleText = CGI->generaltexth->allTexts[35]; - boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->type->getNamePluralTranslated()); - } - else - { - logGlobal->error("Invalid armed instance for garrison window."); - } - } - title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); - - banner = std::make_shared("CREST58", up->getOwner().getNum(), 0, 28, 124); - portrait = std::make_shared("PortraitsLarge", down->portrait, 0, 29, 222); -} - -void CGarrisonWindow::updateGarrisons() -{ - garr->recreateSlots(); -} - -CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) - : CStatusbarWindow(PLAYER_COLORED, "APHLFTBK"), - fort(object), - hero(visitor) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - title = std::make_shared(325, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, fort->getObjectName()); - - heroPic = std::make_shared(30, 60, hero); - - for(int i=0; i < resCount; i++) - { - totalIcons[i] = std::make_shared("SMALRES", i, 0, 104 + 76 * i, 237); - totalLabels[i] = std::make_shared(166 + 76 * i, 253, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); - } - - for(int i = 0; i < slotsCount; i++) - { - upgrade[i] = std::make_shared(Point(107 + i * 76, 171), "", CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); - for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) - upgrade[i]->addImage(image); - - for(int j : {0,1}) - { - slotIcons[i][j] = std::make_shared("SMALRES", 0, 0, 104 + 76 * i, 128 + 20 * j); - slotLabels[i][j] = std::make_shared(168 + 76 * i, 144 + 20 * j, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); - } - } - - upgradeAll = std::make_shared(Point(30, 231), "", CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); - for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) - upgradeAll->addImage(image); - - quit = std::make_shared(Point(294, 275), "IOKAY.DEF", CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); - 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); - updateGarrisons(); -} - -void CHillFortWindow::updateGarrisons() -{ - std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade - - TResources totalSum; // totalSum[resource ID] = value - - 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)); - totalSum += costs[i]; - } - } - - currState[i] = newState; - upgrade[i]->setIndex(currState[i] == -1 ? 0 : currState[i]); - upgrade[i]->block(currState[i] == -1); - upgrade[i]->addHoverText(CButton::NORMAL, getTextForSlot(SlotID(i))); - } - - //"Upgrade all" slot - int newState = 2; - { - TResources myRes = LOCPLINT->cb->getResourceAmount(); - - bool allUpgraded = true;//All creatures are upgraded? - for(int i=0; isetIndex(newState); - - garr->recreateSlots(); - - for(int i = 0; i < slotsCount; i++) - { - //hide all first - for(int j : {0,1}) - { - slotIcons[i][j]->visible = false; - slotLabels[i][j]->setText(""); - } - //if can upgrade or can not afford, draw cost - if(currState[i] == 0 || currState[i] == 2) - { - if(costs[i].nonZero()) - { - //reverse iterator is used to display gold as first element - int j = 0; - for(int res = (int)costs[i].size()-1; (res >= 0) && (j < 2); res--) - { - int val = costs[i][res]; - if(!val) - continue; - - slotIcons[i][j]->visible = true; - slotIcons[i][j]->setFrame(res); - - slotLabels[i][j]->setText(std::to_string(val)); - j++; - } - } - else//free upgrade - print gold image and "Free" text - { - slotIcons[i][0]->visible = true; - slotIcons[i][0]->setFrame(GameResID(EGameResID::GOLD)); - slotLabels[i][0]->setText(CGI->generaltexth->allTexts[344]); - } - } - } - - for(int i = 0; i < resCount; i++) - { - if(totalSum[i] == 0) - { - totalIcons[i]->visible = false; - totalLabels[i]->setText(""); - } - else - { - totalIcons[i]->visible = true; - totalLabels[i]->setText(std::to_string(totalSum[i])); - } - } -} - -void CHillFortWindow::makeDeal(SlotID slot) -{ - 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: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[313 + offset], std::vector>(), soundBase::sound_todo); - break; - case 2: - for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); - LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID[0]); - } - } - break; - } -} - -std::string CHillFortWindow::getTextForSlot(SlotID slot) -{ - if(!hero->getCreature(slot))//we don`t have creature here - return ""; - - std::string str = CGI->generaltexth->allTexts[318]; - int amount = hero->getStackCount(slot); - if(amount == 1) - boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNameSingularTranslated()); - else - boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNamePluralTranslated()); - - return str; -} - -int CHillFortWindow::getState(SlotID slot) -{ - TResources myRes = LOCPLINT->cb->getResourceAmount(); - - if(hero->slotEmpty(slot))//no creature here - return -1; - - UpgradeInfo info; - LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); - if(!info.newID.size())//already upgraded - return 1; - - if(!(info.cost[0] * hero->getStackCount(slot)).canBeAfforded(myRes)) - return 0; - - return 2;//can upgrade -} - -CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): - CStatusbarWindow(PLAYER_COLORED | BORDERED, "TpRank"), - owner(_owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - SThievesGuildInfo tgi; //info to be displayed - LOCPLINT->cb->getThievesGuildInfo(tgi, owner); - - exitb = std::make_shared(Point(748, 556), "TPMAGE1", CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); - statusbar = CGStatusBar::create(3, 555, "TStatBar.bmp", 742); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - //data for information table: - // fields[row][column] = list of id's of players for this box - static std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = - { &SThievesGuildInfo::numOfTowns, &SThievesGuildInfo::numOfHeroes, &SThievesGuildInfo::gold, - &SThievesGuildInfo::woodOre, &SThievesGuildInfo::mercSulfCrystGems, &SThievesGuildInfo::obelisks, - &SThievesGuildInfo::artifacts, &SThievesGuildInfo::army, &SThievesGuildInfo::income }; - - for(int g=0; g<12; ++g) - { - int posY[] = {400, 460, 510}; - int y; - if(g < 9) - y = 52 + 32*g; - else - y = posY[g-9]; - - std::string text = CGI->generaltexth->jktexts[24+g]; - boost::algorithm::trim_if(text,boost::algorithm::is_any_of("\"")); - rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); - } - - auto PRSTRIPS = std::make_shared("PRSTRIPS"); - PRSTRIPS->preload(); - - for(int g=1; g(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 = std::make_shared("itgflags"); - itgflags->preload(); - - //printing flags - for(int g = 0; g < std::size(fields); ++g) //by lines - { - for(int b=0; b<(tgi .* fields[g]).size(); ++b) //by places (1st, 2nd, ...) - { - std::vector &players = (tgi .* fields[g])[b]; //get players with this place in this line - - //position of box - int xpos = 259 + 66 * b; - int ypos = 41 + 32 * g; - - size_t rowLength[2]; //size of each row - rowLength[0] = std::min(players.size(), 4); - rowLength[1] = players.size() - rowLength[0]; - - for(size_t j=0; j < 2; j++) - { - // origin of this row | offset for 2nd row| shift right for short rows - //if we have 2 rows, start either from mid or beginning (depending on count), otherwise center the flags - int rowStartX = xpos + (j ? 6 + ((int)rowLength[j] < 3 ? 12 : 0) : 24 - 6 * (int)rowLength[j]); - 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)); - } - } - } - - static const std::string colorToBox[] = {"PRRED.BMP", "PRBLUE.BMP", "PRTAN.BMP", "PRGREEN.BMP", "PRORANGE.BMP", "PRPURPLE.BMP", "PRTEAL.BMP", "PRROSE.bmp"}; - - //printing best hero - int counter = 0; - for(auto & iter : tgi.colorToBestHero) - { - banners.push_back(std::make_shared(colorToBox[iter.first.getNum()], 253 + 66 * counter, 334)); - if(iter.second.portrait >= 0) - { - bestHeroes.push_back(std::make_shared("PortraitsSmall", iter.second.portrait, 0, 260 + 66 * counter, 360)); - //TODO: r-click info: - // - r-click on hero - // - r-click on primary skill label - if(iter.second.details) - { - primSkillHeaders.push_back(std::make_shared(CGI->generaltexth->allTexts[184], Rect(260 + 66*counter, 396, 52, 64), - 0, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE)); - - for(int i=0; iprimskills.size(); ++i) - { - primSkillValues.push_back(std::make_shared(310 + 66 * counter, 407 + 11*i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, - std::to_string(iter.second.details->primskills[i]))); - } - } - } - counter++; - } - - //printing best creature - counter = 0; - for(auto & it : tgi.bestCreature) - { - if(it.second >= 0) - bestCreatures.push_back(std::make_shared("TWCRPORT", it.second+2, 0, 255 + 66 * counter, 479)); - counter++; - } - - //printing personality - counter = 0; - for(auto & it : tgi.personality) - { - std::string text; - if(it.second == EAiTactic::NONE) - { - text = CGI->generaltexth->arraytxt[172]; - } - else if(it.second != EAiTactic::RANDOM) - { - text = CGI->generaltexth->arraytxt[168 + it.second]; - } - - personalities.push_back(std::make_shared(283 + 66*counter, 459, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text)); - - counter++; - } -} - -CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text) - : CIntObject(LCLICK | DOUBLECLICK), - parent(_parent), - index(_id) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - border = std::make_shared("TPGATES"); - pos = border->pos; - - setRedrawParent(true); - - text = std::make_shared(pos.w/2, pos.h/2, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _text); - select(index == parent->selected); -} - -void CObjectListWindow::CItem::select(bool on) -{ - ui8 mask = UPDATE | SHOWALL; - if(on) - border->recActions |= mask; - else - border->recActions &= ~mask; - redraw();//??? -} - -void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition) -{ - parent->changeSelection(index); -} - -void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) -{ - parent->elementSelected(); -} - -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, "TPGATE"), - onSelect(Callback), - selected(initialSelection) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - items.reserve(_items.size()); - - for(int id : _items) - { - items.push_back(std::make_pair(id, LOCPLINT->cb->getObjInstance(ObjectInstanceID(id))->getObjectName())); - } - - init(titleWidget_, _title, _descr); -} - -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, "TPGATE"), - onSelect(Callback), - selected(initialSelection) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - items.reserve(_items.size()); - - for(size_t i=0; i<_items.size(); i++) - items.push_back(std::make_pair(int(i), _items[i])); - - init(titleWidget_, _title, _descr); -} - -void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr) -{ - titleWidget = titleWidget_; - - title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); - descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); - exit = std::make_shared( Point(228, 402), "ICANCEL.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); - - 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) ); - list->setRedrawParent(true); - - ok = std::make_shared(Point(15, 402), "IOKAY.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); - ok->block(!list->size()); -} - -std::shared_ptr CObjectListWindow::genItem(size_t index) -{ - if(index < items.size()) - return std::make_shared(this, index, items[index].second); - return std::shared_ptr(); -} - -void CObjectListWindow::elementSelected() -{ - std::function toCall = onSelect;//save - int where = items[selected].first; //required variables - close();//then destroy window - toCall(where);//and send selected object -} - -void CObjectListWindow::exitPressed() -{ - std::function toCall = onExit;//save - close();//then destroy window - if(toCall) - toCall(); -} - -void CObjectListWindow::changeSelection(size_t which) -{ - ok->block(false); - if(selected == which) - return; - - for(std::shared_ptr element : list->getItems()) - { - CItem * item = dynamic_cast(element.get()); - if(item) - { - if(item->index == selected) - item->select(false); - - if(item->index == which) - item->select(true); - } - } - selected = which; -} - -void CObjectListWindow::keyPressed (EShortcut key) -{ - int sel = static_cast(selected); - - switch(key) - { - break; case EShortcut::MOVE_UP: - sel -=1; - - break; case EShortcut::MOVE_DOWN: - sel +=1; - - break; case EShortcut::MOVE_PAGE_UP: - sel -=9; - - break; case EShortcut::MOVE_PAGE_DOWN: - sel +=9; - - break; case EShortcut::MOVE_FIRST: - sel = 0; - - break; case EShortcut::MOVE_LAST: - sel = static_cast(items.size()); - - break; default: - return; - } - - vstd::abetween(sel, 0, items.size()-1); - list->scrollTo(sel); - changeSelection(sel); -} +/* + * GUIClasses.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 "GUIClasses.h" + +#include "CCastleInterface.h" +#include "CCreatureWindow.h" +#include "CHeroBackpackWindow.h" +#include "CHeroWindow.h" +#include "InfoWindows.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../CVideoHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" + +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/CreatureCostBox.h" +#include "../widgets/Buttons.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" + +#include "../render/Canvas.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" + +#include "../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../lib/mapObjectConstructors/CommonConstructors.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGMarket.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/ObjectTemplate.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/gameState/SThievesGuildInfo.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/GameSettings.h" +#include "../lib/CondSh.h" +#include "../lib/CSkillHandler.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/TextOperations.h" + +CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) + : CIntObject(LCLICK | SHOW_POPUP), + parent(window), + selected(false), + creature(crea), + amount(totalAmount) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + animation = std::make_shared(1, 1, creature, true, true); + // 1 + 1 px for borders + pos.w = animation->pos.w + 2; + pos.h = animation->pos.h + 2; +} + +void CRecruitmentWindow::CCreatureCard::select(bool on) +{ + selected = on; + redraw(); +} + +void CRecruitmentWindow::CCreatureCard::clickPressed(const Point & cursorPosition) +{ + parent->select(this->shared_from_this()); +} + +void CRecruitmentWindow::CCreatureCard::showPopupWindow(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(creature, true); +} + +void CRecruitmentWindow::CCreatureCard::showAll(Canvas & to) +{ + CIntObject::showAll(to); + if(selected) + to.drawBorder(pos, Colors::RED); + else + to.drawBorder(pos, Colors::YELLOW); +} + +void CRecruitmentWindow::select(std::shared_ptr card) +{ + if(card == selected) + return; + + if(selected) + selected->select(false); + + selected = card; + + if(selected) + selected->select(true); + + if(card) + { + si32 maxAmount = card->creature->maxAmount(LOCPLINT->cb->getResourceAmount()); + + vstd::amin(maxAmount, card->amount); + + slider->setAmount(maxAmount); + + if(slider->getValue() != maxAmount) + slider->scrollTo(maxAmount); + else // if slider already at 0 - emulate call to sliderMoved() + sliderMoved(maxAmount); + + costPerTroopValue->createItems(card->creature->getFullRecruitCost()); + totalCostValue->createItems(card->creature->getFullRecruitCost()); + + costPerTroopValue->set(card->creature->getFullRecruitCost()); + totalCostValue->set(card->creature->getFullRecruitCost() * maxAmount); + + //Recruit %s + title->setText(boost::str(boost::format(CGI->generaltexth->tcommands[21]) % card->creature->getNamePluralTranslated())); + + maxButton->block(maxAmount == 0); + slider->block(maxAmount == 0); + } +} + +void CRecruitmentWindow::close() +{ + if (onClose) + onClose(); + CStatusbarWindow::close(); +} + +void CRecruitmentWindow::buy() +{ + CreatureID crid = selected->creature->getId(); + SlotID dstslot = dst->getSlotFor(crid); + + if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot + { + std::pair toMerge; + bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); + + if (allowMerge && dst->mergableStacks(toMerge)) + { + LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); + } + else + { + std::string txt; + if(dwelling->ID != Obj::TOWN) + { + txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. + boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); + } + else + { + txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. + } + + LOCPLINT->showInfoDialog(txt); + return; + } + } + + onRecruit(crid, slider->getValue()); + if(level >= 0) + close(); +} + +void CRecruitmentWindow::showAll(Canvas & to) +{ + CWindowObject::showAll(to); + + Rect(172, 222, 67, 42) + pos.topLeft(); + + // recruit\total values + to.drawBorder(Rect(172, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); + to.drawBorder(Rect(246, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); + + //cost boxes + to.drawBorder(Rect( 64, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); + to.drawBorder(Rect(322, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); + + //buttons borders + to.drawBorder(Rect(133, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); + to.drawBorder(Rect(211, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); + to.drawBorder(Rect(289, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); +} + +CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset): + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPRCRT")), + onRecruit(Recruit), + onClose(onClose), + level(Level), + dst(Dst), + selected(nullptr), + dwelling(Dwelling) +{ + moveBy(Point(0, y_offset)); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + slider = std::make_shared(Point(176, 279), 135, std::bind(&CRecruitmentWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); + + maxButton = std::make_shared(Point(134, 313), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); + buyButton = std::make_shared(Point(212, 313), AnimationPath::builtin("IBY6432.DEF"), CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); + cancelButton = std::make_shared(Point(290, 313), AnimationPath::builtin("ICN6432.DEF"), CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); + + title = std::make_shared(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); + availableValue = std::make_shared(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + toRecruitValue = std::make_shared(279, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + costPerTroopValue = std::make_shared(Rect(65, 222, 97, 74), CGI->generaltexth->allTexts[346]); + totalCostValue = std::make_shared(Rect(323, 222, 97, 74), CGI->generaltexth->allTexts[466]); + + availableTitle = std::make_shared(205, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[465]); + toRecruitTitle = std::make_shared(279, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[16]); + + availableCreaturesChanged(); +} + +void CRecruitmentWindow::availableCreaturesChanged() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + size_t selectedIndex = 0; + + if(!cards.empty() && selected) // find position of selected item + selectedIndex = std::find(cards.begin(), cards.end(), selected) - cards.begin(); + + select(nullptr); + + cards.clear(); + + for(int i=0; icreatures.size(); i++) + { + //find appropriate level + if(level >= 0 && i != level) + continue; + + int amount = dwelling->creatures[i].first; + + //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)); + } + + const int creatureWidth = 102; + + //normal distance between cards - 18px + int requiredSpace = 18; + //maximum distance we can use without reaching window borders + int availableSpace = pos.w - 50 - creatureWidth * (int)cards.size(); + + if (cards.size() > 1) // avoid division by zero + availableSpace /= (int)cards.size() - 1; + else + availableSpace = 0; + + assert(availableSpace >= 0); + + const int spaceBetween = std::min(requiredSpace, availableSpace); + const int totalCreatureWidth = spaceBetween + creatureWidth; + + //now we know total amount of cards and can move them to correct position + int curx = pos.w / 2 - (creatureWidth*(int)cards.size()/2) - (spaceBetween*((int)cards.size()-1)/2); + for(auto & card : cards) + { + card->moveBy(Point(curx, 64)); + curx += totalCreatureWidth; + } + + //restore selection + select(cards[selectedIndex]); + + if(slider->getValue() == slider->getAmount()) + slider->scrollToMax(); + else // if slider already at 0 - emulate call to sliderMoved() + sliderMoved(slider->getAmount()); +} + +void CRecruitmentWindow::sliderMoved(int to) +{ + if(!selected) + return; + + buyButton->block(!to); + availableValue->setText(std::to_string(selected->amount - to)); + toRecruitValue->setText(std::to_string(to)); + + totalCostValue->set(selected->creature->getFullRecruitCost() * to); +} + +CSplitWindow::CSplitWindow(const CCreature * creature, std::function callback_, int leftMin_, int rightMin_, int leftAmount_, int rightAmount_) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GPUCRDIV")), + callback(callback_), + leftAmount(leftAmount_), + rightAmount(rightAmount_), + leftMin(leftMin_), + rightMin(rightMin_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + int total = leftAmount + rightAmount; + int leftMax = total - rightMin; + int rightMax = total - leftMin; + + ok = std::make_shared(Point(20, 263), AnimationPath::builtin("IOK6432"), CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(214, 263), AnimationPath::builtin("ICN6432"), CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); + + int sliderPosition = total - leftMin - rightMin; + + leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); + rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); + + //add filters to allow only number input + leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); + rightInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, rightMin, rightMax); + + leftInput->setText(std::to_string(leftAmount), false); + rightInput->setText(std::to_string(rightAmount), false); + + animLeft = std::make_shared(20, 54, creature, true, false); + animRight = std::make_shared(177, 54,creature, true, false); + + slider = std::make_shared(Point(21, 194), 257, std::bind(&CSplitWindow::sliderMoved, this, _1), 0, sliderPosition, rightAmount - rightMin, Orientation::HORIZONTAL); + + std::string titleStr = CGI->generaltexth->allTexts[256]; + boost::algorithm::replace_first(titleStr,"%s", creature->getNamePluralTranslated()); + title = std::make_shared(150, 34, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleStr); +} + +void CSplitWindow::setAmountText(std::string text, bool left) +{ + int amount = 0; + if(text.length()) + { + try + { + amount = boost::lexical_cast(text); + } + catch(boost::bad_lexical_cast &) + { + amount = left ? leftAmount : rightAmount; + } + + int total = leftAmount + rightAmount; + if(amount > total) + amount = total; + } + + setAmount(amount, left); + slider->scrollTo(rightAmount - rightMin); +} + +void CSplitWindow::setAmount(int value, bool left) +{ + int total = leftAmount + rightAmount; + leftAmount = left ? value : total - value; + rightAmount = left ? total - value : value; + + leftInput->setText(std::to_string(leftAmount)); + rightInput->setText(std::to_string(rightAmount)); +} + +void CSplitWindow::apply() +{ + callback(leftAmount, rightAmount); + close(); +} + +void CSplitWindow::sliderMoved(int to) +{ + setAmount(rightMin + to, false); +} + +CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, std::function callback) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("LVLUPBKG")), + cb(callback) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + LOCPLINT->showingDialog->setn(true); + + if(!skills.empty()) + { + std::vector> comps; + for(auto & skill : skills) + { + auto comp = std::make_shared(ComponentType::SEC_SKILL, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); + comp->onChoose = std::bind(&CLevelWindow::close, this); + comps.push_back(comp); + } + + box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); + } + + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66); + ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); + + //%s has gained a level. + mainTitle = std::make_shared(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated())); + + //%s is now a level %d %s. + std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445"); + boost::replace_first(levelTitleText, "%s", hero->getNameTranslated()); + boost::replace_first(levelTitleText, "%d", std::to_string(hero->level)); + boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated()); + + levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); + + skillIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), static_cast(pskill), 0, 174, 190); + + skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[static_cast(pskill)] + " +1"); +} + + +CLevelWindow::~CLevelWindow() +{ + //FIXME: call callback if there was nothing to select? + if (box && box->selectedIndex() != -1) + cb(box->selectedIndex()); + + LOCPLINT->showingDialog->setn(false); +} + +CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), + onWindowClosed(onWindowClosed), + tavernObj(TavernObj) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + std::vector h = LOCPLINT->cb->getAvailableHeroes(TavernObj); + if(h.size() < 2) + h.resize(2, nullptr); + + selected = 0; + if(!h[0]) + selected = 1; + if(!h[0] && !h[1]) + selected = -1; + + oldSelected = -1; + + h1 = std::make_shared(selected, 0, 72, 299, h[0]); + h2 = std::make_shared(selected, 1, 162, 299, h[1]); + + title = std::make_shared(197, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[37]); + cost = std::make_shared(320, 328, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(GameConstants::HERO_GOLD_COST)); + heroDescription = std::make_shared("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + heroesForHire = std::make_shared(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]); + + rumor = std::make_shared(LOCPLINT->cb->getTavernRumor(tavernObj), Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + cancel = std::make_shared(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); + recruit = std::make_shared(Point(272, 355), AnimationPath::builtin("TPTAV01.DEF"), CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); + thiefGuild = std::make_shared(Point(22, 428), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); + + if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold + { + recruit->addHoverText(CButton::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)) + { + //Cannot recruit. You already have %d Heroes. + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); + recruit->block(true); + } + else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + { + //Cannot recruit. You already have %d Heroes. + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); + recruit->block(true); + } + else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero) + { + recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. + recruit->block(true); + } + else + { + if(selected == -1) + recruit->block(true); + } + if(LOCPLINT->castleInt) + CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + else if(const auto * townObj = dynamic_cast(TavernObj)) + CCS->videoh->open(townObj->town->clientInfo.tavernVideo); + else + CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); +} + +void CTavernWindow::recruitb() +{ + const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; + const CGObjectInstance *obj = tavernObj; + + LOCPLINT->cb->recruitHero(obj, toBuy); + close(); +} + +void CTavernWindow::thievesguildb() +{ + GH.windows().createAndPushWindow(tavernObj); +} + +void CTavernWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +CTavernWindow::~CTavernWindow() +{ + CCS->videoh->close(); +} + +void CTavernWindow::show(Canvas & to) +{ + CWindowObject::show(to); + + if(selected >= 0) + { + auto sel = selected ? h2 : h1; + + if(selected != oldSelected) + { + // Selected hero just changed. Update RECRUIT button hover text if recruitment is allowed. + oldSelected = selected; + + heroDescription->setText(sel->description); + + //Recruit %s the %s + if (!recruit->isBlocked()) + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated())); + + } + + 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) +{ + if(h) + *_sel = _id; +} + +void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition) +{ + if(h) + GH.windows().createAndPushWindow(std::make_shared(h)); +} + +CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H) + : CIntObject(LCLICK | SHOW_POPUP | HOVER), + h(H), _sel(&sel), _id(id) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + h = H; + pos.x += x; + pos.y += y; + pos.w = 58; + pos.h = 64; + + if(H) + { + hoverName = CGI->generaltexth->tavernInfo[4]; + boost::algorithm::replace_first(hoverName,"%s",H->getNameTranslated()); + + int artifs = (int)h->artifactsWorn.size() + (int)h->artifactsInBackpack.size(); + for(int i=13; i<=17; i++) //war machines and spellbook don't count + if(vstd::contains(h->artifactsWorn, ArtifactPosition(i))) + artifs--; + + description = CGI->generaltexth->allTexts[215]; + boost::algorithm::replace_first(description, "%s", h->getNameTranslated()); + boost::algorithm::replace_first(description, "%d", std::to_string(h->level)); + boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); + boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); + + portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->getIconIndex()); + } +} + +void CTavernWindow::HeroPortrait::hover(bool on) +{ + //Hoverable::hover(on); + if(on) + GH.statusbar()->write(hoverName); + else + GH.statusbar()->clear(); +} + +static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange"; +static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE"; + +static bool isQuickExchangeLayoutAvailable() +{ + return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG)); +} + +CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) + : CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")), + controller(hero1, hero2), + moveStackLeftButtons(), + moveStackRightButtons() +{ + const bool qeLayout = isQuickExchangeLayoutAvailable(); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + heroInst[0] = LOCPLINT->cb->getHero(hero1); + heroInst[1] = LOCPLINT->cb->getHero(hero2); + + auto genTitle = [](const CGHeroInstance * h) + { + boost::format fmt(CGI->generaltexth->allTexts[138]); + fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated(); + return boost::str(fmt); + }; + + 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))); + else + primSkillImages.push_back(std::make_shared(PSKIL32, g, 0, 385, 19 + 36 * g)); + } + + for(int leftRight : {0, 1}) + { + const CGHeroInstance * hero = heroInst.at(leftRight); + + for(int m=0; m(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE)); + + + 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)); + + specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); + + expImages[leftRight] = std::make_shared(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); + 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]->setHero(heroInst[0]); + artifs[1] = std::make_shared(Point(98, 151)); + artifs[1]->setHero(heroInst[1]); + + addSetAndCallbacks(artifs[0]); + addSetAndCallbacks(artifs[1]); + + for(int g=0; g<4; ++g) + { + primSkillAreas.push_back(std::make_shared()); + if (qeLayout) + primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22)); + else + primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); + primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; + primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g)); + primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; + boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); + } + + //heroes related thing + for(int b=0; b < heroInst.size(); b++) + { + 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 + { + if(getPickedArtifact() == nullptr) + LOCPLINT->openHeroWindow(hero); + }); + + 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(); + + experienceAreas[b] = std::make_shared(); + experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); + experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9]; + experienceAreas[b]->text = CGI->generaltexth->allTexts[2]; + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level)); + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1))); + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp)); + + spellPointsAreas[b] = std::make_shared(); + spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); + spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22]; + spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205]; + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated()); + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana)); + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit())); + + morale[b] = std::make_shared(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true); + luck[b] = std::make_shared(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true); + } + + quit = std::make_shared(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); + if(queryID.getNum() > 0) + quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); }); + + questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); + questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); + + Rect barRect(5, 578, 725, 18); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), barRect, 5, 578)); + + //garrison interface + + garr = std::make_shared(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true); + auto splitButtonCallback = [&](){ garr->splitClick(); }; + garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + + if(qeLayout) + { + auto moveArtifacts = [](const std::function moveRoutine) -> void + { + bool moveEquipped = true; + bool moveBackpack = true; + + if(GH.isKeyboardCtrlDown()) + moveBackpack = false; + else if(GH.isKeyboardShiftDown()) + moveEquipped = false; + moveRoutine(moveEquipped, moveBackpack); + }; + + auto moveArmy = [this](const bool leftToRight) -> void + { + std::optional slotId = std::nullopt; + if(auto slot = getSelectedSlotID()) + slotId = slot->getSlot(); + controller.moveArmy(leftToRight, slotId); + }; + + auto openBackpack = [this](const CGHeroInstance * hero) -> void + { + GH.windows().createAndPushWindow(hero); + for(auto artSet : artSets) + GH.windows().topWindow()->addSet(artSet); + }; + + moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, true)); + exchangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), + std::bind(&CExchangeController::swapArmy, &controller)); + moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, false)); + moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);})); + exchangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); + moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); + backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[0])); + backpackButtonLeft->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[1])); + backpackButtonRight->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + + auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID(); + auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID(); + moveAllGarrButtonLeft->block(leftHeroBlock); + exchangeGarrButton->block(leftHeroBlock || rightHeroBlock); + moveAllGarrButtonRight->block(rightHeroBlock); + moveArtifactsButtonLeft->block(leftHeroBlock); + exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock); + moveArtifactsButtonRight->block(rightHeroBlock); + backpackButtonLeft->block(leftHeroBlock); + backpackButtonRight->block(rightHeroBlock); + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + moveStackLeftButtons.push_back( + std::make_shared( + Point(484 + 35 * i, 154), + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), + CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); + moveStackLeftButtons.back()->block(leftHeroBlock); + + moveStackRightButtons.push_back( + std::make_shared( + Point(66 + 35 * i, 154), + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), + CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); + moveStackLeftButtons.back()->block(rightHeroBlock); + } + } + + updateWidgets(); +} + +const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const +{ + return garr->getSelection(); +} + +void CExchangeWindow::updateGarrisons() +{ + garr->recreateSlots(); + + updateWidgets(); +} + +bool CExchangeWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + +void CExchangeWindow::questlog(int whichHero) +{ + CCS->curh->dragAndDropCursor(nullptr); + LOCPLINT->showQuestLog(); +} + +void CExchangeWindow::updateWidgets() +{ + for(size_t leftRight : {0, 1}) + { + const CGHeroInstance * hero = heroInst.at(leftRight); + + for(int m=0; mgetPrimSkillLevel(static_cast(m)); + primSkillValues[leftRight][m]->setText(std::to_string(value)); + } + + for(int m=0; m < hero->secSkills.size(); ++m) + { + int id = hero->secSkills[m].first; + int level = hero->secSkills[m].second; + + secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level); + } + + expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3)); + manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3)); + + morale[leftRight]->set(hero); + luck[leftRight]->set(hero); + } +} + +CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSHIP")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + bgWater = std::make_shared(ImagePath::builtin("TPSHIPBK"), 100, 69); + + auto handler = CGI->objtypeh->getHandlerFor(Obj::BOAT, boatType); + + auto boatConstructor = std::dynamic_pointer_cast(handler); + + assert(boatConstructor); + + if (boatConstructor) + { + 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->center(waterCenter); + } + + // Create resource icons and costs. + std::string goldValue = std::to_string(cost[EGameResID::GOLD]); + std::string woodValue = std::to_string(cost[EGameResID::WOOD]); + + goldCost = std::make_shared(118, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, goldValue); + woodCost = std::make_shared(212, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, woodValue); + + goldPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 100, 244); + woodPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::WOOD), 0, 196, 244); + + quit = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); + build = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30"), CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); + build->addCallback(onBuy); + + for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) + { + if(cost[i] > LOCPLINT->cb->getResourceAmount(i)) + { + build->block(true); + break; + } + } + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + title = std::make_shared(164, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[13]); + costLabel = std::make_shared(164, 220, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[14]); +} + +void CTransformerWindow::CItem::move() +{ + if(left) + moveBy(Point(289, 0)); + else + moveBy(Point(-289, 0)); + left = !left; +} + +void CTransformerWindow::CItem::clickPressed(const Point & cursorPosition) +{ + move(); + parent->redraw(); +} + +void CTransformerWindow::CItem::update() +{ + icon->setFrame(parent->army->getCreature(SlotID(id))->getId() + 2); +} + +CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id_) + : CIntObject(LCLICK), + id(id_), + size(size_), + parent(parent_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + left = true; + pos.w = 58; + pos.h = 64; + + pos.x += 45 + (id%3)*83 + id/6*83; + pos.y += 109 + (id/3)*98; + icon = std::make_shared(AnimationPath::builtin("TWCRPORT"), parent->army->getCreature(SlotID(id))->getId() + 2); + count = std::make_shared(28, 76,FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(size)); +} + +void CTransformerWindow::makeDeal() +{ + for(auto & elem : items) + { + if(!elem->left) + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, SlotID(elem->id), {}, {}, hero); + } +} + +void CTransformerWindow::addAll() +{ + for(auto & elem : items) + { + if(elem->left) + elem->move(); + } + redraw(); +} + +void CTransformerWindow::updateGarrisons() +{ + for(auto & item : items) + item->update(); +} + +bool CTransformerWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == hero; +} + +CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), + hero(_hero), + onWindowClosed(onWindowClosed), + market(_market) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(hero) + army = hero; + else + army = dynamic_cast(market); //for town only + + if(army) + { + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + if(army->getCreature(SlotID(i))) + items.push_back(std::make_shared(this, army->getStackCount(SlotID(i)), i)); + } + } + + all = std::make_shared(Point(146, 416), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); + convert = std::make_shared(Point(269, 416), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(392, 416), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + titleLeft = std::make_shared(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area + titleRight = std::make_shared(153+295, 29, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[486]);//transformer + helpLeft = std::make_shared(CGI->generaltexth->allTexts[487], Rect(26, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//move creatures to create skeletons + helpRight = std::make_shared(CGI->generaltexth->allTexts[488], Rect(320, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//creatures here will become skeletons +} + +void CTransformerWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int Y) + : CIntObject(LCLICK | SHOW_POPUP | HOVER), + ID(_ID), + parent(_parent) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + 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); + + 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; +} + +void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) +{ + if(state() == 2) + GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); +} + +void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(ComponentType::SEC_SKILL, ID, 1)); +} + +void CUniversityWindow::CItem::hover(bool on) +{ + if(on) + GH.statusbar()->write(CGI->skillh->getByIndex(ID)->getNameTranslated()); + else + GH.statusbar()->clear(); +} + +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) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), + hero(_hero), + onWindowClosed(onWindowClosed), + market(_market) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + 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); + } + else if(auto uni = dynamic_cast(_market); uni->appearance) + { + titlePic = std::make_shared(uni->appearance->animationFile, 0); + titleStr = uni->title; + speechStr = uni->speech; + } + else + { + titlePic = std::make_shared(ImagePath::builtin("UNIVBLDG")); + } + + titlePic->center(Point(232 + pos.x, 76 + pos.y)); + + clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); + + std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); + + for(int i=0; i(this, goods[i].as(), 54+i*104, 234)); + + cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CUniversityWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +void CUniversityWindow::makeDeal(SecondarySkill skill) +{ + LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, GameResID(GameResID::GOLD), skill, 1, hero); +} + + +CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, SecondarySkill SKILL, bool available) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), + owner(owner_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + std::string text = CGI->generaltexth->allTexts[608]; + boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); + boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + boost::replace_first(text, "%d", "2000"); + + clerkSpeech = std::make_shared(text, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + name = std::make_shared(230, 37, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + icon = std::make_shared(AnimationPath::builtin("SECSKILL"), SKILL*3+3, 0, 211, 51); + level = std::make_shared(230, 107, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[1]); + + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 210, 210); + cost = std::make_shared(230, 267, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "2000"); + + std::string hoverText = CGI->generaltexth->allTexts[609]; + boost::replace_first(hoverText, "%s", CGI->generaltexth->levels[0]+ " " + CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + + text = CGI->generaltexth->zelp[633].second; + boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); + boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + boost::replace_first(text, "%d", "2000"); + + confirm = std::make_shared(Point(148, 299), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); + confirm->block(!available); + + cancel = std::make_shared(Point(252,299), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CUnivConfirmWindow::makeDeal(SecondarySkill skill) +{ + owner->makeDeal(skill); + close(); +} + +CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GARRISON")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + 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(); } ); + garr->addSplitBtn(split); + } + quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + + std::string titleText; + if(down->tempOwner == up->tempOwner) + { + titleText = CGI->generaltexth->allTexts[709]; + } + else + { + //assume that this is joining monsters dialog + if(up->Slots().size() > 0) + { + titleText = CGI->generaltexth->allTexts[35]; + boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->type->getNamePluralTranslated()); + } + else + { + logGlobal->error("Invalid armed instance for garrison window."); + } + } + title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); + + banner = std::make_shared(AnimationPath::builtin("CREST58"), up->getOwner().getNum(), 0, 28, 124); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->getIconIndex(), 0, 29, 222); +} + +void CGarrisonWindow::updateGarrisons() +{ + garr->recreateSlots(); +} + +bool CGarrisonWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + +CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), + fort(object), + hero(visitor) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + title = std::make_shared(325, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, fort->getObjectName()); + + heroPic = std::make_shared(30, 60, hero); + + for(int i=0; i < resCount; i++) + { + totalIcons[i] = std::make_shared(AnimationPath::builtin("SMALRES"), i, 0, 104 + 76 * i, 237); + totalLabels[i] = std::make_shared(166 + 76 * i, 253, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); + } + + for(int i = 0; i < slotsCount; i++) + { + upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath(), CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); + for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) + upgrade[i]->addImage(AnimationPath::builtin(image)); + + for(int j : {0,1}) + { + slotIcons[i][j] = std::make_shared(AnimationPath::builtin("SMALRES"), 0, 0, 104 + 76 * i, 128 + 20 * j); + slotLabels[i][j] = std::make_shared(168 + 76 * i, 144 + 20 * j, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); + } + } + + upgradeAll = std::make_shared(Point(30, 231), AnimationPath(), CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); + for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) + upgradeAll->addImage(AnimationPath::builtin(image)); + + quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); + 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); + updateGarrisons(); +} + +bool CHillFortWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + +void CHillFortWindow::updateGarrisons() +{ + std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade + + TResources totalSum; // totalSum[resource ID] = value + + 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)); + totalSum += costs[i]; + } + } + + currState[i] = newState; + upgrade[i]->setIndex(currState[i] == -1 ? 0 : currState[i]); + upgrade[i]->block(currState[i] == -1); + upgrade[i]->addHoverText(CButton::NORMAL, getTextForSlot(SlotID(i))); + } + + //"Upgrade all" slot + int newState = 2; + { + TResources myRes = LOCPLINT->cb->getResourceAmount(); + + bool allUpgraded = true;//All creatures are upgraded? + for(int i=0; isetIndex(newState); + + garr->recreateSlots(); + + for(int i = 0; i < slotsCount; i++) + { + //hide all first + for(int j : {0,1}) + { + slotIcons[i][j]->visible = false; + slotLabels[i][j]->setText(""); + } + //if can upgrade or can not afford, draw cost + if(currState[i] == 0 || currState[i] == 2) + { + if(costs[i].nonZero()) + { + //reverse iterator is used to display gold as first element + int j = 0; + for(int res = (int)costs[i].size()-1; (res >= 0) && (j < 2); res--) + { + int val = costs[i][res]; + if(!val) + continue; + + slotIcons[i][j]->visible = true; + slotIcons[i][j]->setFrame(res); + + slotLabels[i][j]->setText(std::to_string(val)); + j++; + } + } + else//free upgrade - print gold image and "Free" text + { + slotIcons[i][0]->visible = true; + slotIcons[i][0]->setFrame(GameResID(EGameResID::GOLD)); + slotLabels[i][0]->setText(CGI->generaltexth->allTexts[344]); + } + } + } + + for(int i = 0; i < resCount; i++) + { + if(totalSum[i] == 0) + { + totalIcons[i]->visible = false; + totalLabels[i]->setText(""); + } + else + { + totalIcons[i]->visible = true; + totalLabels[i]->setText(std::to_string(totalSum[i])); + } + } +} + +void CHillFortWindow::makeDeal(SlotID slot) +{ + 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: + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[313 + offset], std::vector>(), soundBase::sound_todo); + break; + case 2: + for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); + LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID[0]); + } + } + break; + } +} + +std::string CHillFortWindow::getTextForSlot(SlotID slot) +{ + if(!hero->getCreature(slot))//we don`t have creature here + return ""; + + std::string str = CGI->generaltexth->allTexts[318]; + int amount = hero->getStackCount(slot); + if(amount == 1) + boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNameSingularTranslated()); + else + boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNamePluralTranslated()); + + return str; +} + +int CHillFortWindow::getState(SlotID slot) +{ + TResources myRes = LOCPLINT->cb->getResourceAmount(); + + if(hero->slotEmpty(slot))//no creature here + return -1; + + UpgradeInfo info; + LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); + if(!info.newID.size())//already upgraded + return 1; + + if(!(info.cost[0] * hero->getStackCount(slot)).canBeAfforded(myRes)) + return 0; + + return 2;//can upgrade +} + +CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): + CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin("TpRank")), + owner(_owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + SThievesGuildInfo tgi; //info to be displayed + LOCPLINT->cb->getThievesGuildInfo(tgi, owner); + + exitb = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); + statusbar = CGStatusBar::create(3, 555, ImagePath::builtin("TStatBar.bmp"), 742); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + //data for information table: + // fields[row][column] = list of id's of players for this box + static std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = + { &SThievesGuildInfo::numOfTowns, &SThievesGuildInfo::numOfHeroes, &SThievesGuildInfo::gold, + &SThievesGuildInfo::woodOre, &SThievesGuildInfo::mercSulfCrystGems, &SThievesGuildInfo::obelisks, + &SThievesGuildInfo::artifacts, &SThievesGuildInfo::army, &SThievesGuildInfo::income }; + + for(int g=0; g<12; ++g) + { + int posY[] = {400, 460, 510}; + int y; + if(g < 9) + y = 52 + 32*g; + else + y = posY[g-9]; + + std::string text = CGI->generaltexth->jktexts[24+g]; + boost::algorithm::trim_if(text,boost::algorithm::is_any_of("\"")); + 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)); + + 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 + { + for(int b=0; b<(tgi .* fields[g]).size(); ++b) //by places (1st, 2nd, ...) + { + std::vector &players = (tgi .* fields[g])[b]; //get players with this place in this line + + //position of box + int xpos = 259 + 66 * b; + int ypos = 41 + 32 * g; + + size_t rowLength[2]; //size of each row + rowLength[0] = std::min(players.size(), 4); + rowLength[1] = players.size() - rowLength[0]; + + for(size_t j=0; j < 2; j++) + { + // origin of this row | offset for 2nd row| shift right for short rows + //if we have 2 rows, start either from mid or beginning (depending on count), otherwise center the flags + int rowStartX = xpos + (j ? 6 + ((int)rowLength[j] < 3 ? 12 : 0) : 24 - 6 * (int)rowLength[j]); + 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)); + } + } + } + + static const std::string colorToBox[] = {"PRRED.BMP", "PRBLUE.BMP", "PRTAN.BMP", "PRGREEN.BMP", "PRORANGE.BMP", "PRPURPLE.BMP", "PRTEAL.BMP", "PRROSE.bmp"}; + + //printing best hero + int counter = 0; + for(auto & iter : tgi.colorToBestHero) + { + banners.push_back(std::make_shared(ImagePath::builtin(colorToBox[iter.first.getNum()]), 253 + 66 * counter, 334)); + if(iter.second.portraitSource.isValid()) + { + bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.getIconIndex(), 0, 260 + 66 * counter, 360)); + //TODO: r-click info: + // - r-click on hero + // - r-click on primary skill label + if(iter.second.details) + { + primSkillHeaders.push_back(std::make_shared(CGI->generaltexth->allTexts[184], Rect(260 + 66*counter, 396, 52, 64), + 0, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE)); + + for(int i=0; iprimskills.size(); ++i) + { + primSkillValues.push_back(std::make_shared(310 + 66 * counter, 407 + 11*i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, + std::to_string(iter.second.details->primskills[i]))); + } + } + } + counter++; + } + + //printing best creature + counter = 0; + for(auto & it : tgi.bestCreature) + { + if(it.second != CreatureID::NONE) + bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); + counter++; + } + + //printing personality + counter = 0; + for(auto & it : tgi.personality) + { + std::string text; + if(it.second == EAiTactic::NONE) + { + text = CGI->generaltexth->arraytxt[172]; + } + else if(it.second != EAiTactic::RANDOM) + { + text = CGI->generaltexth->arraytxt[168 + static_cast(it.second)]; + } + + personalities.push_back(std::make_shared(283 + 66*counter, 459, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text)); + + counter++; + } +} + +CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text) + : CIntObject(LCLICK | DOUBLECLICK), + parent(_parent), + index(_id) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + border = std::make_shared(ImagePath::builtin("TPGATES")); + pos = border->pos; + + setRedrawParent(true); + + text = std::make_shared(pos.w/2, pos.h/2, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _text); + select(index == parent->selected); +} + +void CObjectListWindow::CItem::select(bool on) +{ + ui8 mask = UPDATE | SHOWALL; + if(on) + border->recActions |= mask; + else + border->recActions &= ~mask; + redraw();//??? +} + +void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition) +{ + parent->changeSelection(index); +} + +void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) +{ + parent->elementSelected(); +} + +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), + onSelect(Callback), + selected(initialSelection) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + items.reserve(_items.size()); + + for(int id : _items) + { + items.push_back(std::make_pair(id, LOCPLINT->cb->getObjInstance(ObjectInstanceID(id))->getObjectName())); + } + + init(titleWidget_, _title, _descr); +} + +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), + onSelect(Callback), + selected(initialSelection) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + items.reserve(_items.size()); + + for(size_t i=0; i<_items.size(); i++) + items.push_back(std::make_pair(int(i), _items[i])); + + init(titleWidget_, _title, _descr); +} + +void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr) +{ + titleWidget = titleWidget_; + + title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); + descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); + exit = std::make_shared( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); + + 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) ); + 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()); +} + +std::shared_ptr CObjectListWindow::genItem(size_t index) +{ + if(index < items.size()) + return std::make_shared(this, index, items[index].second); + return std::shared_ptr(); +} + +void CObjectListWindow::elementSelected() +{ + std::function toCall = onSelect;//save + int where = items[selected].first; //required variables + close();//then destroy window + toCall(where);//and send selected object +} + +void CObjectListWindow::exitPressed() +{ + std::function toCall = onExit;//save + close();//then destroy window + if(toCall) + toCall(); +} + +void CObjectListWindow::changeSelection(size_t which) +{ + ok->block(false); + if(selected == which) + return; + + for(std::shared_ptr element : list->getItems()) + { + CItem * item = dynamic_cast(element.get()); + if(item) + { + if(item->index == selected) + item->select(false); + + if(item->index == which) + item->select(true); + } + } + selected = which; +} + +void CObjectListWindow::keyPressed (EShortcut key) +{ + int sel = static_cast(selected); + + switch(key) + { + break; case EShortcut::MOVE_UP: + sel -=1; + + break; case EShortcut::MOVE_DOWN: + sel +=1; + + break; case EShortcut::MOVE_PAGE_UP: + sel -=9; + + break; case EShortcut::MOVE_PAGE_DOWN: + sel +=9; + + break; case EShortcut::MOVE_FIRST: + sel = 0; + + break; case EShortcut::MOVE_LAST: + sel = static_cast(items.size()); + + break; default: + return; + } + + vstd::abetween(sel, 0, items.size()-1); + list->scrollTo(sel); + changeSelection(sel); +} diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 157377aeb..a58001e5c 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -1,529 +1,512 @@ -/* - * GUIClasses.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 "CWindowObject.h" -#include "../lib/GameConstants.h" -#include "../lib/ResourceSet.h" -#include "../lib/int3.h" -#include "../widgets/CWindowWithArtifacts.h" -#include "../widgets/Images.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGDwelling; -class IMarket; - -VCMI_LIB_NAMESPACE_END - -class CreatureCostBox; -class CCreaturePic; -class MoraleLuckBox; -class CHeroArea; -class CMinorResDataBar; -class CSlider; -class CComponentBox; -class CTextInput; -class CListBox; -class CLabelGroup; -class CToggleButton; -class CGStatusBar; -class CTextBox; -class CResDataBar; -class CGarrisonInt; -class CGarrisonSlot; - -enum class EUserEvent; - -/// Recruitment window where you can recruit creatures -class CRecruitmentWindow : public CStatusbarWindow -{ - class CCreatureCard : public CIntObject, public std::enable_shared_from_this - { - CRecruitmentWindow * parent; - std::shared_ptr animation; - bool selected; - - public: - const CCreature * creature; - si32 amount; - - void select(bool on); - - CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount); - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void showAll(Canvas & to) override; - }; - - std::function onRecruit; //void (int ID, int amount) <-- call to recruit creatures - - int level; - const CArmedInstance * dst; - - std::shared_ptr selected; - std::vector> cards; - - std::shared_ptr slider; - std::shared_ptr maxButton; - std::shared_ptr buyButton; - std::shared_ptr cancelButton; - std::shared_ptr title; - std::shared_ptr availableValue; - std::shared_ptr toRecruitValue; - std::shared_ptr availableTitle; - std::shared_ptr toRecruitTitle; - std::shared_ptr costPerTroopValue; - std::shared_ptr totalCostValue; - - void select(std::shared_ptr card); - void buy(); - void sliderMoved(int to); - - void showAll(Canvas & to) override; -public: - const CGDwelling * const dwelling; - CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, int y_offset = 0); - void availableCreaturesChanged(); -}; - -/// Split window where creatures can be split up into two single unit stacks -class CSplitWindow : public CWindowObject -{ - std::function callback; - int leftAmount; - int rightAmount; - - int leftMin; - int rightMin; - std::shared_ptr title; - std::shared_ptr slider; - std::shared_ptr animLeft; - std::shared_ptr animRight; - std::shared_ptr ok; - std::shared_ptr cancel; - std::shared_ptr leftInput; - std::shared_ptr rightInput; - - void setAmountText(std::string text, bool left); - void setAmount(int value, bool left); - void sliderMoved(int value); - void apply(); - -public: - /** - * creature - displayed creature - * callback(leftAmount, rightAmount) - function to call on close - * leftMin, rightMin - minimal amount of creatures in each stack - * leftAmount, rightAmount - amount of creatures in each stack - */ - CSplitWindow(const CCreature * creature, std::function callback, int leftMin, int rightMin, int leftAmount, int rightAmount); -}; - -/// Raised up level window where you can select one out of two skills -class CLevelWindow : public CWindowObject -{ - std::shared_ptr portrait; - std::shared_ptr ok; - std::shared_ptr mainTitle; - std::shared_ptr levelTitle; - std::shared_ptr skillIcon; - std::shared_ptr skillValue; - - std::shared_ptr box; //skills to select - std::function cb; - - void selectionChanged(unsigned to); - -public: - CLevelWindow(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, std::function callback); - ~CLevelWindow(); -}; - -/// Town portal, castle gate window -class CObjectListWindow : public CWindowObject -{ - class CItem : public CIntObject - { - CObjectListWindow * parent; - std::shared_ptr text; - std::shared_ptr border; - public: - const size_t index; - CItem(CObjectListWindow * parent, size_t id, std::string text); - - void select(bool on); - void clickPressed(const Point & cursorPosition) override; - void clickDouble(const Point & cursorPosition) override; - }; - - std::function onSelect;//called when OK button is pressed, returns id of selected item. - std::shared_ptr titleWidget; - std::shared_ptr title; - std::shared_ptr descr; - - std::shared_ptr list; - std::shared_ptr ok; - std::shared_ptr exit; - - std::vector< std::pair > items;//all items present in list - - void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr); - void exitPressed(); -public: - size_t selected;//index of currently selected item - - std::function onExit;//optional exit callback - - /// 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); - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); - - std::shared_ptr genItem(size_t index); - void elementSelected();//call callback and close this window - void changeSelection(size_t which); - void keyPressed(EShortcut key) override; -}; - -class CTavernWindow : public CStatusbarWindow -{ -public: - class HeroPortrait : public CIntObject - { - public: - std::string hoverName; - std::string description; // "XXX is a level Y ZZZ with N artifacts" - const CGHeroInstance * h; - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover (bool on) override; - HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H); - - private: - int *_sel; - const int _id; - - std::shared_ptr portrait; - }; - - //recruitable heroes - std::shared_ptr h1; - std::shared_ptr h2; //recruitable heroes - - int selected;//0 (left) or 1 (right) - int oldSelected;//0 (left) or 1 (right) - - std::shared_ptr thiefGuild; - std::shared_ptr cancel; - std::shared_ptr recruit; - - const CGObjectInstance * tavernObj; - - std::shared_ptr title; - std::shared_ptr cost; - std::shared_ptr heroesForHire; - std::shared_ptr heroDescription; - - std::shared_ptr rumor; - - CTavernWindow(const CGObjectInstance * TavernObj); - ~CTavernWindow(); - - void recruitb(); - void thievesguildb(); - void show(Canvas & to) override; -}; - -class CCallback; -class CExchangeWindow; - -class CExchangeController -{ -private: - const CGHeroInstance * left; - const CGHeroInstance * right; - std::shared_ptr cb; - CExchangeWindow * view; - -public: - CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2); - std::function onMoveArmyToRight(); - std::function onSwapArmy(); - std::function onMoveArmyToLeft(); - std::function onSwapArtifacts(); - std::function onMoveArtifactsToLeft(); - std::function onMoveArtifactsToRight(); - std::function onMoveStackToLeft(SlotID slotID); - std::function onMoveStackToRight(SlotID slotID); - -private: - void moveArmy(bool leftToRight); - void moveArtifacts(bool leftToRight); - void moveArtifact(const CGHeroInstance * source, const CGHeroInstance * target, ArtifactPosition srcPosition); - void moveStack(const CGHeroInstance * source, const CGHeroInstance * target, SlotID sourceSlot); -}; - -class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts -{ - 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; - std::array, 2> manaImages; - std::array, 2> manaValues; - std::array, 2> portraits; - - std::vector> primSkillAreas; - std::array>, 2> secSkillAreas; - - std::array, 2> heroAreas; - std::array, 2> specialtyAreas; - std::array, 2> experienceAreas; - std::array, 2> spellPointsAreas; - - std::array, 2> morale; - std::array, 2> luck; - - std::shared_ptr quit; - std::array, 2> questlogButton; - - std::shared_ptr garr; - std::shared_ptr moveAllGarrButtonLeft; - std::shared_ptr echangeGarrButton; - std::shared_ptr moveAllGarrButtonRight; - std::shared_ptr moveArtifactsButtonLeft; - std::shared_ptr echangeArtifactsButton; - std::shared_ptr moveArtifactsButtonRight; - std::vector> moveStackLeftButtons; - std::vector> moveStackRightButtons; - CExchangeController controller; - -public: - std::array heroInst; - std::array, 2> artifs; - - void updateGarrisons() override; - - void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right - - void updateWidgets(); - - const CGarrisonSlot * getSelectedSlotID() const; - - CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID); -}; - -/// Here you can buy ships -class CShipyardWindow : public CStatusbarWindow -{ - std::shared_ptr bgWater; - std::shared_ptr bgShip; - - std::shared_ptr title; - std::shared_ptr costLabel; - - std::shared_ptr woodPic; - std::shared_ptr goldPic; - std::shared_ptr woodCost; - std::shared_ptr goldCost; - - std::shared_ptr build; - std::shared_ptr quit; - -public: - CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy); -}; - -/// Creature transformer window -class CTransformerWindow : public CStatusbarWindow, public IGarrisonHolder -{ - class CItem : public CIntObject - { - public: - int id;//position of creature in hero army - bool left;//position of the item - int size; //size of creature stack - CTransformerWindow * parent; - std::shared_ptr icon; - std::shared_ptr count; - - void move(); - void clickPressed(const Point & cursorPosition) override; - void update(); - CItem(CTransformerWindow * parent, int size, int id); - }; - - const CArmedInstance * army;//object with army for transforming (hero or town) - const CGHeroInstance * hero;//only if we have hero in town - const IMarket * market;//market, town garrison is used if hero == nullptr - - std::shared_ptr titleLeft; - std::shared_ptr titleRight; - std::shared_ptr helpLeft; - std::shared_ptr helpRight; - - std::vector> items; - - std::shared_ptr all; - std::shared_ptr convert; - std::shared_ptr cancel; -public: - - void makeDeal(); - void addAll(); - void updateGarrisons() override; - CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero); -}; - -class CUniversityWindow : public CStatusbarWindow -{ - class CItem : public CIntObject - { - std::shared_ptr icon; - std::shared_ptr topBar; - std::shared_ptr bottomBar; - std::shared_ptr name; - std::shared_ptr level; - public: - int 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 - 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; - std::shared_ptr titlePic; - std::shared_ptr title; - std::shared_ptr clerkSpeech; - -public: - CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market); - - void makeDeal(int skill); -}; - -/// Confirmation window for University -class CUnivConfirmWindow : public CStatusbarWindow -{ - std::shared_ptr clerkSpeech; - std::shared_ptr name; - std::shared_ptr level; - std::shared_ptr icon; - - CUniversityWindow * owner; - std::shared_ptr confirm; - std::shared_ptr cancel; - - std::shared_ptr costIcon; - std::shared_ptr cost; - - void makeDeal(int skill); - -public: - CUnivConfirmWindow(CUniversityWindow * PARENT, int SKILL, bool available); -}; - -/// Garrison window where you can take creatures out of the hero to place it on the garrison -class CGarrisonWindow : public CWindowObject, public IGarrisonHolder -{ - std::shared_ptr title; - std::shared_ptr banner; - std::shared_ptr portrait; - - std::shared_ptr garr; - -public: - std::shared_ptr quit; - - CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); - - void updateGarrisons() override; -}; - -/// Hill fort is the building where you can upgrade units -class CHillFortWindow : public CStatusbarWindow, public IGarrisonHolder -{ -private: - static const int slotsCount = 7; - //todo: mithril support - static const int resCount = 7; - - const CGObjectInstance * fort; - const CGHeroInstance * hero; - - std::shared_ptr title; - std::shared_ptr heroPic; - - std::array, resCount> totalIcons; - 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 - - //there is a place for only 2 resources per slot - std::array< std::array, 2>, slotsCount> slotIcons; - std::array< std::array, 2>, slotsCount> slotLabels; - - std::shared_ptr upgradeAll; - std::shared_ptr quit; - - std::shared_ptr garr; - - std::string getDefForSlot(SlotID slot); - 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 -public: - CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); - void updateGarrisons() override;//update buttons after garrison changes -}; - -class CThievesGuildWindow : public CStatusbarWindow -{ - const CGObjectInstance * owner; - - std::shared_ptr exitb; - std::shared_ptr resdatabar; - - std::vector> rowHeaders; - std::vector> columnBackgrounds; - std::vector> columnHeaders; - std::vector> cells; - - std::vector> banners; - std::vector> bestHeroes; - std::vector> primSkillHeaders; - std::vector> primSkillValues; - std::vector> bestCreatures; - std::vector> personalities; -public: - CThievesGuildWindow(const CGObjectInstance * _owner); -}; - +/* + * GUIClasses.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/ResourceSet.h" +#include "../widgets/CExchangeController.h" +#include "../widgets/CWindowWithArtifacts.h" +#include "../widgets/Images.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGDwelling; +class IMarket; + +VCMI_LIB_NAMESPACE_END + +class CreatureCostBox; +class CCreaturePic; +class MoraleLuckBox; +class CHeroArea; +class CSlider; +class CComponentBox; +class CTextInput; +class CListBox; +class CLabelGroup; +class CGStatusBar; +class CTextBox; +class CGarrisonInt; +class CGarrisonSlot; + +enum class EUserEvent; + +/// Recruitment window where you can recruit creatures +class CRecruitmentWindow : public CStatusbarWindow +{ + class CCreatureCard : public CIntObject, public std::enable_shared_from_this + { + CRecruitmentWindow * parent; + std::shared_ptr animation; + bool selected; + + public: + const CCreature * creature; + si32 amount; + + void select(bool on); + + CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount); + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void showAll(Canvas & to) override; + }; + + std::function onRecruit; //void (int ID, int amount) <-- call to recruit creatures + std::function onClose; + + int level; + const CArmedInstance * dst; + + std::shared_ptr selected; + std::vector> cards; + + std::shared_ptr slider; + std::shared_ptr maxButton; + std::shared_ptr buyButton; + std::shared_ptr cancelButton; + std::shared_ptr title; + std::shared_ptr availableValue; + std::shared_ptr toRecruitValue; + std::shared_ptr availableTitle; + std::shared_ptr toRecruitTitle; + std::shared_ptr costPerTroopValue; + std::shared_ptr totalCostValue; + + void select(std::shared_ptr card); + void buy(); + void sliderMoved(int to); + + void showAll(Canvas & to) override; +public: + const CGDwelling * const dwelling; + CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset = 0); + void availableCreaturesChanged(); + void close() override; +}; + +/// Split window where creatures can be split up into two single unit stacks +class CSplitWindow : public CWindowObject +{ + std::function callback; + int leftAmount; + int rightAmount; + + int leftMin; + int rightMin; + std::shared_ptr title; + std::shared_ptr slider; + std::shared_ptr animLeft; + std::shared_ptr animRight; + std::shared_ptr ok; + std::shared_ptr cancel; + std::shared_ptr leftInput; + std::shared_ptr rightInput; + + void setAmountText(std::string text, bool left); + void setAmount(int value, bool left); + void sliderMoved(int value); + void apply(); + +public: + /** + * creature - displayed creature + * callback(leftAmount, rightAmount) - function to call on close + * leftMin, rightMin - minimal amount of creatures in each stack + * leftAmount, rightAmount - amount of creatures in each stack + */ + CSplitWindow(const CCreature * creature, std::function callback, int leftMin, int rightMin, int leftAmount, int rightAmount); +}; + +/// Raised up level window where you can select one out of two skills +class CLevelWindow : public CWindowObject +{ + std::shared_ptr portrait; + std::shared_ptr ok; + std::shared_ptr mainTitle; + std::shared_ptr levelTitle; + std::shared_ptr skillIcon; + std::shared_ptr skillValue; + + std::shared_ptr box; //skills to select + std::function cb; + + void selectionChanged(unsigned to); + +public: + CLevelWindow(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, std::function callback); + ~CLevelWindow(); +}; + +/// Town portal, castle gate window +class CObjectListWindow : public CWindowObject +{ + class CItem : public CIntObject + { + CObjectListWindow * parent; + std::shared_ptr text; + std::shared_ptr border; + public: + const size_t index; + CItem(CObjectListWindow * parent, size_t id, std::string text); + + void select(bool on); + void clickPressed(const Point & cursorPosition) override; + void clickDouble(const Point & cursorPosition) override; + }; + + std::function onSelect;//called when OK button is pressed, returns id of selected item. + std::shared_ptr titleWidget; + std::shared_ptr title; + std::shared_ptr descr; + + std::shared_ptr list; + std::shared_ptr ok; + std::shared_ptr exit; + + std::vector< std::pair > items;//all items present in list + + void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr); + void exitPressed(); +public: + size_t selected;//index of currently selected item + + std::function onExit;//optional exit callback + + /// 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); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); + + std::shared_ptr genItem(size_t index); + void elementSelected();//call callback and close this window + void changeSelection(size_t which); + void keyPressed(EShortcut key) override; +}; + +class CTavernWindow : public CStatusbarWindow +{ + std::function onWindowClosed; + +public: + class HeroPortrait : public CIntObject + { + public: + std::string hoverName; + std::string description; // "XXX is a level Y ZZZ with N artifacts" + const CGHeroInstance * h; + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover (bool on) override; + HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H); + + private: + int *_sel; + const int _id; + + std::shared_ptr portrait; + }; + + //recruitable heroes + std::shared_ptr h1; + std::shared_ptr h2; //recruitable heroes + + int selected;//0 (left) or 1 (right) + int oldSelected;//0 (left) or 1 (right) + + std::shared_ptr thiefGuild; + std::shared_ptr cancel; + std::shared_ptr recruit; + + const CGObjectInstance * tavernObj; + + std::shared_ptr title; + std::shared_ptr cost; + std::shared_ptr heroesForHire; + std::shared_ptr heroDescription; + + std::shared_ptr rumor; + + CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); + ~CTavernWindow(); + + void close() override; + void recruitb(); + void thievesguildb(); + void show(Canvas & to) override; +}; + +class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts +{ + 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; + std::array, 2> manaImages; + std::array, 2> manaValues; + + std::vector> primSkillAreas; + std::array>, 2> secSkillAreas; + + std::array, 2> heroAreas; + std::array, 2> specialtyAreas; + std::array, 2> experienceAreas; + std::array, 2> spellPointsAreas; + + std::array, 2> morale; + std::array, 2> luck; + + std::shared_ptr quit; + std::array, 2> questlogButton; + + std::shared_ptr garr; + std::shared_ptr moveAllGarrButtonLeft; + std::shared_ptr exchangeGarrButton; + std::shared_ptr moveAllGarrButtonRight; + std::shared_ptr moveArtifactsButtonLeft; + std::shared_ptr exchangeArtifactsButton; + std::shared_ptr moveArtifactsButtonRight; + std::vector> moveStackLeftButtons; + std::vector> moveStackRightButtons; + std::shared_ptr backpackButtonLeft; + std::shared_ptr backpackButtonRight; + CExchangeController controller; + +public: + std::array heroInst; + std::array, 2> artifs; + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + + void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right + + void updateWidgets(); + + const CGarrisonSlot * getSelectedSlotID() const; + + CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID); +}; + +/// Here you can buy ships +class CShipyardWindow : public CStatusbarWindow +{ + std::shared_ptr bgWater; + std::shared_ptr bgShip; + + std::shared_ptr title; + std::shared_ptr costLabel; + + std::shared_ptr woodPic; + std::shared_ptr goldPic; + std::shared_ptr woodCost; + std::shared_ptr goldCost; + + std::shared_ptr build; + std::shared_ptr quit; + +public: + CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy); +}; + +/// Creature transformer window +class CTransformerWindow : public CStatusbarWindow, public IGarrisonHolder +{ + class CItem : public CIntObject + { + public: + int id;//position of creature in hero army + bool left;//position of the item + int size; //size of creature stack + CTransformerWindow * parent; + std::shared_ptr icon; + std::shared_ptr count; + + void move(); + void clickPressed(const Point & cursorPosition) override; + void update(); + CItem(CTransformerWindow * parent, int size, int id); + }; + + const CArmedInstance * army;//object with army for transforming (hero or town) + const CGHeroInstance * hero;//only if we have hero in town + const IMarket * market;//market, town garrison is used if hero == nullptr + + std::shared_ptr titleLeft; + std::shared_ptr titleRight; + std::shared_ptr helpLeft; + std::shared_ptr helpRight; + + std::vector> items; + + std::shared_ptr all; + std::shared_ptr convert; + std::shared_ptr cancel; + + std::function onWindowClosed; +public: + + void makeDeal(); + void addAll(); + void close() override; + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; + CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); +}; + +class CUniversityWindow : public CStatusbarWindow +{ + class CItem : public CIntObject + { + std::shared_ptr icon; + 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 + 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; + std::shared_ptr titlePic; + std::shared_ptr title; + std::shared_ptr clerkSpeech; + + std::function onWindowClosed; + +public: + CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); + + void makeDeal(SecondarySkill skill); + void close(); +}; + +/// Confirmation window for University +class CUnivConfirmWindow : public CStatusbarWindow +{ + std::shared_ptr clerkSpeech; + std::shared_ptr name; + std::shared_ptr level; + std::shared_ptr icon; + + CUniversityWindow * owner; + std::shared_ptr confirm; + std::shared_ptr cancel; + + std::shared_ptr costIcon; + std::shared_ptr cost; + + void makeDeal(SecondarySkill skill); + +public: + CUnivConfirmWindow(CUniversityWindow * PARENT, SecondarySkill SKILL, bool available); +}; + +/// Garrison window where you can take creatures out of the hero to place it on the garrison +class CGarrisonWindow : public CWindowObject, public IGarrisonHolder +{ + std::shared_ptr title; + std::shared_ptr banner; + std::shared_ptr portrait; + + std::shared_ptr garr; + +public: + std::shared_ptr quit; + + CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); + + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; +}; + +/// Hill fort is the building where you can upgrade units +class CHillFortWindow : public CStatusbarWindow, public IGarrisonHolder +{ +private: + static const int slotsCount = 7; + //todo: mithril support + static const int resCount = 7; + + const CGObjectInstance * fort; + const CGHeroInstance * hero; + + std::shared_ptr title; + std::shared_ptr heroPic; + + std::array, resCount> totalIcons; + 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 + + //there is a place for only 2 resources per slot + std::array< std::array, 2>, slotsCount> slotIcons; + std::array< std::array, 2>, slotsCount> slotLabels; + + std::shared_ptr upgradeAll; + std::shared_ptr quit; + + std::shared_ptr garr; + + std::string getDefForSlot(SlotID slot); + 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 +public: + CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); + void updateGarrisons() override;//update buttons after garrison changes + bool holdsGarrison(const CArmedInstance * army) override; +}; + +class CThievesGuildWindow : public CStatusbarWindow +{ + const CGObjectInstance * owner; + + std::shared_ptr exitb; + std::shared_ptr resdatabar; + + std::vector> rowHeaders; + std::vector> columnBackgrounds; + std::vector> columnHeaders; + std::vector> cells; + + std::vector> banners; + std::vector> bestHeroes; + std::vector> primSkillHeaders; + std::vector> primSkillValues; + std::vector> bestCreatures; + std::vector> personalities; +public: + CThievesGuildWindow(const CGObjectInstance * _owner); +}; + diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 55d716884..bc55dbb72 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -69,7 +69,7 @@ void CSelWindow::selectionChange(unsigned to) redraw(); } -CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID) +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); ID = askID; @@ -106,6 +106,7 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl addChild(comps[i].get()); components.push_back(comps[i]); comps[i]->onSelect = std::bind(&CSelWindow::selectionChange,this,i); + comps[i]->onChoose = std::bind(&CSelWindow::madeChoiceAndClose,this); if(i<8) comps[i]->assignedKey = vstd::next(EShortcut::SELECT_INDEX_1,i); } @@ -127,6 +128,12 @@ void CSelWindow::madeChoice() LOCPLINT->cb->selectionMade(ret+1,ID); } +void CSelWindow::madeChoiceAndClose() +{ + madeChoice(); + close(); +} + CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo & comps, const TButtonsInfo & Buttons) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -143,7 +150,9 @@ CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo text = std::make_shared(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!text->slider) { - text->resize(text->label->textSize); + int finalWidth = std::min(250, text->label->textSize.x + 32); + int finalHeight = text->label->textSize.y; + text->resize(Point(finalWidth, finalHeight)); } if(buttons.size() == 1) @@ -200,9 +209,9 @@ void CInfoWindow::showInfoDialog(const std::string &text, const TCompsInfo & com void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & components, const CFunctionList &onYes, const CFunctionList &onNo, PlayerColor player) { assert(!LOCPLINT || LOCPLINT->showingDialog->get()); - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); - pom.push_back(std::pair >("ICANCEL.DEF",0)); + std::vector > > pom; + pom.push_back( { AnimationPath::builtin("IOKAY.DEF"), 0 }); + pom.push_back( { AnimationPath::builtin("ICANCEL.DEF"), 0 }); std::shared_ptr temp = std::make_shared(text, player, components, pom); temp->buttons[0]->addCallback( onYes ); @@ -213,8 +222,8 @@ void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & c std::shared_ptr CInfoWindow::create(const std::string &text, PlayerColor playerID, const TCompsInfo & components) { - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); + std::vector > > pom; + pom.push_back({AnimationPath::builtin("IOKAY.DEF"), 0}); return std::make_shared(text, playerID, components, pom); } @@ -344,10 +353,23 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } else { + std::vector components; + if (settings["general"]["enableUiEnhancements"].Bool()) + { + if(LOCPLINT->localState->getCurrentHero()) + components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); + else + components = obj->getPopupComponents(LOCPLINT->playerID); + } + + std::vector> guiComponents; + for (auto & component : components) + guiComponents.push_back(std::make_shared(component)); + if(LOCPLINT->localState->getCurrentHero()) - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero())); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->localState->getCurrentHero()), guiComponents); else - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID)); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->playerID), guiComponents); } } @@ -376,7 +398,7 @@ Point CInfoBoxPopup::toScreen(Point p) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "TOWNQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentTown()); //todo: should this be nearest hero? @@ -386,7 +408,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "HEROQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) { InfoAboutHero iah; LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero());//todo: should this be nearest hero? @@ -396,7 +418,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "TOWNQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(garr, iah); @@ -406,7 +428,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature) - : CWindowObject(RCLICK_POPUP | BORDERED, "DIBOXBCK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position)) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); tooltip = std::make_shared(Point(9, 10), creature); diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index ec84a6693..eea665326 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -15,6 +15,9 @@ VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; +class CGTownInstance; +class CGHeroInstance; class CGGarrison; class CGCreature; class Rect; @@ -46,14 +49,14 @@ public: class CInfoWindow : public CSimpleWindow { public: - using TButtonsInfo = std::vector>>; + using TButtonsInfo = std::vector>>; using TCompsInfo = std::vector>; QueryID ID; //for identification std::shared_ptr text; std::vector> buttons; TCompsInfo components; - virtual void close(); + void close() override; void show(Canvas & to) override; void showAll(Canvas & to) override; @@ -76,7 +79,7 @@ public: class CRClickPopup : public WindowBase { public: - virtual void close(); + virtual void close() override; bool isPopupWindow() const override; static std::shared_ptr createCustomInfoWindow(Point position, const CGObjectInstance * specific); @@ -127,7 +130,8 @@ class CSelWindow : public CInfoWindow public: void selectionChange(unsigned to); void madeChoice(); //looks for selected component and calls callback - CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); + void madeChoiceAndClose(); + CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); //notification - this class inherits important destructor from CInfoWindow }; diff --git a/client/windows/QuickRecruitmentWindow.cpp b/client/windows/QuickRecruitmentWindow.cpp index f2c718018..ae976282b 100644 --- a/client/windows/QuickRecruitmentWindow.cpp +++ b/client/windows/QuickRecruitmentWindow.cpp @@ -31,19 +31,19 @@ void QuickRecruitmentWindow::setButtons() void QuickRecruitmentWindow::setCancelButton() { - cancelButton = std::make_shared(Point((pos.w / 2) + 48, 418), "ICN6432.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + cancelButton = std::make_shared(Point((pos.w / 2) + 48, 418), AnimationPath::builtin("ICN6432.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); cancelButton->setImageOrder(0, 1, 2, 3); } void QuickRecruitmentWindow::setBuyButton() { - buyButton = std::make_shared(Point((pos.w / 2) - 32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purchaseUnits(); }, EShortcut::GLOBAL_ACCEPT); + buyButton = std::make_shared(Point((pos.w / 2) - 32, 418), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(), [&](){ purchaseUnits(); }, EShortcut::GLOBAL_ACCEPT); buyButton->setImageOrder(0, 1, 2, 3); } void QuickRecruitmentWindow::setMaxButton() { - maxButton = std::make_shared(Point((pos.w/2)-112, 418), "IRCBTNS.DEF", CButton::tooltip(), [&](){ maxAllCards(cards); }, EShortcut::RECRUITMENT_MAX); + maxButton = std::make_shared(Point((pos.w/2)-112, 418), AnimationPath::builtin("IRCBTNS.DEF"), CButton::tooltip(), [&](){ maxAllCards(cards); }, EShortcut::RECRUITMENT_MAX); maxButton->setImageOrder(0, 1, 2, 3); } @@ -74,8 +74,8 @@ void QuickRecruitmentWindow::initWindow(Rect startupPosition) pos.w += 108 * (creaturesAmount - 3); pos.x -= 55 * (creaturesAmount - 3); } - backgroundTexture = std::make_shared("DIBOXBCK.pcx", Rect(0, 0, pos.w, pos.h)); - costBackground = std::make_shared("QuickRecruitmentWindow/costBackground.png", pos.w/2-113, 335); + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK.pcx"), Rect(0, 0, pos.w, pos.h)); + costBackground = std::make_shared(ImagePath::builtin("QuickRecruitmentWindow/costBackground.png"), pos.w/2-113, 335); } void QuickRecruitmentWindow::maxAllCards(std::vector > cards) diff --git a/client/windows/QuickRecruitmentWindow.h b/client/windows/QuickRecruitmentWindow.h index cdb36e819..c8aba9fed 100644 --- a/client/windows/QuickRecruitmentWindow.h +++ b/client/windows/QuickRecruitmentWindow.h @@ -11,6 +11,10 @@ #include "../windows/CWindowObject.h" +VCMI_LIB_NAMESPACE_BEGIN +class CGTownInstance; +VCMI_LIB_NAMESPACE_END + class CButton; class CreatureCostBox; class CreaturePurchaseCard; diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 499858037..90db19c93 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -11,7 +11,7 @@ #include "AdventureOptionsTab.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" @@ -44,7 +44,7 @@ AdventureOptionsTab::AdventureOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourceID("config/widgets/settings/adventureOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/adventureOptionsTab.json")); addCallback("playerHeroSpeedChanged", [this](int value) { auto targetLabel = widget("heroSpeedValueLabel"); diff --git a/client/windows/settings/BattleOptionsTab.cpp b/client/windows/settings/BattleOptionsTab.cpp index 1e657d61c..35abf73cb 100644 --- a/client/windows/settings/BattleOptionsTab.cpp +++ b/client/windows/settings/BattleOptionsTab.cpp @@ -13,7 +13,7 @@ #include "../../battle/BattleInterface.h" #include "../../gui/CGuiHandler.h" #include "../../../lib/CConfigHandler.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../../lib/CGeneralTextHandler.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" @@ -23,7 +23,7 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; setRedrawParent(true); - const JsonNode config(ResourceID("config/widgets/settings/battleOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/battleOptionsTab.json")); addCallback("viewGridChanged", [this, owner](bool value) { viewGridChangedCallback(value, owner); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index ae747f90e..c4a6ec84b 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -26,7 +26,7 @@ #include "../../widgets/TextControls.h" #include "../../../lib/CGeneralTextHandler.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" static void setIntSetting(std::string group, std::string field, int value) { @@ -105,7 +105,7 @@ GeneralOptionsTab::GeneralOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourceID("config/widgets/settings/generalOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/generalOptionsTab.json")); addCallback("spellbookAnimationChanged", [](bool value) { setBoolSetting("video", "spellbookAnimation", value); @@ -157,6 +157,15 @@ GeneralOptionsTab::GeneralOptionsTab() { setBoolSetting("general", "hapticFeedback", value); }); + addCallback("enableUiEnhancementsChanged", [](bool value) + { + setBoolSetting("general", "enableUiEnhancements", value); + }); + + addCallback("enableLargeSpellbookChanged", [](bool value) + { + setBoolSetting("gameTweaks", "enableLargeSpellbook", value); + }); //moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content addCallback("availableCreaturesAsDwellingChanged", [=](int value) @@ -198,6 +207,14 @@ GeneralOptionsTab::GeneralOptionsTab() if (hapticFeedbackCheckbox) hapticFeedbackCheckbox->setSelected(settings["general"]["hapticFeedback"].Bool()); + std::shared_ptr enableUiEnhancementsCheckbox = widget("enableUiEnhancementsCheckbox"); + if (enableUiEnhancementsCheckbox) + enableUiEnhancementsCheckbox->setSelected(settings["general"]["enableUiEnhancements"].Bool()); + + std::shared_ptr enableLargeSpellbookCheckbox = widget("enableLargeSpellbookCheckbox"); + if (enableLargeSpellbookCheckbox) + enableLargeSpellbookCheckbox->setSelected(settings["gameTweaks"]["enableLargeSpellbook"].Bool()); + std::shared_ptr musicSlider = widget("musicSlider"); musicSlider->scrollTo(CCS->musich->getVolume()); @@ -281,7 +298,6 @@ void GeneralOptionsTab::setGameResolution(int index) widget("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y)); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } @@ -306,7 +322,6 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive) updateResolutionSelector(); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } @@ -366,7 +381,6 @@ void GeneralOptionsTab::setGameScaling(int index) widget("scalingLabel")->setText(scalingToLabelString(scaling)); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } diff --git a/client/windows/settings/OtherOptionsTab.cpp b/client/windows/settings/OtherOptionsTab.cpp index 90e288e07..c2709b6a6 100644 --- a/client/windows/settings/OtherOptionsTab.cpp +++ b/client/windows/settings/OtherOptionsTab.cpp @@ -11,7 +11,7 @@ #include "OtherOptionsTab.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" #include "CConfigHandler.h" @@ -26,7 +26,7 @@ OtherOptionsTab::OtherOptionsTab() : InterfaceObjectConfigurable() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourceID("config/widgets/settings/otherOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/otherOptionsTab.json")); addCallback("availableCreaturesAsDwellingLabelChanged", [](bool value) { return setBoolSetting("gameTweaks", "availableCreaturesAsDwellingLabel", value); diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index c05592e7d..d122c8a02 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -21,7 +21,7 @@ #include "CGeneralTextHandler.h" #include "CPlayerInterface.h" #include "CServerHandler.h" -#include "filesystem/ResourceID.h" +#include "filesystem/ResourcePath.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "render/Canvas.h" @@ -35,7 +35,7 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourceID("config/widgets/settings/settingsMainContainer.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/settingsMainContainer.json")); addCallback("activateSettingsTab", [this](int tabId) { openTab(tabId); }); addCallback("loadGame", [this](int) { loadGameButtonCallback(); }); addCallback("saveGame", [this](int) { saveGameButtonCallback(); }); @@ -75,6 +75,8 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter std::shared_ptr mainTabs = widget("settingsTabs"); mainTabs->setSelected(defaultTabIndex); + + LOCPLINT->gamePause(true); } std::shared_ptr SettingsMainWindow::createTab(size_t index) @@ -108,6 +110,8 @@ void SettingsMainWindow::close() { if(!GH.windows().isTopWindow(this)) logGlobal->error("Only top interface must be closed"); + + LOCPLINT->gamePause(false); GH.windows().popWindows(1); } diff --git a/cmake_modules/Findfuzzylite.cmake b/cmake_modules/Findfuzzylite.cmake index 4932a7eca..e95ec54e5 100644 --- a/cmake_modules/Findfuzzylite.cmake +++ b/cmake_modules/Findfuzzylite.cmake @@ -21,11 +21,13 @@ find_path(fuzzylite_INCLUDE_DIR fl/fuzzylite.h + fuzzylite/fuzzylite.h HINTS ENV FLDIR PATH_SUFFIXES fl - include/fl + include/fl + include/fuzzylite include ) @@ -62,4 +64,4 @@ if (NOT TARGET "fuzzylite::fuzzylite" AND fuzzylite_FOUND) IMPORTED_LOCATION "${fuzzylite_LIBRARY}") endif() -mark_as_advanced(fuzzylite_LIBRARY fuzzylite_INCLUDE_DIR) \ No newline at end of file +mark_as_advanced(fuzzylite_LIBRARY fuzzylite_INCLUDE_DIR) diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 15efa56c2..bcf8c1979 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -15,7 +15,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/battle/BattleStateInfoForRetreat.cpp ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.cpp ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.cpp - ${MAIN_LIB_DIR}/battle/CCallbackBase.cpp ${MAIN_LIB_DIR}/battle/CObstacleInstance.cpp ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.cpp ${MAIN_LIB_DIR}/battle/CUnitState.cpp @@ -32,6 +31,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.cpp ${MAIN_LIB_DIR}/bonuses/BonusParams.cpp ${MAIN_LIB_DIR}/bonuses/BonusSelector.cpp + ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.cpp ${MAIN_LIB_DIR}/bonuses/CBonusProxy.cpp ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.cpp ${MAIN_LIB_DIR}/bonuses/IBonusBearer.cpp @@ -42,6 +42,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignHandler.cpp ${MAIN_LIB_DIR}/campaign/CampaignState.cpp + ${MAIN_LIB_DIR}/constants/EntityIdentifiers.cpp + ${MAIN_LIB_DIR}/events/ApplyDamage.cpp ${MAIN_LIB_DIR}/events/GameResumed.cpp ${MAIN_LIB_DIR}/events/ObjectVisitEnded.cpp @@ -60,10 +62,9 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/CZipLoader.cpp ${MAIN_LIB_DIR}/filesystem/CZipSaver.cpp ${MAIN_LIB_DIR}/filesystem/FileInfo.cpp - ${MAIN_LIB_DIR}/filesystem/FileStream.cpp ${MAIN_LIB_DIR}/filesystem/Filesystem.cpp ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.cpp - ${MAIN_LIB_DIR}/filesystem/ResourceID.cpp + ${MAIN_LIB_DIR}/filesystem/ResourcePath.cpp ${MAIN_LIB_DIR}/gameState/CGameState.cpp ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.cpp @@ -81,7 +82,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp ${MAIN_LIB_DIR}/mapObjects/CBank.cpp @@ -116,6 +116,16 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.cpp ${MAIN_LIB_DIR}/mapping/ObstacleProxy.cpp + ${MAIN_LIB_DIR}/modding/ActiveModsInSaveList.cpp + ${MAIN_LIB_DIR}/modding/CModHandler.cpp + ${MAIN_LIB_DIR}/modding/CModInfo.cpp + ${MAIN_LIB_DIR}/modding/CModVersion.cpp + ${MAIN_LIB_DIR}/modding/ContentTypeHandler.cpp + ${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp + ${MAIN_LIB_DIR}/modding/ModUtility.cpp + + ${MAIN_LIB_DIR}/networkPacks/NetPacksLib.cpp + ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp ${MAIN_LIB_DIR}/pathfinder/CPathfinder.cpp ${MAIN_LIB_DIR}/pathfinder/NodeStorage.cpp @@ -123,15 +133,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/pathfinder/PathfindingRules.cpp ${MAIN_LIB_DIR}/pathfinder/TurnInfo.cpp - ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects3.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesLobbyPacks.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesServerPacks.cpp - ${MAIN_LIB_DIR}/rewardable/Configuration.cpp ${MAIN_LIB_DIR}/rewardable/Info.cpp ${MAIN_LIB_DIR}/rewardable/Interface.cpp @@ -171,16 +172,16 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.cpp ${MAIN_LIB_DIR}/serializer/BinarySerializer.cpp - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.cpp + ${MAIN_LIB_DIR}/serializer/CLoadFile.cpp ${MAIN_LIB_DIR}/serializer/CMemorySerializer.cpp ${MAIN_LIB_DIR}/serializer/Connection.cpp + ${MAIN_LIB_DIR}/serializer/CSaveFile.cpp ${MAIN_LIB_DIR}/serializer/CSerializer.cpp ${MAIN_LIB_DIR}/serializer/CTypeList.cpp ${MAIN_LIB_DIR}/serializer/JsonDeserializer.cpp ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.cpp ${MAIN_LIB_DIR}/serializer/JsonSerializer.cpp ${MAIN_LIB_DIR}/serializer/JsonUpdater.cpp - ${MAIN_LIB_DIR}/serializer/ILICReader.cpp ${MAIN_LIB_DIR}/spells/AbilityCaster.cpp ${MAIN_LIB_DIR}/spells/AdventureSpellMechanics.cpp @@ -214,6 +215,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/spells/effects/RemoveObstacle.cpp ${MAIN_LIB_DIR}/spells/effects/Sacrifice.cpp + ${MAIN_LIB_DIR}/vstd/DateUtils.cpp ${MAIN_LIB_DIR}/vstd/StringUtils.cpp ${MAIN_LIB_DIR}/ArtifactUtils.cpp @@ -232,8 +234,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CGameInterface.cpp ${MAIN_LIB_DIR}/CGeneralTextHandler.cpp ${MAIN_LIB_DIR}/CHeroHandler.cpp - ${MAIN_LIB_DIR}/CModHandler.cpp - ${MAIN_LIB_DIR}/CModVersion.cpp ${MAIN_LIB_DIR}/CPlayerState.cpp ${MAIN_LIB_DIR}/CRandomGenerator.cpp ${MAIN_LIB_DIR}/CScriptingModule.cpp @@ -241,7 +241,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CStack.cpp ${MAIN_LIB_DIR}/CThreadHelper.cpp ${MAIN_LIB_DIR}/CTownHandler.cpp - ${MAIN_LIB_DIR}/GameConstants.cpp ${MAIN_LIB_DIR}/GameSettings.cpp ${MAIN_LIB_DIR}/IGameCallback.cpp ${MAIN_LIB_DIR}/IHandlerBase.cpp @@ -250,7 +249,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/JsonRandom.cpp ${MAIN_LIB_DIR}/LoadProgress.cpp ${MAIN_LIB_DIR}/LogicalExpression.cpp - ${MAIN_LIB_DIR}/NetPacksLib.cpp ${MAIN_LIB_DIR}/MetaString.cpp ${MAIN_LIB_DIR}/ObstacleHandler.cpp ${MAIN_LIB_DIR}/StartInfo.cpp @@ -260,6 +258,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/ScriptHandler.cpp ${MAIN_LIB_DIR}/TerrainHandler.cpp ${MAIN_LIB_DIR}/TextOperations.cpp + ${MAIN_LIB_DIR}/TurnTimerInfo.cpp ${MAIN_LIB_DIR}/VCMIDirs.cpp ${MAIN_LIB_DIR}/VCMI_Lib.cpp ) @@ -275,10 +274,12 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) set(lib_HEADERS ${MAIN_LIB_DIR}/../include/vstd/CLoggerBase.h ${MAIN_LIB_DIR}/../Global.h + ${MAIN_LIB_DIR}/../AUTHORS.h ${MAIN_LIB_DIR}/StdInc.h ${MAIN_LIB_DIR}/../include/vstd/ContainerUtils.h ${MAIN_LIB_DIR}/../include/vstd/RNG.h + ${MAIN_LIB_DIR}/../include/vstd/DateUtils.h ${MAIN_LIB_DIR}/../include/vstd/StringUtils.h ${MAIN_LIB_DIR}/../include/vcmi/events/AdventureEvents.h @@ -331,7 +332,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/battle/BattleProxy.h ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.h ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.h - ${MAIN_LIB_DIR}/battle/CCallbackBase.h ${MAIN_LIB_DIR}/battle/CObstacleInstance.h ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.h ${MAIN_LIB_DIR}/battle/CUnitState.h @@ -351,6 +351,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.h ${MAIN_LIB_DIR}/bonuses/BonusParams.h ${MAIN_LIB_DIR}/bonuses/BonusSelector.h + ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.h ${MAIN_LIB_DIR}/bonuses/CBonusProxy.h ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.h ${MAIN_LIB_DIR}/bonuses/IBonusBearer.h @@ -363,6 +364,13 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignScenarioPrologEpilog.h ${MAIN_LIB_DIR}/campaign/CampaignState.h + ${MAIN_LIB_DIR}/constants/EntityIdentifiers.h + ${MAIN_LIB_DIR}/constants/Enumerations.h + ${MAIN_LIB_DIR}/constants/IdentifierBase.h + ${MAIN_LIB_DIR}/constants/VariantIdentifier.h + ${MAIN_LIB_DIR}/constants/NumericConstants.h + ${MAIN_LIB_DIR}/constants/StringConstants.h + ${MAIN_LIB_DIR}/events/ApplyDamage.h ${MAIN_LIB_DIR}/events/GameResumed.h ${MAIN_LIB_DIR}/events/ObjectVisitEnded.h @@ -385,11 +393,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/CZipLoader.h ${MAIN_LIB_DIR}/filesystem/CZipSaver.h ${MAIN_LIB_DIR}/filesystem/FileInfo.h - ${MAIN_LIB_DIR}/filesystem/FileStream.h ${MAIN_LIB_DIR}/filesystem/Filesystem.h ${MAIN_LIB_DIR}/filesystem/ISimpleResourceLoader.h ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.h - ${MAIN_LIB_DIR}/filesystem/ResourceID.h + ${MAIN_LIB_DIR}/filesystem/ResourcePath.h ${MAIN_LIB_DIR}/gameState/CGameState.h ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.h @@ -414,7 +421,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/IObjectInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/RandomMapInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.h ${MAIN_LIB_DIR}/mapObjectConstructors/SObjectSounds.h ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h @@ -453,6 +459,34 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.h ${MAIN_LIB_DIR}/mapping/ObstacleProxy.h + ${MAIN_LIB_DIR}/modding/ActiveModsInSaveList.h + ${MAIN_LIB_DIR}/modding/CModHandler.h + ${MAIN_LIB_DIR}/modding/CModInfo.h + ${MAIN_LIB_DIR}/modding/CModVersion.h + ${MAIN_LIB_DIR}/modding/ContentTypeHandler.h + ${MAIN_LIB_DIR}/modding/IdentifierStorage.h + ${MAIN_LIB_DIR}/modding/ModIncompatibility.h + ${MAIN_LIB_DIR}/modding/ModScope.h + ${MAIN_LIB_DIR}/modding/ModUtility.h + ${MAIN_LIB_DIR}/modding/ModVerificationInfo.h + + ${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h + ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h + ${MAIN_LIB_DIR}/networkPacks/Component.h + ${MAIN_LIB_DIR}/networkPacks/EInfoWindowMode.h + ${MAIN_LIB_DIR}/networkPacks/EntityChanges.h + ${MAIN_LIB_DIR}/networkPacks/EOpenWindowMode.h + ${MAIN_LIB_DIR}/networkPacks/NetPacksBase.h + ${MAIN_LIB_DIR}/networkPacks/NetPackVisitor.h + ${MAIN_LIB_DIR}/networkPacks/ObjProperty.h + ${MAIN_LIB_DIR}/networkPacks/PacksForClient.h + ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h + ${MAIN_LIB_DIR}/networkPacks/PacksForLobby.h + ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h + ${MAIN_LIB_DIR}/networkPacks/SetStackEffect.h + ${MAIN_LIB_DIR}/networkPacks/StackLocation.h + ${MAIN_LIB_DIR}/networkPacks/TradeItem.h + ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h ${MAIN_LIB_DIR}/pathfinder/CPathfinder.h @@ -463,6 +497,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/pathfinder/TurnInfo.h ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesClientPacks.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesLobbyPacks.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesMapObjects.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesServerPacks.h ${MAIN_LIB_DIR}/rewardable/Configuration.h ${MAIN_LIB_DIR}/rewardable/Info.h @@ -506,16 +544,16 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.h ${MAIN_LIB_DIR}/serializer/BinarySerializer.h - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.h + ${MAIN_LIB_DIR}/serializer/CLoadFile.h ${MAIN_LIB_DIR}/serializer/CMemorySerializer.h ${MAIN_LIB_DIR}/serializer/Connection.h + ${MAIN_LIB_DIR}/serializer/CSaveFile.h ${MAIN_LIB_DIR}/serializer/CSerializer.h ${MAIN_LIB_DIR}/serializer/CTypeList.h ${MAIN_LIB_DIR}/serializer/JsonDeserializer.h ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.h ${MAIN_LIB_DIR}/serializer/JsonSerializer.h ${MAIN_LIB_DIR}/serializer/JsonUpdater.h - ${MAIN_LIB_DIR}/serializer/ILICReader.h ${MAIN_LIB_DIR}/serializer/Cast.h ${MAIN_LIB_DIR}/spells/AbilityCaster.h @@ -566,8 +604,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CGameInterface.h ${MAIN_LIB_DIR}/CGeneralTextHandler.h ${MAIN_LIB_DIR}/CHeroHandler.h - ${MAIN_LIB_DIR}/CModHandler.h - ${MAIN_LIB_DIR}/CModVersion.h ${MAIN_LIB_DIR}/CondSh.h ${MAIN_LIB_DIR}/ConstTransitivePtr.h ${MAIN_LIB_DIR}/Color.h @@ -588,7 +624,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/IGameEventsReceiver.h ${MAIN_LIB_DIR}/IHandlerBase.h ${MAIN_LIB_DIR}/int3.h - ${MAIN_LIB_DIR}/Interprocess.h ${MAIN_LIB_DIR}/JsonDetail.h ${MAIN_LIB_DIR}/JsonNode.h ${MAIN_LIB_DIR}/JsonRandom.h @@ -596,10 +631,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/LoadProgress.h ${MAIN_LIB_DIR}/LogicalExpression.h ${MAIN_LIB_DIR}/MetaString.h - ${MAIN_LIB_DIR}/NetPacksBase.h - ${MAIN_LIB_DIR}/NetPacks.h - ${MAIN_LIB_DIR}/NetPacksLobby.h - ${MAIN_LIB_DIR}/NetPackVisitor.h ${MAIN_LIB_DIR}/ObstacleHandler.h ${MAIN_LIB_DIR}/Point.h ${MAIN_LIB_DIR}/Rect.h @@ -610,9 +641,9 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/ScriptHandler.h ${MAIN_LIB_DIR}/ScopeGuard.h ${MAIN_LIB_DIR}/StartInfo.h - ${MAIN_LIB_DIR}/StringConstants.h ${MAIN_LIB_DIR}/TerrainHandler.h ${MAIN_LIB_DIR}/TextOperations.h + ${MAIN_LIB_DIR}/TurnTimerInfo.h ${MAIN_LIB_DIR}/UnlockGuard.h ${MAIN_LIB_DIR}/VCMIDirs.h ${MAIN_LIB_DIR}/vcmi_endian.h @@ -655,8 +686,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods ) endif() diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index 7c87e116c..f9fdb6992 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,6 +1,6 @@ set(VCMI_VERSION_MAJOR 1) -set(VCMI_VERSION_MINOR 3) -set(VCMI_VERSION_PATCH 2) +set(VCMI_VERSION_MINOR 4) +set(VCMI_VERSION_PATCH 0) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} -DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR} diff --git a/cmake_modules/create_link.cmake b/cmake_modules/create_link.cmake new file mode 100644 index 000000000..c5ea762ad --- /dev/null +++ b/cmake_modules/create_link.cmake @@ -0,0 +1,14 @@ + +#message(${CMAKE_ARGV0}) # cmake.exe +#message(${CMAKE_ARGV1}) # -P +#message(${CMAKE_ARGV2}) # thisfilename +#message(${CMAKE_ARGV3}) # existing +#message(${CMAKE_ARGV4}) # linkname +if (WIN32) + file(TO_NATIVE_PATH ${CMAKE_ARGV3} existing_native) + file(TO_NATIVE_PATH ${CMAKE_ARGV4} linkname_native) + execute_process(COMMAND cmd.exe /c RD /Q "${linkname_native}") + execute_process(COMMAND cmd.exe /c mklink /J "${linkname_native}" "${existing_native}") +else() + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_ARGV3} ${CMAKE_ARGV4}) +endif() diff --git a/config/artifacts.json b/config/artifacts.json index 98c71050e..c58da9263 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -1,2456 +1,2297 @@ -{ - "spellBook": - { - "index" : 0, - "type" : ["HERO"] - }, - "spellScroll": - { - "index" : 1, - "type" : ["HERO"] - }, - "grail": - { - "index" : 2, - "type" : ["HERO"] - }, - "catapult": - { - "index" : 3, - "type" : ["HERO"], - "warMachine" : "catapult" - }, - "ballista": - { - "index" : 4, - "type" : ["HERO"], - "warMachine" : "ballista" - }, - "ammoCart": - { - "index" : 5, - "type" : ["HERO"], - "warMachine" : "ammoCart" - }, - "firstAidTent": - { - "index" : 6, - "type" : ["HERO"], - "warMachine" : "firstAidTent" - }, - "centaurAxe": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 7, - "type" : ["HERO"] - }, - "blackshardOfTheDeadKnight": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 8, - "type" : ["HERO"] - }, - "greaterGnollsFlail": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 9, - "type" : ["HERO"] - }, - "ogresClubOfHavoc": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 10, - "type" : ["HERO"] - }, - "swordOfHellfire": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 11, - "type" : ["HERO"] - }, - "titansGladius": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 12, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : -3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 12, - "type" : ["HERO"] - }, - "shieldOfTheDwarvenLords": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 13, - "type" : ["HERO"] - }, - "shieldOfTheYawningDead": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 14, - "type" : ["HERO"] - }, - "bucklerOfTheGnollKing": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 15, - "type" : ["HERO"] - }, - "targOfTheRampagingOgre": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 16, - "type" : ["HERO"] - }, - "shieldOfTheDamned": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 17, - "type" : ["HERO"] - }, - "sentinelsShield": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 12, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 0, - "type" : "PRIMARY_SKILL", - "val" : -3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 18, - "type" : ["HERO"] - }, - "helmOfTheAlabasterUnicorn": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 19, - "type" : ["HERO"] - }, - "skullHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 20, - "type" : ["HERO"] - }, - "helmOfChaos": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 21, - "type" : ["HERO"] - }, - "crownOfTheSupremeMagi": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 22, - "type" : ["HERO"] - }, - "hellstormHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 23, - "type" : ["HERO"] - }, - "thunderHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : -2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 24, - "type" : ["HERO"] - }, - "breastplateOfPetrifiedWood": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 25, - "type" : ["HERO"] - }, - "ribCage": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 26, - "type" : ["HERO"] - }, - "scalesOfTheGreaterBasilisk": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 27, - "type" : ["HERO"] - }, - "tunicOfTheCyclopsKing": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 28, - "type" : ["HERO"] - }, - "breastplateOfBrimstone": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 29, - "type" : ["HERO"] - }, - "titansCuirass": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : -2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 30, - "type" : ["HERO"] - }, - "armorOfWonder": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 31, - "type" : ["HERO"] - }, - "sandalsOfTheSaint": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 32, - "type" : ["HERO"] - }, - "celestialNecklaceOfBliss": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 33, - "type" : ["HERO"] - }, - "lionsShieldOfCourage": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 34, - "type" : ["HERO"] - }, - "swordOfJudgement": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 35, - "type" : ["HERO"] - }, - "helmOfHeavenlyEnlightenment": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 36, - "type" : ["HERO"] - }, - "quietEyeOfTheDragon": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 37, - "type" : ["HERO"] - }, - "redDragonFlameTongue": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 38, - "type" : ["HERO"] - }, - "dragonScaleShield": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 39, - "type" : ["HERO"] - }, - "dragonScaleArmor": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 40, - "type" : ["HERO"] - }, - "dragonboneGreaves": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 41, - "type" : ["HERO"] - }, - "dragonWingTabard": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 42, - "type" : ["HERO"] - }, - "necklaceOfDragonteeth": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 43, - "type" : ["HERO"] - }, - "crownOfDragontooth": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 44, - "type" : ["HERO"] - }, - "stillEyeOfTheDragon": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 45, - "type" : ["HERO"] - }, - "cloverOfFortune": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 46, - "type" : ["HERO"] - }, - "cardsOfProphecy": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 47, - "type" : ["HERO"] - }, - "ladybirdOfLuck": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 48, - "type" : ["HERO"] - }, - "badgeOfCourage": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MIND_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 49, - "type" : ["HERO"] - }, - "crestOfValor": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 50, - "type" : ["HERO"] - }, - "glyphOfGallantry": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 51, - "type" : ["HERO"] - }, - "speculum": - { - "bonuses" : [ - { - "type" : "SIGHT_RADIUS", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 52, - "type" : ["HERO"] - }, - "spyglass": - { - "bonuses" : [ - { - "type" : "SIGHT_RADIUS", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 53, - "type" : ["HERO"] - }, - "amuletOfTheUndertaker": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 5, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 54, - "type" : ["HERO"] - }, - "vampiresCowl": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 10, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 55, - "type" : ["HERO"] - }, - "deadMansBoots": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 15, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 56, - "type" : ["HERO"] - }, - "garnitureOfInterference": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 57, - "type" : ["HERO"] - }, - "surcoatOfCounterpoise": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 58, - "type" : ["HERO"] - }, - "bootsOfPolarity": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 15, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 59, - "type" : ["HERO"] - }, - "bowOfElvenCherrywood": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 5, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 60, - "type" : ["HERO"] - }, - "bowstringOfTheUnicornsMane": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 10, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 61, - "type" : ["HERO"] - }, - "angelFeatherArrows": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 15, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 62, - "type" : ["HERO"] - }, - "birdOfPerception": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 5, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 63, - "type" : ["HERO"] - }, - "stoicWatchman": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 10, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 64, - "type" : ["HERO"] - }, - "emblemOfCognizance": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 15, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 65, - "type" : ["HERO"] - }, - "statesmansMedal": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 66, - "type" : ["HERO"] - }, - "diplomatsRing": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 67, - "type" : ["HERO"] - }, - "ambassadorsSash": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 68, - "type" : ["HERO"] - }, - "ringOfTheWayfarer": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 69, - "type" : ["HERO"] - }, - "equestriansGloves": - { - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 1, - "val" : 300, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 70, - "type" : ["HERO"] - }, - "necklaceOfOceanGuidance": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 0, - "val" : 1000, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 71, - "type" : ["HERO"] - }, - "angelWings": - { - "bonuses" : [ - { - "type" : "FLYING_MOVEMENT", - "val" : 0, - "valueType" : "INDEPENDENT_MIN" - } - ], - "index" : 72, - "type" : ["HERO"] - }, - "charmOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 73, - "type" : ["HERO"] - }, - "talismanOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 74, - "type" : ["HERO"] - }, - "mysticOrbOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 75, - "type" : ["HERO"] - }, - "collarOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 76, - "type" : ["HERO"] - }, - "ringOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 77, - "type" : ["HERO"] - }, - "capeOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 78, - "type" : ["HERO"] - }, - "orbOfTheFirmament": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.air", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 79, - "type" : ["HERO"] - }, - "orbOfSilt": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.earth", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 80, - "type" : ["HERO"] - }, - "orbOfTempestuousFire": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.fire", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 81, - "type" : ["HERO"] - }, - "orbOfDrivingRain": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.water", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 82, - "type" : ["HERO"] - }, - "recantersCloak": - { - "index" : 83, - "type" : ["HERO"], - "bonuses": [ - { - "type" : "BLOCK_MAGIC_ABOVE", - "val" : 2, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ] - }, - "spiritOfOppression": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 84, - "type" : ["HERO"] - }, - "hourglassOfTheEvilHour": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 85, - "type" : ["HERO"] - }, - "tomeOfFireMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 1, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 86, - "type" : ["HERO"] - }, - "tomeOfAirMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 0, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 87, - "type" : ["HERO"] - }, - "tomeOfWaterMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 2, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 88, - "type" : ["HERO"] - }, - "tomeOfEarthMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 3, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 89, - "type" : ["HERO"] - }, - "bootsOfLevitation": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "WATER_WALKING", - "val" : 0, - "valueType" : "INDEPENDENT_MIN" - } - ], - "index" : 90, - "type" : ["HERO"] - }, - "goldenBow": - { - "bonuses" : [ - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_WALL_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 91, - "type" : ["HERO"] - }, - "sphereOfPermanence": - { - "bonuses" : [ - { - "subtype" : 35, - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER", - "addInfo" : 1 - } - ], - "index" : 92, - "type" : ["HERO"] - }, - "orbOfVulnerability": - { - "bonuses" : [ - { - "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 0, - "val" : 0, - "valueType" : "BASE_NUMBER", - "propagator": "BATTLE_WIDE" - }, - { - "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 1, - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MAGIC_RESISTANCE", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - }, - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 93, - "type" : ["HERO"] - }, - "ringOfVitality": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 1, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 94, - "type" : ["HERO"] - }, - "ringOfLife": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 1, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 95, - "type" : ["HERO"] - }, - "vialOfLifeblood": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 2, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 96, - "type" : ["HERO"] - }, - "necklaceOfSwiftness": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 97, - "type" : ["HERO"] - }, - "bootsOfSpeed": - { - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 1, - "val" : 600, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 98, - "type" : ["HERO"] - }, - "capeOfVelocity": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 99, - "type" : ["HERO"] - }, - "pendantOfDispassion": - { - "bonuses" : [ - { - "subtype" : "spell.berserk", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 100, - "type" : ["HERO"] - }, - "pendantOfSecondSight": - { - "bonuses" : [ - { - "subtype" : "spell.blind", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 101, - "type" : ["HERO"] - }, - "pendantOfHoliness": - { - "bonuses" : [ - { - "subtype" : "spell.curse", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 102, - "type" : ["HERO"] - }, - "pendantOfLife": - { - "bonuses" : [ - { - "subtype" : "spell.deathRipple", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 103, - "type" : ["HERO"] - }, - "pendantOfDeath": - { - "bonuses" : [ - { - "limiters" : ["IS_UNDEAD"], - "subtype" : "spell.destroyUndead", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 104, - "type" : ["HERO"] - }, - "pendantOfFreeWill": - { - "bonuses" : [ - { - "subtype" : "spell.hypnotize", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 105, - "type" : ["HERO"] - }, - "pendantOfNegativity": - { - "bonuses" : [ - { - "subtype" : "spell.lightningBolt", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.chainLightning", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 106, - "type" : ["HERO"] - }, - "pendantOfTotalRecall": - { - "bonuses" : [ - { - "subtype" : "spell.forgetfulness", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 107, - "type" : ["HERO"] - }, - "pendantOfCourage": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "LUCK", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 108, - "type" : ["HERO"] - }, - "everflowingCrystalCloak": - { - "bonuses" : [ - { - "subtype" : "resource.crystal", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 109, - "type" : ["HERO"] - }, - "ringOfInfiniteGems": - { - "bonuses" : [ - { - "subtype" : "resource.gems", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 110, - "type" : ["HERO"] - }, - "everpouringVialOfMercury": - { - "bonuses" : [ - { - "subtype" : "resource.mercury", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 111, - "type" : ["HERO"] - }, - "inexhaustibleCartOfOre": - { - "bonuses" : [ - { - "subtype" : "resource.ore", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 112, - "type" : ["HERO"] - }, - "eversmokingRingOfSulfur": - { - "bonuses" : [ - { - "subtype" : "resource.sulfur", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 113, - "type" : ["HERO"] - }, - "inexhaustibleCartOfLumber": - { - "bonuses" : [ - { - "subtype" : "resource.wood", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 114, - "type" : ["HERO"] - }, - "endlessSackOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 1000, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 115, - "type" : ["HERO"] - }, - "endlessBagOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 750, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 116, - "type" : ["HERO"] - }, - "endlessPurseOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 500, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 117, - "type" : ["HERO"] - }, - "legsOfLegion": - { - "index" : 118, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 1, - "val" : 5, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "loinsOfLegion": - { - "index" : 119, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 2, - "val" : 4, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "torsoOfLegion": - { - "index" : 120, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 3, - "val" : 3, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "armsOfLegion": - { - "index" : 121, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 4, - "val" : 2, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "headOfLegion": - { - "index" : 122, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 5, - "val" : 1, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "seaCaptainsHat": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "WHIRLPOOL_PROTECTION", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MOVEMENT", - "subtype" : 0, - "val" : 500, - "valueType" : "ADDITIVE_VALUE" - }, - { - "subtype" : "spell.summonBoat", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "spell.scuttleBoat", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - } - ], - "index" : 123, - "type" : ["HERO"] - }, - "spellbindersHat": - { - "bonuses" : [ - { - "subtype" : 5, - "type" : "SPELLS_OF_LEVEL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 124, - "type" : ["HERO"] - }, - "shacklesOfWar": - { - "index" : 125, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "BATTLE_NO_FLEEING", - "propagator": "BATTLE_WIDE" - } - ] - }, - "orbOfInhibition": - { - "index" : 126, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "BLOCK_ALL_MAGIC", - "propagator": "BATTLE_WIDE" - } - ] - }, - "vialOfDragonBlood": - { - "bonuses" : [ - { - "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 127, - "type" : ["HERO"] - }, - "armageddonsBlade": - { - "bonuses" : [ - { - "subtype" : "spell.armageddon", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "spell.armageddon", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER", - "addInfo" : 1 - }, - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 128, - "type" : ["HERO"] - }, - "angelicAlliance": - { - "bonuses" : [ - { - "type" : "NONEVIL_ALIGNMENT_MIX", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.prayer", - "type" : "OPENING_BATTLE_SPELL", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 129, - "type" : ["HERO"], - "components": - [ - "armorOfWonder", - "sandalsOfTheSaint", - "celestialNecklaceOfBliss", - "lionsShieldOfCourage", - "swordOfJudgement", - "helmOfHeavenlyEnlightenment" - ] - }, - "cloakOfTheUndeadKing": - { - "bonuses" : [ - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.skeleton", - "addInfo" : 0 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.walkingDead", - "addInfo" : 1 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.wight", - "addInfo" : 2 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.lich", - "addInfo" : 3 - } - ], - "index" : 130, - "type" : ["HERO"], - "components": - [ - "amuletOfTheUndertaker", - "vampiresCowl", - "deadMansBoots" - ] - }, - "elixirOfLife": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 25, - "valueType" : "PERCENT_TO_BASE", - "limiters" : [ - "noneOf", - "IS_UNDEAD", - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "NON_LIVING" ] - }, - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "GARGOYLE" ] - } - ] - }, - { - "type" : "HP_REGENERATION", - "val" : 50, - "valueType" : "BASE_NUMBER", - "limiters" : [ - "noneOf", - "IS_UNDEAD", - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "NON_LIVING" ] - }, - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "GARGOYLE" ] - } - ] - } - ], - "index" : 131, - "type" : ["HERO"], - "components": - [ - "ringOfVitality", - "ringOfLife", - "vialOfLifeblood" - ] - }, - "armorOfTheDamned": - { - "bonuses" : [ - { - "subtype" : "spell.slow", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.curse", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.weakness", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.misfortune", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 132, - "type" : ["HERO"], - "components": - [ - "blackshardOfTheDeadKnight", - "shieldOfTheYawningDead", - "skullHelmet", - "ribCage" - ] - }, - "statueOfLegion": - { - "index" : 133, - "type" : ["HERO"], - "components": - [ - "legsOfLegion", - "loinsOfLegion", - "torsoOfLegion", - "armsOfLegion", - "headOfLegion" - ], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH_PERCENT", - "val" : 50, - "propagator": "PLAYER_PROPAGATOR" - } - ] - }, - "powerOfTheDragonFather": - { - "index" : 134, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "LEVEL_SPELL_IMMUNITY", - "val" : 4, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "components": - [ - "quietEyeOfTheDragon", - "redDragonFlameTongue", - "dragonScaleShield", - "dragonScaleArmor", - "dragonboneGreaves", - "dragonWingTabard", - "necklaceOfDragonteeth", - "crownOfDragontooth", - "stillEyeOfTheDragon" - ] - }, - "titansThunder": - { - "bonuses" : [ - { - "subtype" : "spell.titanBolt", - "type" : "SPELL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 135, - "type" : ["HERO"], - "components": - [ - "titansGladius", - "sentinelsShield", - "thunderHelmet", - "titansCuirass" - ] - }, - "admiralsHat": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "FREE_SHIP_BOARDING", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 136, - "type" : ["HERO"], - "components": - [ - "necklaceOfOceanGuidance", - "seaCaptainsHat" - ] - }, - "bowOfTheSharpshooter": - { - "bonuses" : [ - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_WALL_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "FREE_SHOOTING", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 137, - "type" : ["HERO"], - "components": - [ - "bowOfElvenCherrywood", - "bowstringOfTheUnicornsMane", - "angelFeatherArrows" - ] - }, - "wizardsWell": - { - "bonuses" : [ - { - "type" : "FULL_MANA_REGENERATION", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 138, - "type" : ["HERO"], - "components": - [ - "charmOfMana", - "talismanOfMana", - "mysticOrbOfMana" - ] - }, - "ringOfTheMagi": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 139, - "type" : ["HERO"], - "components": - [ - "collarOfConjuring", - "ringOfConjuring", - "capeOfConjuring" - ] - }, - "cornucopia": - { - "bonuses" : [ - { - "subtype" : "resource.crystal", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.gems", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.mercury", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.sulfur", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 140, - "type" : ["HERO"], - "components": - [ - "everflowingCrystalCloak", - "ringOfInfiniteGems", - "everpouringVialOfMercury", - "eversmokingRingOfSulfur" - ] - }, - "magicWand": - { - "bonuses" : [ - { - "type" : "CASTS", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.implosion", - "type" : "SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.fireball", - "type" : "SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "RANDOM_SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 2, - "subtype" : "spell.lightningBolt", - "type" : "ENCHANTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 1, - "type" : "REBIRTH", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MANA_DRAIN", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "HEALER", - "val" : 25, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 141, - "type" : ["CREATURE"] - }, - "goldTowerArrow": - { - "bonuses" : [ - { - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ADDITIONAL_ATTACK", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 22, - "type" : "SPELL_LIKE_ATTACK", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "CATAPULT", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ACID_BREATH", - "val" : 20, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 0, - "type" : "SHOTS", - "val" : 200, - "valueType" : "PERCENT_TO_BASE" - }, - { - "addInfo" : 1, - "subtype" : "spell.age", - "type" : "SPELL_BEFORE_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.berserk", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.poison", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.disruptingRay", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 142, - "type" : ["CREATURE"] - }, - "monstersPower": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 100, - "valueType" : "PERCENT_TO_BASE" - }, - { - "subtype" : 2, - "type" : "CREATURE_DAMAGE", - "val" : 100, - "valueType" : "PERCENT_TO_ALL" - }, - { - "type" : "HP_REGENERATION", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "NO_RETALIATION", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "RETURN_AFTER_STRIKE", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ATTACKS_ALL_ADJACENT", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 100, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "SPELL_DAMAGE_REDUCTION", - "val" : 100, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 143, - "type" : ["CREATURE"] - } -} +{ + "spellBook": + { + "index" : 0, + "type" : ["HERO"] + }, + "spellScroll": + { + "index" : 1, + "type" : ["HERO"] + }, + "grail": + { + "index" : 2, + "type" : ["HERO"] + }, + "catapult": + { + "index" : 3, + "type" : ["HERO"], + "warMachine" : "catapult" + }, + "ballista": + { + "index" : 4, + "type" : ["HERO"], + "warMachine" : "ballista" + }, + "ammoCart": + { + "index" : 5, + "type" : ["HERO"], + "warMachine" : "ammoCart" + }, + "firstAidTent": + { + "index" : 6, + "type" : ["HERO"], + "warMachine" : "firstAidTent" + }, + "centaurAxe": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 7, + "type" : ["HERO"] + }, + "blackshardOfTheDeadKnight": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 8, + "type" : ["HERO"] + }, + "greaterGnollsFlail": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 9, + "type" : ["HERO"] + }, + "ogresClubOfHavoc": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 10, + "type" : ["HERO"] + }, + "swordOfHellfire": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 11, + "type" : ["HERO"] + }, + "titansGladius": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 12, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : -3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 12, + "type" : ["HERO"] + }, + "shieldOfTheDwarvenLords": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 13, + "type" : ["HERO"] + }, + "shieldOfTheYawningDead": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 14, + "type" : ["HERO"] + }, + "bucklerOfTheGnollKing": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 15, + "type" : ["HERO"] + }, + "targOfTheRampagingOgre": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 16, + "type" : ["HERO"] + }, + "shieldOfTheDamned": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 17, + "type" : ["HERO"] + }, + "sentinelsShield": + { + "bonuses" : [ + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 12, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : -3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 18, + "type" : ["HERO"] + }, + "helmOfTheAlabasterUnicorn": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 19, + "type" : ["HERO"] + }, + "skullHelmet": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 20, + "type" : ["HERO"] + }, + "helmOfChaos": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 21, + "type" : ["HERO"] + }, + "crownOfTheSupremeMagi": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 22, + "type" : ["HERO"] + }, + "hellstormHelmet": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 23, + "type" : ["HERO"] + }, + "thunderHelmet": + { + "bonuses" : [ + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 10, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : -2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 24, + "type" : ["HERO"] + }, + "breastplateOfPetrifiedWood": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 25, + "type" : ["HERO"] + }, + "ribCage": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 26, + "type" : ["HERO"] + }, + "scalesOfTheGreaterBasilisk": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 27, + "type" : ["HERO"] + }, + "tunicOfTheCyclopsKing": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 28, + "type" : ["HERO"] + }, + "breastplateOfBrimstone": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 29, + "type" : ["HERO"] + }, + "titansCuirass": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 10, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : -2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 30, + "type" : ["HERO"] + }, + "armorOfWonder": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 31, + "type" : ["HERO"] + }, + "sandalsOfTheSaint": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 32, + "type" : ["HERO"] + }, + "celestialNecklaceOfBliss": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 33, + "type" : ["HERO"] + }, + "lionsShieldOfCourage": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 34, + "type" : ["HERO"] + }, + "swordOfJudgement": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 35, + "type" : ["HERO"] + }, + "helmOfHeavenlyEnlightenment": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 36, + "type" : ["HERO"] + }, + "quietEyeOfTheDragon": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 37, + "type" : ["HERO"] + }, + "redDragonFlameTongue": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 38, + "type" : ["HERO"] + }, + "dragonScaleShield": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 39, + "type" : ["HERO"] + }, + "dragonScaleArmor": + { + "bonuses" : [ + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 40, + "type" : ["HERO"] + }, + "dragonboneGreaves": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 41, + "type" : ["HERO"] + }, + "dragonWingTabard": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 42, + "type" : ["HERO"] + }, + "necklaceOfDragonteeth": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 43, + "type" : ["HERO"] + }, + "crownOfDragontooth": + { + "bonuses" : [ + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 44, + "type" : ["HERO"] + }, + "stillEyeOfTheDragon": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 45, + "type" : ["HERO"] + }, + "cloverOfFortune": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 46, + "type" : ["HERO"] + }, + "cardsOfProphecy": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 47, + "type" : ["HERO"] + }, + "ladybirdOfLuck": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 48, + "type" : ["HERO"] + }, + "badgeOfCourage": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MIND_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 49, + "type" : ["HERO"] + }, + "crestOfValor": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 50, + "type" : ["HERO"] + }, + "glyphOfGallantry": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 51, + "type" : ["HERO"] + }, + "speculum": + { + "bonuses" : [ + { + "type" : "SIGHT_RADIUS", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 52, + "type" : ["HERO"] + }, + "spyglass": + { + "bonuses" : [ + { + "type" : "SIGHT_RADIUS", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 53, + "type" : ["HERO"] + }, + "amuletOfTheUndertaker": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 5, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 54, + "type" : ["HERO"] + }, + "vampiresCowl": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 10, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 55, + "type" : ["HERO"] + }, + "deadMansBoots": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 15, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 56, + "type" : ["HERO"] + }, + "garnitureOfInterference": + { + "bonuses" : [ + { + "type" : "MAGIC_RESISTANCE", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 57, + "type" : ["HERO"] + }, + "surcoatOfCounterpoise": + { + "bonuses" : [ + { + "type" : "MAGIC_RESISTANCE", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 58, + "type" : ["HERO"] + }, + "bootsOfPolarity": + { + "bonuses" : [ + { + "type" : "MAGIC_RESISTANCE", + "val" : 15, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 59, + "type" : ["HERO"] + }, + "bowOfElvenCherrywood": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : "damageTypeRanged", + "val" : 5, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + "damageTypeRanged", + { + "type" : "SECONDARY_SKILL", + "id" : "secondarySkill.archery" + } + ] + } + ] + } + ], + "index" : 60, + "type" : ["HERO"] + }, + "bowstringOfTheUnicornsMane": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : "damageTypeRanged", + "val" : 10, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + "damageTypeRanged", + { + "type" : "SECONDARY_SKILL", + "id" : "secondarySkill.archery" + } + ] + } + ] + } + ], + "index" : 61, + "type" : ["HERO"] + }, + "angelFeatherArrows": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : "damageTypeRanged", + "val" : 15, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + "damageTypeRanged", + { + "type" : "SECONDARY_SKILL", + "id" : "secondarySkill.archery" + } + ] + } + ] + } + ], + "index" : 62, + "type" : ["HERO"] + }, + "birdOfPerception": + { + "bonuses" : [ + { + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 5, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 63, + "type" : ["HERO"] + }, + "stoicWatchman": + { + "bonuses" : [ + { + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 10, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 64, + "type" : ["HERO"] + }, + "emblemOfCognizance": + { + "bonuses" : [ + { + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 15, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 65, + "type" : ["HERO"] + }, + "statesmansMedal": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 66, + "type" : ["HERO"] + }, + "diplomatsRing": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 67, + "type" : ["HERO"] + }, + "ambassadorsSash": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 68, + "type" : ["HERO"] + }, + "ringOfTheWayfarer": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 69, + "type" : ["HERO"] + }, + "equestriansGloves": + { + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : "heroMovementLand", + "val" : 300, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 70, + "type" : ["HERO"] + }, + "necklaceOfOceanGuidance": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : "heroMovementSea", + "val" : 1000, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 71, + "type" : ["HERO"] + }, + "angelWings": + { + "bonuses" : [ + { + "type" : "FLYING_MOVEMENT", + "val" : 0, + "valueType" : "INDEPENDENT_MIN" + } + ], + "index" : 72, + "type" : ["HERO"] + }, + "charmOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 73, + "type" : ["HERO"] + }, + "talismanOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 74, + "type" : ["HERO"] + }, + "mysticOrbOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 75, + "type" : ["HERO"] + }, + "collarOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 76, + "type" : ["HERO"] + }, + "ringOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 77, + "type" : ["HERO"] + }, + "capeOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 78, + "type" : ["HERO"] + }, + "orbOfTheFirmament": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.air", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 79, + "type" : ["HERO"] + }, + "orbOfSilt": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.earth", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 80, + "type" : ["HERO"] + }, + "orbOfTempestuousFire": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.fire", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 81, + "type" : ["HERO"] + }, + "orbOfDrivingRain": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.water", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 82, + "type" : ["HERO"] + }, + "recantersCloak": + { + "index" : 83, + "type" : ["HERO"], + "bonuses": [ + { + "type" : "BLOCK_MAGIC_ABOVE", + "val" : 2, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ] + }, + "spiritOfOppression": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 84, + "type" : ["HERO"] + }, + "hourglassOfTheEvilHour": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 85, + "type" : ["HERO"] + }, + "tomeOfFireMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : "fire", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 86, + "type" : ["HERO"] + }, + "tomeOfAirMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : "air", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 87, + "type" : ["HERO"] + }, + "tomeOfWaterMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : "water", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 88, + "type" : ["HERO"] + }, + "tomeOfEarthMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : "earth", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 89, + "type" : ["HERO"] + }, + "bootsOfLevitation": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "WATER_WALKING", + "val" : 0, + "valueType" : "INDEPENDENT_MIN" + } + ], + "index" : 90, + "type" : ["HERO"] + }, + "goldenBow": + { + "bonuses" : [ + { + "limiters" : ["SHOOTER_ONLY"], + "type" : "NO_DISTANCE_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "type" : "NO_WALL_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 91, + "type" : ["HERO"] + }, + "sphereOfPermanence": + { + "bonuses" : [ + { + "subtype" : "dispel", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER", + "addInfo" : 1 + } + ], + "index" : 92, + "type" : ["HERO"] + }, + "orbOfVulnerability": + { + "bonuses" : [ + { + "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", + "subtype" : "immunityBattleWide", + "val" : 0, + "valueType" : "BASE_NUMBER", + "propagator": "BATTLE_WIDE" + }, + { + "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", + "subtype" : "immunityEnemyHero", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MAGIC_RESISTANCE", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + }, + { + "type" : "SPELL_RESISTANCE_AURA", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 93, + "type" : ["HERO"] + }, + "ringOfVitality": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 1, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 94, + "type" : ["HERO"] + }, + "ringOfLife": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 1, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 95, + "type" : ["HERO"] + }, + "vialOfLifeblood": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 2, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 96, + "type" : ["HERO"] + }, + "necklaceOfSwiftness": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 97, + "type" : ["HERO"] + }, + "bootsOfSpeed": + { + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : "heroMovementLand", + "val" : 600, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 98, + "type" : ["HERO"] + }, + "capeOfVelocity": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 99, + "type" : ["HERO"] + }, + "pendantOfDispassion": + { + "bonuses" : [ + { + "subtype" : "spell.berserk", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 100, + "type" : ["HERO"] + }, + "pendantOfSecondSight": + { + "bonuses" : [ + { + "subtype" : "spell.blind", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 101, + "type" : ["HERO"] + }, + "pendantOfHoliness": + { + "bonuses" : [ + { + "subtype" : "spell.curse", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 102, + "type" : ["HERO"] + }, + "pendantOfLife": + { + "bonuses" : [ + { + "subtype" : "spell.deathRipple", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 103, + "type" : ["HERO"] + }, + "pendantOfDeath": + { + "bonuses" : [ + { + "limiters" : ["IS_UNDEAD"], + "subtype" : "spell.destroyUndead", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 104, + "type" : ["HERO"] + }, + "pendantOfFreeWill": + { + "bonuses" : [ + { + "subtype" : "spell.hypnotize", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 105, + "type" : ["HERO"] + }, + "pendantOfNegativity": + { + "bonuses" : [ + { + "subtype" : "spell.lightningBolt", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.chainLightning", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 106, + "type" : ["HERO"] + }, + "pendantOfTotalRecall": + { + "bonuses" : [ + { + "subtype" : "spell.forgetfulness", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 107, + "type" : ["HERO"] + }, + "pendantOfCourage": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "LUCK", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 108, + "type" : ["HERO"] + }, + "everflowingCrystalCloak": + { + "bonuses" : [ + { + "subtype" : "resource.crystal", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 109, + "type" : ["HERO"] + }, + "ringOfInfiniteGems": + { + "bonuses" : [ + { + "subtype" : "resource.gems", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 110, + "type" : ["HERO"] + }, + "everpouringVialOfMercury": + { + "bonuses" : [ + { + "subtype" : "resource.mercury", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 111, + "type" : ["HERO"] + }, + "inexhaustibleCartOfOre": + { + "bonuses" : [ + { + "subtype" : "resource.ore", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 112, + "type" : ["HERO"] + }, + "eversmokingRingOfSulfur": + { + "bonuses" : [ + { + "subtype" : "resource.sulfur", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 113, + "type" : ["HERO"] + }, + "inexhaustibleCartOfLumber": + { + "bonuses" : [ + { + "subtype" : "resource.wood", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 114, + "type" : ["HERO"] + }, + "endlessSackOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 1000, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 115, + "type" : ["HERO"] + }, + "endlessBagOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 750, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 116, + "type" : ["HERO"] + }, + "endlessPurseOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 500, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 117, + "type" : ["HERO"] + }, + "legsOfLegion": + { + "index" : 118, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : "creatureLevel1", + "val" : 5, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "loinsOfLegion": + { + "index" : 119, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : "creatureLevel2", + "val" : 4, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "torsoOfLegion": + { + "index" : 120, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : "creatureLevel3", + "val" : 3, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "armsOfLegion": + { + "index" : 121, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : "creatureLevel4", + "val" : 2, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "headOfLegion": + { + "index" : 122, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : "creatureLevel5", + "val" : 1, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "seaCaptainsHat": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "WHIRLPOOL_PROTECTION", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MOVEMENT", + "subtype" : "heroMovementSea", + "val" : 500, + "valueType" : "ADDITIVE_VALUE" + }, + { + "subtype" : "spell.summonBoat", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "spell.scuttleBoat", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + } + ], + "index" : 123, + "type" : ["HERO"] + }, + "spellbindersHat": + { + "bonuses" : [ + { + "subtype" : "spellLevel5", + "type" : "SPELLS_OF_LEVEL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 124, + "type" : ["HERO"] + }, + "shacklesOfWar": + { + "index" : 125, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "BATTLE_NO_FLEEING", + "propagator": "BATTLE_WIDE" + } + ] + }, + "orbOfInhibition": + { + "index" : 126, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "BLOCK_ALL_MAGIC", + "propagator": "BATTLE_WIDE" + } + ] + }, + "vialOfDragonBlood": + { + "bonuses" : [ + { + "limiters" : ["DRAGON_NATURE"], + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "limiters" : ["DRAGON_NATURE"], + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 127, + "type" : ["HERO"] + }, + "armageddonsBlade": + { + "bonuses" : [ + { + "subtype" : "spell.armageddon", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "spell.armageddon", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER", + "addInfo" : 1 + }, + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 128, + "type" : ["HERO"] + }, + "angelicAlliance": + { + "bonuses" : [ + { + "type" : "NONEVIL_ALIGNMENT_MIX", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.prayer", + "type" : "OPENING_BATTLE_SPELL", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 129, + "type" : ["HERO"], + "components": + [ + "armorOfWonder", + "sandalsOfTheSaint", + "celestialNecklaceOfBliss", + "lionsShieldOfCourage", + "swordOfJudgement", + "helmOfHeavenlyEnlightenment" + ] + }, + "cloakOfTheUndeadKing": + { + "bonuses" : [ + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.skeleton", + "addInfo" : 0 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.wight", + "addInfo" : 2 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.lich", + "addInfo" : 3 + } + ], + "index" : 130, + "type" : ["HERO"], + "components": + [ + "amuletOfTheUndertaker", + "vampiresCowl", + "deadMansBoots" + ] + }, + "elixirOfLife": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 25, + "valueType" : "PERCENT_TO_BASE", + "limiters" : [ + "noneOf", + "IS_UNDEAD", + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "NON_LIVING" ] + }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "GARGOYLE" ] + } + ] + }, + { + "type" : "HP_REGENERATION", + "val" : 50, + "valueType" : "BASE_NUMBER", + "limiters" : [ + "noneOf", + "IS_UNDEAD", + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "NON_LIVING" ] + }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "GARGOYLE" ] + } + ] + } + ], + "index" : 131, + "type" : ["HERO"], + "components": + [ + "ringOfVitality", + "ringOfLife", + "vialOfLifeblood" + ] + }, + "armorOfTheDamned": + { + "bonuses" : [ + { + "subtype" : "spell.slow", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.curse", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.weakness", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.misfortune", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 132, + "type" : ["HERO"], + "components": + [ + "blackshardOfTheDeadKnight", + "shieldOfTheYawningDead", + "skullHelmet", + "ribCage" + ] + }, + "statueOfLegion": + { + "index" : 133, + "type" : ["HERO"], + "components": + [ + "legsOfLegion", + "loinsOfLegion", + "torsoOfLegion", + "armsOfLegion", + "headOfLegion" + ], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH_PERCENT", + "val" : 50, + "propagator": "PLAYER_PROPAGATOR" + } + ] + }, + "powerOfTheDragonFather": + { + "index" : 134, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "LEVEL_SPELL_IMMUNITY", + "val" : 4, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "primarySkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primarySkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "components": + [ + "quietEyeOfTheDragon", + "redDragonFlameTongue", + "dragonScaleShield", + "dragonScaleArmor", + "dragonboneGreaves", + "dragonWingTabard", + "necklaceOfDragonteeth", + "crownOfDragontooth", + "stillEyeOfTheDragon" + ] + }, + "titansThunder": + { + "bonuses" : [ + { + "subtype" : "spell.titanBolt", + "type" : "SPELL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 135, + "type" : ["HERO"], + "components": + [ + "titansGladius", + "sentinelsShield", + "thunderHelmet", + "titansCuirass" + ] + }, + "admiralsHat": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "FREE_SHIP_BOARDING", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 136, + "type" : ["HERO"], + "components": + [ + "necklaceOfOceanGuidance", + "seaCaptainsHat" + ] + }, + "bowOfTheSharpshooter": + { + "bonuses" : [ + { + "limiters" : ["SHOOTER_ONLY"], + "type" : "NO_DISTANCE_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "type" : "NO_WALL_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "type" : "FREE_SHOOTING", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 137, + "type" : ["HERO"], + "components": + [ + "bowOfElvenCherrywood", + "bowstringOfTheUnicornsMane", + "angelFeatherArrows" + ] + }, + "wizardsWell": + { + "bonuses" : [ + { + "type" : "FULL_MANA_REGENERATION", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 138, + "type" : ["HERO"], + "components": + [ + "charmOfMana", + "talismanOfMana", + "mysticOrbOfMana" + ] + }, + "ringOfTheMagi": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 139, + "type" : ["HERO"], + "components": + [ + "collarOfConjuring", + "ringOfConjuring", + "capeOfConjuring" + ] + }, + "cornucopia": + { + "bonuses" : [ + { + "subtype" : "resource.crystal", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.gems", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.mercury", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.sulfur", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 140, + "type" : ["HERO"], + "components": + [ + "everflowingCrystalCloak", + "ringOfInfiniteGems", + "everpouringVialOfMercury", + "eversmokingRingOfSulfur" + ] + }, + // Misiokles: not having these 3 artifacts could lead to crashes. RMG will try to place them in some randoms maps (and crash the game) + "unusedArtifact1": + { + "index" : 141, + "class" : "SPECIAL", + "type" : ["CREATURE"] + }, + "unusedArtifact2": + { + "index" : 142, + "class" : "SPECIAL", + "type" : ["CREATURE"] + }, + "unusedArtifact3": + { + "index" : 143, + "class" : "SPECIAL", + "type" : ["CREATURE"] + } +} diff --git a/config/battlefields.json b/config/battlefields.json index b79f705a3..c711bfc46 100644 --- a/config/battlefields.json +++ b/config/battlefields.json @@ -130,21 +130,18 @@ "bonuses": [ { "type" : "NO_MORALE", - "subtype" : 0, "val" : 0, "valueType" : "INDEPENDENT_MIN", "description" : "Creatures on Cursed Ground" }, { "type" : "NO_LUCK", - "subtype" : 0, "val" : 0, "valueType" : "INDEPENDENT_MIN", "description" : "Creatures on Cursed Ground" }, { "type" : "BLOCK_MAGIC_ABOVE", - "subtype" : 0, "val" : 1, "valueType" : "INDEPENDENT_MIN" } diff --git a/config/bonuses.json b/config/bonuses.json index 66b22babd..877161cf8 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -1,604 +1,572 @@ -//TODO: selector-based config -// school immunities -// LEVEL_SPELL_IMMUNITY - -{ - "ADDITIONAL_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DOUBLE" - } - }, - - "ADDITIONAL_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL1" - } - }, - - "AIR_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPAIR1" - } - }, - - "ATTACKS_ALL_ADJACENT": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_ROUND" - } - }, - - "BLOCKS_RANGED_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/RANGEDBLOCK" - } - }, - - "BLOCKS_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL" - } - }, - - "CATAPULT": - { - "graphics": - { - "icon": "zvs/Lib1.res/Catapult" - } - }, - - "CATAPULT_EXTRA_SHOTS": - { - "hidden": true - }, - - "CHANGES_SPELL_COST_FOR_ALLY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MANA" - } - }, - - "CHANGES_SPELL_COST_FOR_ENEMY": - { - "graphics": - { - "icon": "zvs/Lib1.res/MagicDamper" - } - }, - - "CHARGE_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/ChargeImmune" - } - }, - - "DARKNESS": - { - }, - - "DEATH_STARE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DEATH" - } - }, - - "DEFENSIVE_STANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DEFBON" - } - }, - - "DESTRUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/DESTROYER" - } - }, - - "DOUBLE_DAMAGE_CHANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DBLOW" - } - }, - - "DRAGON_NATURE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DRAGON" - } - }, - - "DISGUISED": - { - "hidden": true - }, - - "EARTH_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPEATH1" - } - }, - - "ENCHANTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST1" - } - }, - - "ENCHANTED": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_BLESS" - } - }, - - "ENEMY_DEFENCE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RDEF" - } - }, - - "FIRE_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPFIRE1" - } - }, - - "FIRE_SHIELD": - { - "graphics": - { - "icon": "zvs/Lib1.res/FireShield" - } - }, - - "FIRST_STRIKE": - { - "graphics": - { - "icon": "zvs/Lib1.res/FIRSTSTRIKE" - } - }, - - "FEAR": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FEAR" - } - }, - - "FEARLESS": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FEARL" - } - }, - - "FLYING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FLY" - } - - }, - - "FREE_SHOOTING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOTA" - } - - }, - - "GARGOYLE": - { - "graphics": - { - "icon": "zvs/Lib1.res/NonLiving" // Just use the NonLiving icon for now - } - }, - - "GENERAL_DAMAGE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/DamageReductionMelee" - } - }, - - "HATE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_HATE" - } - }, - - "HEALER": - { - "graphics": - { - "icon": "zvs/Lib1.res/Healer" - } - }, - - "HP_REGENERATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_TROLL" - } - }, - - "JOUSTING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CHAMP" - } - }, - - "KING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_KING3" - } - }, - - "LEARN_BATTLE_SPELL_CHANCE": - { - "hidden": true - }, - - "LEARN_BATTLE_SPELL_LEVEL_LIMIT": - { - "hidden": true - }, - - "LEVEL_SPELL_IMMUNITY": - { - "graphics": - { - "icon": "" - } - }, - - "LIFE_DRAIN": - { - "graphics": - { - "icon": "zvs/Lib1.res/DrainLife" - } - }, - - "LIMITED_SHOOTING_RANGE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOT" - } - }, - - "MANA_CHANNELING": - { - "graphics": - { - "icon": "zvs/Lib1.res/ManaChannel" - } - }, - - "MANA_DRAIN": - { - "graphics": - { - "icon": "zvs/Lib1.res/ManaDrain" - } - }, - - "MAGIC_MIRROR": - { - "graphics": - { - "icon": "zvs/Lib1.res/MagicMirror" - } - }, - - "MAGIC_RESISTANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DWARF" - } - }, - - "MIND_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MIND" - } - }, - - "NONE": - { - "hidden": true - }, - - "NO_DISTANCE_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DIST" - } - }, - - "NO_MELEE_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MELEE" - } - }, - - "NO_MORALE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MORAL" - } - }, - - "NO_WALL_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_OBST" - } - }, - - "NO_TERRAIN_PENALTY": - { - "hidden": true - }, - - "NON_LIVING": - { - "graphics": - { - "icon": "zvs/Lib1.res/NonLiving" - } - }, - - "RANDOM_SPELLCASTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/RandomBoost" - } - }, - - "PERCENTAGE_DAMAGE_BOOST": - { - "hidden": true - }, - - "RANGED_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/RANGEDCOUNTER" - } - }, - - "RECEPTIVE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_NOFRIM" - } - }, - - "REBIRTH": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_REBIRTH" - } - }, - - "RETURN_AFTER_STRIKE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_HARPY" - } - }, - - "SHOOTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOT" - } - }, - - "SHOOTS_ALL_ADJACENT": - { - "graphics": - { - "icon": "zvs/Lib1.res/AREASHOT" - } - }, - - "SOUL_STEAL": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SUMMON2" - } - }, - - "SPELLCASTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CASTER" - } - }, - - "SPELL_AFTER_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST" - } - }, - - "SPELL_BEFORE_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST2" - } - }, - - "SPELL_DAMAGE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_GOLEM" - } - }, - - "SPELL_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPDISB" //todo: configurable use from spell handler - } - }, - - "SPELL_LIKE_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPDFIRE" - } - }, - - "SPELL_RESISTANCE_AURA": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_UNIC" - } - }, - - "SUMMON_GUARDIANS": - { - "graphics": - { - "icon": "zvs/Lib1.res/SUMMONGUARDS" - } - }, - - "TWO_HEX_ATTACK_BREATH": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_BREATH" - } - }, - - "THREE_HEADED_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/ThreeHeaded" - } - }, - - "TRANSMUTATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SGTYPE" - } - }, - - "UNDEAD": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_UNDEAD" - } - }, - - "UNLIMITED_RETALIATIONS": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL1" - } - }, - - "VISIONS": - { - "hidden": true - }, - - "WATER_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPWATER1" - } - }, - - "WIDE_BREATH": - { - "graphics": - { - "icon": "zvs/Lib1.res/MEGABREATH" - } - } -} - +//TODO: selector-based config +// school immunities +// LEVEL_SPELL_IMMUNITY + +{ + "ADDITIONAL_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DOUBLE" + } + }, + + "ADDITIONAL_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL1" + } + }, + + "ATTACKS_ALL_ADJACENT": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_ROUND" + } + }, + + "BLOCKS_RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDBLOCK" + } + }, + + "BLOCKS_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL" + } + }, + + "CATAPULT": + { + "graphics": + { + "icon": "zvs/Lib1.res/Catapult" + } + }, + + "CATAPULT_EXTRA_SHOTS": + { + "hidden": true + }, + + "CHANGES_SPELL_COST_FOR_ALLY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MANA" + } + }, + + "CHANGES_SPELL_COST_FOR_ENEMY": + { + "graphics": + { + "icon": "zvs/Lib1.res/MagicDamper" + } + }, + + "CHARGE_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/ChargeImmune" + } + }, + + "DARKNESS": + { + }, + + "DEATH_STARE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DEATH" + } + }, + + "DEFENSIVE_STANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DEFBON" + } + }, + + "DESTRUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/DESTROYER" + } + }, + + "DOUBLE_DAMAGE_CHANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DBLOW" + } + }, + + "DRAGON_NATURE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DRAGON" + } + }, + + "DISGUISED": + { + "hidden": true + }, + + "ENCHANTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST1" + } + }, + + "ENCHANTED": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_BLESS" + } + }, + + "ENEMY_DEFENCE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RDEF" + } + }, + + "FIRE_SHIELD": + { + "graphics": + { + "icon": "zvs/Lib1.res/FireShield" + } + }, + + "FIRST_STRIKE": + { + "graphics": + { + "icon": "zvs/Lib1.res/FIRSTSTRIKE" + } + }, + + "FEAR": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FEAR" + } + }, + + "FEARLESS": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FEARL" + } + }, + + "FLYING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FLY" + } + + }, + + "FREE_SHOOTING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOTA" + } + + }, + + "GARGOYLE": + { + "graphics": + { + "icon": "zvs/Lib1.res/NonLiving" // Just use the NonLiving icon for now + } + }, + + "GENERAL_DAMAGE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/DamageReductionMelee" + } + }, + + "HATE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_HATE" + } + }, + + "HEALER": + { + "graphics": + { + "icon": "zvs/Lib1.res/Healer" + } + }, + + "HP_REGENERATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_TROLL" + } + }, + + "JOUSTING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CHAMP" + } + }, + + "KING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_KING3" + } + }, + + "LEARN_BATTLE_SPELL_CHANCE": + { + "hidden": true + }, + + "LEARN_BATTLE_SPELL_LEVEL_LIMIT": + { + "hidden": true + }, + + "LEVEL_SPELL_IMMUNITY": + { + "graphics": + { + "icon": "" + } + }, + + "LIFE_DRAIN": + { + "graphics": + { + "icon": "zvs/Lib1.res/DrainLife" + } + }, + + "LIMITED_SHOOTING_RANGE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOT" + } + }, + + "MANA_CHANNELING": + { + "graphics": + { + "icon": "zvs/Lib1.res/ManaChannel" + } + }, + + "MANA_DRAIN": + { + "graphics": + { + "icon": "zvs/Lib1.res/ManaDrain" + } + }, + + "MAGIC_MIRROR": + { + "graphics": + { + "icon": "zvs/Lib1.res/MagicMirror" + } + }, + + "MAGIC_RESISTANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DWARF" + } + }, + + "MIND_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MIND" + } + }, + + "NONE": + { + "hidden": true + }, + + "NO_DISTANCE_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DIST" + } + }, + + "NO_MELEE_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MELEE" + } + }, + + "NO_MORALE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MORAL" + } + }, + + "NO_WALL_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_OBST" + } + }, + + "NO_TERRAIN_PENALTY": + { + "hidden": true + }, + + "NON_LIVING": + { + "graphics": + { + "icon": "zvs/Lib1.res/NonLiving" + } + }, + + "RANDOM_SPELLCASTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/RandomBoost" + } + }, + + "PERCENTAGE_DAMAGE_BOOST": + { + "hidden": true + }, + + "RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDCOUNTER" + } + }, + + "RECEPTIVE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_NOFRIM" + } + }, + + "REBIRTH": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_REBIRTH" + } + }, + + "RETURN_AFTER_STRIKE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_HARPY" + } + }, + + "SHOOTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOT" + } + }, + + "SHOOTS_ALL_ADJACENT": + { + "graphics": + { + "icon": "zvs/Lib1.res/AREASHOT" + } + }, + + "SOUL_STEAL": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SUMMON2" + } + }, + + "SPELLCASTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CASTER" + } + }, + + "SPELL_AFTER_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST" + } + }, + + "SPELL_BEFORE_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST2" + } + }, + + "SPELL_DAMAGE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_GOLEM" + } + }, + + "SPELL_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SPDISB" //todo: configurable use from spell handler + } + }, + + "SPELL_LIKE_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SPDFIRE" + } + }, + + "SPELL_RESISTANCE_AURA": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_UNIC" + } + }, + + "SUMMON_GUARDIANS": + { + "graphics": + { + "icon": "zvs/Lib1.res/SUMMONGUARDS" + } + }, + + "TWO_HEX_ATTACK_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_BREATH" + } + }, + + "THREE_HEADED_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/ThreeHeaded" + } + }, + + "TRANSMUTATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SGTYPE" + } + }, + + "UNDEAD": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_UNDEAD" + } + }, + + "UNLIMITED_RETALIATIONS": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL1" + } + }, + + "VISIONS": + { + "hidden": true + }, + + "WIDE_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/MEGABREATH" + } + } +} + diff --git a/config/campaignMedia.json b/config/campaignMedia.json index a06a9afa8..c54185837 100644 --- a/config/campaignMedia.json +++ b/config/campaignMedia.json @@ -1,239 +1,239 @@ -{ - "videos": [ - //Restoration of Erathia - //Long live the Queen - "GOOD1A.SMK", //Good1_a - "GOOD1B.SMK", //Good1_b - "GOOD1C.SMK", //Good1_c - //Dungeons and devils - "EVIL1A.SMK", //Evil1_a - "EVIL1B.SMK", //Evil1_b - "EVIL1C.SMK", //Evil1_c - //Spoils of War - "NEUTRALA.SMK", //Neutral1_a - "NEUTRALB.SMK", //Neutral1_b - "NEUTRALC.SMK", //Neutral1_c - //Liberation - "GOOD2A.SMK", //Good2_a - "GOOD2B.SMK", //Good2_b - "GOOD2C.SMK", //Good2_c - "GOOD2D.SMK", //Good2_d - //Long Live the King - "EVIL2A.SMK", //Evil2_a - "EVIL2AP1.SMK", //Evil2ap1 - "EVIL2B.SMK", //Evil2_b - "EVIL2C.SMK", //Evil2_c - "EVIL2D.SMK", //Evil2_d - //Song for the Father - "GOOD3A.SMK", //Good3_a - "GOOD3B.SMK", //Good3_b - "GOOD3C.SMK", //Good3_c - //Seeds Of Discontent - "SECRETA.SMK", //Secret_a - "SECRETB.SMK", //Secret_b - "SECRETC.SMK", //Secret_c - //Armageddon's Blade - //Armageddon's Blade - "H3ABab1.smk", //ArmageddonsBlade_a - "H3ABab2.smk", //ArmageddonsBlade_b - "H3ABab3.smk", //ArmageddonsBlade_c - "H3ABab4.smk", //ArmageddonsBlade_d - "H3ABab5.smk", //ArmageddonsBlade_e - "H3ABab6.smk", //ArmageddonsBlade_f - "H3ABab7.smk", //ArmageddonsBlade_g - "H3ABab8.smk", //ArmageddonsBlade_h - "H3ABab9.smk", //ArmageddonsBlade_end - //Dragon's Blood - "H3ABdb1.smk", //DragonsBlood_a - "H3ABdb2.smk", //DragonsBlood_b - "H3ABdb3.smk", //DragonsBlood_c - "H3ABdb4.smk", //DragonsBlood_d - "H3ABdb5.smk", //DragonsBlood_end - //Dragon Slayer - "H3ABds1.smk", //DragonSlayer_a - "H3ABds2.smk", //DragonSlayer_b - "H3ABds3.smk", //DragonSlayer_c - "H3ABds4.smk", //DragonSlayer_d - "H3ABds5.smk", //DragonSlayer_end - //Festival of Life - "H3ABfl1.smk", //FestivalOfLife_a - "H3ABfl2.smk", //FestivalOfLife_b - "H3ABfl3.smk", //FestivalOfLife_c - "H3ABfl4.smk", //FestivalOfLife_d - "H3ABfl5.smk", //FestivalOfLife_end - //Foolhardy Waywardness - "H3ABfw1.smk", //FoolhardyWaywardness_a - "H3ABfw2.smk", //FoolhardyWaywardness_b - "H3ABfw3.smk", //FoolhardyWaywardness_c - "H3ABfw4.smk", //FoolhardyWaywardness_d - "H3ABfw5.smk", //FoolhardyWaywardness_end - //Playing with Fire - "H3ABpf1.smk", //PlayingWithFire_a - "H3ABpf2.smk", //PlayingWithFire_b - "3ABpf3.smk", //PlayingWithFire_c - "H3ABpf4.smk", //PlayingWithFire_end - //Shadow of Death Campaigns - //Birth of a Barbarian - "H3x2_BBa.smk", //BirthOfABarbarian_a - "H3x2_BBb.smk", //BirthOfABarbarian_b - "H3x2_BBc.smk", //BirthOfABarbarian_c - "H3x2_BBd.smk", //BirthOfABarbarian_d - "H3x2_BBe.smk", //BirthOfABarbarian_e - "H3x2_BBf.smk", //BirthOfABarbarian_end - //Elixir of Life - "H3x2_Ela.smk", //ElixirOfLife_a - "H3x2_Elb.smk", //ElixirOfLife_b - "H3x2_Elc.smk", //ElixirOfLife_c - "H3x2_Eld.smk", //ElixirOfLife_d - "H3x2_Ele.smk", //ElixirOfLife_end - //Hack and Slash - "H3x2_HSa.smk", //HackAndSlash_a - "EVIL2C.SMK", //HackAndSlash_b - "H3x2_HSc.smk", //HackAndSlash_c - "H3x2_HSd.smk", //HackAndSlash_d - "H3x2_HSe.smk", //HackAndSlash_end - //New Beginning - "H3x2_NBa.smk", //NewBeginning_a - "H3x2_NBb.smk", //NewBeginning_b - "H3x2_Nbc.smk", //NewBeginning_c - "H3x2_Nbd.smk", //NewBeginning_d - "H3x2_Nbe.smk", //NewBeginning_end - //Rise of the Necromancer - "H3x2_RNa.smk", //RiseOfTheNecromancer_a - "H3x2_RNb.smk", //RiseOfTheNecromancer_b - "H3x2_RNc.smk", //RiseOfTheNecromancer_c - "H3x2_RNd.smk", //RiseOfTheNecromancer_d - "H3x2_RNe1.smk", //RiseOfTheNecromancer_end - //Spectre of Power - "H3x2_SPa.smk", //SpectreOfPower_a - "H3x2_SPb.smk", //SpectreOfPower_b - "H3x2_SPc.smk", //SpectreOfPower_c - "H3x2_SPd.smk", //SpectreOfPower_d - "H3x2_SPe.smk", //SpectreOfPower_end - //Unholy Alliance - "H3x2_UAa.smk", //UnholyAlliance_a - "H3x2_UAb.smk", //UnholyAlliance_b - "H3x2_UAc.smk", //UnholyAlliance_c - "H3x2_UAd.smk", //UnholyAlliance_d - "H3x2_UAe.smk", //UnholyAlliance_e - "H3x2_UAf.smk", //UnholyAlliance_f - "H3x2_UAg.smk", //UnholyAlliance_g - "H3x2_UAh.smk", //UnholyAlliance_h - "H3x2_UAi.smk", //UnholyAlliance_i - "H3x2_UAj.smk", //UnholyAlliance_j - "H3x2_UAk.smk", //UnholyAlliance_k - "H3x2_UAl.smk", //UnholyAlliance_l - "H3x2_UAm.smk", //UnholyAlliance_end //H3x2_UAm.bik? - ], - - "music" : [ - // Use CmpMusic.txt from H3 instead - ], - - "voice" : [ - //Restoration of Erathia - "G1A", //Long live the Queen 1 - "G1B", //Long live the Queen 2 - "G1C", //Long live the Queen 3 - "E1A.wav", //Dungeons and Devils 1 - "E1B.wav", //Dungeons and Devils 2 - "E1C.wav", //Dungeons and Devils 3 - "N1A", //Spoils of War 1 - "N1B", //Spoils of War 2 - "N1C_D", //Spoils of War 3 - "G2A", //Liberation 1 - "G2B", //Liberation 2 - "G2C", //Liberation 3 - "G2D", //Liberation 4 - "E2A.wav", //Long live the King 1 - "E2AE.wav", //Long live the King 1end - "E2B.wav", //Long live the King 2 - "E2C.wav", //Long live the King 3 - "E2D.wav", //Long live the King 4 - "G3A", //Song for the Father 1 - "G3B", //Song for the Father 2 - "G3C", //Song for the Father 3 - "S1A", //Seeds of discontent 1 - "S1B", //Seeds of discontent 2 - "S1C", //Seeds of discontent 3 - //Armageddon's Blade - "ABvoAB1.wav", //Armageddon's Blade 1 - "ABvoAB2.wav", //Armageddon's Blade 2 - "ABvoAB3.wav", //Armageddon's blade 3 - "ABvoAB4.wav", //Armageddon's blade 4 - "ABvoAB5.wav", //Armageddon's blade 5 - "ABvoAB6.wav", //Armageddon's blade 6 - "ABvoAB7.wav", //Armageddon's blade 7 - "ABvoAB8.wav", //Armageddon's blade 8 - "ABvoAB9.wav", //Armageddon's blade 8end - "ABvoDB1.wav", //Dragon's Blood 1 - "ABvoDB2.wav", //Dragon's Blood 2 - "ABvoDB3.wav", //Dragon's Blood 3 - "ABvoDB4.wav", //Dragon's Blood 4 - "ABvoDB5.wav", //Dragon's Blood 4end - "ABvoDS1.wav", //Dragon Slayer 1 - "ABvoDS2.wav", //Dragon Slayer 2 - "ABvoDS3.wav", //Dragon Slayer 3 - "ABvoDS4.wav", //Dragon Slayer 4 - "ABvoDS5.wav", //Dragon Slayer 4end - "ABvoFL1.wav", //Festival of Life 1 - "ABvoFL2.wav", //Festival of Life 2 - "ABvoFL3.wav", //Festival of Life 3 - "ABvoFL4.wav", //Festival of Life 4 - "ABvoFL5.wav", //Festival of Life 4end - "ABvoFW1.wav", //Foolhardy Waywardness 1 - "ABvoFW2.wav", //Foolhardy Waywardness 2 - "ABvoFW3.wav", //Foolhardy Waywardness 3 - "ABvoFW4.wav", //Foolhardy Waywardness 4 - "ABvoFW5.wav", //Foolhardy Waywardness 4end - "ABvoPF1.wav", //Playing with Fire 1 - "ABvoPF2.wav", //Playing with Fire 2 - "ABvoPF3.wav", //Playing with Fire 3 - "ABvoPF4.wav", //Playing with Fire 3end - //Shadow of Death Campaigns - "H3x2BBa", //Birth of a Barbarian 1 - "H3x2BBb", //Birth of a Barbarian 2 - "H3x2BBc", //Birth of a Barbarian 3 - "H3x2BBd", //Birth of a Barbarian 4 - "H3x2BBe", //Birth of a Barbarian 5 - "H3x2BBf", //Birth of a Barbarian 5end - "H3x2ELa", //Elixir of life 1 - "H3x2ELb", //Elixir of life 2 - "H3x2ELc", //Elixir of life 3 - "H3x2ELd", //Elixir of life 4 - "H3x2ELe", //Elixir of life 4end - "H3x2HSa", //Hack and Slash 1 - "H3x2HSb", //Hack and Slash 2 - "H3x2HSc", //Hack and Slash 3 - "H3x2HSd", //Hack and Slash 4 - "H3x2HSe", //Hack and Slash 4end - "H3x2NBa", //New Beginning 1 - "H3x2NBb", //New Beginning 2 - "H3x2NBc", //New Beginning 3 - "H3x2NBd", //New Beginning 4 - "H3x2NBe", //New Beginning 4end - "H3x2RNa", //Rise of the Necromancer 1 - "H3x2RNb", //Rise of the Necromancer 2 - "H3x2RNc", //Rise of the Necromancer 3 - "H3x2RNd", //Rise of the Necromancer 4 - "H3x2RNe", //Rise of the Necromancer 4end - "H3x2SPa", //Spectre of Power 1 - "H3x2Spb", //Spectre of Power 2 - "H3x2Spc", //Spectre of Power 3 - "H3x2Spd", //Spectre of Power 4 - "H3x2Spe", //Spectre of Power 4end - "H3x2UAa", //Unholy alliance 1 - "H3x2UAb", //Unholy alliance 2 - "H3x2UAc", //Unholy alliance 3 - "H3x2UAd", //Unholy alliance 4 - "H3x2UAe", //Unholy alliance 5 - "H3x2UAf", //Unholy alliance 6 - "H3x2UAg", //Unholy alliance 7 - "H3x2UAh", //Unholy alliance 8 - "H3x2UAi", //Unholy alliance 9 - "H3x2UAj", //Unholy alliance 10 - "H3x2UAk", //Unholy alliance 11 - "H3x2UAl", //Unholy alliance 12 - "H3x2UAm" //Unholy alliance 12end - ] -} +{ + "videos": [ + //Restoration of Erathia + //Long live the Queen + "GOOD1A.SMK", //Good1_a + "GOOD1B.SMK", //Good1_b + "GOOD1C.SMK", //Good1_c + //Dungeons and devils + "EVIL1A.SMK", //Evil1_a + "EVIL1B.SMK", //Evil1_b + "EVIL1C.SMK", //Evil1_c + //Spoils of War + "NEUTRALA.SMK", //Neutral1_a + "NEUTRALB.SMK", //Neutral1_b + "NEUTRALC.SMK", //Neutral1_c + //Liberation + "GOOD2A.SMK", //Good2_a + "GOOD2B.SMK", //Good2_b + "GOOD2C.SMK", //Good2_c + "GOOD2D.SMK", //Good2_d + //Long Live the King + "EVIL2A.SMK", //Evil2_a + "EVIL2AP1.SMK", //Evil2ap1 + "EVIL2B.SMK", //Evil2_b + "EVIL2C.SMK", //Evil2_c + "EVIL2D.SMK", //Evil2_d + //Song for the Father + "GOOD3A.SMK", //Good3_a + "GOOD3B.SMK", //Good3_b + "GOOD3C.SMK", //Good3_c + //Seeds Of Discontent + "SECRETA.SMK", //Secret_a + "SECRETB.SMK", //Secret_b + "SECRETC.SMK", //Secret_c + //Armageddon's Blade + //Armageddon's Blade + "H3ABab1.smk", //ArmageddonsBlade_a + "H3ABab2.smk", //ArmageddonsBlade_b + "H3ABab3.smk", //ArmageddonsBlade_c + "H3ABab4.smk", //ArmageddonsBlade_d + "H3ABab5.smk", //ArmageddonsBlade_e + "H3ABab6.smk", //ArmageddonsBlade_f + "H3ABab7.smk", //ArmageddonsBlade_g + "H3ABab8.smk", //ArmageddonsBlade_h + "H3ABab9.smk", //ArmageddonsBlade_end + //Dragon's Blood + "H3ABdb1.smk", //DragonsBlood_a + "H3ABdb2.smk", //DragonsBlood_b + "H3ABdb3.smk", //DragonsBlood_c + "H3ABdb4.smk", //DragonsBlood_d + "H3ABdb5.smk", //DragonsBlood_end + //Dragon Slayer + "H3ABds1.smk", //DragonSlayer_a + "H3ABds2.smk", //DragonSlayer_b + "H3ABds3.smk", //DragonSlayer_c + "H3ABds4.smk", //DragonSlayer_d + "H3ABds5.smk", //DragonSlayer_end + //Festival of Life + "H3ABfl1.smk", //FestivalOfLife_a + "H3ABfl2.smk", //FestivalOfLife_b + "H3ABfl3.smk", //FestivalOfLife_c + "H3ABfl4.smk", //FestivalOfLife_d + "H3ABfl5.smk", //FestivalOfLife_end + //Foolhardy Waywardness + "H3ABfw1.smk", //FoolhardyWaywardness_a + "H3ABfw2.smk", //FoolhardyWaywardness_b + "H3ABfw3.smk", //FoolhardyWaywardness_c + "H3ABfw4.smk", //FoolhardyWaywardness_d + "H3ABfw5.smk", //FoolhardyWaywardness_end + //Playing with Fire + "H3ABpf1.smk", //PlayingWithFire_a + "H3ABpf2.smk", //PlayingWithFire_b + "3ABpf3.smk", //PlayingWithFire_c + "H3ABpf4.smk", //PlayingWithFire_end + //Shadow of Death Campaigns + //Birth of a Barbarian + "H3x2_BBa.smk", //BirthOfABarbarian_a + "H3x2_BBb.smk", //BirthOfABarbarian_b + "H3x2_BBc.smk", //BirthOfABarbarian_c + "H3x2_BBd.smk", //BirthOfABarbarian_d + "H3x2_BBe.smk", //BirthOfABarbarian_e + "H3x2_BBf.smk", //BirthOfABarbarian_end + //Elixir of Life + "H3x2_Ela.smk", //ElixirOfLife_a + "H3x2_Elb.smk", //ElixirOfLife_b + "H3x2_Elc.smk", //ElixirOfLife_c + "H3x2_Eld.smk", //ElixirOfLife_d + "H3x2_Ele.smk", //ElixirOfLife_end + //Hack and Slash + "H3x2_HSa.smk", //HackAndSlash_a + "EVIL2C.SMK", //HackAndSlash_b + "H3x2_HSc.smk", //HackAndSlash_c + "H3x2_HSd.smk", //HackAndSlash_d + "H3x2_HSe.smk", //HackAndSlash_end + //New Beginning + "H3x2_NBa.smk", //NewBeginning_a + "H3x2_NBb.smk", //NewBeginning_b + "H3x2_Nbc.smk", //NewBeginning_c + "H3x2_Nbd.smk", //NewBeginning_d + "H3x2_Nbe.smk", //NewBeginning_end + //Rise of the Necromancer + "H3x2_RNa.smk", //RiseOfTheNecromancer_a + "H3x2_RNb.smk", //RiseOfTheNecromancer_b + "H3x2_RNc.smk", //RiseOfTheNecromancer_c + "H3x2_RNd.smk", //RiseOfTheNecromancer_d + "H3x2_RNe1.smk", //RiseOfTheNecromancer_end + //Spectre of Power + "H3x2_SPa.smk", //SpectreOfPower_a + "H3x2_SPb.smk", //SpectreOfPower_b + "H3x2_SPc.smk", //SpectreOfPower_c + "H3x2_SPd.smk", //SpectreOfPower_d + "H3x2_SPe.smk", //SpectreOfPower_end + //Unholy Alliance + "H3x2_UAa.smk", //UnholyAlliance_a + "H3x2_UAb.smk", //UnholyAlliance_b + "H3x2_UAc.smk", //UnholyAlliance_c + "H3x2_UAd.smk", //UnholyAlliance_d + "H3x2_UAe.smk", //UnholyAlliance_e + "H3x2_UAf.smk", //UnholyAlliance_f + "H3x2_UAg.smk", //UnholyAlliance_g + "H3x2_UAh.smk", //UnholyAlliance_h + "H3x2_UAi.smk", //UnholyAlliance_i + "H3x2_UAj.smk", //UnholyAlliance_j + "H3x2_UAk.smk", //UnholyAlliance_k + "H3x2_UAl.smk", //UnholyAlliance_l + "H3x2_UAm.smk", //UnholyAlliance_end //H3x2_UAm.bik? + ], + + "music" : [ + // Use CmpMusic.txt from H3 instead + ], + + "voice" : [ + //Restoration of Erathia + "G1A", //Long live the Queen 1 + "G1B", //Long live the Queen 2 + "G1C", //Long live the Queen 3 + "E1A.wav", //Dungeons and Devils 1 + "E1B.wav", //Dungeons and Devils 2 + "E1C.wav", //Dungeons and Devils 3 + "N1A", //Spoils of War 1 + "N1B", //Spoils of War 2 + "N1C_D", //Spoils of War 3 + "G2A", //Liberation 1 + "G2B", //Liberation 2 + "G2C", //Liberation 3 + "G2D", //Liberation 4 + "E2A.wav", //Long live the King 1 + "E2AE.wav", //Long live the King 1end + "E2B.wav", //Long live the King 2 + "E2C.wav", //Long live the King 3 + "E2D.wav", //Long live the King 4 + "G3A", //Song for the Father 1 + "G3B", //Song for the Father 2 + "G3C", //Song for the Father 3 + "S1A", //Seeds of discontent 1 + "S1B", //Seeds of discontent 2 + "S1C", //Seeds of discontent 3 + //Armageddon's Blade + "ABvoAB1.wav", //Armageddon's Blade 1 + "ABvoAB2.wav", //Armageddon's Blade 2 + "ABvoAB3.wav", //Armageddon's blade 3 + "ABvoAB4.wav", //Armageddon's blade 4 + "ABvoAB5.wav", //Armageddon's blade 5 + "ABvoAB6.wav", //Armageddon's blade 6 + "ABvoAB7.wav", //Armageddon's blade 7 + "ABvoAB8.wav", //Armageddon's blade 8 + "ABvoAB9.wav", //Armageddon's blade 8end + "ABvoDB1.wav", //Dragon's Blood 1 + "ABvoDB2.wav", //Dragon's Blood 2 + "ABvoDB3.wav", //Dragon's Blood 3 + "ABvoDB4.wav", //Dragon's Blood 4 + "ABvoDB5.wav", //Dragon's Blood 4end + "ABvoDS1.wav", //Dragon Slayer 1 + "ABvoDS2.wav", //Dragon Slayer 2 + "ABvoDS3.wav", //Dragon Slayer 3 + "ABvoDS4.wav", //Dragon Slayer 4 + "ABvoDS5.wav", //Dragon Slayer 4end + "ABvoFL1.wav", //Festival of Life 1 + "ABvoFL2.wav", //Festival of Life 2 + "ABvoFL3.wav", //Festival of Life 3 + "ABvoFL4.wav", //Festival of Life 4 + "ABvoFL5.wav", //Festival of Life 4end + "ABvoFW1.wav", //Foolhardy Waywardness 1 + "ABvoFW2.wav", //Foolhardy Waywardness 2 + "ABvoFW3.wav", //Foolhardy Waywardness 3 + "ABvoFW4.wav", //Foolhardy Waywardness 4 + "ABvoFW5.wav", //Foolhardy Waywardness 4end + "ABvoPF1.wav", //Playing with Fire 1 + "ABvoPF2.wav", //Playing with Fire 2 + "ABvoPF3.wav", //Playing with Fire 3 + "ABvoPF4.wav", //Playing with Fire 3end + //Shadow of Death Campaigns + "H3x2BBa", //Birth of a Barbarian 1 + "H3x2BBb", //Birth of a Barbarian 2 + "H3x2BBc", //Birth of a Barbarian 3 + "H3x2BBd", //Birth of a Barbarian 4 + "H3x2BBe", //Birth of a Barbarian 5 + "H3x2BBf", //Birth of a Barbarian 5end + "H3x2ELa", //Elixir of life 1 + "H3x2ELb", //Elixir of life 2 + "H3x2ELc", //Elixir of life 3 + "H3x2ELd", //Elixir of life 4 + "H3x2ELe", //Elixir of life 4end + "H3x2HSa", //Hack and Slash 1 + "H3x2HSb", //Hack and Slash 2 + "H3x2HSc", //Hack and Slash 3 + "H3x2HSd", //Hack and Slash 4 + "H3x2HSe", //Hack and Slash 4end + "H3x2NBa", //New Beginning 1 + "H3x2NBb", //New Beginning 2 + "H3x2NBc", //New Beginning 3 + "H3x2NBd", //New Beginning 4 + "H3x2NBe", //New Beginning 4end + "H3x2RNa", //Rise of the Necromancer 1 + "H3x2RNb", //Rise of the Necromancer 2 + "H3x2RNc", //Rise of the Necromancer 3 + "H3x2RNd", //Rise of the Necromancer 4 + "H3x2RNe", //Rise of the Necromancer 4end + "H3x2SPa", //Spectre of Power 1 + "H3x2Spb", //Spectre of Power 2 + "H3x2Spc", //Spectre of Power 3 + "H3x2Spd", //Spectre of Power 4 + "H3x2Spe", //Spectre of Power 4end + "H3x2UAa", //Unholy alliance 1 + "H3x2UAb", //Unholy alliance 2 + "H3x2UAc", //Unholy alliance 3 + "H3x2UAd", //Unholy alliance 4 + "H3x2UAe", //Unholy alliance 5 + "H3x2UAf", //Unholy alliance 6 + "H3x2UAg", //Unholy alliance 7 + "H3x2UAh", //Unholy alliance 8 + "H3x2UAi", //Unholy alliance 9 + "H3x2UAj", //Unholy alliance 10 + "H3x2UAk", //Unholy alliance 11 + "H3x2UAl", //Unholy alliance 12 + "H3x2UAm" //Unholy alliance 12end + ] +} diff --git a/config/campaignSets.json b/config/campaignSets.json index 1a0cd3194..eccafe67d 100644 --- a/config/campaignSets.json +++ b/config/campaignSets.json @@ -5,13 +5,13 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/GOOD1.H3C", "image":"CAMPGD1S", "video":"CGOOD1", "open": true }, - { "x":539, "y":72, "file":"DATA/EVIL1.H3C", "image":"CAMPEV1S", "video":"CEVIL1", "open": true }, - { "x":43, "y":245, "file":"DATA/GOOD2.H3C", "image":"CAMPGD2S", "video":"CGOOD2", "open": true }, - { "x":313, "y":244, "file":"DATA/NEUTRAL1.H3C", "image":"CAMPNEUS", "video":"CNEUTRAL", "open": true }, - { "x":586, "y":246, "file":"DATA/EVIL2.H3C", "image":"CAMPEV2S", "video":"CEVIL2", "open": true }, - { "x":34, "y":417, "file":"DATA/GOOD3.H3C", "image":"CAMPGD3S", "video":"CGOOD3", "open": true }, - { "x":404, "y":414, "file":"DATA/SECRET1.H3C", "image":"CAMPSCTS", "video":"CSECRET", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/GOOD1", "image":"CAMPGD1S", "video":"CGOOD1", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/EVIL1", "image":"CAMPEV1S", "video":"CEVIL1", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/GOOD2", "image":"CAMPGD2S", "video":"CGOOD2", "requires": [1, 2, 4] }, + { "id": 4, "x":313, "y":244, "file":"DATA/NEUTRAL1", "image":"CAMPNEUS", "video":"CNEUTRAL", "requires": [] }, + { "id": 5, "x":586, "y":246, "file":"DATA/EVIL2", "image":"CAMPEV2S", "video":"CEVIL2", "requires": [1, 2, 4] }, + { "id": 6, "x":34, "y":417, "file":"DATA/GOOD3", "image":"CAMPGD3S", "video":"CGOOD3", "requires": [3, 5] }, + { "id": 7, "x":404, "y":414, "file":"DATA/SECRET1", "image":"CAMPSCTS", "video":"CSECRET", "requires": [6] } ] }, "ab" : @@ -25,12 +25,12 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/AB.H3C", "image":"CAMP1AB7", "video":"C1ab7", "open": true }, - { "x":539, "y":72, "file":"DATA/BLOOD.H3C", "image":"CAMP1DB2", "video":"C1db2", "open": true }, - { "x":43, "y":245, "file":"DATA/SLAYER.H3C", "image":"CAMP1DS1", "video":"C1ds1", "open": true }, - { "x":313, "y":244, "file":"DATA/FESTIVAL.H3C", "image":"CAMP1FL3", "video":"C1fl3", "open": true }, - { "x":586, "y":246, "file":"DATA/FIRE.H3C", "image":"CAMP1PF2", "video":"C1pf2", "open": true }, - { "x":34, "y":417, "file":"DATA/FOOL.H3C", "image":"CAMP1FW1", "video":"C1fw1", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/AB", "image":"CAMP1AB7", "video":"C1ab7", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/BLOOD", "image":"CAMP1DB2", "video":"C1db2", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/SLAYER", "image":"CAMP1DS1", "video":"C1ds1", "requires": [] }, + { "id": 4, "x":313, "y":244, "file":"DATA/FESTIVAL", "image":"CAMP1FL3", "video":"C1fl3", "requires": [] }, + { "id": 5, "x":586, "y":246, "file":"DATA/FIRE", "image":"CAMP1PF2", "video":"C1pf2", "requires": [] }, + { "id": 6, "x":34, "y":417, "file":"DATA/FOOL", "image":"CAMP1FW1", "video":"C1fw1", "requires": [1, 2, 3, 4, 5] } ] }, "sod": @@ -39,26 +39,13 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/GEM.H3C", "image":"CAMPNB1", "video":"NEW", "open": true }, - { "x":539, "y":72, "file":"DATA/GELU.H3C", "image":"CAMPEL1", "video":"ELIXIR", "open": true }, - { "x":43, "y":245, "file":"DATA/CRAG.H3C", "image":"CAMPHS1", "video":"HACK", "open": true }, - { "x":313, "y":244, "file":"DATA/SANDRO.H3C", "image":"CAMPRN1", "video":"RISE", "open": true }, - { "x":586, "y":246, "file":"DATA/YOG.H3C", "image":"CAMPBB1", "video":"BIRTH", "open": true }, - { "x":34, "y":417, "file":"DATA/FINAL.H3C", "image":"CAMPUA1", "video":"UNHOLY", "open": true }, - { "x":404, "y":414, "file":"DATA/SECRET.H3C", "image":"CAMPSP1", "video":"SPECTRE", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/GEM", "image":"CAMPNB1", "video":"NEW", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/GELU", "image":"CAMPEL1", "video":"ELIXIR", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/CRAG", "image":"CAMPHS1", "video":"HACK", "requires": [] }, + { "id": 4, "x":313, "y":244, "file":"DATA/SANDRO", "image":"CAMPRN1", "video":"RISE", "requires": [1, 2, 3, 5] }, + { "id": 5, "x":586, "y":246, "file":"DATA/YOG", "image":"CAMPBB1", "video":"BIRTH", "requires": [] }, + { "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] } ] } -// "wog" : -// { -// /// wog campaigns, currently has no assigned button in campaign screen and thus unused -// "images" : [ {"x": 0, "y": 0, "name":"CAMPZALL"} ], -// "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27}, -// "items": -// [ -// { "x":90, "y":72, "file":"DATA/ZC1.H3C", "image":"CAMPZ01", "open": true}, -// { "x":539, "y":72, "file":"DATA/ZC2.H3C", "image":"CAMPZ02", "open": true}, -// { "x":43, "y":245, "file":"DATA/ZC3.H3C", "image":"CAMPZ03", "open": true}, -// { "x":311, "y":242, "file":"DATA/ZC4.H3C", "image":"CAMPZ04", "open": true} -// ] -// } } diff --git a/config/commanders.json b/config/commanders.json index a87159e08..d1516cf15 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -1,41 +1,41 @@ - -{ - //Commander receives these bonuses on level-up - "bonusPerLevel": - [ - ["CREATURE_DAMAGE", 1, 1, 0 ], //+1 minimum damage - ["CREATURE_DAMAGE", 2, 2, 0 ], //+2 maximum damage - ["STACK_HEALTH", 5, 0, 0 ] //+5 hp - ], - //Value of bonuses given by each skill level - "skillLevels": - [ - {"name": "ATTACK", "levels": [2, 5, 9, 15, 25]}, //0 - {"name": "DEFENSE", "levels": [4, 10, 18, 30, 50]}, //1 - {"name": "HEALTH", "levels": [10, 25, 45, 70, 100]}, //2 - {"name": "DAMAGE", "levels": [10, 25, 45, 70, 100]}, //3 - {"name": "SPEED", "levels": [1, 2, 3, 4, 6]}, //4 - {"name": "SPELL_POWER", "levels": [1, 3, 6, 14, 29]}, //5 - {"name": "CASTS", "levels": [1, 2, 3, 4, 5]}, - {"name": "RESISTANCE", "levels": [5, 15, 35, 60, 90]} - ], - "abilityRequirements": - //Two secondary skills needed for each special ability - [ - {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, 0, 0 ], "skills": [0, 1]}, - {"ability": ["FEAR", 0, 0, 0 ], "skills": [0, 2]}, - {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, -1, 0 ], "skills": [0, 3]}, - {"ability": ["SHOOTER", 0, 0, 0 ], "skills": [0, 4]}, - {"ability": ["BLOCKS_RETALIATION", 0, 1, 0 ], "skills": [0,5]}, - {"ability": ["UNLIMITED_RETALIATIONS", 0, 0, 0 ], "skills": [1, 2]}, - {"ability": ["ATTACKS_ALL_ADJACENT", 0, 0, 0 ], "skills": [1, 3]}, - {"ability": ["BLOCK", 30, 0, 0 ], "skills": [1, 4]}, - {"ability": ["FIRE_SHIELD", 1, 1, 0 ], "skills": [1, 5]}, - {"ability": ["ADDITIONAL_ATTACK", 1, 0, 0 ], "skills": [2, 3]}, - {"ability": ["HP_REGENERATION", 50, 0, 0 ], "skills": [2, 4]}, - {"ability": ["SPELL_AFTER_ATTACK", 30, "spell.paralyze", 0 ], "skills": [2, 5]}, - {"ability": ["JOUSTING", 5, 0, 0 ], "skills": [3, 4]}, - {"ability": ["DEATH_STARE", 1, 1, 0 ], "skills": [3,5]}, - {"ability": ["FLYING", 0, 0, 0 ], "skills": [4,5]} - ] -} + +{ + //Commander receives these bonuses on level-up + "bonusPerLevel": + [ + ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+2 minimum damage + ["CREATURE_DAMAGE", 4, "creatureDamageMax", 0 ], //+4 maximum damage + ["STACK_HEALTH", 20, null, 0 ] //+5 hp + ], + //Value of bonuses given by each skill level + "skillLevels": + [ + {"name": "ATTACK", "levels": [2, 5, 9, 15, 25]}, //0 + {"name": "DEFENSE", "levels": [4, 10, 18, 30, 50]}, //1 + {"name": "HEALTH", "levels": [10, 25, 45, 70, 100]}, //2 + {"name": "DAMAGE", "levels": [10, 25, 45, 70, 100]}, //3 + {"name": "SPEED", "levels": [1, 2, 3, 4, 6]}, //4 + {"name": "SPELL_POWER", "levels": [1, 3, 6, 14, 29]}, //5 + {"name": "CASTS", "levels": [1, 2, 3, 4, 5]}, + {"name": "RESISTANCE", "levels": [5, 15, 35, 60, 90]} + ], + "abilityRequirements": + //Two secondary skills needed for each special ability + [ + {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, null, 0 ], "skills": [0, 1]}, + {"ability": ["FEAR", 0, null, 0 ], "skills": [0, 2]}, + {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, null, 0 ], "skills": [0, 3]}, + {"ability": ["SHOOTER", 0, null, 0 ], "skills": [0, 4]}, + {"ability": ["BLOCKS_RETALIATION", 0, null, 0 ], "skills": [0,5]}, + {"ability": ["UNLIMITED_RETALIATIONS", 0, null, 0 ], "skills": [1, 2]}, + {"ability": ["ATTACKS_ALL_ADJACENT", 0, null, 0 ], "skills": [1, 3]}, + {"ability": ["NONE", 30, null, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn + {"ability": ["FIRE_SHIELD", 1, null, 0 ], "skills": [1, 5]}, + {"ability": ["ADDITIONAL_ATTACK", 1, null, 0 ], "skills": [2, 3]}, + {"ability": ["HP_REGENERATION", 50, null, 0 ], "skills": [2, 4]}, + {"ability": ["SPELL_AFTER_ATTACK", 30, "spell.paralyze", 0 ], "skills": [2, 5]}, + {"ability": ["JOUSTING", 5, null, 0 ], "skills": [3, 4]}, + {"ability": ["DEATH_STARE", 1, "deathStareCommander", 0 ], "skills": [3,5]}, + {"ability": ["FLYING", 0, "movementFlying", 0 ], "skills": [4,5]} + ] +} diff --git a/config/creatures/conflux.json b/config/creatures/conflux.json index 48156e276..397d9e154 100755 --- a/config/creatures/conflux.json +++ b/config/creatures/conflux.json @@ -154,8 +154,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "frostRingVulnerablity" : { @@ -246,10 +246,15 @@ "subtype" : "spell.armageddon", "val" : 100 }, - "immuneToWater" : + "immuneToIceBolt" : { - "type" : "WATER_IMMUNITY", - "subtype" : 2 //immune to damage spells only + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" }, "oppositeFire" : { @@ -440,10 +445,15 @@ "subtype" : "spell.armageddon", "val" : 100 }, - "immuneToWater" : + "immuneToIceBolt" : { - "type" : "WATER_IMMUNITY", - "subtype" : 2 //immune to damage spells only + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" }, "oppositeFire" : { @@ -661,8 +671,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "frostRingVulnerablity" : { @@ -732,8 +742,8 @@ { "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 //this IS important + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" } }, "graphics" : @@ -763,8 +773,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 //this IS important + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "rebirth" : { diff --git a/config/creatures/fortress.json b/config/creatures/fortress.json index 4353e40f6..1b3169052 100644 --- a/config/creatures/fortress.json +++ b/config/creatures/fortress.json @@ -114,7 +114,7 @@ "deathStare" : { "type" : "DEATH_STARE", - "subtype" : 0, + "subtype" : "deathStareGorgon", "val" : 10 } }, diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 7f39cd53e..204432246 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -272,8 +272,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" } }, "upgrades": ["efreetSultan"], @@ -315,8 +315,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "fireShield" : { @@ -361,7 +361,7 @@ "FLYING_ARMY" : { // type loaded from crtraits - "subtype" : 1 // teleports + "subtype" : "movementTeleporting" }, "descreaseLuck" : { @@ -415,7 +415,7 @@ "FLYING_ARMY" : { // type loaded from crtraits - "subtype" : 1 // teleports + "subtype" : "movementTeleporting" }, "descreaseLuck" : { diff --git a/config/creatures/necropolis.json b/config/creatures/necropolis.json index f38abe04a..54f1e8cec 100644 --- a/config/creatures/necropolis.json +++ b/config/creatures/necropolis.json @@ -151,8 +151,7 @@ { "noRetalitation" : { - "type" : "BLOCKS_RETALIATION", - "subtype" : 1 + "type" : "BLOCKS_RETALIATION" } }, "upgrades": ["vampireLord"], @@ -180,8 +179,7 @@ { "noRetalitation" : { - "type" : "BLOCKS_RETALIATION", - "subtype" : 1 + "type" : "BLOCKS_RETALIATION" }, "drainsLife" : { diff --git a/config/creatures/neutral.json b/config/creatures/neutral.json index a5f073ba6..a967218cd 100644 --- a/config/creatures/neutral.json +++ b/config/creatures/neutral.json @@ -529,7 +529,7 @@ "visionsMonsters" : { "type" : "VISIONS", - "subtype" : 0, + "subtype" : "visionsMonsters", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" @@ -537,7 +537,7 @@ "visionsHeroes" : { "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" @@ -545,7 +545,7 @@ "visionsTowns" : { "type" : "VISIONS", - "subtype" : 2, + "subtype" : "visionsTowns", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" diff --git a/config/difficulty.json b/config/difficulty.json new file mode 100644 index 000000000..a3e8beffa --- /dev/null +++ b/config/difficulty.json @@ -0,0 +1,70 @@ +//Configured difficulty +{ + "human": + { + "pawn": + { + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, + "ai": + { + "pawn": + { + "resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, +} + diff --git a/config/factions/dungeon.json b/config/factions/dungeon.json index ac347b9b1..f864b282e 100644 --- a/config/factions/dungeon.json +++ b/config/factions/dungeon.json @@ -178,7 +178,7 @@ "special3": { "type" : "portalOfSummoning" }, "special4": { "type" : "experienceVisitingBonus" }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, - "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.spellpower", "val": 12 } ] }, + "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.spellpower", "val": 12 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/factions/fortress.json b/config/factions/fortress.json index 403d840ca..38002253a 100644 --- a/config/factions/fortress.json +++ b/config/factions/fortress.json @@ -177,8 +177,8 @@ "special3": { "type" : "attackGarrisonBonus", "requires" : [ "special2" ] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ - { "type": "PRIMARY_SKILL", "subtype": "primSkill.attack", "val": 10 }, - { "type": "PRIMARY_SKILL", "subtype": "primSkill.defence", "val": 10 } + { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 10 }, + { "type": "PRIMARY_SKILL", "subtype": "primarySkill.defence", "val": 10 } ] }, diff --git a/config/factions/stronghold.json b/config/factions/stronghold.json index eed97bb94..5e8afda84 100644 --- a/config/factions/stronghold.json +++ b/config/factions/stronghold.json @@ -173,7 +173,7 @@ "special3": { "type" : "ballistaYard", "requires" : [ "blacksmith" ] }, "special4": { "type" : "attackVisitingBonus", "requires" : [ "fort" ] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, - "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.attack", "val": 20 } ] }, + "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 20 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/factions/tower.json b/config/factions/tower.json index 572a9e156..488759b91 100644 --- a/config/factions/tower.json +++ b/config/factions/tower.json @@ -175,7 +175,7 @@ "special2": { "type" : "lookoutTower", "height" : "high", "requires" : [ "fort" ] }, "special3": { "type" : "library", "requires" : [ "mageGuild1" ] }, "special4": { "type" : "knowledgeVisitingBonus", "requires" : [ "mageGuild1" ] }, - "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.knowledge", "val": 15 } ] }, + "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/gameConfig.json b/config/gameConfig.json index c6ed7d8d8..5b6e17b5c 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -42,20 +42,29 @@ "config/heroes/stronghold.json", "config/heroes/fortress.json", "config/heroes/conflux.json", - "config/heroes/special.json" + "config/heroes/special.json", + "config/heroes/portraits.json" ], "objects" : [ - "config/objects/generic.json", - "config/objects/moddables.json", + "config/objects/cartographer.json", + "config/objects/coverOfDarkness.json", "config/objects/creatureBanks.json", "config/objects/dwellings.json", - "config/objects/rewardableOncePerWeek.json", - "config/objects/rewardablePickable.json", - "config/objects/rewardableOnceVisitable.json", + "config/objects/generic.json", + "config/objects/magicSpring.json", + "config/objects/magicWell.json", + "config/objects/moddables.json", + "config/objects/observatory.json", + "config/objects/rewardableBonusing.json", "config/objects/rewardableOncePerHero.json", - "config/objects/rewardableBonusing.json" + "config/objects/rewardableOncePerWeek.json", + "config/objects/rewardableOnceVisitable.json", + "config/objects/rewardablePickable.json", + "config/objects/scholar.json", + "config/objects/shrine.json", + "config/objects/witchHut.json" ], "artifacts" : @@ -218,6 +227,11 @@ "special2" : 19, // bloodObelisk "special3" : 18 // glyphsOfFear } + }, + + "portraits" : { + "catherine" : 128, // In "RoE" Catherine only has portrait + "portraitGeneralKendal" : 129 } }, "armageddonsBlade" : { @@ -228,11 +242,29 @@ "special1" : 10, // artifactMerchants "special2" : 18 // magicUniversity } + }, + + "portraits" : { + "pasis" : 128, + "thunar" : 129, + "portraitGeneralKendal" : 156, + "portraitYoungCristian" : 157, + "portraitOrdwald" : 158 } }, "shadowOfDeath" : { "supported" : true, - "iconIndex" : 2 + "iconIndex" : 2, + + "portraits" : { + "portraitGeneralKendal" : 156, + "portraitYoungCristian" : 157, + "portraitOrdwald" : 158, + "portraitFinneas" : 159, + "portraitYoungGem" : 160, + "portraitYoungSandro" : 161, + "portraitYoungYog" : 162 + } }, "jsonVCMI" : { "supported" : true, @@ -398,7 +430,7 @@ "landMovement" : { "type" : "MOVEMENT", //Basic land movement - "subtype" : 1, + "subtype" : "heroMovementLand", "val" : 1300, "valueType" : "BASE_NUMBER", "updater" : { @@ -414,7 +446,7 @@ "seaMovement" : { "type" : "MOVEMENT", //Basic sea movement - "subtype" : 0, + "subtype" : "heroMovementSea", "val" : 1500, "valueType" : "BASE_NUMBER" } diff --git a/config/heroes/castle.json b/config/heroes/castle.json index 634897819..9397c3316 100644 --- a/config/heroes/castle.json +++ b/config/heroes/castle.json @@ -13,7 +13,7 @@ "bonuses" : { "archery" : { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "updater" : "TIMES_HERO_LEVEL", "val" : 5, "valueType" : "PERCENT_TO_TARGET_TYPE", @@ -64,7 +64,7 @@ "bonuses" : { "navigation" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -183,7 +183,7 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "GENERAL_DAMAGE_PREMY", - 1, + null, { "type" : "SPELL_EFFECT", "id" : "spell.bless" @@ -268,7 +268,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/conflux.json b/config/heroes/conflux.json index 9a530a24c..1bda11cee 100755 --- a/config/heroes/conflux.json +++ b/config/heroes/conflux.json @@ -21,8 +21,8 @@ "val" : 3 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -48,16 +48,16 @@ "bonuses" : { "damage" : { "type" : "CREATURE_DAMAGE", - "subtype" : 0, + "subtype" : "creatureDamageBoth", "val" : 5 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1 } @@ -85,17 +85,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 2 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -120,7 +120,7 @@ "type" : "CREATURE_TYPE_LIMITER" } ], - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -149,8 +149,8 @@ "val" : 3 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -176,16 +176,16 @@ "bonuses" : { "damage" : { "type" : "CREATURE_DAMAGE", - "subtype" : 0, + "subtype" : "creatureDamageMin", "val" : 5 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1 } @@ -212,17 +212,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 2 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -248,7 +248,7 @@ "type" : "CREATURE_TYPE_LIMITER" } ], - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 } diff --git a/config/heroes/dungeon.json b/config/heroes/dungeon.json index 60de72575..e08959d35 100644 --- a/config/heroes/dungeon.json +++ b/config/heroes/dungeon.json @@ -88,7 +88,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -229,7 +229,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/fortress.json b/config/heroes/fortress.json index 206164ff3..e43c41ec9 100644 --- a/config/heroes/fortress.json +++ b/config/heroes/fortress.json @@ -69,7 +69,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -192,7 +192,7 @@ "bonuses" : { "navigation" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -307,7 +307,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/necropolis.json b/config/heroes/necropolis.json index f738b387e..491d377f8 100644 --- a/config/heroes/necropolis.json +++ b/config/heroes/necropolis.json @@ -214,7 +214,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/portraits.json b/config/heroes/portraits.json new file mode 100644 index 000000000..d1fa147aa --- /dev/null +++ b/config/heroes/portraits.json @@ -0,0 +1,206 @@ +{ + // additional empty heroes for correct loading of hero portraits set in map editor + "portraitGeneralKendal" : + { + "class" : "knight", + "special" : true, + "images": { + "large" : "HPL129MK", + "small" : "HPS129MK", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "pikeman", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungCristian" : + { + "class" : "knight", + "special" : true, + "images": { + "large" : "HPL002SH", + "small" : "HPS002SH", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "pikeman", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitOrdwald" : + { + "class" : "druid", + "special" : true, + "images": { + "large" : "HPL132Wl", + "small" : "HPS132Wl", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "centaur", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitFinneas" : + { + "class" : "necromancer", + "special" : true, + "images": { + "large" : "HPL133Nc", + "small" : "HPS133Nc", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "skeleton", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungGem" : + { + "class" : "druid", + "special" : true, + "images": { + "large" : "HPL134Nc", + "small" : "HPS134Nc", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "centaur", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungSandro" : + { + "class" : "necromancer", + "special" : true, + "images": { + "large" : "HPL135Wi", + "small" : "HPS135Wi", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "skeleton", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungYog" : + { + "class" : "wizard", + "special" : true, + "images": { + "large" : "HPL136Wi", + "small" : "HPS136Wi", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "gremlin", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + } +} \ No newline at end of file diff --git a/config/heroes/rampart.json b/config/heroes/rampart.json index a4bfbfc8b..1001e553b 100644 --- a/config/heroes/rampart.json +++ b/config/heroes/rampart.json @@ -13,7 +13,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -132,7 +132,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -245,7 +245,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/special.json b/config/heroes/special.json index e26c2ff58..b6b945461 100644 --- a/config/heroes/special.json +++ b/config/heroes/special.json @@ -117,17 +117,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 10 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5 } @@ -156,17 +156,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 10 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5 } @@ -197,8 +197,8 @@ "val" : 5 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -241,8 +241,8 @@ "val" : 5 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } }, "army" : @@ -313,12 +313,12 @@ }, "bonuses" : { "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 4 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 }, diff --git a/config/heroes/stronghold.json b/config/heroes/stronghold.json index 6f08653b6..13238de3d 100644 --- a/config/heroes/stronghold.json +++ b/config/heroes/stronghold.json @@ -95,7 +95,7 @@ "specialty" : { "bonuses" : { "offence" : { - "subtype" : 0, + "subtype" : "damageTypeMelee", "type" : "PERCENTAGE_DAMAGE_BOOST", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -173,7 +173,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -237,13 +237,13 @@ ], "specialty" : { "bonuses" : { - "sorcery" : { - "targetSourceType" : "SECONDARY_SKILL", - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.any", + "offence" : { + "subtype" : "damageTypeMelee", + "type" : "PERCENTAGE_DAMAGE_BOOST", "updater" : "TIMES_HERO_LEVEL", "val" : 5, - "valueType" : "PERCENT_TO_TARGET_TYPE" + "valueType" : "PERCENT_TO_TARGET_TYPE", + "targetSourceType" : "SECONDARY_SKILL" } } } @@ -262,7 +262,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/tower.json b/config/heroes/tower.json index 582f242d2..0fecfc54b 100644 --- a/config/heroes/tower.json +++ b/config/heroes/tower.json @@ -58,7 +58,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -67,7 +67,7 @@ } } }, - "torosar ": + "torosar": { "index": 36, "class" : "alchemist", @@ -191,7 +191,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json new file mode 100644 index 000000000..b0feda288 --- /dev/null +++ b/config/highscoreCreatures.json @@ -0,0 +1,122 @@ +{ + "creatures": [ + { "min" : 1, "max" : 4, "creature": "imp" }, + { "min" : 5, "max" : 8, "creature": "gremlin" }, + { "min" : 9, "max" : 12, "creature": "gnoll" }, + { "min" : 13, "max" : 16, "creature": "troglodyte" }, + { "min" : 17, "max" : 20, "creature": "familiar" }, + { "min" : 21, "max" : 24, "creature": "skeleton" }, + { "min" : 25, "max" : 28, "creature": "goblin" }, + { "min" : 29, "max" : 32, "creature": "masterGremlin" }, + { "min" : 33, "max" : 36, "creature": "hobgoblin" }, + { "min" : 37, "max" : 40, "creature": "pikeman" }, + { "min" : 41, "max" : 44, "creature": "infernalTroglodyte" }, + { "min" : 45, "max" : 48, "creature": "skeletonWarrior" }, + { "min" : 49, "max" : 52, "creature": "gnollMarauder" }, + { "min" : 53, "max" : 56, "creature": "walkingDead" }, + { "min" : 57, "max" : 60, "creature": "centaur" }, + { "min" : 61, "max" : 64, "creature": "halberdier" }, + { "min" : 65, "max" : 68, "creature": "archer" }, + { "min" : 69, "max" : 72, "creature": "lizardman" }, + { "min" : 73, "max" : 76, "creature": "zombie" }, + { "min" : 77, "max" : 80, "creature": "goblinWolfRider" }, + { "min" : 81, "max" : 84, "creature": "centaurCaptain" }, + { "min" : 85, "max" : 88, "creature": "dwarf" }, + { "min" : 89, "max" : 92, "creature": "harpy" }, + { "min" : 93, "max" : 96, "creature": "lizardWarrior" }, + { "min" : 97, "max" : 100, "creature": "gog" }, + { "min" : 101, "max" : 104, "creature": "stoneGargoyle" }, + { "min" : 105, "max" : 108, "creature": "sharpshooter" }, + { "min" : 109, "max" : 112, "creature": "orc" }, + { "min" : 113, "max" : 116, "creature": "obsidianGargoyle" }, + { "min" : 117, "max" : 120, "creature": "hobgoblinWolfRider" }, + { "min" : 121, "max" : 124, "creature": "battleDwarf" }, + { "min" : 125, "max" : 128, "creature": "woodElf" }, + { "min" : 129, "max" : 132, "creature": "harpyHag" }, + { "min" : 133, "max" : 136, "creature": "magog" }, + { "min" : 137, "max" : 140, "creature": "orcChieftain" }, + { "min" : 141, "max" : 144, "creature": "stoneGolem" }, + { "min" : 145, "max" : 148, "creature": "wight" }, + { "min" : 149, "max" : 152, "creature": "serpentFly" }, + { "min" : 153, "max" : 156, "creature": "dragonFly" }, + { "min" : 157, "max" : 160, "creature": "wraith" }, + { "min" : 161, "max" : 164, "creature": "waterElemental" }, + { "min" : 165, "max" : 168, "creature": "earthElemental" }, + { "min" : 169, "max" : 172, "creature": "grandElf" }, + { "min" : 173, "max" : 176, "creature": "beholder" }, + { "min" : 177, "max" : 180, "creature": "fireElemental" }, + { "min" : 181, "max" : 184, "creature": "griffin" }, + { "min" : 185, "max" : 187, "creature": "airElemental" }, + { "min" : 188, "max" : 190, "creature": "hellHound" }, + { "min" : 191, "max" : 193, "creature": "evilEye" }, + { "min" : 194, "max" : 196, "creature": "cerberus" }, + { "min" : 197, "max" : 199, "creature": "ironGolem" }, + { "min" : 200, "max" : 202, "creature": "ogre" }, + { "min" : 203, "max" : 205, "creature": "swordsman" }, + { "min" : 206, "max" : 208, "creature": "demon" }, + { "min" : 209, "max" : 211, "creature": "royalGriffin" }, + { "min" : 212, "max" : 214, "creature": "hornedDemon" }, + { "min" : 215, "max" : 217, "creature": "monk" }, + { "min" : 218, "max" : 220, "creature": "dendroidGuard" }, + { "min" : 221, "max" : 223, "creature": "medusa" }, + { "min" : 224, "max" : 226, "creature": "pegasus" }, + { "min" : 227, "max" : 229, "creature": "silverPegasus" }, + { "min" : 230, "max" : 232, "creature": "basilisk" }, + { "min" : 233, "max" : 235, "creature": "vampire" }, + { "min" : 236, "max" : 238, "creature": "mage" }, + { "min" : 239, "max" : 241, "creature": "medusaQueen" }, + { "min" : 242, "max" : 244, "creature": "crusader" }, + { "min" : 245, "max" : 247, "creature": "goldGolem" }, + { "min" : 248, "max" : 250, "creature": "ogreMage" }, + { "min" : 251, "max" : 253, "creature": "archMage" }, + { "min" : 254, "max" : 256, "creature": "greaterBasilisk" }, + { "min" : 257, "max" : 259, "creature": "zealot" }, + { "min" : 260, "max" : 262, "creature": "pitFiend" }, + { "min" : 263, "max" : 265, "creature": "diamondGolem" }, + { "min" : 266, "max" : 268, "creature": "vampireLord" }, + { "min" : 269, "max" : 271, "creature": "dendroidSoldier" }, + { "min" : 272, "max" : 274, "creature": "minotaur" }, + { "min" : 275, "max" : 277, "creature": "lich" }, + { "min" : 278, "max" : 280, "creature": "genie" }, + { "min" : 281, "max" : 283, "creature": "gorgon" }, + { "min" : 284, "max" : 286, "creature": "masterGenie" }, + { "min" : 287, "max" : 289, "creature": "roc" }, + { "min" : 290, "max" : 292, "creature": "mightyGorgon" }, + { "min" : 293, "max" : 295, "creature": "minotaurKing" }, + { "min" : 296, "max" : 298, "creature": "powerLich" }, + { "min" : 299, "max" : 301, "creature": "thunderbird" }, + { "min" : 302, "max" : 304, "creature": "pitLord" }, + { "min" : 305, "max" : 307, "creature": "cyclop" }, + { "min" : 308, "max" : 310, "creature": "wyvern" }, + { "min" : 311, "max" : 313, "creature": "cyclopKing" }, + { "min" : 314, "max" : 316, "creature": "wyvernMonarch" }, + { "min" : 317, "max" : 319, "creature": "manticore" }, + { "min" : 320, "max" : 322, "creature": "scorpicore" }, + { "min" : 323, "max" : 325, "creature": "efreet" }, + { "min" : 326, "max" : 328, "creature": "unicorn" }, + { "min" : 329, "max" : 331, "creature": "efreetSultan" }, + { "min" : 332, "max" : 334, "creature": "cavalier" }, + { "min" : 335, "max" : 337, "creature": "naga" }, + { "min" : 338, "max" : 340, "creature": "warUnicorn" }, + { "min" : 341, "max" : 343, "creature": "blackKnight" }, + { "min" : 344, "max" : 346, "creature": "champion" }, + { "min" : 347, "max" : 349, "creature": "dreadKnight" }, + { "min" : 350, "max" : 352, "creature": "nagaQueen" }, + { "min" : 353, "max" : 355, "creature": "behemoth" }, + { "min" : 356, "max" : 358, "creature": "boneDragon" }, + { "min" : 359, "max" : 361, "creature": "giant" }, + { "min" : 362, "max" : 364, "creature": "hydra" }, + { "min" : 365, "max" : 367, "creature": "ghostDragon" }, + { "min" : 368, "max" : 370, "creature": "redDragon" }, + { "min" : 371, "max" : 373, "creature": "greenDragon" }, + { "min" : 374, "max" : 376, "creature": "angel" }, + { "min" : 377, "max" : 379, "creature": "devil" }, + { "min" : 380, "max" : 382, "creature": "chaosHydra" }, + { "min" : 383, "max" : 385, "creature": "ancientBehemoth" }, + { "min" : 386, "max" : 388, "creature": "archDevil" }, + { "min" : 389, "max" : 391, "creature": "titan" }, + { "min" : 392, "max" : 394, "creature": "goldDragon" }, + { "min" : 395, "max" : 397, "creature": "blackDragon" }, + { "min" : 398, "max" : 500, "creature": "archangel" } + ] +} \ No newline at end of file diff --git a/config/mainmenu.json b/config/mainmenu.json index 9cd4a6923..e9e357ecf 100644 --- a/config/mainmenu.json +++ b/config/mainmenu.json @@ -2,7 +2,11 @@ //images used in game selection screen "game-select" : ["gamselb0", "gamselb1"], - "loading" : ["loadbar"], + "loading" : + { + "background" : ["loadbar"], + "x": 395, "y": 548, "size": 18, "amount": 20, "name": "loadprog" + }, //Main menu window, consists of several sub-menus aka items "window": diff --git a/config/mapOverrides.json b/config/mapOverrides.json index c275c9ee7..c3a2ddf63 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -7,7 +7,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -35,7 +35,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -47,7 +47,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -75,6 +75,46 @@ "victoryIconIndex" : 11, "victoryString" : "core.vcdesc.0" }, + "data/evil2:0" : { // A Gryphon's Heart + "defeatIconIndex" : 2, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "daysPassed", { "value" : 84 } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.254" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "transport", { "position" : [ 16, 23, 0 ], "type" : "artifact.spiritOfOppression" } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.293", + "type" : "victory" + }, + "message" : "core.genrltxt.292" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 10, + "victoryString" : "core.vcdesc.11" + }, "data/secret1:0" : { // The Grail "defeatIconIndex" : 2, "defeatString" : "core.lcdesc.3", @@ -95,7 +135,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 2 } ] + [ "haveArtifact", { "type" : "artifact.grail" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -123,7 +163,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -165,7 +205,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -177,7 +217,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 128 } ] + [ "haveArtifact", { "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -206,7 +246,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 66, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 7, 66, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -220,7 +260,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 18, 68, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 18, 68, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -234,7 +274,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 58, 12, 0 ], "type" : 34 } ] //Gelu + [ "control", { "position" : [ 58, 12, 0 ], "type" : "hero" } ] //Gelu ] ], "effect" : { @@ -247,7 +287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 33, 37, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 33, 37, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -276,7 +316,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 3, 0 ], "type" : 34 } ] // Gelu + [ "control", { "position" : [ 4, 3, 0 ], "type" : "hero" } ] // Gelu ] ], "effect" : { @@ -290,7 +330,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 12, 63, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 12, 63, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -304,7 +344,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 63, 51, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 63, 51, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -329,7 +369,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 35, 36, 0 ], "type" : 128 } ] + [ "transport", { "position" : [ 35, 36, 0 ], "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -357,7 +397,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -381,7 +421,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 53 } ] + [ "control", { "type" : "mine" } ] ], "effect" : { "messageToSend" : "core.genrltxt.291", @@ -409,7 +449,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -430,7 +470,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -457,7 +497,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -478,7 +518,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -505,7 +545,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -526,7 +566,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -554,7 +594,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -568,7 +608,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 4, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -582,7 +622,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 15, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 15, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -596,7 +636,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -646,7 +686,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 94, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 94, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -660,7 +700,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 93, 51, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 93, 51, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -674,7 +714,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 85, 64, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 85, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -688,7 +728,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [100, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [100, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -726,7 +766,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 38, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 38, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -740,7 +780,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 32, 7, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 32, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -754,7 +794,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 8, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 8, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -768,7 +808,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -798,7 +838,7 @@ "message" : "core.genrltxt.7" }, "specialVictory" : { - "condition" : [ "destroy", { "type" : 54} ], + "condition" : [ "destroy", { "type" : "monster"} ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", "type" : "victory" @@ -817,7 +857,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -838,7 +878,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -865,7 +905,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -893,7 +933,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "type" : 54} ] + [ "destroy", { "type" : "monster"} ] ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", @@ -914,7 +954,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -954,7 +994,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1004,7 +1044,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 19, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 19, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1018,7 +1058,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 17, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 17, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1028,7 +1068,7 @@ "message" : "core.genrltxt.253" }, "specialVictory" : { - "condition" : [ "haveArtifact", { "type" : 54 } ], + "condition" : [ "haveArtifact", { "type" : "artifact.amuletOfTheUndertaker" } ], "effect" : { "messageToSend" : "core.genrltxt.281", "type" : "victory" @@ -1056,7 +1096,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1070,7 +1110,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1083,7 +1123,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 9, 11, 0 ], "type" : 55 } ] + [ "transport", { "position" : [ 9, 11, 0 ], "type" : "artifact.vampiresCowl" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1112,7 +1152,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 55, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 55, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1126,7 +1166,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 53, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 53, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1139,7 +1179,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 53, 16, 0 ], "type" : 56 } ] + [ "transport", { "position" : [ 53, 16, 0 ], "type" : "artifact.deadMansBoots" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1167,7 +1207,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1179,7 +1219,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 20 } ] + [ "haveArtifact", { "type" : "artifact.skullHelmet" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1207,7 +1247,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1219,7 +1259,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 8 } ] + [ "haveArtifact", { "type" : "artifact.blackshardOfTheDeadKnight" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1247,7 +1287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1259,7 +1299,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 26 } ] + [ "haveArtifact", { "type" : "artifact.ribCage" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1287,7 +1327,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1299,7 +1339,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 14 } ] + [ "haveArtifact", { "type" : "artifact.shieldOfTheYawningDead" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1328,7 +1368,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 56, 54, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 56, 54, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1342,7 +1382,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1380,7 +1420,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1394,7 +1434,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 67, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 67, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1431,7 +1471,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1455,7 +1495,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 68, 4, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 68, 4, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1484,7 +1524,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 57, 12, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 57, 12, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1498,7 +1538,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 11, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1511,7 +1551,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 8, 29, 1 ], "type" : 34 } ] + [ "destroy", { "position" : [ 8, 29, 1 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -1540,7 +1580,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 54, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 54, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1554,7 +1594,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 13, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 13, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1592,7 +1632,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1606,7 +1646,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 36, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 36, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1619,7 +1659,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 36, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 36, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1648,7 +1688,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1662,7 +1702,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 44, 9, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 44, 9, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1706,7 +1746,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 70, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1720,7 +1760,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 70, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 70, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1764,7 +1804,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 13, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 7, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1778,7 +1818,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 15, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 15, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1792,7 +1832,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 6, 103, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 6, 103, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1806,7 +1846,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 105, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 105, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1849,7 +1889,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 14, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 14, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1863,7 +1903,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 69, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1877,7 +1917,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 38, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 38, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1891,7 +1931,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1940,7 +1980,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1964,7 +2004,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 4, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 4, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1992,7 +2032,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2004,7 +2044,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveResources", { "type" : 6, "value" : 100000 } ] + [ "haveResources", { "type" : "gold", "value" : 100000 } ] ], "effect" : { "messageToSend" : "core.genrltxt.279", @@ -2032,7 +2072,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2056,7 +2096,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 31, 26, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 31, 26, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -2084,7 +2124,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", diff --git a/config/objects/cartographer.json b/config/objects/cartographer.json new file mode 100644 index 000000000..4df883c5c --- /dev/null +++ b/config/objects/cartographer.json @@ -0,0 +1,97 @@ +{ + "cartographer" : { + "index" :13, + "handler": "configurable", + "lastReservedIndex" : 2, + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "cartographerWater" : { + "index" : 0, + "aiValue" : 5000, + "rmg" : { + "zoneLimit" : 1, + "value" : 5000, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "water" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 25, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "water" : 1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerLand" : { + "index" : 1, + "aiValue": 10000, + "rmg" : { + "zoneLimit" : 1, + "value" : 10000, + "rarity" : 2 + }, + "compatibilityIdentifiers" : [ "land" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 26, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "surface" : 1, + "water" : -1, + "rock" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerSubterranean" : { + "index" : 2, + "aiValue" : 7500, + "rmg" : { + "zoneLimit" : 1, + "value" : 7500, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "subterra" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 27, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "subterra" : 1, + "water" : -1, + "rock" : -1, + "surface" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + } + } + } +} \ No newline at end of file diff --git a/config/objects/coverOfDarkness.json b/config/objects/coverOfDarkness.json new file mode 100644 index 000000000..0cb733547 --- /dev/null +++ b/config/objects/coverOfDarkness.json @@ -0,0 +1,35 @@ +{ + "coverOfDarkness" : { + "index" :15, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "coverOfDarkness" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 31, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index 2e117356c..cfcaf2a7a 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -1055,9 +1055,8 @@ { "chance": 100, "guards": [ - { "amount": 20, "type": "goldGolem" }, + { "amount": 40, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" }, - { "amount": 20, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" } ], "combat_value": 786, diff --git a/config/objects/generic.json b/config/objects/generic.json index 49c6e01ca..b3f2e967a 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -10,7 +10,11 @@ } }, "types" : { - "prison" : { "index" : 0, "aiValue" : 5000 } + "prison" : { + "index" : 0, + "aiValue" : 5000, + "removable": true + } } }, @@ -136,6 +140,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "templates" : { "normal" : { "animation" : "ava0128.def", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } }, @@ -150,70 +155,7 @@ "types" : { "object" : { "index" : 0, - "rmg" : { - } - } - } - }, - - "redwoodObservatory" : { - "index" :58, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "templates" : - { - "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, - "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } - }, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "pillarOfFire" : { - "index" :60, - "handler" : "observatory", - "base" : { - "sounds" : { - "ambient" : ["LOOPFIRE"], - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "coverOfDarkness" : { - "index" :15, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, + "removable": true, "rmg" : { } } @@ -294,78 +236,6 @@ } } }, - "shrineOfMagicLevel1" : {//incantation - "index" :88, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 500, - "rmg" : { - "value" : 500, - "rarity" : 100 - }, - "visitText" : 127, - "spell" : { - "level" : 1 - } - } - } - }, - "shrineOfMagicLevel2" : {//gesture - "index" :89, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 2000, - "rmg" : { - "value" : 2000, - "rarity" : 100 - }, - "visitText" : 128, - "spell" : { - "level" : 2 - } - } - } - }, - "shrineOfMagicLevel3" : {//thinking - "index" :90, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 3000, - "rmg" : { - "value" : 3000, - "rarity" : 100 - }, - "visitText" : 129, - "spell" : { - "level" : 3 - } - } - } - }, "eyeOfTheMagi" : { "index" :27, "handler" : "magi", @@ -449,31 +319,12 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } } }, - "scholar" : { - "index" :81, - "handler" : "scholar", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"], - "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "value" : 1500, - "rarity" : 100 - } - } - } - }, "shipyard" : { "index" :87, "handler" : "shipyard", @@ -548,7 +399,7 @@ "templates" : { "green" : { "animation" : "avxdent.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["grass", "swamp", "dirt"] }, - "brown" : { "animation" : "avxdend0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["sand", "lava", "rough", "snow", "subterra"] }, + "brown" : { "animation" : "avxdend0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["sand", "lava", "rough", "snow", "subterra"] } }, "rmg" : { "value" : 100, @@ -587,26 +438,6 @@ } } }, - "witchHut" : { - "index" :113, - "handler" : "witch", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "zoneLimit" : 3, - "value" : 1500, - "rarity" : 80 - } - } - } - }, "questGuard" : { "index" :215, "handler" : "questGuard", @@ -620,6 +451,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "rmg" : { } } @@ -671,6 +503,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 2000, "rarity" : 150 @@ -687,6 +520,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 5000, "rarity" : 150 @@ -703,6 +537,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 10000, "rarity" : 150 @@ -719,6 +554,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 20000, "rarity" : 150 @@ -748,6 +584,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon1", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -760,6 +597,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon2", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -772,6 +610,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon3", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -784,6 +623,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon4", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -808,6 +648,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon6", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -820,6 +661,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon7", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -863,6 +705,7 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } @@ -876,6 +719,21 @@ "index" : 0, "aiValue" : 0, "rmg" : { + }, + "templates" : { + "normal" : { + "animation" : "AVXMKTT0", + "editorAnimation" : "AVXMKTT0", + "visitableFrom" : [ + "---", + "--+", + "+++" + ], + "mask" : [ + "VV", + "BV" + ] + } } } } @@ -884,6 +742,7 @@ "index" :95, "handler": "generic", "base" : { + "blockVisit": true, "sounds" : { "ambient" : ["LOOPTAV"], "visit" : ["STORE"] diff --git a/config/objects/magicSpring.json b/config/objects/magicSpring.json new file mode 100644 index 000000000..6013353c3 --- /dev/null +++ b/config/objects/magicSpring.json @@ -0,0 +1,44 @@ +{ + "magicSpring" : { + "index" : 48, + "handler": "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFOUN"], + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicSpring" : { + "index" : 0, + "aiValue" : 500, + //banned due to problems with 2 viistable offsets + //"rmg" : { + // "zoneLimit" : 1, + // "value" : 500, + // "rarity" : 50 + //}, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 76, + "onVisitedMessage" : 75, + "description" : "@core.xtrainfo.15", + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : 74, + "manaPercentage" : 200 + } + ] + } + } + } +} diff --git a/config/objects/magicWell.json b/config/objects/magicWell.json new file mode 100644 index 000000000..4f15e1aa2 --- /dev/null +++ b/config/objects/magicWell.json @@ -0,0 +1,39 @@ +{ + "magicWell" : { + "index" :49, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicWell" : { + "index" : 0, + "aiValue" : 250, + "rmg" : { + "zoneLimit" : 1, + "value" : 250, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 79, + "onVisitedMessage" : 78, + "description" : "@core.xtrainfo.25", + "visitMode" : "bonus", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 100 } ] + }, + "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], + "message" : 77, + "manaPercentage" : 100 + } + ] + }, + } + } +} diff --git a/config/objects/moddables.json b/config/objects/moddables.json index da0b7e793..b97606eb9 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -8,6 +8,7 @@ "index" :5, "handler": "artifact", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -25,6 +26,7 @@ "handler": "hero", "base" : { "aiValue" : 7500, + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VVV", "VAV"] @@ -40,6 +42,7 @@ "index" :54, "handler": "monster", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -55,6 +58,7 @@ "index" :76, "handler": "randomResource", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -85,6 +89,7 @@ "handler": "resource", "lastReservedIndex" : 6, "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -138,6 +143,7 @@ "lastReservedIndex" : 2, "base" : { "aiValue" : 0, + "removable": true, "layer" : "sail", "onboardAssaultAllowed" : true, "onboardVisitAllowed" : true, @@ -175,6 +181,7 @@ "lastReservedIndex" : 7, "base" : { "aiValue" : 0, + "removable": true, "sounds" : { "visit" : ["CAVEHEAD"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -255,23 +262,6 @@ } }, - // subtype: different revealed areas - "cartographer" : { - "index" :13, - "handler": "cartographer", - "lastReservedIndex" : 2, - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "water" : { "index" : 0, "aiValue" : 5000, "rmg" : { "zoneLimit" : 1, "value" : 5000, "rarity" : 20 } }, - "land" : { "index" : 1, "aiValue": 10000, "rmg" : { "zoneLimit" : 1, "value" : 10000, "rarity" : 20 } }, - "subterra" : { "index" : 2, "aiValue" : 7500, "rmg" : { "zoneLimit" : 1, "value" : 7500, "rarity" : 20 } } - } - }, - // subtype: resource ID "mine" : { "index" :53, diff --git a/config/objects/observatory.json b/config/objects/observatory.json new file mode 100644 index 000000000..c4999b4aa --- /dev/null +++ b/config/objects/observatory.json @@ -0,0 +1,79 @@ +{ + "redwoodObservatory" : { + "index" :58, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "redwoodObservatory" : { + "index" : 0, + "aiValue" : 750, + "templates" : + { + "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, + "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } + }, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 98, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "pillarOfFire" : { + "index" :60, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFIRE"], + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "pillarOfFire" : { + "index" : 0, + "aiValue" : 750, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 99, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index 64f1855c0..9461689ec 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -23,6 +23,7 @@ "blockedVisitable" : true, "onVisitedMessage" : 22, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -54,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 30, + "description" : "@core.xtrainfo.1", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -87,6 +89,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 50, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -119,6 +122,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 56, + "description" : "@core.xtrainfo.3", "visitMode" : "bonus", "selectMode" : "selectFirst", "resetParameters" : { @@ -172,6 +176,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 58, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -204,6 +209,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 63, + "description" : "@core.xtrainfo.22", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -261,6 +267,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 82, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -292,6 +299,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 95, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -348,7 +356,7 @@ }, "message" : 138, "movePoints" : 400, - "bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1, "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ], + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ], "changeCreatures" : { "cavalier" : "champion" } @@ -356,7 +364,7 @@ { "message" : 137, "movePoints" : 400, - "bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1, "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] } ] } @@ -383,6 +391,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 141, + "description" : "@core.xtrainfo.23", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -420,6 +429,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 111, + "description" : "@core.xtrainfo.17", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -455,6 +465,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 167, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index e6bac178a..779575307 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -55,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 40, + "description" : "@core.xtrainfo.7", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -86,6 +87,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 60, + "description" : "@core.xtrainfo.4", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -117,6 +119,7 @@ "onVisitedMessage" : 67, "onEmptyMessage" : 68, + "description" : "@core.xtrainfo.6", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -161,6 +164,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 81, + "description" : "@core.xtrainfo.8", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -192,6 +196,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 101, + "description" : "@core.xtrainfo.11", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -233,28 +238,34 @@ } ], "onVisitedMessage" : 147, + "description" : "@core.xtrainfo.18", "visitMode" : "hero", "selectMode" : "selectFirst", "canRefuse" : true, + "showScoutedPreview" : true, + "rewards" : [ { + "description" : "@core.arraytxt.202", "message" : 148, "appearChance" : { "max" : 34 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { + "description" : "@core.arraytxt.203", "message" : 149, "appearChance" : { "min" : 34, "max" : 67 }, "limiter" : { "resources" : { "gold" : 2000 } }, "resources" : { "gold" : -2000 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { + "description" : "@core.arraytxt.204", "message" : 151, "appearChance" : { "min" : 67 }, "limiter" : { "resources" : { "gems" : 10 } }, "resources" : { "gems" : -10 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, ] } @@ -282,6 +293,7 @@ "onSelectMessage" : 71, "onVisitedMessage" : 72, "onEmptyMessage" : 73, + "description" : "@core.xtrainfo.9", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -322,6 +334,7 @@ "onSelectMessage" : 158, "onVisitedMessage" : 159, "onEmptyMessage" : 160, + "description" : "@core.xtrainfo.10", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -360,6 +373,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 144, + "description" : "@core.xtrainfo.5", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index e83375b8c..7b2005848 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -1,82 +1,4 @@ { - /// These are objects that covered by concept of "configurable object" and have their entire configuration in this config - "magicWell" : { - "index" :49, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicWell" : { - "index" : 0, - "aiValue" : 250, - "rmg" : { - "zoneLimit" : 1, - "value" : 250, - "rarity" : 100 - }, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 79, - "onVisitedMessage" : 78, - "visitMode" : "bonus", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 100 } ] - }, - "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], - "message" : 77, - "manaPercentage" : 100 - } - ] - }, - } - }, - "magicSpring" : { - "index" : 48, - "handler": "configurable", - "base" : { - "sounds" : { - "ambient" : ["LOOPFOUN"], - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicSpring" : { - "index" : 0, - "aiValue" : 500, - //banned due to problems with 2 viistable offsets - //"rmg" : { - // "zoneLimit" : 1, - // "value" : 500, - // "rarity" : 50 - //}, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 76, - "onVisitedMessage" : 75, - "resetParameters" : { - "period" : 7, - "visitors" : true - }, - "visitMode" : "once", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 200 } ] - }, - "message" : 74, - "manaPercentage" : 200 - } - ] - } - } - }, "mysticalGarden" : { "index" : 55, "handler": "configurable", diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 353b2d666..5999d2cb5 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -41,6 +41,7 @@ "index" : 22, "handler": "configurable", "base" : { + "blockedVisitable" : true, "sounds" : { "visit" : ["MYSTERY"] } @@ -54,9 +55,7 @@ "rarity" : 100 }, "compatibilityIdentifiers" : [ "object" ], - "onVisitedMessage" : 38, - "blockedVisitable" : true, "visitMode" : "once", "selectMode" : "selectFirst", "rewards" : [ @@ -154,7 +153,7 @@ "onVisited" : [ { "message" : 163, - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ], "rewards" : [ @@ -162,25 +161,25 @@ "appearChance" : { "max" : 30 }, "message" : 162, "artifacts" : [ { "class" : "TREASURE" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 30, "max" : 80 }, "message" : 162, "artifacts" : [ { "class" : "MINOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 80, "max" : 95 }, "message" : 162, "artifacts" : [ { "class" : "MAJOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 95 }, "message" : 162, "artifacts" : [ { "class" : "RELIC" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ] } diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index b5756a528..764065a30 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -5,6 +5,8 @@ "index" : 12, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "ambient" : ["LOOPCAMP"], "visit" : ["EXPERNCE"], @@ -20,8 +22,6 @@ "rarity" : 500 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -48,6 +48,8 @@ "index" : 29, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["GENIE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -62,15 +64,13 @@ "rarity" : 100 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ { "message" : 51, "appearChance" : { "max" : 25 }, - "removeObject" : true, + "removeObject" : true }, { "message" : 52, @@ -106,6 +106,8 @@ "index" : 82, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -120,8 +122,6 @@ "rarity" : 500 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -157,6 +157,8 @@ "index" : 86, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["TREASURE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -171,8 +173,6 @@ "rarity" : 50 }, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -208,6 +208,7 @@ "index" : 101, "handler": "configurable", "base" : { + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -235,7 +236,7 @@ }, { "appearChance" : { "max" : 33 }, - "gainedExp" : 1500, + "heroExperience" : 1500, "removeObject" : true, }, { @@ -245,7 +246,7 @@ }, { "appearChance" : { "min" : 33, "max" : 65 }, - "gainedExp" : 1000, + "heroExperience" : 1000, "removeObject" : true, }, { @@ -255,7 +256,7 @@ }, { "appearChance" : { "min" : 65, "max" : 95 }, - "gainedExp" : 500, + "heroExperience" : 500, "removeObject" : true, }, { diff --git a/config/objects/scholar.json b/config/objects/scholar.json new file mode 100644 index 000000000..c6d1ed139 --- /dev/null +++ b/config/objects/scholar.json @@ -0,0 +1,94 @@ +{ + "scholar" : { + "index" :81, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"], + "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] + } + }, + "types" : { + "scholar" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "value" : 1500, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "unlimited", + "blockedVisitable" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + } + }, + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading + } + }, + "primarySkill" : { + "gainedStat" : { // Note: this variable name is used by engine for H3M loading + } + } + }, + "selectMode" : "selectFirst", + "rewards" : [ + { + "appearChance" : { "min" : 0, "max" : 33 }, + "message" : 115, + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "removeObject" : true + }, + { + "appearChance" : { "min" : 33, "max" : 66 }, + "message" : 115, + "limiter" : { + // Hero does not have this skill at expert + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 3 + } + } + ], + // And have either free skill slot or this skill + "anyOf" : [ + { + "canLearnSkills" : true + }, + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "secondary" : { + "@gainedSkill" : 1 + }, + "removeObject" : true + }, + { + // Always present - fallback if hero can't learn secondary / spell + "message" : 115, + "primary" : { + "@gainedStat" : 1 + }, + "removeObject" : true + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/shrine.json b/config/objects/shrine.json new file mode 100644 index 000000000..44bd2a952 --- /dev/null +++ b/config/objects/shrine.json @@ -0,0 +1,209 @@ +{ + "shrineOfMagicLevel1" : {//incantation + "index" :88, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel1" : { + "index" : 0, + "aiValue" : 500, + "rmg" : { + "value" : 500, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.19", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 1 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 127, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 127, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 127, "%s.", 131 ] // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel2" : {//gesture + "index" :89, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel2" : { + "index" : 0, + "aiValue" : 2000, + "rmg" : { + "value" : 2000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.20", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 2 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 128, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 128, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 128, "%s.", 131 ] // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel3" : {//thinking + "index" :90, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel3" : { + "index" : 0, + "aiValue" : 3000, + "rmg" : { + "value" : 3000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.21", + "showScoutedPreview" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 3 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "description" : "@core.genrltxt.355", + "message" : [ 129, "%s." ] // You learn new spell + } + ], + "onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellBook" + } + ] + }, + "message" : [ 129, "%s.", 130 ] // No Wisdom + }, + { + "message" : [ 129, "%s.", 131 ] // No spellbook + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/witchHut.json b/config/objects/witchHut.json new file mode 100644 index 000000000..ad5d036df --- /dev/null +++ b/config/objects/witchHut.json @@ -0,0 +1,65 @@ +{ + "witchHut" : { + "index" :113, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "witchHut" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "zoneLimit" : 3, + "value" : 1500, + "rarity" : 80 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "@core.xtrainfo.12", + "showScoutedPreview" : true, + + "variables" : { + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } + }, + "visitLimiter" : { + "secondary" : { + "@gainedSkill" : 1 + } + }, + "rewards" : [ + { + "limiter" : { + "canLearnSkills" : true, + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "description" : 355, + "secondary" : { + "@gainedSkill" : 1 + }, + "message" : 171 // Witch teaches you skill + } + ], + "onVisitedMessage" : 172, // You already known this skill + "onEmptyMessage" : 173 // You know too much (no free slots) + } + } + } +} \ No newline at end of file diff --git a/config/obstacles.json b/config/obstacles.json index aae6c7530..52c8de672 100644 --- a/config/obstacles.json +++ b/config/obstacles.json @@ -1,1199 +1,1199 @@ -// Defines battle obstacles. We have two vectors of them: -// * "obstacles" are usual obtacles, that are randomly placed in the battlefield. -// * "absoluteObstacles" are a little special: there can be only one such obstacle in the battlefield and its position is always the same. -// -// Their properties: -// * "allowedTerrains" vector of terrain types (TT format) where obstacle is appropriate -// * "specialBattlefields" vector of battlefield images (BI format) where obstacle is appropriate. If there is a special battlefield image, then only this list is checked. Otherwise it's ignored. -// * "blockedTiles": for absolute obstacles contains absolute coordinates. For usual obstacles contains offsets relative to the obstacle position (that is bottom left corner). If obstacle is placed in an odd row (counting from 0) and the blocked tile is in an even row, position will be shifted one tile to the left. Thanks to that ie. -16 is always top-right hex, no matter where the obstale will get placed. -// * "width" for usual obstacles it's count of tiles that must be free to the right for obstacle to be placed. For absolute obstacles, it's x offset for the graphics. -// * "height" for usual obstacles it's count of tiles that must be free to the top for obstacle to be placed. For absolute obstacles, it's y offset for the graphics. -// * "animation" is name of the graphics. It's def file for usual obstacles and bitmap for the absolute ones. - -{ - "0": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObDino1.def", - "absolute" : false - }, - "1": - { - "allowedTerrains" : ["dirt", "sand", "rough", "subterra"], - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObDino2.def", - "foreground" : true, - "absolute" : false - }, - "2": - { - "allowedTerrains" : ["dirt"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, -14, -15, -16], - "animation" : "ObDino3.def", - "absolute" : false - }, - "3": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSkel1.def", - "absolute" : false - }, - "4": - { - "allowedTerrains" : ["dirt", "rough", "subterra"], - "specialBattlefields" : ["sand_shore", "cursed_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSkel2.def", - "absolute" : false - }, - "5": - { - "allowedTerrains" : ["dirt"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3], - "animation" : "ObBDT01.def", - "absolute" : false - }, - "6": - { - "allowedTerrains" : ["dirt"], - "width" : 3, - "height" : 2, - "blockedTiles" : [-15, -16], - "animation" : "ObDRk01.def", - "absolute" : false - }, - "7": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDRk02.def", - "foreground" : true, - "absolute" : false - }, - "8": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [-16], - "animation" : "ObDRk03.def", - "absolute" : false - }, - "9": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDRk04.def", - "foreground" : true, - "absolute" : false - }, - "10": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDSh01.def", - "foreground" : true, - "absolute" : false - }, - "11": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObDTF03.def", - "foreground" : true, - "absolute" : false - }, - "12": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3], - "animation" : "ObDtS03.def", - "foreground" : true, - "absolute" : false - }, - "13": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15], - "animation" : "ObDtS04.def", - "absolute" : false - }, - "14": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [2, -15, -16], - "animation" : "ObDtS14.def", - "absolute" : false - }, - "15": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [1, -16, -33], - "animation" : "ObDtS15.def", - "absolute" : false - }, - "16": - { - "allowedTerrains" : ["sand"], - "width" : 4, - "height" : 4, - "blockedTiles" : [-15, -16, -32, -33, -48, -49], - "animation" : "ObDsM01.def", - "absolute" : false - }, - "17": - { - "allowedTerrains" : ["sand"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, -15, -16], - "animation" : "ObDsS02.def", - "absolute" : false - }, - "18": - { - "allowedTerrains" : ["sand"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObDsS17.def", - "absolute" : false - }, - "19": - { - "allowedTerrains" : ["grass", "swamp"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObGLg01.def", - "absolute" : false - }, - "20": - { - "allowedTerrains" : ["grass", "swamp"], - "specialBattlefields" : ["magic_plains"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObGRk01.def", - "foreground" : true, - "absolute" : false - }, - "21": - { - "allowedTerrains" : ["grass", "swamp"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObGSt01.def", - "absolute" : false - }, - "22": - { - "allowedTerrains" : ["grass"], - "specialBattlefields" : ["magic_plains"], - "width" : 6, - "height" : 2, - "blockedTiles" : [1, 2, 3, 4, -13, -14, -15, -16], - "animation" : "ObGrS01.def", - "absolute" : false - }, - "23": - { - "allowedTerrains" : ["grass"], - "width" : 7, - "height" : 1, - "blockedTiles" : [1, 2], - "animation" : "OBGrS02.def", - "absolute" : false - }, - "24": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS01.def", - "absolute" : false - }, - "25": - { - "allowedTerrains" : ["snow"], - "width" : 5, - "height" : 1, - "blockedTiles" : [1, 2, 3, 4], - "animation" : "ObSnS02.def", - "absolute" : false - }, - "26": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, -16, -33], - "animation" : "ObSnS03.def", - "absolute" : false - }, - "27": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS04.def", - "absolute" : false - }, - "28": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [1], - "animation" : "ObSnS05.def", - "absolute" : false - }, - "29": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2], - "animation" : "ObSnS06.def", - "foreground" : true, - "absolute" : false - }, - "30": - { - "allowedTerrains" : ["snow"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSnS07.def", - "absolute" : false - }, - "31": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS08.def", - "foreground" : true, - "absolute" : false - }, - "32": - { - "allowedTerrains" : ["snow"], - "width" : 7, - "height" : 2, - "blockedTiles" : [2, 3, 4, 5, -13, -14, -15, -16], - "animation" : "ObSnS09.def", - "absolute" : false - }, - "33": - { - "allowedTerrains" : ["snow"], - "width" : 5, - "height" : 5, - "blockedTiles" : [3, -13, -14, -15, -33, -49, -66], - "animation" : "ObSnS10.def", - "absolute" : false - }, - "34": - { - "allowedTerrains" : ["swamp"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0], - "animation" : "ObSwS01.def", - "foreground" : true, - "absolute" : false - }, - "35": - { - "allowedTerrains" : ["swamp"], - "width" : 8, - "height" : 3, - "blockedTiles" : [-10, -11, -12, -13, -14, -15, -16], - "animation" : "ObSwS02.def", - "absolute" : false - }, - "36": - { - "allowedTerrains" : ["swamp"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSwS03.def", - "foreground" : true, - "absolute" : false - }, - "37": - { - "allowedTerrains" : ["swamp"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSwS04.def", - "foreground" : true, - "absolute" : false - }, - "38": - { - "allowedTerrains" : ["swamp"], - "width" : 5, - "height" : 4, - "blockedTiles" : [-13, -14, -15, -16, -30, -31, -32, -33], - "animation" : "ObSwS11b.def", - "absolute" : false - }, - "39": - { - "allowedTerrains" : ["swamp"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-16, -17, -31, -32, -33, -34], - "animation" : "ObSwS13a.def", - "absolute" : false - }, - "40": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1, -16], - "animation" : "ObRgS01.def", - "absolute" : false - }, - "41": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-14, -15, -16, -32, -33], - "animation" : "ObRgS02.def", - "absolute" : false - }, - "42": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15, -16], - "animation" : "ObRgS03.def", - "absolute" : false - }, - "43": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-16, -32, -33], - "animation" : "ObRgS04.def", - "absolute" : false - }, - "44": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-15, -16, -32], - "animation" : "ObRgS05.def", - "absolute" : false - }, - "45": - { - "allowedTerrains" : ["subterra"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, -15, -16], - "animation" : "ObSuS01.def", - "foreground" : true, - "absolute" : false - }, - "46": - { - "allowedTerrains" : ["subterra"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSuS02.def", - "foreground" : true, - "absolute" : false - }, - "47": - { - "allowedTerrains" : ["subterra"], - "width" : 4, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], - "animation" : "ObSuS11b.def", - "foreground" : true, - "absolute" : false - }, - "48": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-14, -32, -33], - "animation" : "ObLvS01.def", - "absolute" : false - }, - "49": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, 2, -14, -15, -16], - "animation" : "ObLvS02.def", - "absolute" : false - }, - "50": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -30, -31, -32, -33], - "animation" : "ObLvS03.def", - "absolute" : false - }, - "51": - { - "allowedTerrains" : ["lava"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObLvS04.def", - "foreground" : true, - "absolute" : false - }, - "52": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 4, - "blockedTiles" : [-14, -15, -32, -33, -49, -50], - "animation" : "ObLvS09.def", - "absolute" : false - }, - "53": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -16, -30, -31], - "animation" : "ObLvS17.def", - "absolute" : false - }, - "54": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -16, -31, -32, -33], - "animation" : "ObLvS22.def", - "absolute" : false - }, - "55": - { - "allowedTerrains" : ["water"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-15, -16, -33], - "animation" : "ObBtS04.def", - "absolute" : false - }, - "56": - { - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, -15, -16], - "animation" : "ObBhS02.def", - "absolute" : false - }, - "57": - { - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObBhS03.def", - "foreground" : true, - "absolute" : false - }, - "58": - { - "specialBattlefields" : ["sand_shore"], - "width" : 5, - "height" : 2, - "blockedTiles" : [1, 2, 3, -14, -15, -16], - "animation" : "ObBhS11a.def", - "absolute" : false - }, - "59": - { - "specialBattlefields" : ["sand_shore"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, -14, -15], - "animation" : "ObBhS12b.def", - "absolute" : false - }, - "60": - { - "specialBattlefields" : ["sand_shore"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1, -16], - "animation" : "ObBhS14b.def", - "absolute" : false - }, - "61": - { - "specialBattlefields" : ["holy_ground"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObHGs00.def", - "foreground" : true, - "absolute" : false - }, - "62": - { - "specialBattlefields" : ["holy_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObHGs01.def", - "foreground" : true, - "absolute" : false - }, - "63": - { - "specialBattlefields" : ["holy_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [1], - "animation" : "ObHGs02.def", - "foreground" : true, - "absolute" : false - }, - "64": - { - "specialBattlefields" : ["holy_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObHGs03.def", - "foreground" : true, - "absolute" : false - }, - "65": - { - "specialBattlefields" : ["holy_ground"], - "width" : 4, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3], - "animation" : "ObHGs04.def", - "foreground" : true, - "absolute" : false - }, - "66": - { - "specialBattlefields" : ["evil_fog"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObEFs00.def", - "foreground" : true, - "absolute" : false - }, - "67": - { - "specialBattlefields" : ["evil_fog"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObEFs01.def", - "foreground" : true, - "absolute" : false - }, - "68": - { - "specialBattlefields" : ["evil_fog"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObEFs02.def", - "foreground" : true, - "absolute" : false - }, - "69": - { - "specialBattlefields" : ["evil_fog"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2], - "animation" : "ObEFs03.def", - "foreground" : true, - "absolute" : false - }, - "70": - { - "specialBattlefields" : ["evil_fog"], - "width" : 6, - "height" : 2, - "blockedTiles" : [1, 2, 3, -12, -13], - "animation" : "ObEFs04.def", - "foreground" : true, - "absolute" : false - }, - "71": - { - "specialBattlefields" : ["clover_field"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObCFs00.def", - "absolute" : false - }, - "72": - { - "specialBattlefields" : ["clover_field"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObCFs01.def", - "absolute" : false - }, - "73": - { - "specialBattlefields" : ["clover_field"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15, -16], - "animation" : "ObCFs02.def", - "absolute" : false - }, - "74": - { - "specialBattlefields" : ["clover_field"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, 2, -14, -15, -16], - "animation" : "ObCFs03.def", - "absolute" : false - }, - "75": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObLPs00.def", - "absolute" : false - }, - "76": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObLPs01.def", - "absolute" : false - }, - "77": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, -15, -16], - "animation" : "ObLPs02.def", - "absolute" : false - }, - "78": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 5, - "height" : 2, - "blockedTiles" : [1, 2, 3, -13, -14, -15, -16], - "animation" : "ObLPs03.def", - "absolute" : false - }, - "79": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObFFs00.def", - "foreground" : true, - "absolute" : false - }, - "80": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObFFs01.def", - "foreground" : true, - "absolute" : false - }, - "81": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2, -15], - "animation" : "ObFFs02.def", - "foreground" : true, - "absolute" : false - }, - "82": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObFFs03.def", - "foreground" : true, - "absolute" : false - }, - "83": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], - "animation" : "ObFFs04.def", - "foreground" : true, - "absolute" : false - }, - "84": - { - "specialBattlefields" : ["rocklands"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObRLs00.def", - "foreground" : true, - "absolute" : false - }, - "85": - { - "specialBattlefields" : ["rocklands"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObRLs01.def", - "foreground" : true, - "absolute" : false - }, - "86": - { - "specialBattlefields" : ["rocklands"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObRLs02.def", - "foreground" : true, - "absolute" : false - }, - "87": - { - "specialBattlefields" : ["rocklands"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObRLs03.def", - "foreground" : true, - "absolute" : false - }, - "88": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObMCs00.def", - "absolute" : false - }, - "89": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 2, - "height" : 2, - "blockedTiles" : [1, -16], - "animation" : "ObMCs01.def", - "absolute" : false - }, - "90": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, -14, -15], - "animation" : "ObMCs02.def", - "absolute" : false - }, - - "100": - { - "allowedTerrains" : ["dirt"], - "width" : 124, - "height" : 254, - "blockedTiles" : [80, 94, 95, 96, 97, 105, 106, 107, 108, 109, 110], - "animation" : "ObDtL04.pcx", - "absolute" : true - }, - "101": - { - "allowedTerrains" : ["dirt"], - "width" : 256, - "height" : 254, - "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], - "animation" : "ObDtL06.pcx", - "absolute" : true - }, - "102": - { - "allowedTerrains" : ["dirt"], - "width" : 168, - "height" : 212, - "blockedTiles" : [60, 61, 62, 63, 64, 72, 73, 74, 75, 76, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149], - "animation" : "ObDtL10.pcx", - "absolute" : true - }, - "103": - { - "allowedTerrains" : ["dirt"], - "width" : 124, - "height" : 254, - "blockedTiles" : [88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObDtL02.pcx", - "absolute" : true - }, - "104": - { - "allowedTerrains" : ["dirt"], - "width" : 146, - "height" : 254, - "blockedTiles" : [76, 77, 78, 79, 80, 89, 90, 91, 92, 93], - "animation" : "ObDtL03.pcx", - "absolute" : true - }, - "105": - { - "allowedTerrains" : ["grass"], - "width" : 173, - "height" : 221, - "blockedTiles" : [55, 56, 57, 58, 75, 76, 77, 95, 112, 113, 131], - "animation" : "ObGrL01.pcx", - "absolute" : true - }, - "106": - { - "allowedTerrains" : ["grass"], - "width" : 180, - "height" : 264, - "blockedTiles" : [81, 91, 92, 93, 94, 95, 96, 97, 98, 106, 107, 123], - "animation" : "ObGrL02.pcx", - "absolute" : true - }, - "107": - { - "allowedTerrains" : ["snow"], - "width" : 166, - "height" : 255, - "blockedTiles" : [76, 77, 78, 79, 91, 92, 93, 97, 98, 106, 107, 108], - "animation" : "ObSnL01.pcx", - "absolute" : true - }, - "108": - { - "allowedTerrains" : ["snow"], - "width" : 302, - "height" : 172, - "blockedTiles" : [41, 42, 43, 58, 75, 92, 108, 126, 143], - "animation" : "ObSnL14.pcx", - "absolute" : true - }, - "109": - { - "allowedTerrains" : ["swamp"], - "width" : 300, - "height" : 170, - "blockedTiles" : [40, 41, 58, 59, 74, 75, 92, 93, 109, 110, 111, 127, 128, 129, 130], - "animation" : "ObSwL15.pcx", - "absolute" : true - }, - "110": - { - "allowedTerrains" : ["swamp"], - "width" : 278, - "height" : 171, - "blockedTiles" : [43, 60, 61, 77, 93, 94, 95, 109, 110, 126, 127], - "animation" : "ObSwL14.pcx", - "absolute" : true - }, - "111": - { - "allowedTerrains" : ["swamp"], - "width" : 256, - "height" : 254, - "blockedTiles" : [74, 75, 76, 77, 91, 92, 93, 94, 95, 109, 110, 111, 112], - "animation" : "ObSwL22.pcx", - "absolute" : true - }, - "112": - { - "allowedTerrains" : ["lava"], - "width" : 124, - "height" : 254, - "blockedTiles" : [77, 78, 79, 80, 81, 91, 92, 93, 94, 105, 106, 107], - "animation" : "ObLvL01.pcx", - "absolute" : true - }, - "113": - { - "allowedTerrains" : ["lava"], - "width" : 256, - "height" : 128, - "blockedTiles" : [43, 60, 61, 76, 77, 93, 109, 126, 127, 142, 143], - "animation" : "OBLvL02.pcx", - "absolute" : true - }, - "114": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 186, - "height" : 212, - "blockedTiles" : [55, 72, 90, 107, 125, 126, 127, 128, 129, 130, 131, 132], - "animation" : "ObRgL01.pcx", - "absolute" : true - }, - "115": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 347, - "height" : 174, - "blockedTiles" : [41, 59, 76, 94, 111, 129, 143, 144, 145], - "animation" : "ObRgL02.pcx", - "absolute" : true - }, - "116": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 294, - "height" : 169, - "blockedTiles" : [40, 41, 42, 43, 58, 75, 93, 110, 128, 145], - "animation" : "ObRgL03.pcx", - "absolute" : true - }, - "117": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 165, - "height" : 257, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 105], - "animation" : "ObRgL04.pcx", - "absolute" : true - }, - "118": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 208, - "height" : 268, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], - "animation" : "ObRgL05.pcx", - "absolute" : true - }, - "119": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 252, - "height" : 254, - "blockedTiles" : [73, 74, 75, 76, 77, 78, 91, 92, 93, 94], - "animation" : "ObRgL06.pcx", - "absolute" : true - }, - "120": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 278, - "height" : 128, - "blockedTiles" : [23, 40, 58, 75, 93, 110, 128, 145, 163], - "animation" : "ObRgL15.pcx", - "absolute" : true - }, - "121": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 208, - "height" : 268, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], - "animation" : "ObRgL05.pcx", - "absolute" : true - }, - "122": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 168, - "height" : 212, - "blockedTiles" : [73, 74, 75, 76, 77, 78, 79, 90, 91, 92, 93, 94, 95, 96, 97, 106, 107, 108, 109, 110, 111, 112], - "animation" : "ObRgL22.pcx", - "absolute" : true - }, - "123": - { - "specialBattlefields" : ["sand_shore"], - "width" : 147, - "height" : 264, - "blockedTiles" : [72, 73, 74, 75, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObBhL02.pcx", - "absolute" : true - }, - "124": - { - "specialBattlefields" : ["sand_shore"], - "width" : 178, - "height" : 262, - "blockedTiles" : [71, 72, 73, 74, 75, 76, 77, 78, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObBhL03.pcx", - "absolute" : true - }, - "125": - { - "specialBattlefields" : ["sand_shore"], - "width" : 173, - "height" : 257, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 90, 105, 106], - "animation" : "ObBhL05.pcx", - "absolute" : true - }, - "126": - { - "specialBattlefields" : ["sand_shore"], - "width" : 241, - "height" : 272, - "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], - "animation" : "ObBhL06.pcx", - "absolute" : true - }, - "127": - { - "specialBattlefields" : ["sand_shore"], - "width" : 261, - "height" : 129, - "blockedTiles" : [27, 28, 43, 44, 60, 61, 76, 77, 93, 94, 109, 110, 126, 127, 142, 143, 159], - "animation" : "ObBhL14.pcx", - "absolute" : true - }, - "128": - { - "specialBattlefields" : ["sand_shore"], - "width" : 180, - "height" : 154, - "blockedTiles" : [22, 38, 39, 40, 44, 45, 46, 55, 56, 57, 62, 63, 123, 124, 125, 130, 131, 140, 141, 146, 147, 148], - "animation" : "ObBhL16.pcx", - "absolute" : true - }, - "129": - { - "specialBattlefields" : ["clover_field"], - "width" : 304, - "height" : 264, - "blockedTiles" : [76, 77, 92, 93, 94, 95, 109, 110, 111], - "animation" : "ObCFL00.pcx", - "absolute" : true - }, - "130": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 256, - "height" : 257, - "blockedTiles" : [76, 77, 78, 92, 93, 94, 107, 108, 109], - "animation" : "ObLPL00.pcx", - "absolute" : true - }, - "131": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 257, - "height" : 255, - "blockedTiles" : [76, 77, 91, 92, 93, 94, 95, 108, 109, 110, 111], - "animation" : "ObFFL00.pcx", - "absolute" : true - }, - "132": - { - "specialBattlefields" : ["rocklands"], - "width" : 277, - "height" : 218, - "blockedTiles" : [60, 61, 75, 76, 77, 91, 92, 93, 94, 95], - "animation" : "ObRLL00.pcx", - "absolute" : true - }, - "133": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 300, - "height" : 214, - "blockedTiles" : [59, 60, 74, 75, 76, 93, 94, 95, 111, 112], - "animation" : "ObMCL00.pcx", - "absolute" : true - } -} +// Defines battle obstacles. We have two vectors of them: +// * "obstacles" are usual obtacles, that are randomly placed in the battlefield. +// * "absoluteObstacles" are a little special: there can be only one such obstacle in the battlefield and its position is always the same. +// +// Their properties: +// * "allowedTerrains" vector of terrain types (TT format) where obstacle is appropriate +// * "specialBattlefields" vector of battlefield images (BI format) where obstacle is appropriate. If there is a special battlefield image, then only this list is checked. Otherwise it's ignored. +// * "blockedTiles": for absolute obstacles contains absolute coordinates. For usual obstacles contains offsets relative to the obstacle position (that is bottom left corner). If obstacle is placed in an odd row (counting from 0) and the blocked tile is in an even row, position will be shifted one tile to the left. Thanks to that ie. -16 is always top-right hex, no matter where the obstale will get placed. +// * "width" for usual obstacles it's count of tiles that must be free to the right for obstacle to be placed. For absolute obstacles, it's x offset for the graphics. +// * "height" for usual obstacles it's count of tiles that must be free to the top for obstacle to be placed. For absolute obstacles, it's y offset for the graphics. +// * "animation" is name of the graphics. It's def file for usual obstacles and bitmap for the absolute ones. + +{ + "0": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObDino1.def", + "absolute" : false + }, + "1": + { + "allowedTerrains" : ["dirt", "sand", "rough", "subterra"], + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObDino2.def", + "foreground" : true, + "absolute" : false + }, + "2": + { + "allowedTerrains" : ["dirt"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, -14, -15, -16], + "animation" : "ObDino3.def", + "absolute" : false + }, + "3": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSkel1.def", + "absolute" : false + }, + "4": + { + "allowedTerrains" : ["dirt", "rough", "subterra"], + "specialBattlefields" : ["sand_shore", "cursed_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSkel2.def", + "absolute" : false + }, + "5": + { + "allowedTerrains" : ["dirt"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3], + "animation" : "ObBDT01.def", + "absolute" : false + }, + "6": + { + "allowedTerrains" : ["dirt"], + "width" : 3, + "height" : 2, + "blockedTiles" : [-15, -16], + "animation" : "ObDRk01.def", + "absolute" : false + }, + "7": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDRk02.def", + "foreground" : true, + "absolute" : false + }, + "8": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [-16], + "animation" : "ObDRk03.def", + "absolute" : false + }, + "9": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDRk04.def", + "foreground" : true, + "absolute" : false + }, + "10": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDSh01.def", + "foreground" : true, + "absolute" : false + }, + "11": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObDTF03.def", + "foreground" : true, + "absolute" : false + }, + "12": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3], + "animation" : "ObDtS03.def", + "foreground" : true, + "absolute" : false + }, + "13": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15], + "animation" : "ObDtS04.def", + "absolute" : false + }, + "14": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [2, -15, -16], + "animation" : "ObDtS14.def", + "absolute" : false + }, + "15": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [1, -16, -33], + "animation" : "ObDtS15.def", + "absolute" : false + }, + "16": + { + "allowedTerrains" : ["sand"], + "width" : 4, + "height" : 4, + "blockedTiles" : [-15, -16, -32, -33, -48, -49], + "animation" : "ObDsM01.def", + "absolute" : false + }, + "17": + { + "allowedTerrains" : ["sand"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, -15, -16], + "animation" : "ObDsS02.def", + "absolute" : false + }, + "18": + { + "allowedTerrains" : ["sand"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObDsS17.def", + "absolute" : false + }, + "19": + { + "allowedTerrains" : ["grass", "swamp"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObGLg01.def", + "absolute" : false + }, + "20": + { + "allowedTerrains" : ["grass", "swamp"], + "specialBattlefields" : ["magic_plains"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObGRk01.def", + "foreground" : true, + "absolute" : false + }, + "21": + { + "allowedTerrains" : ["grass", "swamp"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObGSt01.def", + "absolute" : false + }, + "22": + { + "allowedTerrains" : ["grass"], + "specialBattlefields" : ["magic_plains"], + "width" : 6, + "height" : 2, + "blockedTiles" : [1, 2, 3, 4, -13, -14, -15, -16], + "animation" : "ObGrS01.def", + "absolute" : false + }, + "23": + { + "allowedTerrains" : ["grass"], + "width" : 7, + "height" : 1, + "blockedTiles" : [1, 2], + "animation" : "OBGrS02.def", + "absolute" : false + }, + "24": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS01.def", + "absolute" : false + }, + "25": + { + "allowedTerrains" : ["snow"], + "width" : 5, + "height" : 1, + "blockedTiles" : [1, 2, 3, 4], + "animation" : "ObSnS02.def", + "absolute" : false + }, + "26": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, -16, -33], + "animation" : "ObSnS03.def", + "absolute" : false + }, + "27": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS04.def", + "absolute" : false + }, + "28": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [1], + "animation" : "ObSnS05.def", + "absolute" : false + }, + "29": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2], + "animation" : "ObSnS06.def", + "foreground" : true, + "absolute" : false + }, + "30": + { + "allowedTerrains" : ["snow"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSnS07.def", + "absolute" : false + }, + "31": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS08.def", + "foreground" : true, + "absolute" : false + }, + "32": + { + "allowedTerrains" : ["snow"], + "width" : 7, + "height" : 2, + "blockedTiles" : [2, 3, 4, 5, -13, -14, -15, -16], + "animation" : "ObSnS09.def", + "absolute" : false + }, + "33": + { + "allowedTerrains" : ["snow"], + "width" : 5, + "height" : 5, + "blockedTiles" : [3, -13, -14, -15, -33, -49, -66], + "animation" : "ObSnS10.def", + "absolute" : false + }, + "34": + { + "allowedTerrains" : ["swamp"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0], + "animation" : "ObSwS01.def", + "foreground" : true, + "absolute" : false + }, + "35": + { + "allowedTerrains" : ["swamp"], + "width" : 8, + "height" : 3, + "blockedTiles" : [-10, -11, -12, -13, -14, -15, -16], + "animation" : "ObSwS02.def", + "absolute" : false + }, + "36": + { + "allowedTerrains" : ["swamp"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSwS03.def", + "foreground" : true, + "absolute" : false + }, + "37": + { + "allowedTerrains" : ["swamp"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSwS04.def", + "foreground" : true, + "absolute" : false + }, + "38": + { + "allowedTerrains" : ["swamp"], + "width" : 5, + "height" : 4, + "blockedTiles" : [-13, -14, -15, -16, -30, -31, -32, -33], + "animation" : "ObSwS11b.def", + "absolute" : false + }, + "39": + { + "allowedTerrains" : ["swamp"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-16, -17, -31, -32, -33, -34], + "animation" : "ObSwS13a.def", + "absolute" : false + }, + "40": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1, -16], + "animation" : "ObRgS01.def", + "absolute" : false + }, + "41": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-14, -15, -16, -32, -33], + "animation" : "ObRgS02.def", + "absolute" : false + }, + "42": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15, -16], + "animation" : "ObRgS03.def", + "absolute" : false + }, + "43": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-16, -32, -33], + "animation" : "ObRgS04.def", + "absolute" : false + }, + "44": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-15, -16, -32], + "animation" : "ObRgS05.def", + "absolute" : false + }, + "45": + { + "allowedTerrains" : ["subterra"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, -15, -16], + "animation" : "ObSuS01.def", + "foreground" : true, + "absolute" : false + }, + "46": + { + "allowedTerrains" : ["subterra"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSuS02.def", + "foreground" : true, + "absolute" : false + }, + "47": + { + "allowedTerrains" : ["subterra"], + "width" : 4, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], + "animation" : "ObSuS11b.def", + "foreground" : true, + "absolute" : false + }, + "48": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-14, -32, -33], + "animation" : "ObLvS01.def", + "absolute" : false + }, + "49": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, 2, -14, -15, -16], + "animation" : "ObLvS02.def", + "absolute" : false + }, + "50": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -30, -31, -32, -33], + "animation" : "ObLvS03.def", + "absolute" : false + }, + "51": + { + "allowedTerrains" : ["lava"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObLvS04.def", + "foreground" : true, + "absolute" : false + }, + "52": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 4, + "blockedTiles" : [-14, -15, -32, -33, -49, -50], + "animation" : "ObLvS09.def", + "absolute" : false + }, + "53": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -16, -30, -31], + "animation" : "ObLvS17.def", + "absolute" : false + }, + "54": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -16, -31, -32, -33], + "animation" : "ObLvS22.def", + "absolute" : false + }, + "55": + { + "allowedTerrains" : ["water"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-15, -16, -33], + "animation" : "ObBtS04.def", + "absolute" : false + }, + "56": + { + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, -15, -16], + "animation" : "ObBhS02.def", + "absolute" : false + }, + "57": + { + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObBhS03.def", + "foreground" : true, + "absolute" : false + }, + "58": + { + "specialBattlefields" : ["sand_shore"], + "width" : 5, + "height" : 2, + "blockedTiles" : [1, 2, 3, -14, -15, -16], + "animation" : "ObBhS11a.def", + "absolute" : false + }, + "59": + { + "specialBattlefields" : ["sand_shore"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, -14, -15], + "animation" : "ObBhS12b.def", + "absolute" : false + }, + "60": + { + "specialBattlefields" : ["sand_shore"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1, -16], + "animation" : "ObBhS14b.def", + "absolute" : false + }, + "61": + { + "specialBattlefields" : ["holy_ground"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObHGs00.def", + "foreground" : true, + "absolute" : false + }, + "62": + { + "specialBattlefields" : ["holy_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObHGs01.def", + "foreground" : true, + "absolute" : false + }, + "63": + { + "specialBattlefields" : ["holy_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [1], + "animation" : "ObHGs02.def", + "foreground" : true, + "absolute" : false + }, + "64": + { + "specialBattlefields" : ["holy_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObHGs03.def", + "foreground" : true, + "absolute" : false + }, + "65": + { + "specialBattlefields" : ["holy_ground"], + "width" : 4, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3], + "animation" : "ObHGs04.def", + "foreground" : true, + "absolute" : false + }, + "66": + { + "specialBattlefields" : ["evil_fog"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObEFs00.def", + "foreground" : true, + "absolute" : false + }, + "67": + { + "specialBattlefields" : ["evil_fog"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObEFs01.def", + "foreground" : true, + "absolute" : false + }, + "68": + { + "specialBattlefields" : ["evil_fog"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObEFs02.def", + "foreground" : true, + "absolute" : false + }, + "69": + { + "specialBattlefields" : ["evil_fog"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2], + "animation" : "ObEFs03.def", + "foreground" : true, + "absolute" : false + }, + "70": + { + "specialBattlefields" : ["evil_fog"], + "width" : 6, + "height" : 2, + "blockedTiles" : [1, 2, 3, -12, -13], + "animation" : "ObEFs04.def", + "foreground" : true, + "absolute" : false + }, + "71": + { + "specialBattlefields" : ["clover_field"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObCFs00.def", + "absolute" : false + }, + "72": + { + "specialBattlefields" : ["clover_field"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObCFs01.def", + "absolute" : false + }, + "73": + { + "specialBattlefields" : ["clover_field"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15, -16], + "animation" : "ObCFs02.def", + "absolute" : false + }, + "74": + { + "specialBattlefields" : ["clover_field"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, 2, -14, -15, -16], + "animation" : "ObCFs03.def", + "absolute" : false + }, + "75": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObLPs00.def", + "absolute" : false + }, + "76": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObLPs01.def", + "absolute" : false + }, + "77": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, -15, -16], + "animation" : "ObLPs02.def", + "absolute" : false + }, + "78": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 5, + "height" : 2, + "blockedTiles" : [1, 2, 3, -13, -14, -15, -16], + "animation" : "ObLPs03.def", + "absolute" : false + }, + "79": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObFFs00.def", + "foreground" : true, + "absolute" : false + }, + "80": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObFFs01.def", + "foreground" : true, + "absolute" : false + }, + "81": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2, -15], + "animation" : "ObFFs02.def", + "foreground" : true, + "absolute" : false + }, + "82": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObFFs03.def", + "foreground" : true, + "absolute" : false + }, + "83": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], + "animation" : "ObFFs04.def", + "foreground" : true, + "absolute" : false + }, + "84": + { + "specialBattlefields" : ["rocklands"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObRLs00.def", + "foreground" : true, + "absolute" : false + }, + "85": + { + "specialBattlefields" : ["rocklands"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObRLs01.def", + "foreground" : true, + "absolute" : false + }, + "86": + { + "specialBattlefields" : ["rocklands"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObRLs02.def", + "foreground" : true, + "absolute" : false + }, + "87": + { + "specialBattlefields" : ["rocklands"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObRLs03.def", + "foreground" : true, + "absolute" : false + }, + "88": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObMCs00.def", + "absolute" : false + }, + "89": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 2, + "height" : 2, + "blockedTiles" : [1, -16], + "animation" : "ObMCs01.def", + "absolute" : false + }, + "90": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, -14, -15], + "animation" : "ObMCs02.def", + "absolute" : false + }, + + "100": + { + "allowedTerrains" : ["dirt"], + "width" : 124, + "height" : 254, + "blockedTiles" : [80, 94, 95, 96, 97, 105, 106, 107, 108, 109, 110], + "animation" : "ObDtL04.pcx", + "absolute" : true + }, + "101": + { + "allowedTerrains" : ["dirt"], + "width" : 256, + "height" : 254, + "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], + "animation" : "ObDtL06.pcx", + "absolute" : true + }, + "102": + { + "allowedTerrains" : ["dirt"], + "width" : 168, + "height" : 212, + "blockedTiles" : [60, 61, 62, 63, 64, 72, 73, 74, 75, 76, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149], + "animation" : "ObDtL10.pcx", + "absolute" : true + }, + "103": + { + "allowedTerrains" : ["dirt"], + "width" : 124, + "height" : 254, + "blockedTiles" : [88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObDtL02.pcx", + "absolute" : true + }, + "104": + { + "allowedTerrains" : ["dirt"], + "width" : 146, + "height" : 254, + "blockedTiles" : [76, 77, 78, 79, 80, 89, 90, 91, 92, 93], + "animation" : "ObDtL03.pcx", + "absolute" : true + }, + "105": + { + "allowedTerrains" : ["grass"], + "width" : 173, + "height" : 221, + "blockedTiles" : [55, 56, 57, 58, 75, 76, 77, 95, 112, 113, 131], + "animation" : "ObGrL01.pcx", + "absolute" : true + }, + "106": + { + "allowedTerrains" : ["grass"], + "width" : 180, + "height" : 264, + "blockedTiles" : [81, 91, 92, 93, 94, 95, 96, 97, 98, 106, 107, 123], + "animation" : "ObGrL02.pcx", + "absolute" : true + }, + "107": + { + "allowedTerrains" : ["snow"], + "width" : 166, + "height" : 255, + "blockedTiles" : [76, 77, 78, 79, 91, 92, 93, 97, 98, 106, 107, 108], + "animation" : "ObSnL01.pcx", + "absolute" : true + }, + "108": + { + "allowedTerrains" : ["snow"], + "width" : 302, + "height" : 172, + "blockedTiles" : [41, 42, 43, 58, 75, 92, 108, 126, 143], + "animation" : "ObSnL14.pcx", + "absolute" : true + }, + "109": + { + "allowedTerrains" : ["swamp"], + "width" : 300, + "height" : 170, + "blockedTiles" : [40, 41, 58, 59, 74, 75, 92, 93, 109, 110, 111, 127, 128, 129, 130], + "animation" : "ObSwL15.pcx", + "absolute" : true + }, + "110": + { + "allowedTerrains" : ["swamp"], + "width" : 278, + "height" : 171, + "blockedTiles" : [43, 60, 61, 77, 93, 94, 95, 109, 110, 126, 127], + "animation" : "ObSwL14.pcx", + "absolute" : true + }, + "111": + { + "allowedTerrains" : ["swamp"], + "width" : 256, + "height" : 254, + "blockedTiles" : [74, 75, 76, 77, 91, 92, 93, 94, 95, 109, 110, 111, 112], + "animation" : "ObSwL22.pcx", + "absolute" : true + }, + "112": + { + "allowedTerrains" : ["lava"], + "width" : 124, + "height" : 254, + "blockedTiles" : [77, 78, 79, 80, 81, 91, 92, 93, 94, 105, 106, 107], + "animation" : "ObLvL01.pcx", + "absolute" : true + }, + "113": + { + "allowedTerrains" : ["lava"], + "width" : 256, + "height" : 128, + "blockedTiles" : [43, 60, 61, 76, 77, 93, 109, 126, 127, 142, 143], + "animation" : "OBLvL02.pcx", + "absolute" : true + }, + "114": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 186, + "height" : 212, + "blockedTiles" : [55, 72, 90, 107, 125, 126, 127, 128, 129, 130, 131, 132], + "animation" : "ObRgL01.pcx", + "absolute" : true + }, + "115": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 347, + "height" : 174, + "blockedTiles" : [41, 59, 76, 94, 111, 129, 143, 144, 145], + "animation" : "ObRgL02.pcx", + "absolute" : true + }, + "116": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 294, + "height" : 169, + "blockedTiles" : [40, 41, 42, 43, 58, 75, 93, 110, 128, 145], + "animation" : "ObRgL03.pcx", + "absolute" : true + }, + "117": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 165, + "height" : 257, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 105], + "animation" : "ObRgL04.pcx", + "absolute" : true + }, + "118": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 208, + "height" : 268, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], + "animation" : "ObRgL05.pcx", + "absolute" : true + }, + "119": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 252, + "height" : 254, + "blockedTiles" : [73, 74, 75, 76, 77, 78, 91, 92, 93, 94], + "animation" : "ObRgL06.pcx", + "absolute" : true + }, + "120": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 278, + "height" : 128, + "blockedTiles" : [23, 40, 58, 75, 93, 110, 128, 145, 163], + "animation" : "ObRgL15.pcx", + "absolute" : true + }, + "121": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 208, + "height" : 268, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], + "animation" : "ObRgL05.pcx", + "absolute" : true + }, + "122": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 168, + "height" : 212, + "blockedTiles" : [73, 74, 75, 76, 77, 78, 79, 90, 91, 92, 93, 94, 95, 96, 97, 106, 107, 108, 109, 110, 111, 112], + "animation" : "ObRgL22.pcx", + "absolute" : true + }, + "123": + { + "specialBattlefields" : ["sand_shore"], + "width" : 147, + "height" : 264, + "blockedTiles" : [72, 73, 74, 75, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObBhL02.pcx", + "absolute" : true + }, + "124": + { + "specialBattlefields" : ["sand_shore"], + "width" : 178, + "height" : 262, + "blockedTiles" : [71, 72, 73, 74, 75, 76, 77, 78, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObBhL03.pcx", + "absolute" : true + }, + "125": + { + "specialBattlefields" : ["sand_shore"], + "width" : 173, + "height" : 257, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 90, 105, 106], + "animation" : "ObBhL05.pcx", + "absolute" : true + }, + "126": + { + "specialBattlefields" : ["sand_shore"], + "width" : 241, + "height" : 272, + "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], + "animation" : "ObBhL06.pcx", + "absolute" : true + }, + "127": + { + "specialBattlefields" : ["sand_shore"], + "width" : 261, + "height" : 129, + "blockedTiles" : [27, 28, 43, 44, 60, 61, 76, 77, 93, 94, 109, 110, 126, 127, 142, 143, 159], + "animation" : "ObBhL14.pcx", + "absolute" : true + }, + "128": + { + "specialBattlefields" : ["sand_shore"], + "width" : 180, + "height" : 154, + "blockedTiles" : [22, 38, 39, 40, 44, 45, 46, 55, 56, 57, 62, 63, 123, 124, 125, 130, 131, 140, 141, 146, 147, 148], + "animation" : "ObBhL16.pcx", + "absolute" : true + }, + "129": + { + "specialBattlefields" : ["clover_field"], + "width" : 304, + "height" : 264, + "blockedTiles" : [76, 77, 92, 93, 94, 95, 109, 110, 111], + "animation" : "ObCFL00.pcx", + "absolute" : true + }, + "130": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 256, + "height" : 257, + "blockedTiles" : [76, 77, 78, 92, 93, 94, 107, 108, 109], + "animation" : "ObLPL00.pcx", + "absolute" : true + }, + "131": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 257, + "height" : 255, + "blockedTiles" : [76, 77, 91, 92, 93, 94, 95, 108, 109, 110, 111], + "animation" : "ObFFL00.pcx", + "absolute" : true + }, + "132": + { + "specialBattlefields" : ["rocklands"], + "width" : 277, + "height" : 218, + "blockedTiles" : [60, 61, 75, 76, 77, 91, 92, 93, 94, 95], + "animation" : "ObRLL00.pcx", + "absolute" : true + }, + "133": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 300, + "height" : 214, + "blockedTiles" : [59, 60, 74, 75, 76, 93, 94, 95, 111, 112], + "animation" : "ObMCL00.pcx", + "absolute" : true + } +} diff --git a/config/schemas/battlefield.json b/config/schemas/battlefield.json index 4fa7cf1f5..3135c2fb4 100644 --- a/config/schemas/battlefield.json +++ b/config/schemas/battlefield.json @@ -8,7 +8,7 @@ "properties" : { "name" : { "type" : "string", - "description" : "Name of the battleground" + "description" : "Human-readable name of the battlefield" }, "isSpecial" : { "type" : "boolean", @@ -16,17 +16,17 @@ }, "bonuses": { "type":"array", - "description": "Bonuses provided by this battleground using bonus system", + "description": "List of bonuses that will affect all battles on this battlefield", "items": { "$ref" : "bonus.json" } }, "graphics" : { "type" : "string", "format" : "imageFile", - "description" : "BMP battleground resource" + "description" : "Background image for this battlefield" }, "impassableHexes" : { "type" : "array", - "description" : "Battle hexes always impassable for this type of battlefield (ship to ship for instance)", + "description" : "List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles)", "items" : { "type" : "number" } diff --git a/config/schemas/bonus.json b/config/schemas/bonus.json index e578243e0..959348661 100644 --- a/config/schemas/bonus.json +++ b/config/schemas/bonus.json @@ -44,10 +44,7 @@ "description" : "type" }, "subtype" : { - "anyOf" : [ - { "type" : "string" }, - { "type" : "number" } - ], + "type" : "string", "description" : "subtype" }, "sourceID" : { diff --git a/config/schemas/creature.json b/config/schemas/creature.json index 58a09ca12..d5bc82167 100644 --- a/config/schemas/creature.json +++ b/config/schemas/creature.json @@ -250,10 +250,6 @@ "timeBetweenFidgets" : { "type" : "number", "description" : "How often creature will play idling animation" - }, - "troopCountLocationOffset" : { - "type" : "number", - "description" : "Position of troop count label?" } } }, diff --git a/config/schemas/hero.json b/config/schemas/hero.json index 3b434ae07..419836386 100644 --- a/config/schemas/hero.json +++ b/config/schemas/hero.json @@ -114,16 +114,17 @@ "properties" : { "base" : { "type" : "object", - "description" : "Will be merged with all bonuses." + "additionalProperties" : true, + "description" : "Section that will be added into every bonus instance, for use in specialties with multiple similar bonuses." }, "bonuses" : { "type" : "object", - "description" : "Set of bonuses", + "description" : "List of bonuses added by this specialty. See bonus format for more details", "additionalProperties" : { "$ref" : "bonus.json" } }, "creature" : { "type" : "string", - "description" : "Name of base creature to grant standard specialty to." + "description" : "Shortcut for defining creature specialty, using standard H3 rules." } } }, diff --git a/config/schemas/heroClass.json b/config/schemas/heroClass.json index c090a1816..9efdddaca 100644 --- a/config/schemas/heroClass.json +++ b/config/schemas/heroClass.json @@ -54,10 +54,15 @@ } }, "mapObject" : { + // TODO: this entry should be merged with corresponding base entry in hero object type and validated as objectType + // "$ref" : "objectType.json", + "type" : "object", "properties" : { "filters" : { "type" : "object", - "additionalProperties" : { "type" : "array" } + "additionalProperties" : { + "type" : "array" + } } } }, diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 2c1fc8501..36bc7baa8 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -55,13 +55,17 @@ }, "modType" : { "type" : "string", - "enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Artifacts", "AI" ], + "enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Artifacts", "AI" ], "description" : "Type of mod, e.g. Town, Artifacts, Graphical." }, "author" : { "type" : "string", "description" : "Author of the mod. Can be nickname, real name or name of team" }, + "downloadSize": { + "type" : "number", + "description" : "Approximate size of mod, compressed by zip algorithm, in Mb" + }, "contact" : { "type" : "string", "description" : "Home page of mod or link to forum thread" diff --git a/config/schemas/objectType.json b/config/schemas/objectType.json index 5a955fa78..272e5d266 100644 --- a/config/schemas/objectType.json +++ b/config/schemas/objectType.json @@ -13,8 +13,27 @@ "type" : "number" }, "base" : { + "additionalProperties" : true, // Not validated on its own - instead data copied to main object and validated as part of it "type" : "object" }, + "rmg" : { + "additionalProperties" : false, + "type" : "object", + "properties" : { + "value" : { + "type" : "number" + }, + "mapLimit" : { + "type" : "number" + }, + "zoneLimit" : { + "type" : "number" + }, + "rarity" : { + "type" : "number" + } + } + }, "templates" : { "type" : "object", "additionalProperties" : { diff --git a/config/schemas/obstacle.json b/config/schemas/obstacle.json index 780f224f9..bbda0e589 100644 --- a/config/schemas/obstacle.json +++ b/config/schemas/obstacle.json @@ -16,12 +16,12 @@ "properties" : { "allowedTerrains" : { "type" : "array", - "description" : "Obstacles can be place on specified terrains only", + "description" : "List of terrains on which this obstacle can be used", "items" : { "type" : "string" } }, "specialBattlefields" : { "type" : "array", - "description" : "Obstacles can be placed on specified specified battlefields", + "description" : "List of special battlefields on which this obstacle can be used", "items" : { "type" : "string" } }, "absolute" : { diff --git a/config/schemas/river.json b/config/schemas/river.json index 38e0d780e..db88affaf 100644 --- a/config/schemas/river.json +++ b/config/schemas/river.json @@ -9,7 +9,7 @@ "shortIdentifier" : { "type" : "string", - "description" : "Two-letters unique indentifier for this road. Used in map format" + "description" : "Two-letters unique indentifier for this river. Used in map format" }, "text" : { @@ -29,7 +29,7 @@ }, "paletteAnimation" : { "type" : "array", - "description" : "If defined, terrain will be animated using palette color cycling effect", + "description" : "If defined, river will be animated using palette color cycling effect", "items" : { "type" : "object", diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 3dbbaf0b1..c65cf374d 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", "gameTweaks" ], + "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ], "definitions" : { "logLevelEnum" : { "type" : "string", @@ -118,7 +118,7 @@ }, "useSavePrefix" : { "type": "boolean", - "default": false + "default": true }, "savePrefix" : { "type": "string", @@ -149,7 +149,8 @@ "driver", "displayIndex", "showfps", - "targetfps" + "targetfps", + "vsync" ], "properties" : { "resolution" : { @@ -207,6 +208,10 @@ "targetfps" : { "type" : "number", "default" : 60 + }, + "vsync" : { + "type" : "boolean", + "default" : true } } }, @@ -230,7 +235,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -250,7 +255,7 @@ }, "quickCombat" : { "type" : "boolean", - "default" : true + "default" : false }, "objectAnimation" : { "type" : "boolean", @@ -274,7 +279,15 @@ "leftButtonDrag" : { "type" : "boolean", "default" : false - } + }, + "smoothDragging" : { + "type" : "boolean", + "default" : true + }, + "backgroundDimLevel" : { + "type" : "number", + "default" : 128 + }, } }, "battle" : { @@ -487,7 +500,7 @@ }, "defaultRepositoryURL" : { "type" : "string", - "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.3.json", + "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.4.json", }, "extraRepositoryEnabled" : { "type" : "boolean", @@ -539,6 +552,18 @@ } } }, + "lobby" : { + "type" : "object", + "additionalProperties" : false, + "default" : {}, + "required" : [ "mapPreview" ], + "properties" : { + "mapPreview" : { + "type" : "boolean", + "default" : true + } + } + }, "gameTweaks" : { "type" : "object", "default" : {}, @@ -551,7 +576,8 @@ "compactTownCreatureInfo", "infoBarPick", "skipBattleIntroMusic", - "infoBarCreatureManagement" + "infoBarCreatureManagement", + "enableLargeSpellbook" ], "properties" : { "showGrid" : { @@ -585,6 +611,10 @@ "infoBarCreatureManagement": { "type" : "boolean", "default" : false + }, + "enableLargeSpellbook" : { + "type": "boolean", + "default": false } } } diff --git a/config/schemas/skill.json b/config/schemas/skill.json index 12a77f4af..162f84de6 100644 --- a/config/schemas/skill.json +++ b/config/schemas/skill.json @@ -1,99 +1,98 @@ -{ - "type" : "object", - "$schema" : "http://json-schema.org/draft-04/schema", - "title" : "VCMI skill format", - "description" : "Format used to replace bonuses provided by secondary skills in VCMI", - "definitions" : { - "skillBonus" : { - "type" : "object", - "description" : "Set of bonuses provided by skill at given level", - "required" : ["description", "effects"], - "properties" : { - "description" : { - "type" : "string", - "description" : "localizable description" - }, - "images" : { - "type" : "object", - "description" : "Skill icons of varying size", - "properties" : { - "small" : { - "type" : "string", - "description" : "32x32 skill icon", - "format" : "imageFile" - }, - "medium" : { - "type" : "string", - "description" : "44x44 skill icon", - "format" : "imageFile" - }, - "large" : { - "type" : "string", - "description" : "82x93 skill icon", - "format" : "imageFile" - } - } - }, - "effects" : { - "type" : "object", - "additionalProperties" : { - "$ref" : "bonus.json" - } - } - } - } - }, - "required" : ["name", "basic", "advanced", "expert"], - "properties" : { - "name" : { - "type" : "string", - "description" : "Mandatory, localizable skill name" - }, - "index" : { - "type" : "number", - "description" : "Internal, numeric id of skill, required for existing skills" - }, - "obligatoryMajor" : { - "type" : "boolean", - "description" : "This skill is major obligatory (like H3 Wisdom)" - }, - "obligatoryMinor" : { - "type" : "boolean", - "description" : "This skill is minor obligatory (like H3 Magic school)" - }, - "gainChance" : { - "description" : "Chance for the skill to be offered on level-up (heroClass may override)", - "type" : "object", - "required" : ["might", "magic"], - "properties" : { - "might" : { - "type" : "number", - "description" : "Chance for hero classes with might affinity" - }, - "magic" : { - "type" : "number", - "description" : "Chance for hero classes with magic affinity" - } - } - ] - }, - "base" : { - "type" : "object", - "description" : "will be merged with all levels", - "additionalProperties" : true - }, - "basic" : { - "$ref" : "#/definitions/skillBonus" - }, - "advanced" : { - "$ref" : "#/definitions/skillBonus" - }, - "expert" : { - "$ref" : "#/definitions/skillBonus" - } - }, - "onlyOnWaterMap" : { - "type" : "boolean", - "description" : "It true, skill won't be available on a map without water" - } -} +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI skill format", + "description" : "Format used to replace bonuses provided by secondary skills in VCMI", + "definitions" : { + "skillBonus" : { + "type" : "object", + "description" : "Set of bonuses provided by skill at given level", + "required" : ["description", "effects"], + "properties" : { + "description" : { + "type" : "string", + "description" : "localizable description" + }, + "images" : { + "type" : "object", + "description" : "Skill icons of varying size", + "properties" : { + "small" : { + "type" : "string", + "description" : "32x32 skill icon", + "format" : "imageFile" + }, + "medium" : { + "type" : "string", + "description" : "44x44 skill icon", + "format" : "imageFile" + }, + "large" : { + "type" : "string", + "description" : "82x93 skill icon", + "format" : "imageFile" + } + } + }, + "effects" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "bonus.json" + } + } + } + } + }, + "required" : ["name", "basic", "advanced", "expert"], + "properties" : { + "name" : { + "type" : "string", + "description" : "Mandatory, localizable skill name" + }, + "index" : { + "type" : "number", + "description" : "Internal, numeric id of skill, required for existing skills" + }, + "obligatoryMajor" : { + "type" : "boolean", + "description" : "This skill is major obligatory (like H3 Wisdom)" + }, + "obligatoryMinor" : { + "type" : "boolean", + "description" : "This skill is minor obligatory (like H3 Magic school)" + }, + "gainChance" : { + "description" : "Chance for the skill to be offered on level-up (heroClass may override)", + "type" : "object", + "required" : ["might", "magic"], + "properties" : { + "might" : { + "type" : "number", + "description" : "Chance for hero classes with might affinity" + }, + "magic" : { + "type" : "number", + "description" : "Chance for hero classes with magic affinity" + } + } + }, + "base" : { + "type" : "object", + "description" : "will be merged with all levels", + "additionalProperties" : true + }, + "basic" : { + "$ref" : "#/definitions/skillBonus" + }, + "advanced" : { + "$ref" : "#/definitions/skillBonus" + }, + "expert" : { + "$ref" : "#/definitions/skillBonus" + } + }, + "onlyOnWaterMap" : { + "type" : "boolean", + "description" : "It true, skill won't be available on a map without water" + } +} diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 7de694c37..8a29cd88b 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -1,317 +1,318 @@ -{ - "type" : "object", - "$schema" : "http://json-schema.org/draft-04/schema", - "title" : "VCMI spell format", - "description" : "Format used to define new spells in VCMI", - "definitions" : { - "animationQueue" : { - "type" : "array", - "items" : { - "anyOf" :[ - { - //dummy animation, pause, Value - frame count - "type" : "number" - }, - { - //assumed verticalPosition: top - "type" : "string", - "format" : "defFile" - }, - { - "type" : "object", - "properties" : { - "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, - "defName" : {"type" : "string", "format" : "defFile"} - }, - "additionalProperties" : false - } - ] - } - }, - "animation" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "affect" : {"$ref" : "#/definitions/animationQueue"}, - "hit" : {"$ref" : "#/definitions/animationQueue"}, - "cast" : {"$ref" : "#/definitions/animationQueue"}, - "projectile" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : { - "defName" : {"type" : "string", "format" : "defFile"}, - "minimumAngle" : {"type" : "number", "minimum" : 0} - }, - "additionalProperties" : false - } - } - } - }, - "flags" : { - "type" : "object", - "additionalProperties" : { - "type" : "boolean" - } - }, - "levelInfo" : { - "type" : "object", - "required" :["range", "description", "cost", "power", "aiValue"], - - "additionalProperties" : false, - "properties" : { - "description" : { - "type" : "string", - "description" : "Localizable description. Use {xxx} for formatting" - }, - "cost" : { - "type" : "number", - "description" : "Cost in mana points" - }, - "power" : { - "type" : "number" - }, - "range" : { - "type" : "string", - "description" : "spell range description in SRSL" - }, - "aiValue" : { - "type" : "number" - }, - "effects" : { - "type" : "object", - "description" : "Timed effects (updated by prolongation)", - "additionalProperties" : { - "$ref" : "bonus.json" - } - }, - "cumulativeEffects" : { - "type" : "object", - "description" : "Timed effects (updated by unique bonus)", - "additionalProperties" : { - "$ref" : "bonus.json" - } - }, - "battleEffects" : { - "type" : "object", - "additionalProperties" : { - "type" : "object" - } - }, - "targetModifier" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "smart" : { - "type" : "boolean", - "description" : "true: friendly/hostile based on positiveness; false: all targets" - }, - "clearTarget" : - { - "type" : "boolean", - "description" : "LOCATION target only. Target hex/tile must be clear" - }, - "clearAffected" : - { - "type" : "boolean", - "description" : "LOCATION target only. All affected hexes/tile must be clear" - } - } - } - } - }, - "texts" : { - "type" : "object", - "additionalProperties" : false - } - }, - "required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"], - "additionalProperties" : false, - "properties" : { - "index" : { - "type" : "number", - "description" : "numeric id of spell required only for original spells, prohibited for new spells" - }, - "name" : { - "type" : "string", - "description" : "Localizable name" - }, - "type" : { - "type" : "string", - "enum" : ["adventure", "combat", "ability"], - "description" : "Spell type" - }, - "school" : { - "type" : "object", - "description" : "Spell schools", - "additionalProperties" : false, - "properties" : { - "air" : {"type" : "boolean"}, - "fire" : {"type" : "boolean"}, - "earth" : {"type" : "boolean"}, - "water" : {"type" : "boolean"} - } - }, - "level" : { - "type" : "number", - "description" : "Spell level", - "minimum" : 0, - "maximum" : 5 - }, - "power" : { - "type" : "number", - "description" : "Base power" - }, - "defaultGainChance" : { - "type" : "number", - "description" : "Gain chance by default for all factions" - - }, - "gainChance" : { - "type" : "object", - "description" : "Chance in % to gain for faction. NOTE: this field is merged with faction config", - "additionalProperties" : { - "type" : "number", - "minimum" : 0 - } - }, - "targetType" : { - "type" : "string", - "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", - "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] - }, - "counters" : { - "$ref" : "#/definitions/flags", - "description" : "Flags structure ids of countering spells" - }, - "flags" : { - "type" : "object", - "description" : "Various flags", - "additionalProperties" : false, - "properties" : { - "indifferent" : { - "type" : "boolean", - "description" : "Spell is indifferent for target" - }, - "negative" : { - "type" : "boolean", - "description" : "Spell is negative for target" - }, - "positive" : { - "type" : "boolean", - "description" : "Spell is positive for target" - }, - "damage" : { - "type" : "boolean", - "description" : "Spell does damage (direct or indirect)" - }, - "offensive" : { - "type" : "boolean", - "description" : "Spell does direct damage (implicitly sets damage and negative)" - }, - "rising" : { - "type" : "boolean", - "description" : "Rising spell (implicitly sets positive)" - }, - "special" : { - "type" : "boolean", - "description" : "Special spell. Can be given only by BonusType::SPELL" - }, - "nonMagical" : { - "type" : "boolean", - "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." - } - } - }, - "immunity" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, any one of these bonus grants immunity" - }, - "absoluteImmunity" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names. Any one of these bonus grants immunity, can't be negated" - }, - "limit" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, presence of all bonuses required to be affected by." - }, - "absoluteLimit" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." - }, - "targetCondition" : { - "type" : "object", - "additionalProperties" : true - }, - "animation" : {"$ref" : "#/definitions/animation"}, - "graphics" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "iconBook" : { - "type" : "string", - "description" : "Resourse path of icon for spellbook" , - "format" : "imageFile" - }, - "iconScroll" : { - "type" : "string", - "description" : "Resourse path of icon for spell scrolls", - "format" : "imageFile" - }, - "iconEffect" : { - "type" : "string", - "description" : "Resourse path of icon for spell effects during battle" , - "format" : "imageFile" - }, - "iconImmune" : { - "type" : "string", - "description" : "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)", - "format" : "imageFile" - }, - "iconScenarioBonus" : { - "type" : "string", - "description" : "Resourse path of icon for scenario bonus" , - "format" : "imageFile" - } - } - }, - "sounds" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "cast" : { - "type" : "string", - "description" : "Resourse path of cast sound" - } - } - }, - "levels" : { - "type" : "object", - "additionalProperties" : false, - "required" : ["none", "basic", "advanced", "expert"], - "properties" : { - "base" : { - "type" : "object", - "description" : "will be merged with all levels", - "additionalProperties" : true - }, - "none" : { - "$ref" : "#/definitions/levelInfo" - }, - "basic" : { - "$ref" : "#/definitions/levelInfo" - }, - "advanced" : { - "$ref" : "#/definitions/levelInfo" - }, - "expert" : { - "$ref" : "#/definitions/levelInfo" - } - } - }, - "onlyOnWaterMap" : { - "type" : "boolean", - "description" : "It true, spell won't be available on a map without water" - } - }, -} +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI spell format", + "description" : "Format used to define new spells in VCMI", + "definitions" : { + "animationQueue" : { + "type" : "array", + "items" : { + "anyOf" :[ + { + //dummy animation, pause, Value - frame count + "type" : "number" + }, + { + //assumed verticalPosition: top + "type" : "string", + "format" : "defFile" + }, + { + "type" : "object", + "properties" : { + "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, + "defName" : {"type" : "string", "format" : "defFile"}, + "effectName" : { "type" : "string" } + }, + "additionalProperties" : false + } + ] + } + }, + "animation" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "affect" : {"$ref" : "#/definitions/animationQueue"}, + "hit" : {"$ref" : "#/definitions/animationQueue"}, + "cast" : {"$ref" : "#/definitions/animationQueue"}, + "projectile" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "defName" : {"type" : "string", "format" : "defFile"}, + "minimumAngle" : {"type" : "number", "minimum" : 0} + }, + "additionalProperties" : false + } + } + } + }, + "flags" : { + "type" : "object", + "additionalProperties" : { + "type" : "boolean" + } + }, + "levelInfo" : { + "type" : "object", + "required" :["range", "description", "cost", "power", "aiValue"], + + "additionalProperties" : false, + "properties" : { + "description" : { + "type" : "string", + "description" : "Localizable description. Use {xxx} for formatting" + }, + "cost" : { + "type" : "number", + "description" : "Cost in mana points" + }, + "power" : { + "type" : "number" + }, + "range" : { + "type" : "string", + "description" : "spell range description in SRSL" + }, + "aiValue" : { + "type" : "number" + }, + "effects" : { + "type" : "object", + "description" : "Timed effects (updated by prolongation)", + "additionalProperties" : { + "$ref" : "bonus.json" + } + }, + "cumulativeEffects" : { + "type" : "object", + "description" : "Timed effects (updated by unique bonus)", + "additionalProperties" : { + "$ref" : "bonus.json" + } + }, + "battleEffects" : { + "type" : "object", + "additionalProperties" : { + "type" : "object" + } + }, + "targetModifier" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "smart" : { + "type" : "boolean", + "description" : "true: friendly/hostile based on positiveness; false: all targets" + }, + "clearTarget" : + { + "type" : "boolean", + "description" : "LOCATION target only. Target hex/tile must be clear" + }, + "clearAffected" : + { + "type" : "boolean", + "description" : "LOCATION target only. All affected hexes/tile must be clear" + } + } + } + } + }, + "texts" : { + "type" : "object", + "additionalProperties" : false + } + }, + "required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"], + "additionalProperties" : false, + "properties" : { + "index" : { + "type" : "number", + "description" : "numeric id of spell required only for original spells, prohibited for new spells" + }, + "name" : { + "type" : "string", + "description" : "Localizable name of this spell" + }, + "type" : { + "type" : "string", + "enum" : ["adventure", "combat", "ability"], + "description" : "Spell type" + }, + "school" : { + "type" : "object", + "description" : "List of spell schools this spell belongs to", + "additionalProperties" : false, + "properties" : { + "air" : {"type" : "boolean"}, + "fire" : {"type" : "boolean"}, + "earth" : {"type" : "boolean"}, + "water" : {"type" : "boolean"} + } + }, + "level" : { + "type" : "number", + "description" : "Spell level", + "minimum" : 0, + "maximum" : 5 + }, + "power" : { + "type" : "number", + "description" : "Base power of the spell" + }, + "defaultGainChance" : { + "type" : "number", + "description" : "Gain chance by default for all factions" + + }, + "gainChance" : { + "type" : "object", + "description" : "Chance for this spell to appear in Mage Guild of a specific faction", + "additionalProperties" : { + "type" : "number", + "minimum" : 0 + } + }, + "targetType" : { + "type" : "string", + "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", + "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] + }, + "counters" : { + "$ref" : "#/definitions/flags", + "description" : "Flags structure ids of countering spells" + }, + "flags" : { + "type" : "object", + "description" : "Various flags", + "additionalProperties" : false, + "properties" : { + "indifferent" : { + "type" : "boolean", + "description" : "Spell is indifferent for target" + }, + "negative" : { + "type" : "boolean", + "description" : "Spell is negative for target" + }, + "positive" : { + "type" : "boolean", + "description" : "Spell is positive for target" + }, + "damage" : { + "type" : "boolean", + "description" : "Spell does damage (direct or indirect)" + }, + "offensive" : { + "type" : "boolean", + "description" : "Spell does direct damage (implicitly sets damage and negative)" + }, + "rising" : { + "type" : "boolean", + "description" : "Rising spell (implicitly sets positive)" + }, + "special" : { + "type" : "boolean", + "description" : "Special spell. Can be given only by BonusType::SPELL" + }, + "nonMagical" : { + "type" : "boolean", + "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." + } + } + }, + "immunity" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, any one of these bonus grants immunity" + }, + "absoluteImmunity" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names. Any one of these bonus grants immunity, can't be negated" + }, + "limit" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, presence of all bonuses required to be affected by." + }, + "absoluteLimit" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." + }, + "targetCondition" : { + "type" : "object", + "additionalProperties" : true + }, + "animation" : {"$ref" : "#/definitions/animation"}, + "graphics" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "iconBook" : { + "type" : "string", + "description" : "Resourse path of icon for spellbook" , + "format" : "imageFile" + }, + "iconScroll" : { + "type" : "string", + "description" : "Resourse path of icon for spell scrolls", + "format" : "imageFile" + }, + "iconEffect" : { + "type" : "string", + "description" : "Resourse path of icon for spell effects during battle" , + "format" : "imageFile" + }, + "iconImmune" : { + "type" : "string", + "description" : "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)", + "format" : "imageFile" + }, + "iconScenarioBonus" : { + "type" : "string", + "description" : "Resourse path of icon for scenario bonus" , + "format" : "imageFile" + } + } + }, + "sounds" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "cast" : { + "type" : "string", + "description" : "Resourse path of cast sound" + } + } + }, + "levels" : { + "type" : "object", + "additionalProperties" : false, + "required" : ["none", "basic", "advanced", "expert"], + "properties" : { + "base" : { + "type" : "object", + "description" : "will be merged with all levels", + "additionalProperties" : true + }, + "none" : { + "$ref" : "#/definitions/levelInfo" + }, + "basic" : { + "$ref" : "#/definitions/levelInfo" + }, + "advanced" : { + "$ref" : "#/definitions/levelInfo" + }, + "expert" : { + "$ref" : "#/definitions/levelInfo" + } + } + }, + "onlyOnWaterMap" : { + "type" : "boolean", + "description" : "If true, spell won't be available on a map without water" + } + } +} diff --git a/config/schemas/template.json b/config/schemas/template.json index 5e0e8934f..67cbca49f 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -8,16 +8,35 @@ "zone" : { "type" : "object", "required" : ["type", "monsters", "size"], + "additionalProperties" : false, "properties" : { - "type" : {"$ref" : "#/definitions/type"}, - "size" : {"$ref" : "#/definitions/size"}, - "playerTowns" : {"$ref" : "#/definitions/playerTowns"}, - "neuralTowns" : {"$ref" : "#/definitions/neuralTowns"}, - "townsAreSameType" : {"$ref" : "#/definitions/townsAreSameType"}, - "terrainTypes": {"$ref" : "#/definitions/terrains"}, - "bannedTerrains": {"$ref" : "#/definitions/terrains"}, - "monsters" : {"$ref" : "#/definitions/monsters"}, - "bannedMonsters" : {"$ref" : "#/definitions/monsters"}, + "type" : { + "type" : "string", + "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + }, + "size" : { "type" : "number", "minimum" : 1 }, + "owner" : {}, + "playerTowns" : {"$ref" : "#/definitions/towns"}, + "neutralTowns" : {"$ref" : "#/definitions/towns"}, + "matchTerrainToTown" : { "type" : "boolean"}, + "minesLikeZone" : { "type" : "number" }, + "terrainTypeLikeZone" : { "type" : "number" }, + "treasureLikeZone" : { "type" : "number" }, + + "terrainTypes": {"$ref" : "#/definitions/stringArray"}, + "bannedTerrains": {"$ref" : "#/definitions/stringArray"}, + + "townsAreSameType" : { "type" : "boolean"}, + "allowedMonsters" : {"$ref" : "#/definitions/stringArray"}, + "bannedMonsters" : {"$ref" : "#/definitions/stringArray"}, + "allowedTowns" : {"$ref" : "#/definitions/stringArray"}, + "bannedTowns" : {"$ref" : "#/definitions/stringArray"}, + + "monsters" : { + "type" : "string", + "enum" : ["weak", "normal", "strong", "none"] + }, + "mines" : {"$ref" : "#/definitions/mines"}, "treasure" : { "type" : "array", @@ -27,21 +46,39 @@ "min" : {"type" : "number", "minimum" : 0}, "max" : {"type" : "number", "minimum" : 0}, "density" : {"type" : "number", "minimum" : 1} - }, - "additionalProperties" : false + }, + "additionalProperties" : false } - } - } + } + } }, - "type" : { - "type" : "string", + + "towns" : { + "type" : "object", "additionalProperties" : false, - "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + "properties" : { + "towns" : { "type" : "number" }, + "castles" : { "type" : "number" }, + "townDensity" : { "type" : "number" }, + "castleDensity" : { "type" : "number" } + } }, - "size" : { - "type" : "number", - "minimum" : 1, - "additionalProperties" : false + "stringArray" : { + "type" : "array", + "items" : { "type" : "string" } + }, + "mines" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "gold" : { "type" : "number"}, + "wood" : { "type" : "number"}, + "ore" : { "type" : "number"}, + "mercury" : { "type" : "number"}, + "sulfur" : { "type" : "number"}, + "crystal" : { "type" : "number"}, + "gems" : { "type" : "number"} + } }, "connection" : { @@ -62,7 +99,6 @@ "type": { "type" : "string", - "additionalProperties" : false, "enum" : ["wide", "fictive", "repulsive"] } } @@ -70,26 +106,47 @@ "waterContent" : { "enum" : ["none", "normal", "islands"], - "additionalProperties" : false, "type" : "string" } }, + + "additionalProperties" : false, "properties" : { - "required" : ["zones", "connections"], - "additionalProperties" : false, - "description" : { + "required" : ["zones", "connections", "minSize", "maxSize", "players"], + + "players" : { + "description" : "Number of players that will be present on map (human or AI)", + "type": "string" + }, + "humans" : { + "description" : "Optional, number of AI-only players", + "type": "string" + }, + "minSize" : { + "description" : "Minimal size of the map, e.g. 'm+u' or '120x120x1", + "type": "string" + }, + "maxSize" : { + "description" : "Maximal size of the map, e.g. 'm+u' or '120x120x1", + "type": "string" + }, + "name" : { + "description" : "Optional name - useful to have several template variations with same name", "type": "string" }, "zones" : { + "description" : "List of named zones", "type" : "object", - "additionalProperties" : {"$ref" : "#/definitions/zone" } + "additionalProperties" : {"$ref" : "#/definitions/zone" } }, "connections" : { + "description" : "List of connections between zones", "type" : "array", "items" : {"$ref" : "#/definitions/connection"} }, "allowedWaterContent" : { + "description" : "Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified", "type" : "array", "items" : {"$ref" : "#/definitions/waterContent"} } diff --git a/config/schemas/terrain.json b/config/schemas/terrain.json index e9e5f2b91..bc518106b 100644 --- a/config/schemas/terrain.json +++ b/config/schemas/terrain.json @@ -22,7 +22,7 @@ "description" : "Type of this terrain. Can be land, water, subterranean or rock", "items" : { - "enum" : ["LAND", "WATER", "SUB", "ROCK", "SURFACE"], + "enum" : ["WATER", "SUB", "ROCK", "SURFACE"], "type" : "string" } }, @@ -67,7 +67,7 @@ "battleFields" : { "type" : "array", - "description" : "array of battleFields for this terrain", + "description" : "List of battleFields that can be used on this terrain", "items" : { "type" : "string" @@ -76,7 +76,7 @@ "minimapUnblocked" : { "type" : "array", - "description" : "Color of terrain on minimap without unpassable objects", + "description" : "Color of terrain on minimap without unpassable objects. RGB triplet, 0-255 range", "minItems" : 3, "maxItems" : 3, "items" : @@ -87,7 +87,7 @@ "minimapBlocked" : { "type" : "array", - "description" : "Color of terrain on minimap with unpassable objects", + "description" : "Color of terrain on minimap with unpassable objects. RGB triplet, 0-255 range", "minItems" : 3, "maxItems" : 3, "items" : @@ -104,14 +104,14 @@ "sounds" : { "type" : "object", - "description" : "list of sounds for this terrain", + "description" : "List of sounds for this terrain", "additionalProperties" : false, "properties" : { "ambient" : { "type" : "array", - "description" : "list of ambient sounds for this terrain", + "description" : "List of ambient sounds for this terrain", "items" : { "type" : "string", @@ -135,7 +135,7 @@ "prohibitTransitions" : { "type" : "array", - "description" : "array or terrain names, which is prohibited to make transition from/to", + "description" : "List or terrain names, which is prohibited to make transition from/to", "items" : { "type" : "string" @@ -149,7 +149,7 @@ "terrainViewPatterns" : { "type" : "string", - "description" : "Can be normal, dirt, water, rock" + "description" : "Represents layout of tile orientations in terrain tiles file" }, "index" : { diff --git a/config/schemas/townBuilding.json b/config/schemas/townBuilding.json index 43b1344ee..7a86acdab 100644 --- a/config/schemas/townBuilding.json +++ b/config/schemas/townBuilding.json @@ -17,7 +17,7 @@ "additionalItems" : { "description" : "Following items that contain expression elements", - "$ref" : "#/definitions/buidingRequirement" + "$ref" : "#/definitions/buildingRequirement" } } }, diff --git a/config/skills.json b/config/skills.json index 601d48d1d..71974db01 100644 --- a/config/skills.json +++ b/config/skills.json @@ -31,7 +31,7 @@ "effects" : { "main" : { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "valueType" : "BASE_NUMBER" } } @@ -57,7 +57,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "valueType" : "PERCENT_TO_BASE" } @@ -144,7 +144,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "valueType" : "PERCENT_TO_BASE" } @@ -310,12 +310,10 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "valueType" : "BASE_NUMBER" }, "val2" : { - "subtype" : -1, "type" : "LEARN_BATTLE_SPELL_LEVEL_LIMIT", "valueType" : "BASE_NUMBER" } @@ -518,7 +516,6 @@ "base" : { "effects" : { "main" : { - "subtype" : -1, "type" : "LEARN_MEETING_SPELL_LIMIT", "valueType" : "BASE_NUMBER" } @@ -657,7 +654,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, + "subtype" : "damageTypeMelee", "type" : "PERCENTAGE_DAMAGE_BOOST", "valueType" : "BASE_NUMBER" } @@ -685,7 +682,7 @@ "effects" : { "main" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "valueType" : "BASE_NUMBER" } } diff --git a/config/spells/ability.json b/config/spells/ability.json index b864ca09f..d9e945722 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -141,13 +141,13 @@ "attack" : { "val" : -2, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "duration" : "N_TURNS" }, "defence" : { "val" : -2, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "duration" : "N_TURNS" } } @@ -384,7 +384,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "duration" : "PERMANENT", "valueType" : "ADDITIVE_VALUE" } diff --git a/config/spells/adventure.json b/config/spells/adventure.json index aa6c63787..ef79c679e 100644 --- a/config/spells/adventure.json +++ b/config/spells/adventure.json @@ -46,7 +46,7 @@ "effects" : { "visionsMonsters" : { "type" : "VISIONS", - "subtype" : 0, + "subtype" : "visionsMonsters", "duration" : "ONE_DAY", "val" : 1, "valueType" : "INDEPENDENT_MAX" @@ -60,10 +60,10 @@ }, "visionsHeroes" :{ "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "duration" : "ONE_DAY", "val" : 2, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" } } @@ -75,17 +75,17 @@ }, "visionsHeroes" :{ "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "duration" : "ONE_DAY", "val" : 3, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" }, "visionsTowns" :{ "type" : "VISIONS", - "subtype" : 2, + "subtype" : "visionsTowns", "duration" : "ONE_DAY", "val" : 3, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" } } } @@ -123,7 +123,6 @@ "effects" : { "stealth" : { "type" : "DISGUISED", - "subtype" : 0, //required "duration" : "ONE_DAY", "val" : 1, "valueType" : "INDEPENDENT_MAX" diff --git a/config/spells/moats.json b/config/spells/moats.json index d7869b469..71eea7fe8 100644 --- a/config/spells/moats.json +++ b/config/spells/moats.json @@ -72,7 +72,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -169,7 +169,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -317,7 +317,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -414,7 +414,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -511,7 +511,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -608,7 +608,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -705,7 +705,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } diff --git a/config/spells/timed.json b/config/spells/timed.json index 758e28abb..0817cbad1 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -17,7 +17,7 @@ "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : 0, + "subtype" : "damageTypeMelee", "duration" : "N_TURNS" } } @@ -48,7 +48,7 @@ "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : 1, + "subtype" : "damageTypeRanged", "duration" : "N_TURNS" } } @@ -367,7 +367,6 @@ "alwaysMaximumDamage" : { "val" : 0, "type" : "ALWAYS_MAXIMUM_DAMAGE", - "subtype" : -1, "valueType" : "INDEPENDENT_MAX", "duration" : "N_TURNS" } @@ -421,7 +420,6 @@ "addInfo" : 0, "val" : 0, "type" : "ALWAYS_MINIMUM_DAMAGE", - "subtype" : -1, "valueType" : "INDEPENDENT_MAX", "duration" : "N_TURNS" } @@ -476,7 +474,7 @@ "primarySkill" : { "val" : 3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "effectRange" : "ONLY_MELEE_FIGHT", "duration" : "N_TURNS" } @@ -527,7 +525,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : 3, "effectRange" : "ONLY_DISTANCE_FIGHT", "duration" : "N_TURNS" @@ -576,7 +574,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : -3, "duration" : "N_TURNS" } @@ -623,7 +621,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : 3, "duration" : "N_TURNS" } @@ -667,7 +665,7 @@ "cumulativeEffects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : -3, "valueType" : "ADDITIVE_VALUE", "duration" : "PERMANENT" @@ -710,13 +708,13 @@ "effects" : { "attack" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : 2, "duration" : "N_TURNS" }, "defence" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : 2, "duration" : "N_TURNS" }, @@ -1401,7 +1399,7 @@ "notActive" : { "val" : 0, "type" : "NOT_ACTIVE", - "subtype" : 62, + "subtype" : "blind", "duration" : [ "UNTIL_BEING_ATTACKED", "N_TURNS" diff --git a/config/startres.json b/config/startres.json deleted file mode 100644 index dc96714e1..000000000 --- a/config/startres.json +++ /dev/null @@ -1,31 +0,0 @@ -// Starting resources, ordered by difficulty level (0 to 4) -{ - "difficulty": - [ - { - "human": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - "ai": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 } - }, - - { - "human": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, - "ai": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 } - }, - - { - "human": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - } - ] -} - diff --git a/config/textColors.json b/config/textColors.json new file mode 100644 index 000000000..6873a64ab --- /dev/null +++ b/config/textColors.json @@ -0,0 +1,171 @@ +{ + "colors": { + "r": "F80000", + "g": "00FC00", + "b": "0000F8", + "y": "F8FC00", + "o": "F88000", + "p": "F800F8", + "c": "00FCF8", + "k": "000000", + "w": "FFFFFF", + "Regular": "F8F0D8", + "Highlight": "E8D478", + "H3Red": "F80000", + "H3Cyan": "00FCF8", + "H3Green": "00FC00", + "H3Blue": "0000F8", + "H3Yellow": "F8FC00", + "H3Orange": "F88000", + "H3Purple": "F800F8", + "H3Pink": "C07888", + "Black": "000000", + "Navy": "000080", + "DarkBlue": "00008B", + "MediumBlue": "0000CD", + "Blue": "0000FF", + "DarkGreen": "006400", + "Green": "008000", + "Teal": "008080", + "DarkCyan": "008B8B", + "DeepSkyBlue": "00BFFF", + "DarkTurquoise": "00CED1", + "MediumSpringGreen": "00FA9A", + "Lime": "00FF00", + "SpringGreen": "00FF7F", + "Aqua": "00FFFF", + "Cyan": "00FFFF", + "MidnightBlue": "191970", + "DodgerBlue": "1E90FF", + "LightSeaGreen": "20B2AA", + "ForestGreen": "228B22", + "SeaGreen": "2E8B57", + "DarkSlateGray": "2F4F4F", + "DarkSlateGrey": "2F4F4F", + "LimeGreen": "32CD32", + "MediumSeaGreen": "3CB371", + "Turquoise": "40E0D0", + "RoyalBlue": "4169E1", + "SteelBlue": "4682B4", + "DarkSlateBlue": "483D8B", + "MediumTurquoise": "48D1CC", + "Indigo": "4B0082", + "DarkOliveGreen": "556B2F", + "CadetBlue": "5F9EA0", + "CornflowerBlue": "6495ED", + "RebeccaPurple": "663399", + "MediumAquaMarine": "66CDAA", + "DimGray": "696969", + "DimGrey": "696969", + "SlateBlue": "6A5ACD", + "OliveDrab": "6B8E23", + "SlateGray": "708090", + "SlateGrey": "708090", + "LightSlateGray": "778899", + "LightSlateGrey": "778899", + "MediumSlateBlue": "7B68EE", + "LawnGreen": "7CFC00", + "Chartreuse": "7FFF00", + "Aquamarine": "7FFFD4", + "Maroon": "800000", + "Purple": "800080", + "Olive": "808000", + "Gray": "808080", + "Grey": "808080", + "SkyBlue": "87CEEB", + "LightSkyBlue": "87CEFA", + "BlueViolet": "8A2BE2", + "DarkRed": "8B0000", + "DarkMagenta": "8B008B", + "SaddleBrown": "8B4513", + "DarkSeaGreen": "8FBC8F", + "LightGreen": "90EE90", + "MediumPurple": "9370DB", + "DarkViolet": "9400D3", + "PaleGreen": "98FB98", + "DarkOrchid": "9932CC", + "YellowGreen": "9ACD32", + "Sienna": "A0522D", + "Brown": "A52A2A", + "DarkGray": "A9A9A9", + "DarkGrey": "A9A9A9", + "LightBlue": "ADD8E6", + "GreenYellow": "ADFF2F", + "PaleTurquoise": "AFEEEE", + "LightSteelBlue": "B0C4DE", + "PowderBlue": "B0E0E6", + "FireBrick": "B22222", + "DarkGoldenRod": "B8860B", + "MediumOrchid": "BA55D3", + "RosyBrown": "BC8F8F", + "DarkKhaki": "BDB76B", + "Silver": "C0C0C0", + "MediumVioletRed": "C71585", + "IndianRed": "CD5C5C", + "Peru": "CD853F", + "Chocolate": "D2691E", + "Tan": "D2B48C", + "LightGray": "D3D3D3", + "LightGrey": "D3D3D3", + "Thistle": "D8BFD8", + "Orchid": "DA70D6", + "GoldenRod": "DAA520", + "PaleVioletRed": "DB7093", + "Crimson": "DC143C", + "Gainsboro": "DCDCDC", + "Plum": "DDA0DD", + "BurlyWood": "DEB887", + "LightCyan": "E0FFFF", + "Lavender": "E6E6FA", + "DarkSalmon": "E9967A", + "Violet": "EE82EE", + "PaleGoldenRod": "EEE8AA", + "LightCoral": "F08080", + "Khaki": "F0E68C", + "AliceBlue": "F0F8FF", + "HoneyDew": "F0FFF0", + "Azure": "F0FFFF", + "SandyBrown": "F4A460", + "Wheat": "F5DEB3", + "Beige": "F5F5DC", + "WhiteSmoke": "F5F5F5", + "MintCream": "F5FFFA", + "GhostWhite": "F8F8FF", + "Salmon": "FA8072", + "AntiqueWhite": "FAEBD7", + "Linen": "FAF0E6", + "LightGoldenRodYellow": "FAFAD2", + "OldLace": "FDF5E6", + "Red": "FF0000", + "Fuchsia": "FF00FF", + "Magenta": "FF00FF", + "DeepPink": "FF1493", + "OrangeRed": "FF4500", + "Tomato": "FF6347", + "HotPink": "FF69B4", + "Coral": "FF7F50", + "DarkOrange": "FF8C00", + "LightSalmon": "FFA07A", + "Orange": "FFA500", + "LightPink": "FFB6C1", + "Pink": "FFC0CB", + "Gold": "FFD700", + "PeachPuff": "FFDAB9", + "NavajoWhite": "FFDEAD", + "Moccasin": "FFE4B5", + "Bisque": "FFE4C4", + "MistyRose": "FFE4E1", + "BlanchedAlmond": "FFEBCD", + "PapayaWhip": "FFEFD5", + "LavenderBlush": "FFF0F5", + "SeaShell": "FFF5EE", + "Cornsilk": "FFF8DC", + "LemonChiffon": "FFFACD", + "FloralWhite": "FFFAF0", + "Snow": "FFFAFA", + "Yellow": "FFFF00", + "LightYellow": "FFFFE0", + "Ivory": "FFFFF0", + "White": "FFFFFF" + } +} diff --git a/config/widgets/advancedOptionsTab.json b/config/widgets/advancedOptionsTab.json new file mode 100644 index 000000000..1175ce8b1 --- /dev/null +++ b/config/widgets/advancedOptionsTab.json @@ -0,0 +1,129 @@ +{ + "items": + [ + { + "name": "background", + "type": "picture", + "image": "ADVOPTBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.515", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "core.genrltxt.516", + "rect": {"x": 60, "y": 50, "w": 320, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelPlayerNameAndHandicap", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.517", + "rect": {"x": 58, "y": 92, "w": 100, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingTown", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.518", + "rect": {"x": 163, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingHero", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.519", + "rect": {"x": 239, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingBonus", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.520", + "rect": {"x": 315, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + // timer + { + "type": "label", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.521", + "position": {"x": 222, "y": 544} + }, + + { + "name": "labelTurnDurationValue", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 319, "y": 565} + }, + + { + "name": "sliderTurnDuration", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 55, "y": 557}, + "size": 194, + "callback": "setTimerPreset", + "itemsVisible": 1, + "itemsTotal": 11, + "selected": 11, + "style": "blue", + "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, + "panningStep": 20 + }, + ], + + "variables": + { + "timerPresets" : + [ + [0, 60, 0, 0, false, false], + [0, 120, 0, 0, false, false], + [0, 240, 0, 0, false, false], + [0, 360, 0, 0, false, false], + [0, 480, 0, 0, false, false], + [0, 600, 0, 0, false, false], + [0, 900, 0, 0, false, false], + [0, 1200, 0, 0, false, false], + [0, 1500, 0, 0, false, false], + [0, 1800, 0, 0, false, false], + [0, 0, 0, 0, false, false], + ] + } +} diff --git a/config/widgets/adventureMap.json b/config/widgets/adventureMap.json index 36dd1e501..a398ad697 100644 --- a/config/widgets/adventureMap.json +++ b/config/widgets/adventureMap.json @@ -1,7 +1,7 @@ { "options" : { // player-colored images used for background - "imagesPlayerColored" : [ "AdvMap.pcx" ], + "imagesPlayerColored" : [ "AdvMap.pcx" ] }, "items": @@ -315,7 +315,7 @@ "item" : { "top" : 16, "left": 2, "width" : 48, "height" : 32 }, "itemsOffset" : { "x" : 0, "y" : 32 }, "itemsCount" : 5 - }, + } ] }, { @@ -331,7 +331,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 0, "bottom" : 0, "left" : 0, "right" : 0 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, // Town / Hero lists for large (664+) vertical resolution @@ -402,7 +402,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 192, "bottom" : 3, "right" : 57, "width" : 64 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, { @@ -418,7 +418,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 0, "bottom" : 0, "left" : 0, "right" : 0 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, { @@ -509,7 +509,7 @@ { "type": "adventureMapContainer", "hideWhen" : "mapLayerSurface", - "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 } + "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 }, "items" : [ { "type": "adventureMapButton", @@ -532,7 +532,7 @@ { "type": "adventureMapContainer", "hideWhen" : "mapLayerUnderground", - "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 } + "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 }, "items" : [ { "type": "adventureMapButton", diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json new file mode 100644 index 000000000..d451afda4 --- /dev/null +++ b/config/widgets/mapOverview.json @@ -0,0 +1,156 @@ +{ + "items": + [ + { + "name": "background", + "type": "texture", + "image": "DIBOXBCK", + "rect": {"w": 428, "h": 379} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 5, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.scenarioName", + "position": {"x": 214, "y": 15} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 30, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "name": "mapName", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 214, "y": 40} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 55, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.mapPreview", + "position": {"x": 214, "y": 65} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 29, "y": 79, "w": 171, "h": 171}, + "color": [0, 0, 0, 255], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.lobby.noPreview", + "position": {"x": 114, "y": 164} + }, + { + "type": "drawMinimap", + "id": 0, + "rect": {"x": 30, "y": 80, "w": 169, "h": 169} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 228, "y": 79, "w": 171, "h": 171}, + "color": [0, 0, 0, 255], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "name": "noUnderground", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.lobby.noUnderground", + "position": {"x": 313, "y": 164} + }, + { + "type": "drawMinimap", + "id": 1, + "rect": {"x": 229, "y": 80, "w": 169, "h": 169} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 254, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.creationDate", + "position": {"x": 214, "y": 264} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 279, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "name": "date", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 214, "y": 289} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 304, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.filepath", + "position": {"x": 214, "y": 314} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x": 5, "y": 329, "w": 418, "h": 45}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "textBox", + "name": "fileName", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "rect": {"x": 10, "y": 334, "w": 408, "h": 35} + } + ], + + "variables": + { + "mapPreviewForSaves": true + } +} diff --git a/config/widgets/playerOptionsTab.json b/config/widgets/playerOptionsTab.json new file mode 100644 index 000000000..65a36caa6 --- /dev/null +++ b/config/widgets/playerOptionsTab.json @@ -0,0 +1,121 @@ +{ + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "name": "background", + "type": "picture", + "image": "ADVOPTBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.515", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "core.genrltxt.516", + "rect": {"x": 60, "y": 50, "w": 320, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelPlayerNameAndHandicap", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.517", + "rect": {"x": 58, "y": 92, "w": 100, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingTown", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.518", + "rect": {"x": 163, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingHero", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.519", + "rect": {"x": 239, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingBonus", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.520", + "rect": {"x": 315, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "type" : "dropDownTimers", + "name": "timerPresetSelector", + "position": {"x": 56, "y": 535}, + "dropDownPosition": {"x": 0, "y": -260} + }, + { + "type" : "dropDownSimturns", + "name": "simturnsPresetSelector", + "position": {"x": 56, "y": 555}, + "dropDownPosition": {"x": 0, "y": -160} + } + ], + + "variables": + { + "timerPresets" : + [ + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 960, 480, 120, 0, true, false], + [ 960, 480, 75, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 120, 90, 60, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] + ] + } +} diff --git a/config/widgets/settings/battleOptionsTab.json b/config/widgets/settings/battleOptionsTab.json index 8f47a3fd5..3b5df7a76 100644 --- a/config/widgets/settings/battleOptionsTab.json +++ b/config/widgets/settings/battleOptionsTab.json @@ -69,7 +69,6 @@ [ { "name": "enableAutocombatSpellsCheckbox", - "help": "vcmi.battleOptions.enableAutocombatSpells", "callback": "enableAutocombatSpellsChanged" } ] diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index f8a5a4602..41d3b57a8 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -7,7 +7,7 @@ "name": "lineLabelsEnd", "type": "texture", "image": "settingsWindow/lineHorizontal", - "rect": { "x" : 5, "y" : 289, "w": 365, "h": 3} + "rect": { "x" : 5, "y" : 349, "w": 365, "h": 3} }, { "type" : "labelTitle", @@ -21,7 +21,7 @@ }, { "type" : "labelTitle", - "position": {"x": 10, "y": 295}, + "position": {"x": 10, "y": 355}, "text": "vcmi.systemOptions.townsGroup" }, /////////////////////////////////////// Left section - Video Settings @@ -50,6 +50,9 @@ { "text": "vcmi.systemOptions.framerateButton.hover" }, + { + "text": "vcmi.systemOptions.enableLargeSpellbookButton.hover" + }, { "text": "core.genrltxt.577" }, @@ -61,6 +64,9 @@ { "text": "vcmi.systemOptions.hapticFeedbackButton.hover", "created" : "mobile" + }, + { + "text": "vcmi.systemOptions.enableUiEnhancementsButton.hover" } ] }, @@ -99,6 +105,11 @@ "help": "vcmi.systemOptions.framerateButton", "callback": "framerateChanged" }, + { + "name": "enableLargeSpellbookCheckbox", + "help": "vcmi.systemOptions.enableLargeSpellbookButton", + "callback": "enableLargeSpellbookChanged" + }, { "name": "spellbookAnimationCheckbox", "help": "core.help.364", @@ -116,6 +127,11 @@ "help": "vcmi.systemOptions.hapticFeedbackButton", "callback": "hapticFeedbackChanged", "created" : "mobile" + }, + { + "name": "enableUiEnhancementsCheckbox", + "help": "vcmi.systemOptions.enableUiEnhancementsButton", + "callback": "enableUiEnhancementsChanged" } ] }, @@ -168,7 +184,7 @@ { "type" : "verticalLayout", "customType" : "labelDescription", - "position": {"x": 45, "y": 325}, + "position": {"x": 45, "y": 385}, "items" : [ { "text": "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover", @@ -181,7 +197,7 @@ { "name": "availableCreaturesAsDwellingPicker", "type": "toggleGroup", - "position": {"x": 10, "y": 323}, + "position": {"x": 10, "y": 383}, "items": [ { @@ -203,7 +219,7 @@ "name": "compactTownCreatureInfoLabel", "type" : "verticalLayout", "customType" : "labelDescription", - "position": {"x": 45, "y": 385}, + "position": {"x": 45, "y": 445}, "items" : [ { "text": "vcmi.otherOptions.compactTownCreatureInfo.hover", @@ -214,7 +230,7 @@ "name": "compactTownCreatureInfoCheckbox", "type": "checkbox", "help": "vcmi.otherOptions.compactTownCreatureInfo", - "position": {"x": 10, "y": 383}, + "position": {"x": 10, "y": 443}, "callback": "compactTownCreatureInfoChanged" } ] diff --git a/config/widgets/turnOptionsDropdownLibrary.json b/config/widgets/turnOptionsDropdownLibrary.json new file mode 100644 index 000000000..162fbd775 --- /dev/null +++ b/config/widgets/turnOptionsDropdownLibrary.json @@ -0,0 +1,520 @@ +{ + "dropDownBackground" : + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 1, "y": 1, "w": 219, "h": 19}, + "color": [0, 0, 0, 128], + "colorLine": [0, 0, 0, 128] + }, + "dropDownLabel": + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "position": {"x": 4, "y": 0} + }, + "dropDownHover": + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 2, "y": 2, "w": 216, "h": 16}, + "color": [0, 0, 0, 0], + "colorLine": [255, 255, 0, 255] + }, + "dropDownTimers" : + { + "name": "timerPresetSelector", + "type": "comboBox", + "image": "lobby/dropdown", + "imageOrder": [0, 0, 0, 0], + "items": + [ + { + "name": "timer", + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.turnTime.select" + } + ], + "dropDown": + { + "items": + [ + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x": 0, "y": 0, "w": 220, "h": 260} + }, + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 0, "y": 0, "w": 220, "h": 260}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "item", + "position": {"x": 0, "y": 0}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.unlimited" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 20}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 40}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 60}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.5" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 80}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.10" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 100}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.20" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 120}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.classic.30" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 140}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.20" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 160}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.16" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 180}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.8" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 200}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 220}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 240}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.turnTime.chess.1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + } + ] + } + }, + "dropDownSimturns" : + { + "name": "timerPresetSelector", + "type": "comboBox", + "image": "lobby/dropdown", + "imageOrder": [0, 0, 0, 0], + "items": + [ + { + "name": "timer", + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturns.select" + } + ], + "dropDown": + { + "items": + [ + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x": 0, "y": 0, "w": 220, "h": 160} + }, + { + "type": "transparentFilledRectangle", + "visible": false, + "rect": {"x": 0, "y": 0, "w": 220, "h": 160}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "item", + "position": {"x": 0, "y": 0}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.none" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 20}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContactMax" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 40}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 60}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 80}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.tillContact4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 100}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked1" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 120}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked2" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + }, + { + "type": "item", + "position": {"x": 0, "y": 140}, + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + "items": + [ + { + "type": "dropDownBackground" + }, + { + "type": "dropDownLabel", + "name": "labelName", + "text": "vcmi.optionsTab.simturns.blocked4" + }, + { + "type": "dropDownHover", + "name": "hoverImage" + } + ] + } + ] + } + } +} diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json new file mode 100644 index 000000000..79cdd1737 --- /dev/null +++ b/config/widgets/turnOptionsTab.json @@ -0,0 +1,365 @@ +{ + "library" : "config/widgets/turnOptionsDropdownLibrary.json", + + "customTypes" : { + "verticalLayout66" : { + "type" : "layout", + "vertical" : true, + "dynamic" : false, + "distance" : 66 + }, + "labelTitle" : { + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow" + }, + "labelDescription" : { + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 0, "y": 0, "w": 300, "h": 35}, + "adoptHeight": true + }, + "timeInput" : { + "type": "textInput", + "alignment": "center", + "text": "00:00", + "rect": {"x": 0, "y": 0, "w": 86, "h": 23}, + "offset": {"x": 0, "y": 0} + }, + "timeInputBackground" : { + "type": "transparentFilledRectangle", + "rect": {"x": 0, "y": 0, "w": 86, "h": 23}, + "color": [0, 0, 0, 128], + "colorLine": [64, 80, 128, 128] + } + }, + + "items": + [ + { + "name": "background", + "type": "picture", + "image": "RANMAPBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "vcmi.optionsTab.turnOptions.hover", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.optionsTab.turnOptions.help", + "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, + "adoptHeight": true + }, + +// { +// "type": "label", +// "font": "medium", +// "alignment": "center", +// "color": "yellow", +// "text": "vcmi.optionsTab.selectPreset", +// "position": {"x": 105, "y": 100} +// }, + { + "type" : "dropDownTimers", + "name": "timerPresetSelector", + "position": {"x": 160, "y": 78}, + "dropDownPosition": {"x": 0, "y": 20} + }, + { + "type" : "dropDownSimturns", + "name": "simturnsPresetSelector", + "position": {"x": 160, "y": 98}, + "dropDownPosition": {"x": 0, "y": 20} + }, + + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 416, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [80, 96, 160, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 417, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [32, 40, 128, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 466, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [80, 96, 160, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 467, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [32, 40, 128, 128] + }, + { + "type" : "verticalLayout66", + "customType" : "labelTitle", + "position": {"x": 70, "y": 133}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldTurn.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldUnit.hover" + }, + { + "text": "vcmi.optionsTab.simturnsTitle" + } + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "labelDescription", + "position": {"x": 70, "y": 155}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "name": "chessFieldTurnLabel" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "name": "chessFieldUnitLabel" + } + ] + }, + + { + "name": "buttonTurnTimerAccumulate", + "position": {"x": 160, "y": 195}, + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setTurnTimerAccumulate" + }, + { + "name": "buttonUnitTimerAccumulate", + "position": {"x": 160, "y": 327}, + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setUnitTimerAccumulate" + }, + + { + "type" : "labelTitle", + "position": {"x": 195, "y": 199}, + "text" : "vcmi.optionsTab.accumulate" + }, + { + "type" : "labelTitle", + "position": {"x": 195, "y": 331}, + "text" : "vcmi.optionsTab.accumulate" + }, + + { + "type" : "verticalLayout66", + "customType" : "timeInputBackground", + "position": {"x": 294, "y": 129}, + "items": + [ + {}, + {}, + {}, + {} + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "timeInput", + "position": {"x": 294, "y": 129}, + "items": + [ + { + "name": "chessFieldBase", + "callback": "parseAndSetTimer_base", + "help": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "name": "chessFieldTurn", + "callback": "parseAndSetTimer_turn", + "help": "vcmi.optionsTab.chessFieldTurn.help" + }, + { + "name": "chessFieldBattle", + "callback": "parseAndSetTimer_battle", + "help": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "name": "chessFieldUnit", + "callback": "parseAndSetTimer_unit", + "help": "vcmi.optionsTab.chessFieldUnit.help" + } + ] + }, + + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMin.hover", + "position": {"x": 70, "y": 420} + }, + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMax.hover", + "position": {"x": 70, "y": 470} + }, + + { + "name": "simturnsDurationMin", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 178, "y": 420}, + "size": 200, + "callback": "setSimturnDurationMin", + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168 ], + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + { + "name": "simturnsDurationMax", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 178, "y": 470}, + "size": 200, + "callback": "setSimturnDurationMax", + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168, 1000000 ], + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + { + "name": "labelSimturnsDurationValueMin", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 278, "y": 428} + }, + { + "name": "labelSimturnsDurationValueMax", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 278, "y": 478} + }, + { + "text": "vcmi.optionsTab.simturnsMin.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 430, "w": 300, "h": 40} + }, + { + "text": "vcmi.optionsTab.simturnsMax.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 480, "w": 300, "h": 40} + }, + { + "name": "buttonSimturnsAI", + "position": {"x": 70, "y": 535}, + "type": "toggleButton", + "image": "lobby/checkbox", + "callback" : "setSimturnAI" + }, + { + "name": "labelSimturnsAI", + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow", + "text": "vcmi.optionsTab.simturnsAI.hover", + "position": {"x": 110, "y": 540} + } + ], + + "variables": + { + "timerPresets" : + [ + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 1200, 600, 120, 0, true, false], + [ 960, 480, 90, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 240, 120, 30, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] + ] + } +} diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json new file mode 100644 index 000000000..5965ed2f0 --- /dev/null +++ b/config/widgets/turnTimer.json @@ -0,0 +1,34 @@ +{ + "items": + [ + { //backgound color + "type": "drawRect", + "rect": {"x": 4, "y": 4, "w": 72, "h": 24}, + "color": [10, 10, 10, 255] + }, + + { //clocks icon + "type": "image", + "image": "VCMI/BATTLEQUEUE/STATESSMALL", + "frame": 1, + "position": {"x": 4, "y": 6} + }, + + { //timer field label + "name": "timer", + "type": "label", + "font": "big", + "alignment": "left", + "color": "yellow", + "text": "", + "position": {"x": 26, "y": 2} + }, + ], + + "variables": + { + "notificationTime": [0, 1, 2, 3, 4, 5, 20], + "notificationSound": "WE5", + "textColorFromPlayerColor": true + } +} diff --git a/debian/changelog b/debian/changelog index 0761d2ed4..0c7935b15 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.4.0) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 8 Dec 2023 16:00:00 +0200 + vcmi (1.3.2) jammy; urgency=medium * New upstream release diff --git a/docs/Readme.md b/docs/Readme.md index 77ccb0964..6902f1bf6 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,81 +1,79 @@ -[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.2) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) - -# VCMI Project - -VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. - -## Links - - * Homepage: https://vcmi.eu/ - * Forums: https://forum.vcmi.eu/ - * Bugtracker: https://github.com/vcmi/vcmi/issues - * Slack: https://slack.vcmi.eu/ - * Discord: https://discord.gg/chBT42V - -## Latest release - -Latest release can be found [in Github Releases page](https://github.com/vcmi/vcmi/releases/latest). As of right now we plan to have major releases around 3 times per year. Daily builds are still available at [builds.vcmi.download](https://builds.vcmi.download/branch/develop/) but they are not guaranteed to be stable. So we encourage everybody to use them and report found bugs so that we can fix them. -Loading saves made with different major version of VCMI is usually **not** supported, so you may want to finish your ongoing games before updating. -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) - -## Documentation and guidelines for players - -- [General information about VCMI Project](players/Manual.md) -- [Frequently asked questions](https://vcmi.eu/faq/) (external link) -- [Game mechanics](players/Game_Mechanics.md) -- [Bug reporting guidelines](players/Bug_Reporting_Guidelines.md) -- [Cheat codes](players/Cheat_Codes.md) -- [Privacy Policy](players/Privacy_Policy.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) - -## 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) -- [Building VCMI for macOS](developers/Building_macOS.md) -- [Building VCMI for Windows](developers/Building_Windows.md) -- [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) -- [Code Structure](developers/Code_Structure.md) -- [Logging API](developers/Logging_API.md) -- [Lua Scripting System](developers/Lua_Scripting_System.md) -- [Serialization](developers/Serialization.md) - -## Documentation and guidelines for maintainers - -- [Project Infrastructure](maintainers/Project_Infrastructure.md) -- [Release Process](maintainers/Release_Process.md) -- [Ubuntu PPA](maintainers/Ubuntu_PPA.md) - -## 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 - -Copyright (C) 2007-2023 VCMI Team (check AUTHORS file for the contributors list) +[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.0) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) + +# VCMI Project + +VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. + +## Links + + * Homepage: https://vcmi.eu/ + * Forums: https://forum.vcmi.eu/ + * Bugtracker: https://github.com/vcmi/vcmi/issues + * Slack: https://slack.vcmi.eu/ + * Discord: https://discord.gg/chBT42V + +## Latest release + +Latest release can be found [in Github Releases page](https://github.com/vcmi/vcmi/releases/latest). As of right now we plan to have major releases around 3 times per year. Daily builds are still available at [builds.vcmi.download](https://builds.vcmi.download/branch/develop/) but they are not guaranteed to be stable. So we encourage everybody to use them and report found bugs so that we can fix them. +Loading saves made with different major version of VCMI is usually **not** supported, so you may want to finish your ongoing games before updating. +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) + +## Documentation and guidelines for players + +- [General information about VCMI Project](players/Manual.md) +- [Frequently asked questions](https://vcmi.eu/faq/) (external link) +- [Game mechanics](players/Game_Mechanics.md) +- [Bug reporting guidelines](players/Bug_Reporting_Guidelines.md) +- [Cheat codes](players/Cheat_Codes.md) +- [Privacy Policy](players/Privacy_Policy.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) + +## 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) +- [Building VCMI for macOS](developers/Building_macOS.md) +- [Building VCMI for Windows](developers/Building_Windows.md) +- [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) +- [Code Structure](developers/Code_Structure.md) +- [Logging API](developers/Logging_API.md) +- [Lua Scripting System](developers/Lua_Scripting_System.md) +- [Serialization](developers/Serialization.md) + +## Documentation and guidelines for maintainers + +- [Project Infrastructure](maintainers/Project_Infrastructure.md) +- [Release Process](maintainers/Release_Process.md) +- [Ubuntu PPA](maintainers/Ubuntu_PPA.md) + +## 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 + +Copyright (C) 2007-2023 VCMI Team (check AUTHORS file for the contributors list) diff --git a/docs/developers/AI.md b/docs/developers/AI.md new file mode 100644 index 000000000..7f1c61b65 --- /dev/null +++ b/docs/developers/AI.md @@ -0,0 +1,34 @@ +< [Documentation](../Readme.md) / AI Description + +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. + +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's most important classes are the following: + +- AttackPossibility - one particular way to attack on the battlefield. Each AttackPossibility instance has multiple ...DamageReduce attributes. These represent how much damage an enemy will lose after our attack. Effects can reduce this damage. We add them up and this value is used as attack score. + +- PotentialTargets - a set of all AttackPossibility instances + +- BattleExchangeVariant - it is an extension of AttackPossibility, a result of a set of units attacking each other for a fixed number of turns according to the turn order. It is kind of an oversimplified battle simulation. A set of units is restricted according to AttackPossibility which particular exchange extends. Exchanges can be waited (when stacks/units wait for a better time to attack) and non-waited (when stack acts right away). For non-waited exchanges the first attack score is taken from AttackPossibility (together with various effects like 2 hex breath, shooters blocking and so on). All the other attacks are simplified, only respect retaliations. At the end we have a final score. + +- BattleExchangeEvaluator - calculates all possible BattleExchangeVariants and selects the best + +- 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 diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index cbd6a14f7..ef0c2c909 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -10,17 +10,19 @@ The following instructions apply to **v1.2 and later**. For earlier versions the 2. JDK 11, not necessarily from Oracle 3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/ 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 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 + - install with Android Studio + - install with `sdkmanager` command line tool + - download from https://developer.android.com/ndk/downloads + - 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 ## Obtaining source code Clone https://github.com/vcmi/vcmi with submodules. Example for command line: -```sh +``` git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -30,24 +32,24 @@ We use Conan package manager to build/consume dependencies, find detailed usage branch](https://github.com/vcmi/vcmi/tree/master/docs/conan.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) +- `android-32` to build for 32-bit architecture (armeabi-v7a) +- `android-64` to build for 64-bit architecture (aarch64-v8a) ### NDK and Conan 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. You need to create your own Conan profile that imports our Android profile and adds 2 new lines (you can of course just copy everything from our profile into yours without including) and then pass this new profile to `conan install`: +- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. You need to create your own Conan profile that imports our Android profile and adds 2 new lines (you can of course just copy everything from our profile into yours without including) and then pass this new profile to `conan install`: -```sh +``` include(/path/to/vcmi/CI/conan/android-64) [tool_requires] android-ndk/r25c ``` -- to use an already installed NDK, you can simply pass it on the command line to `conan install`: +- to use an already installed NDK, you can simply pass it on the command line to `conan install`: -```sh +``` conan install -c tools.android:ndk_path=/path/to/ndk ... ``` @@ -59,8 +61,8 @@ Building for Android is a 2-step process. First, native C++ code is compiled to This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset. Example: -```sh -cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug --toolchain ... +``` +cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_C_COMPILER_LAUNCHER=ccache --toolchain ... cmake --build ../build ``` @@ -83,4 +85,4 @@ cd android APK will appear in `android/vcmi-app/build/outputs/apk/debug` directory which you can then install to your device with `adb install -r /path/to/apk` (adb command is from Android command line tools). -If you wish to build and install to your device in single action, use `installDebug` instead of `assembleDebug`. \ No newline at end of file +If you wish to build and install to your device in single action, use `installDebug` instead of `assembleDebug`. diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index d274ac35c..5bd2f871c 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -19,13 +19,15 @@ To compile, the following packages (and their development counterparts) are need - 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 +- Optional: + - if you want to build scripting modules: LuaJIT + - to speed up recompilation: Ccache ## On Debian-based systems (e.g. Ubuntu) For Ubuntu and Debian you need to install this list of packages: -`sudo apt-get install cmake g++ libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev` +`sudo apt-get install cmake g++ clang libsdl2-dev libsdl2-image-dev libsdl2-ttf-dev libsdl2-mixer-dev zlib1g-dev libavformat-dev libswscale-dev libboost-dev libboost-filesystem-dev libboost-system-dev libboost-thread-dev libboost-program-options-dev libboost-locale-dev qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev ninja-build ccache` Alternatively if you have VCMI installed from repository or PPA you can use: @@ -33,7 +35,7 @@ Alternatively if you have VCMI installed from repository or PPA you can use: ## 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 zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel tbb-devel luajit-devel fuzzylite-devel` +`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 zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel tbb-devel luajit-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. @@ -69,10 +71,13 @@ cmake ../vcmi # Additional options that you may want to use: ## To enable debugging: -`cmake ../vcmi -DCMAKE_BUILD_TYPE=Debug` +`cmake ../vcmi -D CMAKE_BUILD_TYPE=Debug` **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. +## To use ccache: +`cmake ../vcmi -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_C_COMPILER_LAUNCHER=ccache` + ## Trigger build `cmake --build . -- -j2` diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index cab0722dc..1ba751662 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -10,7 +10,9 @@ Windows builds can be made in more than one way and with more than one tool. Thi - Git or git GUI, for example, SourceTree [download link](http://www.sourcetreeapp.com/download) - 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) -- Optionally, to create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) +- Optional: + - 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 @@ -75,6 +77,10 @@ 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 + +Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in. + # Build VCMI #### From GIT GUI @@ -97,7 +103,11 @@ For the list of the packages used you can also consult [vcmi-deps-windows readme ## Compile VCMI with Visual Studio - Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio -- Select `Release` build type in combobox +- 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 - 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! diff --git a/docs/developers/Building_iOS.md b/docs/developers/Building_iOS.md index 7b06136b4..e16bc1ff6 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -2,22 +2,26 @@ ## Requirements -1. **macOS** -2. Xcode: -3. CMake 3.21+: `brew install --cask cmake` or get from +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` ## Obtaining source code Clone with submodules. Example for command line: -`git clone --recurse-submodules https://github.com/vcmi/vcmi.git` +``` +git clone --recurse-submodules https://github.com/vcmi/vcmi.git +``` ## Obtaining dependencies There are 2 ways to get prebuilt dependencies: -- [Conan package manager](https://github.com/vcmi/vcmi/tree/develop/docs/conan.md) - recommended. Note that the link points to the cutting-edge state in `develop` branch, for the latest release check the same document in the [master branch (https://github.com/vcmi/vcmi/tree/master/docs/conan.md). -- [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) - can be used if you have Xcode 11/12 or to build for simulator / armv7 device +- [Conan package manager](https://github.com/vcmi/vcmi/tree/develop/docs/conan.md) - recommended. Note that the link points to the cutting-edge state in `develop` branch, for the latest release check the same document in the [master branch (https://github.com/vcmi/vcmi/tree/master/docs/conan.md). +- [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) - can be used if you have Xcode 11/12 or to build for simulator / armv7 device ## Configuring project @@ -25,17 +29,21 @@ Only Xcode generator (`-G Xcode`) is supported! As a minimum, you must pass the following variables to CMake: -- `BUNDLE_IDENTIFIER_PREFIX`: unique bundle identifier prefix, something like `com.MY-NAME` -- (if using legacy dependencies) `CMAKE_PREFIX_PATH`: path to the downloaded dependencies, e.g. `~/Downloads/vcmi-ios-depends/build/iphoneos` +- `BUNDLE_IDENTIFIER_PREFIX`: unique bundle identifier prefix, something like `com.MY-NAME` +- (if using legacy dependencies) `CMAKE_PREFIX_PATH`: path to the downloaded dependencies, e.g. `~/Downloads/vcmi-ios-depends/build/iphoneos` There're a few [CMake presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html): for device (Conan and legacy dependencies) and for simulator, named `ios-device-conan`, `ios-device` and `ios-simulator` respectively. You can also create your local "user preset" to avoid typing variables each time, see example [here](https://gist.github.com/kambala-decapitator/59438030c34b53aed7d3895aaa48b718). 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` +``` +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. +By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. + +If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON` ### Building for device @@ -53,7 +61,9 @@ You must also install game files, see [Installation on iOS](../players/Installat ### From command line -`cmake --build `` --target vcmiclient -- -quiet` +``` +cmake --build --target vcmiclient -- -quiet +``` You can pass additional xcodebuild options after the `--`. Here `-quiet` is passed to reduce amount of output. @@ -67,4 +77,4 @@ Invoke `cpack` after building: `cpack -C Release` -This will generate file with extension **ipa** if you use CMake 3.25+and **zip** otherwise (simply change extension to ipa). \ No newline at end of file +This will generate file with extension **ipa** if you use CMake 3.25+and **zip** otherwise (simply change extension to ipa). diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index a84297852..9052d2915 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -2,18 +2,22 @@ # Requirements -1. C++ toolchain, either of: - - Xcode Command Line Tools (aka CLT): `sudo xcode-select --install` - - Xcode IDE: - - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) -2. CMake: `brew install --cask cmake` or get from -3. (optional) Ninja: `brew install ninja` or get from +1. C++ toolchain, either of: + - Xcode Command Line Tools (aka CLT): `sudo xcode-select --install` + - Xcode IDE: + - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) +2. CMake: `brew install --cask cmake` or get from +4. Optional: + * Ninja: `brew install ninja` or get it from + * CCache to speed up recompilation: `brew install ccache` # Obtaining source code Clone with submodules. Example for command line: -`git clone --recurse-submodules https://github.com/vcmi/vcmi.git` +``` +git clone --recurse-submodules https://github.com/vcmi/vcmi.git +``` # Obtaining dependencies @@ -25,52 +29,50 @@ Please find detailed instructions in [VCMI repository](https://github.com/vcmi/v On the step where you need to replace **PROFILE**, choose: -- if you're on an Intel Mac: `macos-intel` -- if you're on an Apple Silicon Mac: `macos-arm` +- if you're on an Intel Mac: `macos-intel` +- if you're on an Apple Silicon Mac: `macos-arm` Note: if you wish to build 1.0 release in non-`Release` configuration, you should define `USE_CONAN_WITH_ALL_CONFIGS=1` environment variable when executing `conan install`. ## Homebrew -1. [Install Homebrew](https://brew.sh/) -2. Install dependencies: -`brew install boost minizip sdl2 sdl2_image sdl2_mixer sdl2_ttf tbb` -3. If you want to watch in-game videos, also install FFmpeg: -`brew install ffmpeg@4` -4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher): - - `brew install qt@5` for Qt 5 or `brew install qt` for Qt 6 - - using [Qt Online Installer](https://www.qt.io/download) - choose **Go open source** +1. [Install Homebrew](https://brew.sh/) +2. Install dependencies: `brew install boost minizip sdl2 sdl2_image sdl2_mixer sdl2_ttf tbb` +3. If you want to watch in-game videos, also install FFmpeg: `brew install ffmpeg@4` +4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher): + - `brew install qt@5` for Qt 5 or `brew install qt` for Qt 6 + - using [Qt Online Installer](https://www.qt.io/download) - choose **Go open source** # Preparing build environment This applies only to Xcode-based toolchain. If `xcrun -f clang` prints errors, then use either of the following ways: -- select an Xcode instance from Xcode application - Preferences - Locations - Command Line Tools -- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path: for example, - `sudo xcode-select -s /Library/Developer/CommandLineTools` -- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path: for example, `export DEVELOPER_DIR=/Applications/Xcode.app` +- select an Xcode instance from Xcode application - Preferences - Locations - Command Line Tools +- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path: for example, `sudo xcode-select -s /Library/Developer/CommandLineTools` +- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path: for example, `export DEVELOPER_DIR=/Applications/Xcode.app` # Configuring project for building Note that if you wish to use Qt Creator IDE, you should skip this step and configure respective variables inside the IDE. -1. In Terminal `cd` to the source code directory -2. Start assembling CMake invocation: type `cmake -S . -B BUILD_DIR` where *BUILD_DIR* can be any path, **don't press Return** -3. Decide which CMake generator you want to use: - - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'` - - Ninja (if you have installed it): pass `-G Ninja` - - Xcode IDE (if you have installed it): pass `-G Xcode` -4. If you picked Makefiles or Ninja, pick desired *build type* - either of Debug / RelWithDebInfo / Release / MinSizeRel - and pass it in `CMAKE_BUILD_TYPE` option, for example: `-D CMAKE_BUILD_TYPE=Release`. If you don't pass this option, `RelWithDebInfo` will be used. -5. If you don't want to build Launcher, pass `-D ENABLE_LAUNCHER=OFF` -6. You can also pass `-Wno-dev` if you're not interested in CMake developer warnings -7. Next step depends on the dependency manager you have picked: - - Conan: pass `-D CMAKE_TOOLCHAIN_FILE=conan-generated/conan_toolchain.cmake` where **conan-generated** must be replaced with your directory choice - - Homebrew: if you installed FFmpeg or Qt 5, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon): - - if you installed FFmpeg, insert `$(brew --prefix ffmpeg@4)` - - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)` - - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64` - - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"` -8. now press Return +1. In Terminal `cd` to the source code directory +2. Start assembling CMake invocation: type `cmake -S . -B BUILD_DIR` where *BUILD_DIR* can be any path, **don't press Return** +3. Decide which CMake generator you want to use: + - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'` + - Ninja (if you have installed it): pass `-G Ninja` + - Xcode IDE (if you have installed it): pass `-G Xcode` +4. If you picked Makefiles or Ninja, pick desired *build type* - either of Debug / RelWithDebInfo / Release / MinSizeRel - and pass it in `CMAKE_BUILD_TYPE` option, for example: `-D CMAKE_BUILD_TYPE=Release`. If you don't pass this option, `RelWithDebInfo` will be used. +5. If you don't want to build Launcher, pass `-D ENABLE_LAUNCHER=OFF` +6. You can also pass `-Wno-dev` if you're not interested in CMake developer warnings +7. Next step depends on the dependency manager you have picked: + - Conan: pass `-D CMAKE_TOOLCHAIN_FILE=conan-generated/conan_toolchain.cmake` where **conan-generated** must be replaced with your directory choice + - Homebrew: if you installed FFmpeg or Qt 5, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon): + - if you installed FFmpeg, insert `$(brew --prefix ffmpeg@4)` + - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)` + - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64` + - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"` +8. If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON` +9. Now press Return # Building project @@ -82,10 +84,10 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h ## From command line -`cmake --build ` +`cmake --build ` -- If using Makefiles generator, you'd want to utilize all your CPU cores by appending `-- -j$(sysctl -n hw.ncpu)` to the above -- If using Xcode generator, you can also choose which configuration to build by appending `--config ` to the above, for example: `--config Debug` +- If using Makefiles generator, you'd want to utilize all your CPU cores by appending `-- -j$(sysctl -n hw.ncpu)` to the above +- If using Xcode generator, you can also choose which configuration to build by appending `--config ` to the above, for example: `--config Debug` # Packaging project into DMG file @@ -97,27 +99,23 @@ If you use Conan, it's expected that you use **conan-generated** directory at st You can run VCMI from DMG, but it's will also work from your IDE be it Xcode or Qt Creator. -Alternatively you can run binaries directly from "bin" directory: +Alternatively you can run binaries directly from the **bin** directory: - BUILD_DIR/bin/vcmilauncher - BUILD_DIR/bin/vcmiclient - BUILD_DIR/bin/vcmiserver +- BUILD_DIR/bin/vcmilauncher +- BUILD_DIR/bin/vcmiclient +- BUILD_DIR/bin/vcmiserver -CMake include commands to copy all needed assets from source directory into "bin" on each build. They'll work when you build from Xcode too. +CMake include commands to copy all needed assets from source directory into the **bin** directory on each build. They'll work when you build from Xcode too. # Some useful debugging tips Anyone who might want to debug builds, but new to macOS could find following commands useful: -- To attach DMG file from command line use -`hdiutil attach vcmi-1.0.dmg` -- Detach volume: -`hdiutil detach /Volumes/vcmi-1.0` -- To view dependency paths -`otool -L /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` -- To display load commands such as LC_RPATH -`otool -l /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` +- To attach DMG file from command line use `hdiutil attach vcmi-1.0.dmg` +- Detach volume: `hdiutil detach /Volumes/vcmi-1.0` +- To view dependency paths: `otool -L /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` +- To display load commands such as `LC_RPATH`: `otool -l /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` # Troubleshooting -In case of troubles you can always consult our CI build scripts or contact the dev team via slack \ No newline at end of file +In case of troubles you can always consult our CI build scripts or contact the dev team via slack. diff --git a/docs/developers/Conan.md b/docs/developers/Conan.md index d592ef26e..f4b9409d1 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -10,10 +10,12 @@ The following platforms are supported and known to work, others might require ch - **macOS**: x86_64 (Intel) - target 10.13 (High Sierra), arm64 (Apple Silicon) - target 11.0 (Big Sur) - **iOS**: arm64 - target 12.0 +- **Windows**: x86_64 and x86 fully supported with building from Linux +- **Android**: armeabi-v7a (32-bit ARM) - target 4.4 (API level 19), aarch64-v8a (64-bit ARM) - target 5.0 (API level 21) ## Getting started -1. [Install Conan](https://docs.conan.io/en/latest/installation.html) +1. [Install Conan](https://docs.conan.io/en/latest/installation.html). Currently we support only Conan v1, you can install it with `pip` like this: `pip3 install 'conan<2.0'` 2. Execute in terminal: `conan profile new default --detect` ## Download dependencies @@ -22,36 +24,45 @@ The following platforms are supported and known to work, others might require ch 1. Check if your build environment can use the prebuilt binaries: basically, that your compiler version (or Xcode major version) matches the information below. If you're unsure, simply advance to the next step. - - macOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode and Xcode CLT 13.x - - iOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode 13.x + - **macOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode and Xcode CLT 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo) + - **iOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo) + - **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: - [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, otherwise pick **vcmi-deps-windows-conan.tgz** + - [Android](https://github.com/vcmi/vcmi-dependencies/releases) -3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders). +3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: + + 1. Open file `~/.conan/data/qt/5.15.x/_/_/export/conanfile.py` (`5.15.x` is a placeholder), search for string `Designer` (there should be only one match), comment this line and the one above it by inserting `#` in the beginning, and save the file. + 2. (optional) If you don't want to use Rosetta, follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders). Make sure **not** to copy `qt.conf`! ## Generate CMake integration Conan needs to generate CMake toolchain file to make dependencies available to CMake. See `CMakeDeps` and `CMakeToolchain` [in the official documentation](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake.html) for details. -In terminal `cd` to the VCMI source directory and run the following command. If you want to download prebuilt binaries from ConanCenter, also read the [next section](#using-prebuilt-binaries-from-conancenter). +In terminal `cd` to the VCMI source directory and run the following command. Also check subsections for additional requirements on consuming prebuilt binaries. -```sh +
 conan install . \
   --install-folder=conan-generated \
   --no-imports \
   --build=never \
   --profile:build=default \
   --profile:host=CI/conan/PROFILE
-```
+
The highlighted parts can be adjusted: - ***conan-generated***: directory (absolute or relative) where the generated files will appear. This value is used in CMake presets from VCMI, but you can actually use any directory and override it in your local CMake presets. - ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build recipes, that are not present in your local cache, from source. - ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile). +- ***note for Windows x86***: use profile mingw32-linux.jinja for building instead of mingw64-linux.jinja If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead. @@ -59,6 +70,10 @@ VCMI "recipe" also has some options that you can specify. For example, if you do _Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/en/latest/reference/commands/consumer/install.html) or by executing `conan help install`. +### Using our prebuilt binaries for macOS/iOS + +We use custom recipes for some libraries that are provided by the OS. You must additionally pass `-o with_apple_system_libs=True` for `conan install` to use them. + ### Using prebuilt binaries from ConanCenter First, check if binaries for [your platform](https://github.com/conan-io/conan-center-index/blob/master/docs/supported_platforms_and_configurations.md) are produced. @@ -68,6 +83,25 @@ You must adjust the above `conan install` command: 1. Replace ***CI/conan/PROFILE*** with `default`. 2. Additionally pass `-o default_options_of_requirements=True`: this disables all custom options of our `conanfile.py` to match ConanCenter builds. +### Building dependencies from source + +This subsection describes platform specifics to build libraries from source properly. + +#### 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. +- 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 + +Android has issues loading self-built shared Zlib library because binary name is identical to the system one, so we enforce using the OS-provided library. To achieve that, follow [below instructions](#using-recipes-for-system-libraries), you only need `zlib` directory. + +##### Using recipes for system libraries + +1. Clone/download https://github.com/kambala-decapitator/conan-system-libs +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. + ## Configure project for building You must pass the generated toolchain file to CMake invocation. @@ -81,13 +115,14 @@ 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 \ --build=never \ --profile:build=default \ - --profile:host=CI/conan/macos-intel + --profile:host=CI/conan/macos-intel \ + -o with_apple_system_libs=True cmake -S . -B build -G Xcode \ --toolchain conan-generated/conan_toolchain.cmake @@ -97,7 +132,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 \ @@ -112,13 +147,14 @@ 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 \ --build=never \ --profile:build=default \ - --profile:host=CI/conan/ios-arm64 + --profile:host=CI/conan/ios-arm64 \ + -o with_apple_system_libs=True cmake --preset ios-conan ``` @@ -147,3 +183,28 @@ cmake --preset ios-conan ] } ``` + +### Build VCMI with all deps for 32-bit windows in Ubuntu 22.04 WSL +```powershell +wsl --install +wsl --install -d Ubuntu +ubuntu +``` +Next steps are identical both in WSL and in real Ubuntu 22.04 +```bash +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 +sed -i 's/x86-64/i686/g' CI/mingw-ubuntu/before-install.sh +sudo ./CI/mingw-ubuntu/before-install.sh +conan install . \ + --install-folder=conan-generated \ + --no-imports \ + --build=missing \ + --profile:build=default \ + --profile:host=CI/conan/mingw32-linux \ + -c tools.cmake.cmaketoolchain.presets:max_schema_version=2 +cmake --preset windows-mingw-conan-linux +cmake --build --preset windows-mingw-conan-linux --target package +``` +After that, you will have functional VCMI installer for 32-bit windows. diff --git a/docs/developers/Logging_API.md b/docs/developers/Logging_API.md index 65ea11ea0..6de4f7cc6 100644 --- a/docs/developers/Logging_API.md +++ b/docs/developers/Logging_API.md @@ -104,7 +104,7 @@ 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 occured, if a exception has been thrown (can result in program termination) +- 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' @@ -160,4 +160,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. \ No newline at end of file +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. diff --git a/docs/maintainers/Release_Process.md b/docs/maintainers/Release_Process.md index 2e23e41b7..9c70164a2 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,48 +1,70 @@ < [Documentation](../Readme.md) / Release Process -## 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 `maj.min.patch` -* `beta` branch is for stabilization of ongoing release. We don't merge new features into `beta` branch - only bug fixes are acceptable. Hotfixes for already released software should be delivered to this branch as well. -* `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one step ahead of 'beta` +# 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. -## Release process step-by-step -Assuming that all features planned to be released are implemented in `develop` branch and next release version will be `1.x.0` +# 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`. -### Start of stabilization stage -Should be done several weeks before planned release date +# 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-4 weeks before planned release date. All major features should be finished at this point. -- Create [milestone](https://github.com/vcmi/vcmi/milestones) named `Release 1.x` -- Specify new milestone for all regression bugs and major bugs related to new functionality - Create `beta` branch from `develop` -- Bump vcmi version on `develop` branch -- Bump version for linux on `develop` branch +- Bump vcmi version in CMake on `develop` branch +- Bump version for Linux on `develop` branch +- Bump version and build ID for Android on `develop` branch + +### Initial release setup (hotfix releases only) + +- Bump vcmi version in CMake on `beta` branch +- Bump version for Linux on `beta` branch +- Bump version and build ID for Android on `beta` branch ### Release preparation stage -Should be done several days before planned release date +Should be done 1 week before release. Release date should be decided at this point. -- Make sure to announce codebase freeze deadline to all developers -- Update [release notes](https://github.com/vcmi/vcmi/blob/develop/ChangeLog.md) -- Update release date for Linux packaging. See [example](https://github.com/vcmi/vcmi/pull/1258) -- Update release date for Android packaging. See [example](https://github.com/vcmi/vcmi/pull/2090) -- Create draft release page, specify `1.x.0` as tag for `master` after publishing -- Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu +- Make sure to announce codebase freeze deadline (1 day before release) to all developers +- Create pull request for release preparation tasks targeting `beta`: +- - Update [changelog](https://github.com/vcmi/vcmi/blob/develop/ChangeLog.md) +- - Update release date for Linux packaging. See [example](https://github.com/vcmi/vcmi/pull/1258) +- - Update build ID for Android packaging. See [example](https://github.com/vcmi/vcmi/pull/2090) +- - Update downloads counter in readme.md. See [example](https://github.com/vcmi/vcmi/pull/2091) + +### 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" +- 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 ### Release publishing phase Should be done on release date -- Check that all items from milestone `Release 1.x` completed -- Merge `beta` into `master` -- Check that artifacts for all platforms are available on `master` branch - Trigger builds for new release on Ubuntu PPA -- Attach build artifacts for all platforms to release page -- Merge prepared pull requests +- Publish new release on Google Play - Publish release page - -### After release publish - -- Close `Release 1.x` milestone -- Write announcements in social networks - Merge `master` into `develop` -- Update downloads counter in readme.md. See [example](https://github.com/vcmi/vcmi/pull/2091) +- Merge prepared PR's for vcmi-updates and for website (if any) +- Close `Release 1.x` project +- Write announcements in social networks diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 5cd6ef760..1f1ff2205 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -34,7 +34,7 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def ... ], - // Allow overriding individual frames in file + // Alternative to "sequences". Allow overriding individual frames in file. Generally should not be used in the same time as "sequences" "images" : [ { @@ -53,6 +53,57 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def } ``` +# Examples + +## 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 + "basepath" : "interface/MyButton", // all images are located in this directory + + "images" : + [ + {"frame" : 0, "file" : "active.png" }, + {"frame" : 1, "file" : "pressed.png" }, + {"frame" : 2, "file" : "blocked.png" }, + {"frame" : 3, "file" : "highlighted.png" }, + ] +} +``` + +## Replacing simple animation + +This json file allows defining one animation sequence, for example for adventure map objects or for town buildings. + +```javascript + "basepath" : "myTown/myBuilding", // all images are located in this directory + + "sequences" : + [ + { + "group" : 0, + "frames" : [ + "frame01.png", + "frame02.png", + "frame03.png", + "frame04.png", + "frame05.png" + ... + ] + } + ] +} +``` + +## Replacing creature animation + +TODO + # Creature animation groups Animation for creatures consist from multiple groups, with each group diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 873d43e37..6836066cc 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -6,13 +6,13 @@ The bonuses were grouped according to their original purpose. The bonus system a ### NONE -Special bonus that gives no effect +Bonus placeholder that gives no effect ### MORALE Changes morale of affected units -- val: change in morale +- val: change in morale, eg. 1, -2 ### LUCK @@ -29,7 +29,7 @@ Changes mastery level of spells of affected heroes and units. Examples are magic ### DARKNESS -On each turn, hides area in fog of war around affected town for all players other than town owner +On each turn, hides area in fog of war around affected town for all players other than town owner. Currently does not work for any entities other than towns. - val: radius in tiles @@ -39,26 +39,34 @@ On each turn, hides area in fog of war around affected town for all players othe Increases amount of movement points available to affected hero on new turn -- subtype: 0 - sea, subtype 1 - land +- 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 Allows movement over water for affected heroes -- subtype: 1 - without penalty, 2 - with penalty +- val: Penalty to movement, in percent (Basic Water Walk - 40, Advanced Water Walk - 20) ### FLYING_MOVEMENT Allows flying movement for affected heroes -- subtype: 1 - without penalty, 2 - with penalty +- val: Penalty to movement, in percent ### NO_TERRAIN_PENALTY Eliminates terrain penalty on certain terrain types for affected heroes (Nomads ability). -- subtype: type of terrain +Note: to eliminate all terrain penalties see ROUGH_TERRAIN_DISCOUNT bonus + +- subtype: type of terrain, eg `terrain.sand` + +### TERRAIN_NATIVE + +Affected units will view any terrain as native. This means army containing these creature will have no movement penalty, and will be able to see Mines and Quick Sand in battle. ### PRIMARY_SKILL @@ -85,47 +93,47 @@ Restores entire mana pool for affected heroes on new turn ### NONEVIL_ALIGNMENT_MIX -Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale +Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale (Angelic Alliance effect) ### SURRENDER_DISCOUNT Changes surrender cost for affected heroes -- val: decrease in cost, in precentage +- val: decrease in cost, in percentage ### IMPROVED_NECROMANCY -TODO: Determine units which is raised by necromancy skill. +Allows to raise different creatures than Skeletons after battle. - subtype: creature raised - val: Necromancer power -- addInfo: limiter by Necromancer power -- Example (from Necromancy skill): +- addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) +- Example (from Cloak Of The Undead King): -` "power" : {` -` "type" : "IMPROVED_NECROMANCY",` -` "subtype" : "creature.skeleton",` -` "addInfo" : 0` -` }` +```jsonc +{ + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 +} +``` ### LEARN_BATTLE_SPELL_CHANCE -Determines chance for affected heroes to learn spell casted by enemy hero after battle +Determines chance for affected heroes to learn spell cast by enemy hero after battle - val: chance to learn spell, percentage ### LEARN_BATTLE_SPELL_LEVEL_LIMIT -Allows affected heroes to learn spell casted by enemy hero after battle +Allows affected heroes to learn spell cast by enemy hero after battle -- subtype: must be set to -1 - val: maximal level of spell that can be learned ### LEARN_MEETING_SPELL_LIMIT Allows affected heroes to learn spells from each other during hero exchange -- subtype: must be set to -1 - val: maximal level of spell that can be learned ### ROUGH_TERRAIN_DISCOUNT @@ -201,7 +209,7 @@ Defines maximum level of spells than hero can learn from any source (Wisdom) ### SPECIAL_SPELL_LEV -Gives additional bonus to effect of specific spell based on level of creature it is casted on +Gives additional bonus to effect of specific spell based on level of creature it is cast on - subtype: identifier of affected spell - val: bonus to spell effect, percentage, divided by target creature level @@ -222,13 +230,43 @@ Gives additional bonus to effect of specific spell ### SPECIAL_PECULIAR_ENCHANT -TODO: blesses and curses with id = val dependent on unit's level +Gives creature under effect of this spell additional bonus, which is hardcoded and depends on the creature tier. -- subtype: 0 or 1 for Coronius +- subtype: affected spell identifier, ie. `spell.haste` + +### SPECIAL_ADD_VALUE_ENCHANT + +Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray decrease target's defense by additional 2 points: + +```jsonc +"disruptingRay" : { + "addInfo" : -2, + "subtype" : "spell.disruptingRay", + "type" : "SPECIAL_ADD_VALUE_ENCHANT" +} +`````` + +- subtype: affected spell identifier +- additionalInfo: value to add + +### SPECIAL_FIXED_VALUE_ENCHANT + +Spell affecting creature has fixed effect, eg. hero Melody has constant spell effect of +3: + +```jsonc +"fortune" : { + "addInfo" : 3, + "subtype" : "spell.fortune", + "type" : "SPECIAL_FIXED_VALUE_ENCHANT" +} +``` + +- subtype: affected spell identifier +- additionalInfo = fixed value ### SPECIAL_UPGRADE -Allows creature upgrade for affected armies +Allows creature being upgraded to another creature (Gelu, Dracon) - subtype: identifier of creature that can being upgraded - addInfo: identifier of creature to which perform an upgrade @@ -237,9 +275,10 @@ Allows creature upgrade for affected armies ### SPELL_DURATION -Changes duration of timed spells casted by affected hero +Changes duration of timed spells cast by affected hero - val: additional duration, turns +- subtype: optional, identifier of affected spells, or all if not set ### SPELL @@ -252,7 +291,7 @@ Allows affected heroes to cast specified spell, even if this spell is banned in Allows affected heroes to cast any spell of specified level. Does not grant spells banned in map options. -- subtype: spell level +- subtype: spell level, in form "spellLevelX" where X is desired level (1-5) ### SPELLS_OF_SCHOOL @@ -272,22 +311,25 @@ Affected heroes will add specified resources amounts to player treasure on new d Increases weekly growth of creatures in affected towns (Legion artifacts) - value: number of additional weekly creatures -- subtype: level of affected dwellings +- subtype: dwelling level, in form `creatureLevelX` where X is desired level (1-7) ### CREATURE_GROWTH_PERCENT -Increases weekly growth of creatures in affected towns +Increases weekly growth of creatures in affected towns (Statue of Legion) - val: additional growth, percentage ### BATTLE_NO_FLEEING -Heroes affected by this bonus can not retreat or surrender in battle +Heroes affected by this bonus can not retreat or surrender in battle (Shackles of War effect) ### NEGATE_ALL_NATURAL_IMMUNITIES -- subtype: TODO -Orb of Vulnerability +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 ### OPENING_BATTLE_SPELL @@ -298,7 +340,7 @@ In battle, army affected by this bonus will cast spell at the very start of the ### FREE_SHIP_BOARDING -Heroes affected by this bonus will keep all their movement points when embarking or disembarking ship +Heroes affected by this bonus will not lose all movement points when embarking or disembarking ship. Movement points are converted depending on max land and water movement range. ### WHIRLPOOL_PROTECTION @@ -322,7 +364,10 @@ Increases movement speed of units in battle Increases base damage of creature in battle -- subtype: 0 = both min and max, 1 = min, 2 = max +- subtype: + - creatureDamageMin: increases only minimal damage + - creatureDamageMax: increases only maximal damage + - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -345,11 +390,11 @@ Affected unit is considered to be a gargoyle and not affected by certain spells ### UNDEAD -Affected unit is considered to be undead +Affected unit is considered to be undead, which makes it immune to many effects, and also reduce morale of allied living units. ### SIEGE_WEAPON -Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. +Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. All War Machines should have this bonus. ### DRAGON_NATURE @@ -379,7 +424,9 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield -- subtype: 0 - flying unit, 1 - teleporting unit +- subtype: + - movementFlying: creature will fly (slowly move across battlefield) + - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. ### SHOOTER @@ -387,21 +434,21 @@ Affected unit can shoot ### CHARGE_IMMUNITY -Affected unit is immune to JOUSTING ability +Affected unit is immune to JOUSTING ability of (ie. Champions). ### ADDITIONAL_ATTACK -Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if can +Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if able. - val: number of additional attacks to perform ### UNLIMITED_RETALIATIONS -Affected unit will always retaliate if able +Affected unit will always retaliate if able (Royal Griffin) ### ADDITIONAL_RETALIATION -Affected unit can retaliate multiple times per turn +Affected unit can retaliate multiple times per turn (basic Griffin) - value: number of additional retaliations @@ -415,7 +462,7 @@ Affected unit will deal more damage based on movement distance (Champions) Affected unit will deal more damage when attacking specific creature -- subtype - identifier of hated creature, +- subtype - identifier of hated creature, ie. "creature.genie" - val - additional damage, percentage ### SPELL_LIKE_ATTACK @@ -443,420 +490,479 @@ Affected unit can return to his starting location after attack (Harpies) ### ENEMY_DEFENCE_REDUCTION -Affected unit will ignore specified percentage of attacked unit defence (Behemoth) +Affected unit will ignore specified percentage of attacked unit defense (Behemoth) -- val: amount of defence points to ignore, percentage +- val: amount of defense points to ignore, percentage ### GENERAL_DAMAGE_REDUCTION -- subtype - 0 - shield (melee) , 1 - air shield effect (ranged), -1 - - armorer secondary skill (all, since 1.2) +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 ### PERCENTAGE_DAMAGE_BOOST -- subtype: -1 - any damage (not used), 0 - melee damage (offence), 1 - - ranged damage (archery) +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 ### GENERAL_ATTACK_REDUCTION -eg. while stoned or blinded - in % +Affected units will deal reduced damage when attacking other units (Blind or Paralyze spells) -- subtype: -1 - any damage, 0 - melee damage, 1 - ranged damage +- val: damage reduction, percentage ### DEFENSIVE_STANCE -WoG ability. +Affected units will receive increased bonus to defense while defending -- val - bonus to defense while defending +- val: additional bonus to defense, in skill points ### NO_DISTANCE_PENALTY +Affected unit will have no distance penalty when shooting. Does not negates wall penalties in sieges + ### NO_MELEE_PENALTY -Ranged stack does full damage in melee. +Affected ranged unit will deal full damage in melee attacks ### NO_WALL_PENALTY +Affected unit will deal full damage when shooting over walls in sieges. Does not negates distance penalty + ### FREE_SHOOTING -stacks can shoot even if otherwise blocked (sharpshooter's bow effect) +Affected unit can use ranged attack even when blocked by enemy unit, like with Bow of the Sharpshooter relic ### BLOCKS_RETALIATION -eg. naga +Affected unit will never receive retaliations when attacking ### SOUL_STEAL -WoG ability. Gains new creatures for each enemy killed +Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed -- subtype: 0 - gained units survive after battle, 1 - they do not +- subtype: + - soulStealPermanent: creature will stay after the battle + - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION -WoG ability. % chance to transform attacked unit to other type +Affected units have chance to transform attacked unit to other creature type -- val: chance to trigger in % -- subtype: 0 - resurrection based on HP, 1 - based on unit count -- addInfo: additional info - target creature ID (attacker default) +- 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 +- addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS -WoG ability. Summon guardians surrounding desired stack +When battle starts, affected units will be surrounded from all side with summoned units -- val - amount in % of stack count -- subtype = creature ID +- val: amount of units to summon, per stack, percentage of summoner stack size +- subtype: identifier of creature to summon ### RANGED_RETALIATION -Allows shooters to perform ranged retaliation - -- no additional parameters +Affected units will retaliate against ranged attacks, if able ### BLOCKS_RANGED_RETALIATION -Disallows ranged retaliation for shooter unit, BLOCKS_RETALIATION bonus -is for melee retaliation only - -- no additional parameters +Affected unit will never receive counterattack in ranged attacks. Counters RANGED_RETALIATION bonus ### WIDE_BREATH -Dragon breath affecting multiple nearby hexes - -- no additional parameters +Affected unit will attack units on all hexes that surround attacked hex ### FIRST_STRIKE -First counterattack, then attack if possible - -- no additional parameters +Affected unit will retaliate before enemy attacks, if able ### SHOOTS_ALL_ADJACENT -H4 Cyclops-like shoot (attacks all hexes neighboring with target) -without spell-like mechanics - -- no additional parameters +Affected unit will attack units on all hexes that surround attacked hex in ranged attacks ### DESTRUCTION -Kills extra units after hit +Affected unit will kills additional units after attack -- subtype: 0 - kill percentage of units, 1 - kill amount -- val: chance in percent to trigger -- addInfo: amount/percentage to kill +- val: chance to trigger, percentage +- subtype: + - destructionKillPercentage: kill percentage of units, + - destructionKillAmount: kill amount +- addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE -Limits shooting range and/or changes long range penalty +Affected unit can use ranged attacks only within specified range - val: max shooting range in hexes -- addInfo: optional - sets range for "broken arrow" half damage - mechanic - default is 10 +- addInfo: optional, range at which ranged penalty will trigger (default is 10) ## Special abilities ### CATAPULT -- subtype: ability to use when catapulting (usually it contains ballistics parameters, ability for standard catapult and affected by ballistics is core:spell.catapultShot) +Affected unit can attack walls during siege battles (Cyclops) + +- subtype: spell that describes attack parameters ### CATAPULT_EXTRA_SHOTS -- subtype: ability to be affected. Ability of CATAPULT bonus should match. Used for ballistics secondary skill with subtype of core:spell.catapultShot. -- val: ability level to be used with catapult. Additional shots configured in ability level, not here. +Defines spell mastery level for spell used by CATAPULT bonus +- subtype: affected spell +- val: spell mastery level to use ### MANUAL_CONTROL -- manually control warmachine with -- id = subtype -- val = chance +Hero can control war machine affected by this bonus + +- subtype: creature identifier of affected war machine +- val: chance to control unit, percentage ### CHANGES_SPELL_COST_FOR_ALLY -eg. mage +Affected units will decrease spell cost for their hero (Mage). If multiple units have this bonus only best value will be used -- value - reduction in mana points +- val: reduction in mana points. ### CHANGES_SPELL_COST_FOR_ENEMY -eg. Silver Pegasus +Affected units will increase spell cost for enemy hero (Silver Pegasus). If multiple units have this bonus only best value will be used -- value - mana points penalty +- val: increase in mana points. ### SPELL_RESISTANCE_AURA -eg. unicorns +All units adjacent to affected unit will receive additional spell resistance bonus. If multiple adjacent units have this bonus only best value will be used -- value - resistance bonus in % for adjacent creatures +- val: additional resistance bonus, percentage ### HP_REGENERATION -creature regenerates val HP every new round +Affected unit will regenerate portion of its health points on its turn. -- val: amount of HP regeneration per round +- val: amount of health points gained per round ### MANA_DRAIN -value - spell points per turn +Affected unit will drain specified amount of mana points from enemy hero on each turn + +val: spell points per turn to take ### MANA_CHANNELING -eg. familiar +Affected unit will give his hero specified portion of mana points spent by enemy hero -- value in % +- val: spell points to give, percentage ### LIFE_DRAIN -- val - precentage of life drained +Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage (Vampire Lord) + +- val: percentage of damage that will be converted into health points ### DOUBLE_DAMAGE_CHANCE -eg. dread knight +Affected unit has chance to deal double damage on attack (Death Knight) -- value in % +- val: chance to trigger, percentage ### FEAR +If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance (Azure Dragon). Blocked by FEARLESS bonus. + ### HEALER -First aid tent +Affected unit acts as healing tent and can heal allied units on each turn -- subtype: ability used for healing. +- subtype: identifier of spell that will be used for healing action ### FIRE_SHIELD +When affected unit is attacked, portion of received damage will be also dealt to the attacked. Units immune to fire magic will not receive this damage. Only melee attacks will trigger this bonus + +- val: amount to deal in return, percentage + ### MAGIC_MIRROR -- value - chance of redirecting in % +If affected unit is targeted by a spell it will reflect spell to a random enemy unit + +- val: chance to trigger, percentage ### ACID_BREATH -- val - damage per creature after attack, -- additional info - chance in percent +Affected unit will deal additional damage after attack + +- val - additional damage to deal, multiplied by unit stack size +- additional info: chance to trigger, percentage ### DEATH_STARE -- subtype: 0 - gorgon, 1 - commander -- val: if subtype is 0, its the (average) percentage of killed - creatures related to size of attacking stack +Affected unit will kill additional units after attack + +- subtype: + - deathStareGorgon: random amount + - deathStareCommander: fixed amount +- val: + - for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula + - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val ### SPECIAL_CRYSTAL_GENERATION -Crystal dragon crystal generation +If player has affected unit under his control in any army, he will receive additional 3 crystals on new week (Crystal Dragons) ### NO_SPELLCAST_BY_DEFAULT -Spellcast will not be default attack option for this creature +Affected unit will not use spellcast as default attack option ## Creature spellcasting and activated abilities ### SPELLCASTER -Creature gain activated ability to cast a spell. Example: Archangel, -Faerie Dragon +Affected units can cast a spell as targeted action (Archangel, Faerie Dragon). Use CASTS bonus to specify how many times per combat creature can use spellcasting. Use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER bonuses to set spell power. -- subtype - spell id -- value - level of school -- additional info - weighted chance - -use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER -for calculating the power (since 1.2 those bonuses can be used for -calculating CATAPULT and HEALER bonuses) +- subtype: spell identifier +- value: spell mastery level +- additional info: weighted chance to select this spell. Can be omitted for always available spells ### ENCHANTER -for Enchanter spells +Affected unit will cast specified spell before his turn (Enchanter) -- val - skill level -- subtype - spell id -- additionalInfo - cooldown (number of turns) +- val - spell mastery level +- subtype - spell identifier +- additionalInfo - cooldown before next cast, in number of turns ### RANDOM_SPELLCASTER -eg. master genie +Affected unit can cast randomly selected beneficial spell on its turn (Master Genie) - val - spell mastery level +### CASTS + +Determines how many times per combat affected creature can cast its targeted spell + +- val: number of casts available per combat + ### SPELL_AFTER_ATTACK -- subtype - spell id -- value - chance % +- subtype - spell id, eg. spell.iceBolt +- value - chance (percent) - additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only + - X - spell level + - Y = 0 - all attacks, 1 - shot only, 2 - melee only ### SPELL_BEFORE_ATTACK - subtype - spell id - value - chance % - additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only + - X - spell level + - Y = 0 - all attacks, 1 - shot only, 2 - melee only -### CASTS +### SPECIFIC_SPELL_POWER -how many times creature can cast activated spell - -### SPECIFIC_SPELL_POWER - -- value used for Thunderbolt and Resurrection casted by units, also - for Healing secondary skill (for core:spell.firstAid used by First - Aid tent) +- 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 ### CREATURE_SPELL_POWER -- value per unit, divided by 100 (so faerie Dragons have 500) +- value: Spell Power of offensive spell cast unit, divided by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack. ### CREATURE_ENCHANT_POWER -total duration of spells casted by creature + - val: Total duration of spells cast by creature, in turns ### REBIRTH -- val - percent of total stack HP restored -- subtype = 0 - regular, 1 - at least one unit (sacred Phoenix) +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. +- subtype: + - rebirthRegular: Phoenix, as described above. + - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) ### ENCHANTED -Stack is permanently enchanted with spell subID of skill level = val, if val > 3 then spell is mass and has level of val-3. Enchantment is refreshed every turn. +Affected unit is permanently enchanted with a spell, that is cast again every turn + +- subtype: spell identifier +- val: spell mastery level. If above 3, then spell has mass effect with mastery level of (val-3) ## Spell immunities ### LEVEL_SPELL_IMMUNITY -creature is immune to all spell with level below or equal to value of this bonus +Affected unit is immune to all spell with level below or equal to value of this bonus + +- val: level of spell up to which this unit is immune to ### MAGIC_RESISTANCE -- value - percent +Affected unit has a chance to resist hostile spell and avoid its effects + +- val: chance to trigger, percentage ### SPELL_DAMAGE_REDUCTION -eg. golems +Affected unit receives decreased damage from spells of specific school (Golems) -- value - reduction in % -- subtype - spell school; -1 - all, 0 - air, 1 - fire, 2 - water, 3 - - earth +- val: reduction to damage, percentage +- subtype - spell school identifier ### MORE_DAMAGE_FROM_SPELL -- value - damage increase in % -- subtype - spell id +Affected unit receives increased damage from specific spell -### FIRE_IMMUNITY,WATER_IMMUNITY, EARTH_IMMUNITY, AIR_IMMUNITY - -- subtype 0 - all, 1 - all except positive, 2 - only damage spells +- val: increase to damage, percentage +- subtype: spell identifer ### MIND_IMMUNITY -Creature is immune to all mind spells. +Affected creature is immune to all mind spells and receives reduced damage from Mind Elemental ### SPELL_IMMUNITY -- subtype - spell id -- ainfo - 0 - normal, 1 - absolute +Affected unit is completely immune to effects of specific spell + +- subtype: identifier of spell to which unit is immune + +### SPELL_SCHOOL_IMMUNITY + +Affected unit is immune to all spells of a specified spell school + +- subtype: spell school to which this unit is immune to ### RECEPTIVE -WoG ability. Creature accepts all friendly spells even though it would -be normally immune to it. +Affected unit can be affected by all friendly spells even it would be normally immune to such spell. # Spell effects ### POISON +Unit affected by poison will lose 10% of max health every combat turn, up to `val`. After that, effect ends. + - val - max health penalty from poison possible ### SLAYER -- value - spell level +Affected unit will deal increased damage to creatures with KING bonus -### BIND_EFFECT - -doesn't do anything particular, works as a marker +- val: skill mastery of Slayer spell, only creatures with lower or equal value of KING bonus are affected ### FORGETFULL -forgetfulness spell effect +Affected unit has its ranged attack power reduced (Forgetfulness) -- value - level +- val: if 0 or 1, damage is reduced by 50%. If greater than 1 then creature can not use ranged attacks ### NOT_ACTIVE +Affected unit can not act and is excluded from turn order (Blind, Stone Gaze, Paralyze) + +- subtype: spell that caused this effect, optional + ### ALWAYS_MINIMUM_DAMAGE -unit does its minimum damage from range +Affected creature always deals its minimum damage -- subtype: -1 - any attack, 0 - melee, 1 - ranged -- value: additional damage penalty (it'll subtracted from dmg) -- additional info - multiplicative anti-bonus for dmg in % \[eg 20 - means that creature will inflict 80% of normal minimal dmg\] +- val: additional decrease to unit's minimum damage, points. Can not reduce damage to zero ### ALWAYS_MAXIMUM_DAMAGE -eg. bless effect +Affected creature always deals its maximum damage. If unit is also affected by ALWAYS_MINIMUM_DAMAGE then only additional bonus to damage will be applied -- subtype: -1 - any attack, 0 - melee, 1 - ranged -- value: additional damage -- additional info - multiplicative bonus for dmg in % +- val: additional increase to unit's maximum damage, points ### ATTACKS_NEAREST_CREATURE -while in berserk +Affected unit can not be controlled by player and instead it will attempt to move and attack nearest unit, friend or foe ### IN_FRENZY -- value - level +Affected unit's defense is reduced to 0 and is transferred to attack with specified multiplier + +- val: multiplier factor with which defense is transferred to attack (percentage) ### HYPNOTIZED +Affected unit is considered to be hypnotized and will be controlled by enemy player + ### NO_RETALIATION -Eg. when blinded or paralyzed. +Affected unit will never retaliate to an attack (Blind, Paralyze) -# Undocumented +# Others -### LEVEL_COUNTER -for commander artifacts +### NEGATIVE_EFFECTS_IMMUNITY + +Affected unit is immune to all negative spells of specified spell school + +- subtype: affected spell school ### BLOCK_MAGIC_ABOVE -blocks casting spells of the level > value + +Blocks casting spells of the level above specified one in battles affected by this bonus + +- val: level above which spellcasting is blocked ### BLOCK_ALL_MAGIC -blocks casting spells -### SPELL_IMMUNITY -subid - spell id +Blocks casting of all spells in battles affected by this bonus ### GENERAL_DAMAGE_PREMY -### ADDITIONAL_UNITS -val of units with id = subtype will be added to hero's army at the beginning of battle +Affected unit will deal more damage in all attacks (Adela specialty) -### SPOILS_OF_WAR -val * 10^-6 * gained exp resources of subtype will be given to hero after battle - -### BLOCK +- val: additional damage, percentage ### DISGUISED -subtype - spell level + +Affected heroes will be under effect of Disguise spell, hiding some of their information from opposing players + +- val: spell mastery level ### VISIONS -subtype - spell level -### SYNERGY_TARGET -dummy skill for alternative upgrades mod +Affected heroes will be under effect of Visions spell, revealing information of enemy objects in specific range + +- 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 ### BLOCK_MAGIC_BELOW -blocks casting spells of the level < value -### SPECIAL_ADD_VALUE_ENCHANT -specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add +Blocks casting spells of the level below specified one in battles affected by this bonus -### SPECIAL_FIXED_VALUE_ENCHANT -specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. +- val: level below which spellcasting is blocked + +### BIND_EFFECT + +Dummy bonus that acts as marker for Dendroid's Bind ability + +### SYNERGY_TARGET + +Dummy skill for alternative upgrades mod ### TOWN_MAGIC_WELL -one-time pseudo-bonus to implement Magic Well in the town + +Internal bonus, do not use + +### LEVEL_COUNTER + +Internal bonus, do not use + diff --git a/docs/modders/Bonus/Bonus_Updaters.md b/docs/modders/Bonus/Bonus_Updaters.md index 3839d6653..372bc4472 100644 --- a/docs/modders/Bonus/Bonus_Updaters.md +++ b/docs/modders/Bonus/Bonus_Updaters.md @@ -1,5 +1,7 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / [Bonus Format](../Bonus_Format.md) / Bonus Updaters +TODO: this page may be incorrect or outdated + # List of Bonus Updaters Updaters come in two forms: simple and complex. Simple updaters take no diff --git a/docs/modders/Bonus_Format.md b/docs/modders/Bonus_Format.md index bd51188a7..0075bbdb9 100644 --- a/docs/modders/Bonus_Format.md +++ b/docs/modders/Bonus_Format.md @@ -6,29 +6,59 @@ All parameters but type are optional. ``` javascript { + // Type of the bonus. See Bonus Types for full list "type": "BONUS_TYPE", + + // Subtype of the bonus. Function depends on bonus type. "subtype": 0, + + // Value of the bonus. Function depends on bonus type. "val" : 0, + + // Describes how this bonus is accumulated with other bonuses of the same type "valueType": "VALUE_TYPE", + + // Additional info that bonus might need. Function depends on bonus type. "addInfo" : 0, // or [1, 2, ...] + // How long this bonus should be active until removal. + // May use multiple triggers, in which case first event will remove this bonus "duration" : "BONUS_DURATION", //or ["BONUS_DURATION1", "BONUS_DURATION2", ...]" + + // How long this bonus should remain, in days or battle turns (depending on bonus duration) "turns" : 0, + // TODO "targetSourceType" : "SOURCE_TYPE", + + // TODO "sourceType" : "SOURCE_TYPE", + + // TODO "sourceID" : 0, + + // TODO "effectRange" : "EFFECT_RANGE", + // TODO "limiters" : [ "PREDEFINED_LIMITER", optional_parameters (...), //whhich one is preferred? {"type" : LIMITER_TYPE, "parameters" : [1,2,3]} ], + // TODO "propagator" : ["PROPAGATOR_TYPE", optional_parameters (...)], + + // TODO "updater" : {Bonus Updater}, + + // TODO "propagationUpdater" : {Bonus Updater, but works during propagation}, + + // TODO "description" : "", + + // TODO "stacking" : "" } ``` @@ -47,44 +77,8 @@ All parameters but type are optional. ## Subtype resolution All string identifiers of items can be used in "subtype" field. This allows cross-referencing between the mods and make config file more readable. - -### Available prefixes - -- creature. -- artifact. -- skill: -``` javascript - "pathfinding", "archery", "logistics", "scouting", "diplomacy", - "navigation", "leadership", "wisdom", "mysticism", "luck", - "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", - "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", - "artillery", "learning", "offence", "armorer", "intelligence", - "sorcery", "resistance", "firstAid" -``` - -- resource: -``` javascript - "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" -``` - -- hero. -- faction. -- spell. -- primarySkill -``` javascript - "attack", "defence", "spellpower", "knowledge" -``` - -- terrain: -``` javascript - "dirt", "sand", "grass", "snow", "swamp", "rough", "subterra", "lava", "water", "rock" -``` - -- spellSchool -```javascript -"any", "fire", "air", "water", "earth" -``` - +See [Game Identifiers](Game_Identifiers.md) for full list of available identifiers + ### Example ``` javascript diff --git a/docs/modders/Building_Bonuses.md b/docs/modders/Building_Bonuses.md index 59804a915..b52aac1ee 100644 --- a/docs/modders/Building_Bonuses.md +++ b/docs/modders/Building_Bonuses.md @@ -3,6 +3,8 @@ 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 Hardcoded functionalities, selectable but not configurable. In future diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 32500d269..2458fb320 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -88,6 +88,7 @@ Prolog and epilog properties are optional { "video": "NEUTRALA.smk", //video to show "music": "musicFile.ogg", //music to play, should be located in music directory + "voice": "musicFile.wav", //voice to play, should be located in sounds directory "text": "some long text" //text to be shown } ``` diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index d13af3a64..9d3234e10 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -517,6 +517,22 @@ Configurable object has following structure: `"text"`: [text](#text), +### TextBox + +`"type": "textBox"` + +`"name": "string"` optional, object name + +`"font"`: [font](#font) + +`"alignment"`: [alignment](#text-alignment), + +`"color"`: [color](#color), + +`"text"`: [text](#text), + +`"rect"`: [rect](#rect) + ### Picture `"type": "picture"` @@ -559,6 +575,18 @@ Filling area with texture `"rect"`: [rect](#rect) +### TransparentFilledRectangle + +`"type": "transparentFilledRectangle"` + +`"name": "string"` optional, object name + +`"color"`: [color](#color) fill color of rectangle (supports transparency) + +`"colorLine"`: [color](#color) optional, 1px border color + +`"rect"`: [rect](#rect) + ### Animation `"type": "animation"` diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md new file mode 100644 index 000000000..75c44d768 --- /dev/null +++ b/docs/modders/Difficulty.md @@ -0,0 +1,67 @@ +< [Documentation](../Readme.md) / [Modding](Readme.md) / 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 + +Difficulty configuration is located in [config/difficulty.json](../config/difficulty.json) file and can be overriden by mods. + +## Format summary + +``` javascript +{ + "human": //parameters impacting human players only + { + "pawn": //parameters for specific difficulty + { + //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 + "globalBonuses": [], + //bonuses will be given to player every battle + "battleBonuses": [] + }, + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + }, + "ai": //parameters impacting AI players only + { + "pawn": {}, //parameters for specific difficulty + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + } +} +``` + +## Bonuses + +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). + +`globalBonuses` are given to player on the begining 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`. + +For both types of bonuses, `source` should be specified as `OTHER`. + +## Example + +```js +{ //will give 150% extra health to all players' creatures if specified in "battleBonuses" array + "type" : "STACK_HEALTH", + "val" : 150, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "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 diff --git a/docs/modders/Entities_Format/Battle_Obstacle_Format.md b/docs/modders/Entities_Format/Battle_Obstacle_Format.md index e0d64eeb4..8454539d1 100644 --- a/docs/modders/Entities_Format/Battle_Obstacle_Format.md +++ b/docs/modders/Entities_Format/Battle_Obstacle_Format.md @@ -1,3 +1,27 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Battle Obstacle Format -TODO \ No newline at end of file +```jsonc + // List of terrains on which this obstacle can be used + "allowedTerrains" : [] + + // List of special battlefields on which this obstacle can be used + "specialBattlefields" : [] + + // If set to true, this obstacle will use absolute coordinates. Only one such obstacle can appear on the battlefield + "absolute" : false + + // Width of an obstacle, in hexes + "width" : 1 + + // Height of an obstacle, in hexes + "height" : 1 + + // List of tiles blocked by an obstacles. For non-absolute obstacles uses relative hex indices + "blockedTiles" : [ 0, 20, 50 ] + + // For absolute obstacle - image with static obstacle. For non-absolute - animation with an obstacle + "animation" : "", + + // 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 bdbc098c5..616b262a9 100644 --- a/docs/modders/Entities_Format/Battlefield_Format.md +++ b/docs/modders/Entities_Format/Battlefield_Format.md @@ -1,3 +1,19 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Battlefield Format -TODO \ No newline at end of file +```jsonc + // Human-readable name of the battlefield + "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 + + // List of bonuses that will affect all battles on this battlefield + "bonuses" : { BONUS_FORMAT } + + // Background image for this battlefield + "graphics" : "" + + // List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles) + "impassableHexes" : [ 10, 20, 50 ] +``` \ No newline at end of file diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 4601cb423..a67b1c45f 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/docs/modders/Entities_Format/Hero_Class_Format.md @@ -26,14 +26,14 @@ In order to make functional hero class you also need: } }, - // Description of map object representing this hero class. See map template format for details + // Description of map object representing this hero class. "mapObject" : { // Optional, hero ID-base filter, using same rules as building requirements "filters" : { "mutare" : [ "anyOf", [ "mutare" ], [ "mutareDrake" ]] }, - // List of templates used for this object, normally - only one is needed + // List of templates used for this object, normally - only one is needed. See map template format for details "templates" : { "normal" : { "animation" : "AH00_.def" } } diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index def2a12cb..646c35611 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -119,14 +119,17 @@ In order to make functional hero you also need: // Description of specialty mechanics using bonuses (with updaters) "specialty" : { - // to be merged with all bonuses, use for specialties with multiple similar bonuses (optional) + + // Optional. Section that will be added into every bonus instance, for use in specialties with multiple similar bonuses "base" : {common bonus properties}, + + // List of bonuses added by this specialty. See bonus format for more details "bonuses" : { // use updaters for bonuses that grow with level "someBonus" : {Bonus Format}, "anotherOne" : {Bonus Format} }, - // adds creature specialty following the HMM3 default formula + // Optional. Shortcut for defining creature specialty, using standard H3 rules "creature" : "griffin" } } diff --git a/docs/modders/Entities_Format/River_Format.md b/docs/modders/Entities_Format/River_Format.md index 8b157aa90..1b4f96cda 100644 --- a/docs/modders/Entities_Format/River_Format.md +++ b/docs/modders/Entities_Format/River_Format.md @@ -1,3 +1,31 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / River Format -TODO \ No newline at end of file +## Format + +```jsonc +"newRiver" : +{ + // Two-letters unique indentifier for this river. Used in map format + "shortIdentifier" : "mr", + + // Human-readable name of the river + "text" : "My Road", + + // Name of file with river graphics + "tilesFilename" : "myRiver.def" + + // Name of file with river delta graphics + // TODO: describe how exactly to use this tricky field + "delta" : "", + + // If defined, river will be animated using palette color cycling effect + // Game will cycle "length" colors starting from "start" (zero-based index) on each animation update every 180ms + // Color numbering uses palette color indexes, as seen in image editor + // Note that some tools for working with .def files may reorder palette. + // To avoid this, it is possible to use json with indexed png images instead of def files + "paletteAnimation" : [ + { "start" : 10, "length" : 5 }, + ... + ] +} +``` \ 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 a15c0af36..c615decdf 100644 --- a/docs/modders/Entities_Format/Road_Format.md +++ b/docs/modders/Entities_Format/Road_Format.md @@ -1,3 +1,20 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Road Format -TODO \ No newline at end of file +## Format + +```jsonc +"newRoad" : +{ + // Two-letters unique indentifier for this road. Used in map format + "shortIdentifier" : "mr", + + // Human-readable name of the road + "text" : "My Road", + + // Name of file with road graphics + "tilesFilename" : "myRoad.def" + + // How many movement points needed to move hero + "moveCost" : 66 +} +``` \ 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 3bd3882e3..350851acf 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -5,63 +5,79 @@ ``` javascript { "spellName": - { //numeric id of spell required only for original spells, prohibited for new spells - "index": 0, - //Original Heroes 3 info - //Mandatory, spell type - "type": "adventure",//"adventure", "combat", "ability" + { + // Mandatory. Spell type + // Allowed values: "adventure", "combat", "ability" + "type": "adventure", - //Mandatory, spell target type - "targetType":"NO_TARGET",//"CREATURE","OBSTACLE"."LOCATION" + // Mandatory. Spell target type + // "NO_TARGET" - instant cast no aiming (e.g. Armageddon) + // "CREATURE" - target is unit (e.g. Resurrection) + // "OBSTACLE" - target is obstacle (e.g. Remove Obstacle) + // "LOCATION" - target is location (e.g. Fire Wall) + "targetType":"NO_TARGET", - //Mandatory + // Localizable name of this spell "name": "Localizable name", - //Mandatory, flags structure of school names, Spell schools this spell belongs to + + // Mandatory. List of spell schools this spell belongs to "school": {"air":true, "earth":true, "fire":true, "water":true}, - //number, mandatory, Spell level, value in range 1-5 + + // Mandatory. Spell level, value in range 1-5, or 0 for abilities "level": 1, - //Mandatory, base power + + // Mandatory. Base power of the spell "power": 10, - //Mandatory, default chance for this spell to appear in Mage Guilds - //Used only if chance for a faction is not set in gainChance field + + // Mandatory. Default chance for this spell to appear in Mage Guilds + // Used only if chance for a faction is not set in gainChance field "defaultGainChance": 0, - //Optional, chance for it to appear in Mage Guild of a specific faction - //NOTE: this field is linker with faction configuration + + // Chance for this spell to appear in Mage Guild of a specific faction + // Symmetric property of "guildSpells" property in towns "gainChance": { - "factionName": 3 + "factionName" : 3 }, - //VCMI info - + "animation":{}, - //countering spells, flags structure of spell ids (spell. prefix is required) - "counters": {"spell.spellID1":true, ...} + // List of spells that will be countered by this spell + "counters": { + "spellID" : true, + ... + }, - //Mandatory,flags structure: - // indifferent, negative, positive - Positiveness of spell for target (required) - // damage - spell does damage (direct or indirect) - // offensive - direct damage (implicitly sets damage and negative) - // rising - rising spell (implicitly sets positive) - // summoning //todo: - // special - can be obtained only with bonus::SPELL + //Mandatory. List of flags that describe this spell + // positive - this spell is positive to target (buff) + // negative - this spell is negative to target (debuff) + // indifferent - spell is neither positive, nor negative + // damage - spell does damage (direct or indirect) + // offensive - direct damage (implicitly sets damage and negative) + // rising - rising spell (implicitly sets positive) + // special - this spell is normally unavailable and can only be received explicitly, e.g. from bonus SPELL + // nonMagical - this spell is not affected by Sorcery or magic resistance. School resistances apply. + "flags" : { + "positive": true, + }, + + // If true, spell won't be available on a map without water + "onlyOnWaterMap" : true, - "flags" : {"flag1": true, "flag2": true}, - - //DEPRECATED | optional| no default | flags structure of bonus names,any one of these bonus grants immunity. Negatable by the Orb. + //TODO: DEPRECATED | optional| no default | flags structure of bonus names,any one of these bonus grants immunity. Negatable by the Orb. "immunity": {"BONUS_NAME":true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names + //TODO: DEPRECATED | optional| no default | flags structure of bonus names //any one of these bonus grants immunity, cant be negated "absoluteImmunity": {"BONUS_NAME": true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Negatable by the Orb. + //TODO: DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Negatable by the Orb. "limit": {"BONUS_NAME": true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Cant be negated + //TODO: DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Cant be negated "absoluteLimit": {"BONUS_NAME": true, ...}, - //[WIP] optional | default no limit no immunity + //TODO: optional | default no limit no immunity // "targetCondition" { //at least one required to be affected @@ -86,44 +102,34 @@ } } - - //graphics; mandatory; object; "graphics": { - // ! will be moved to bonus type config in next bonus config version - // iconImmune - OPTIONAL; string; - //resource path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES) + // resource path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES) "iconImmune":"ZVS/LIB1.RES/E_SPMET", - - // iconScenarioBonus- mandatory, string, image resource path - //resource path of icon for scenario bonus + // resource path of icon for scenario bonus "iconScenarioBonus": "MYSPELL_B", - // iconEffect- mandatory, string, image resource path - //resource path of icon for spell effects during battle + // resource path of icon for spell effects during battle "iconEffect": "MYSPELL_E", - // iconBook- mandatory, string, image resource path - //resource path of icon for spellbook + // resource path of icon for spellbook "iconBook": "MYSPELL_E", - // iconScroll- mandatory, string, image resource path - //resource path of icon for spell scrolls + // resource path of icon for spell scrolls "iconScroll": "MYSPELL_E" }, - //OPTIONAL; object; TODO "sounds": { - //OPTIONAL; resourse path, casting sound + //Resourse path of cast sound "cast":"LIGHTBLT" }, - //Mandatory structure - //configuration for no skill, basic, adv, expert + // Mandatory structure + // configuration for no skill, basic, adv, expert "levels":{ "base": {Spell level base format}, "none": {Spell level format}, @@ -138,8 +144,9 @@ # Animation format -``` javascript +TODO +``` javascript { "projectile": [ {"minimumAngle": 0 ,"defName":"C20SPX4"}, @@ -148,6 +155,7 @@ {"minimumAngle": 1.20 ,"defName":"C20SPX1"}, {"minimumAngle": 1.50 ,"defName":"C20SPX0"} ], + "cast" : [] "hit":["C20SPX"], "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] } @@ -162,28 +170,25 @@ 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 - "base":{ - - "range": 0 + "range": 0 }, "expert":{ -"range": "X" + "range": "X" } ``` # Spell level format +TODO + ``` javascript { - //Mandatory, localizable description - //Use {xxx} for formatting + //Mandatory, localizable description. Use {xxx} for formatting "description": "", - - //Mandatory, number, - //cost in mana points + //Mandatory, cost in mana points "cost": 1, //Mandatory, number @@ -194,14 +199,13 @@ This will make spell affect single target on all levels except expert, where it //Mandatory, flags structure //TODO // modifiers make sense for creature target - // - // "targetModifier": { "smart": false, //true: friendly/hostile based on positiveness; false: all targets "clearTarget": false, "clearAffected": false, - } + }, + //Mandatory //spell range description in SRSL // range "X" + smart modifier = enchanter casting, expert massive spells @@ -246,6 +250,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## Special effect common format +TODO + ``` javascript "mod:effectId":{ @@ -265,6 +271,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## catapult +TODO + ``` javascript "mod:effectId":{ @@ -282,6 +290,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## Clone +TODO + Configurable version of Clone spell. ``` javascript @@ -296,6 +306,8 @@ Configurable version of Clone spell. ## Damage effect +TODO + If effect is automatic, spell behave like offensive spell (uses power, levelPower etc) ``` javascript @@ -313,34 +325,36 @@ If effect is automatic, spell behave like offensive spell (uses power, levelPowe ## Dispel -documetation +TODO ## Heal -documetation +TODO ## Obstacle -documetation +TODO ## Remove obstacle -documetation +TODO ## Sacrifice -documetation +TODO ## Summon -documetation +TODO ## Teleport -documetation +TODO ## Timed +TODO + If effect is automatic, spell behave like \[de\]buff spell (effect and cumulativeEffects ignored) @@ -362,6 +376,8 @@ cumulativeEffects ignored) ## Targets, ranges, modifiers +TODO + - CREATURE target (only battle spells) - range 0: smart assumed single creature target - range "X" + smart modifier = enchanter casting, expert massive spells diff --git a/docs/modders/Entities_Format/Terrain_Format.md b/docs/modders/Entities_Format/Terrain_Format.md index f8361514c..22a856aa1 100644 --- a/docs/modders/Entities_Format/Terrain_Format.md +++ b/docs/modders/Entities_Format/Terrain_Format.md @@ -1,3 +1,78 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Terrain Format -TODO \ No newline at end of file +## Format + +```jsonc +"newTerrain" : +{ + // Two-letters unique indentifier for this terrain. Used in map format + "shortIdentifier" : "mt", + + // Human-readable name of the terrain + "text" : "My Road", + + // 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 + // 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" ], + + // Name of file with road graphics + "tiles" : "myRoad.def", + + // How many movement points needed to move hero on this terrain + "moveCost" : 150, + + // The name of rock type terrain which will be used as borders in the underground + // By default, H3 terrain "rock" will be used + "rockTerrain" : "rock", + + // River type which should be used for that terrain + "river" : "", + + // If defined, terrain will be animated using palette color cycling effect + // Game will cycle "length" colors starting from "start" (zero-based index) on each animation update every 180ms + // Color numbering uses palette color indexes, as seen in image editor + // Note that some tools for working with .def files may reorder palette. + // To avoid this, it is possible to use json with indexed png images instead of def files + "paletteAnimation" : [ + { "start" : 10, "length" : 5 }, + ... + ], + + // List of battleFields that can be used on this terrain + "battleFields" : [ ] + + // Color of terrain on minimap without unpassable objects. RGB triplet, 0-255 range + "minimapUnblocked" : [ 150, 100, 50 ], + + // 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" : "", + + "sounds" : { + // List of ambient sounds for this terrain + "ambient" : [ "" ] + }, + + // Hero movement sound for this terrain, version for moving on tiles with road + "horseSound" : "", + + // Hero movement sound for this terrain, version for moving on tiles without road + "horseSoundPenalty" : "", + + // List or terrain names, which is prohibited to make transition from/to + "prohibitTransitions" : [ "" ], + + // If sand/dirt transition required from/to other terrains + "transitionRequired" : false, + + // Represents layout of tile orientations in terrain tiles file + // Can be normal, dirt, water, rock, or hota + "terrainViewPatterns" : "", + +} +``` \ No newline at end of file diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md new file mode 100644 index 000000000..14c3742e6 --- /dev/null +++ b/docs/modders/Game_Identifiers.md @@ -0,0 +1,778 @@ +< [Documentation](../Readme.md) / [Modding](Readme.md) / 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 + +### artifact + +- artifact.admiralsHat +- artifact.ambassadorsSash +- artifact.ammoCart +- artifact.amuletOfTheUndertaker +- artifact.angelFeatherArrows +- artifact.angelWings +- artifact.angelicAlliance +- artifact.armageddonsBlade +- artifact.armorOfTheDamned +- artifact.armorOfWonder +- artifact.armsOfLegion +- artifact.badgeOfCourage +- artifact.ballista +- artifact.birdOfPerception +- artifact.blackshardOfTheDeadKnight +- artifact.bootsOfLevitation +- artifact.bootsOfPolarity +- artifact.bootsOfSpeed +- artifact.bowOfElvenCherrywood +- artifact.bowOfTheSharpshooter +- artifact.bowstringOfTheUnicornsMane +- artifact.breastplateOfBrimstone +- artifact.breastplateOfPetrifiedWood +- artifact.bucklerOfTheGnollKing +- artifact.capeOfConjuring +- artifact.capeOfVelocity +- artifact.cardsOfProphecy +- artifact.catapult +- artifact.celestialNecklaceOfBliss +- artifact.centaurAxe +- artifact.charmOfMana +- artifact.cloakOfTheUndeadKing +- artifact.cloverOfFortune +- artifact.collarOfConjuring +- artifact.cornucopia +- artifact.crestOfValor +- artifact.crownOfDragontooth +- artifact.crownOfTheSupremeMagi +- artifact.deadMansBoots +- artifact.diplomatsRing +- artifact.dragonScaleArmor +- artifact.dragonScaleShield +- artifact.dragonWingTabard +- artifact.dragonboneGreaves +- artifact.elixirOfLife +- artifact.emblemOfCognizance +- artifact.endlessBagOfGold +- artifact.endlessPurseOfGold +- artifact.endlessSackOfGold +- artifact.equestriansGloves +- artifact.everflowingCrystalCloak +- artifact.everpouringVialOfMercury +- artifact.eversmokingRingOfSulfur +- artifact.firstAidTent +- artifact.garnitureOfInterference +- artifact.glyphOfGallantry +- artifact.goldenBow +- artifact.grail +- artifact.greaterGnollsFlail +- artifact.headOfLegion +- artifact.hellstormHelmet +- artifact.helmOfChaos +- artifact.helmOfHeavenlyEnlightenment +- artifact.helmOfTheAlabasterUnicorn +- artifact.hourglassOfTheEvilHour +- artifact.inexhaustibleCartOfLumber +- artifact.inexhaustibleCartOfOre +- artifact.ladybirdOfLuck +- artifact.legsOfLegion +- artifact.lionsShieldOfCourage +- artifact.loinsOfLegion +- artifact.mysticOrbOfMana +- artifact.necklaceOfDragonteeth +- artifact.necklaceOfOceanGuidance +- artifact.necklaceOfSwiftness +- artifact.ogresClubOfHavoc +- artifact.orbOfDrivingRain +- artifact.orbOfInhibition +- artifact.orbOfSilt +- artifact.orbOfTempestuousFire +- artifact.orbOfTheFirmament +- artifact.orbOfVulnerability +- artifact.pendantOfCourage +- artifact.pendantOfDeath +- artifact.pendantOfDispassion +- artifact.pendantOfFreeWill +- artifact.pendantOfHoliness +- artifact.pendantOfLife +- artifact.pendantOfNegativity +- artifact.pendantOfSecondSight +- artifact.pendantOfTotalRecall +- artifact.powerOfTheDragonFather +- artifact.quietEyeOfTheDragon +- artifact.recantersCloak +- artifact.redDragonFlameTongue +- artifact.ribCage +- artifact.ringOfConjuring +- artifact.ringOfInfiniteGems +- artifact.ringOfLife +- artifact.ringOfTheMagi +- artifact.ringOfTheWayfarer +- artifact.ringOfVitality +- artifact.sandalsOfTheSaint +- artifact.scalesOfTheGreaterBasilisk +- artifact.seaCaptainsHat +- artifact.sentinelsShield +- artifact.shacklesOfWar +- artifact.shieldOfTheDamned +- artifact.shieldOfTheDwarvenLords +- artifact.shieldOfTheYawningDead +- artifact.skullHelmet +- artifact.speculum +- artifact.spellBook +- artifact.spellScroll +- artifact.spellbindersHat +- artifact.sphereOfPermanence +- artifact.spiritOfOppression +- artifact.spyglass +- artifact.statesmansMedal +- artifact.statueOfLegion +- artifact.stillEyeOfTheDragon +- artifact.stoicWatchman +- artifact.surcoatOfCounterpoise +- artifact.swordOfHellfire +- artifact.swordOfJudgement +- artifact.talismanOfMana +- artifact.targOfTheRampagingOgre +- artifact.thunderHelmet +- artifact.titansCuirass +- artifact.titansGladius +- artifact.titansThunder +- artifact.tomeOfAirMagic +- artifact.tomeOfEarthMagic +- artifact.tomeOfFireMagic +- artifact.tomeOfWaterMagic +- artifact.torsoOfLegion +- artifact.tunicOfTheCyclopsKing +- artifact.unusedArtifact1 +- artifact.unusedArtifact2 +- artifact.unusedArtifact3 +- artifact.vampiresCowl +- artifact.vialOfDragonBlood +- artifact.vialOfLifeblood +- artifact.wizardsWell + +### battlefield + +- battlefield.clover_field +- battlefield.cursed_ground +- battlefield.dirt_birches +- battlefield.dirt_hills +- battlefield.dirt_pines +- battlefield.evil_fog +- battlefield.fiery_fields +- battlefield.grass_hills +- battlefield.grass_pines +- battlefield.holy_ground +- battlefield.lava +- battlefield.lucid_pools +- battlefield.magic_clouds +- battlefield.magic_plains +- battlefield.rocklands +- battlefield.rough +- battlefield.sand_mesas +- battlefield.sand_shore +- battlefield.ship +- battlefield.ship_to_ship +- battlefield.snow_mountains +- battlefield.snow_trees +- battlefield.subterranean +- battlefield.swamp_trees + +### creature + +- creature.airElemental +- creature.airElementals +- creature.ammoCart +- creature.ancientBehemoth +- creature.angel +- creature.apprenticeGremlin +- creature.archDevil +- creature.archMage +- creature.archangel +- creature.archer +- creature.arrowTower +- creature.azureDragon +- creature.ballista +- creature.basilisk +- creature.battleDwarf +- creature.behemoth +- creature.beholder +- creature.blackDragon +- creature.blackKnight +- creature.boar +- creature.boneDragon +- creature.catapult +- creature.cavalier +- creature.centaur +- creature.centaurCaptain +- creature.cerberus +- creature.champion +- creature.chaosHydra +- creature.crusader +- creature.crystalDragon +- creature.cyclop +- creature.cyclopKing +- creature.demon +- creature.dendroidGuard +- creature.dendroidSoldier +- creature.devil +- creature.diamondGolem +- creature.dragonFly +- creature.dreadKnight +- creature.dwarf +- creature.earthElemental +- creature.efreet +- creature.efreetSultan +- creature.enchanter +- creature.enchanters +- creature.energyElemental +- creature.evilEye +- creature.fairieDragon +- creature.familiar +- creature.fireDragonFly +- creature.fireElemental +- creature.firebird +- creature.firstAidTent +- creature.genie +- creature.ghostDragon +- creature.giant +- creature.gnoll +- creature.gnollMarauder +- creature.goblin +- creature.goblinWolfRider +- creature.goblins +- creature.gog +- creature.goldDragon +- creature.goldGolem +- creature.gorgon +- creature.grandElf +- creature.greaterBasilisk +- creature.greenDragon +- creature.gremlin +- creature.griffin +- creature.halberdier +- creature.halfling +- creature.harpy +- creature.harpyHag +- creature.hellHound +- creature.hobgoblin +- creature.hobgoblinWolfRider +- creature.hornedDemon +- creature.hydra +- creature.iceElemental +- creature.imp +- creature.infernalTroglodyte +- creature.ironGolem +- creature.lich +- creature.lightCrossbowman +- creature.lizardWarrior +- creature.lizardman +- creature.mage +- creature.magicElemental +- creature.magmaElemental +- creature.magog +- creature.manticore +- creature.marksman +- creature.masterGenie +- creature.masterGremlin +- creature.medusa +- creature.medusaQueen +- creature.mightyGorgon +- creature.minotaur +- creature.minotaurKing +- creature.monk +- creature.mummy +- creature.naga +- creature.nagaQueen +- creature.nomad +- creature.obsidianGargoyle +- creature.ogre +- creature.ogreMage +- creature.orc +- creature.orcChieftain +- creature.peasant +- creature.pegasus +- creature.phoenix +- creature.pikeman +- creature.pitFiend +- creature.pitLord +- creature.pixie +- creature.pixies +- creature.powerLich +- creature.primitiveLizardman +- creature.psychicElemental +- creature.redDragon +- creature.roc +- creature.rogue +- creature.royalGriffin +- creature.rustDragon +- creature.scorpicore +- creature.serpentFly +- creature.sharpshooter +- creature.sharpshooters +- creature.silverPegasus +- creature.skeleton +- creature.skeletonWarrior +- creature.sprite +- creature.stoneGargoyle +- creature.stoneGolem +- creature.stormElemental +- creature.swordsman +- creature.thunderbird +- creature.titan +- creature.troglodyte +- creature.troll +- creature.unicorn +- creature.unused122 +- creature.unused124 +- creature.unused126 +- creature.unused128 +- creature.vampire +- creature.vampireLord +- creature.walkingDead +- creature.warUnicorn +- creature.waterElemental +- creature.waterElementals +- creature.wight +- creature.woodElf +- creature.wraith +- creature.wyvern +- creature.wyvernMonarch +- creature.zealot +- creature.zombie +- creature.zombieLord + +### faction + +- faction.castle +- faction.conflux +- faction.dungeon +- faction.fortress +- faction.inferno +- faction.necropolis +- faction.neutral +- faction.rampart +- faction.stronghold +- faction.tower + +### hero + +- hero.adela +- hero.adelaide +- hero.adrienne +- hero.aenain +- hero.aeris +- hero.aine +- hero.aislinn +- hero.ajit +- hero.alagar +- hero.alamar +- hero.alkin +- hero.andra +- hero.arlach +- hero.ash +- hero.astral +- hero.axsis +- hero.ayden +- hero.boragus +- hero.brissa +- hero.broghild +- hero.bron +- hero.caitlin +- hero.calh +- hero.calid +- hero.catherine +- hero.charna +- hero.christian +- hero.ciele +- hero.clancy +- hero.clavius +- hero.coronius +- hero.cragHack +- hero.cuthbert +- hero.cyra +- hero.dace +- hero.damacon +- hero.daremyth +- hero.darkstorn +- hero.deemer +- hero.dessa +- hero.dracon +- hero.drakon +- hero.edric +- hero.elleshar +- hero.erdamon +- hero.fafner +- hero.fiona +- hero.fiur +- hero.galthran +- hero.gelare +- hero.gelu +- hero.gem +- hero.geon +- hero.gerwulf +- hero.gird +- hero.gretchin +- hero.grindan +- hero.gundula +- hero.gunnar +- hero.gurnisson +- hero.halon +- hero.ignatius +- hero.ignissa +- hero.ingham +- hero.inteus +- hero.iona +- hero.isra +- hero.ivor +- hero.jabarkas +- hero.jaegar +- hero.jeddite +- hero.jenova +- hero.josephine +- hero.kalt +- hero.kilgor +- hero.korbac +- hero.krellion +- hero.kyrre +- hero.labetha +- hero.lacus +- hero.lordHaart +- hero.lorelei +- hero.loynis +- hero.luna +- hero.malcom +- hero.malekith +- hero.marius +- hero.melodia +- hero.mephala +- hero.merist +- hero.mirlanda +- hero.moandor +- hero.monere +- hero.mutare +- hero.mutareDrake +- hero.nagash +- hero.neela +- hero.nimbus +- hero.nymus +- hero.octavia +- hero.olema +- hero.oris +- hero.orrin +- hero.pasis +- hero.piquedram +- hero.pyre +- hero.rashka +- hero.rion +- hero.rissa +- hero.roland +- hero.rosic +- hero.ryland +- hero.sandro +- hero.sanya +- hero.saurug +- hero.sephinroth +- hero.septienna +- hero.serena +- hero.shakti +- hero.shiva +- hero.sirMullich +- hero.solmyr +- hero.sorsha +- hero.straker +- hero.styg +- hero.sylvia +- hero.synca +- hero.tamika +- hero.tazar +- hero.terek +- hero.thane +- hero.thant +- hero.theodorus +- hero.thorgrim +- hero.thunar +- hero.tiva +- hero.torosar +- hero.tyraxor +- hero.tyris +- hero.ufretin +- hero.uland +- hero.undeadHaart +- hero.valeska +- hero.verdish +- hero.vey +- hero.vidomina +- hero.vokial +- hero.voy +- hero.wystan +- hero.xarfax +- hero.xeron +- hero.xsi +- hero.xyron +- hero.yog +- hero.zubin +- hero.zydar + +### heroClass + +- heroClass.alchemist +- heroClass.barbarian +- heroClass.battlemage +- heroClass.beastmaster +- heroClass.cleric +- heroClass.deathknight +- heroClass.demoniac +- heroClass.druid +- heroClass.elementalist +- heroClass.heretic +- heroClass.knight +- heroClass.necromancer +- heroClass.overlord +- heroClass.planeswalker +- heroClass.ranger +- heroClass.warlock +- heroClass.witch +- heroClass.wizard + +### playerColor + +- playerColor.blue +- playerColor.green +- playerColor.orange +- playerColor.pink +- playerColor.purple +- playerColor.red +- playerColor.tan +- playerColor.teal + +### primSkill + +Deprecated, please use primarySkill instead + +- primSkill.attack +- primSkill.defence +- primSkill.knowledge +- primSkill.spellpower + +### primarySkill + +- primarySkill.attack +- primarySkill.defence +- primarySkill.knowledge +- primarySkill.spellpower + +### resource + +- resource.crystal +- resource.gems +- resource.gold +- resource.mercury +- resource.mithril +- resource.ore +- resource.sulfur +- resource.wood + +### river + +- river.iceRiver +- river.lavaRiver +- river.mudRiver +- river.waterRiver + +### road + +- road.cobblestoneRoad +- road.dirtRoad +- road.gravelRoad + +### secondarySkill + +- secondarySkill.airMagic +- secondarySkill.archery +- secondarySkill.armorer +- secondarySkill.artillery +- secondarySkill.ballistics +- secondarySkill.diplomacy +- secondarySkill.eagleEye +- secondarySkill.earthMagic +- secondarySkill.estates +- secondarySkill.fireMagic +- secondarySkill.firstAid +- secondarySkill.intelligence +- secondarySkill.leadership +- secondarySkill.learning +- secondarySkill.logistics +- secondarySkill.luck +- secondarySkill.mysticism +- secondarySkill.navigation +- secondarySkill.necromancy +- secondarySkill.offence +- secondarySkill.pathfinding +- secondarySkill.resistance +- secondarySkill.scholar +- secondarySkill.scouting +- secondarySkill.sorcery +- secondarySkill.tactics +- secondarySkill.waterMagic +- secondarySkill.wisdom + +### skill + +Deprecated, please use secondarySkill instead + +- skill.airMagic +- skill.archery +- skill.armorer +- skill.artillery +- skill.ballistics +- skill.diplomacy +- skill.eagleEye +- skill.earthMagic +- skill.estates +- skill.fireMagic +- skill.firstAid +- skill.intelligence +- skill.leadership +- skill.learning +- skill.logistics +- skill.luck +- skill.mysticism +- skill.navigation +- skill.necromancy +- skill.offence +- skill.pathfinding +- skill.resistance +- skill.scholar +- skill.scouting +- skill.sorcery +- skill.tactics +- skill.waterMagic +- skill.wisdom + +### spell + +- spell.acidBreath +- spell.acidBreathDamage +- spell.age +- spell.airElemental +- spell.airShield +- spell.animateDead +- spell.antiMagic +- spell.armageddon +- spell.berserk +- spell.bind +- spell.bless +- spell.blind +- spell.bloodlust +- spell.castleMoat +- spell.castleMoatTrigger +- spell.catapultShot +- spell.chainLightning +- spell.clone +- spell.counterstrike +- spell.cure +- spell.curse +- spell.cyclopsShot +- spell.deathCloud +- spell.deathRipple +- spell.deathStare +- spell.destroyUndead +- spell.dimensionDoor +- spell.disease +- spell.disguise +- spell.dispel +- spell.dispelHelpful +- spell.disruptingRay +- spell.dungeonMoat +- spell.dungeonMoatTrigger +- spell.earthElemental +- spell.earthquake +- spell.fireElemental +- spell.fireShield +- spell.fireWall +- spell.fireWallTrigger +- spell.fireball +- spell.firstAid +- spell.fly +- spell.forceField +- spell.forgetfulness +- spell.fortressMoat +- spell.fortressMoatTrigger +- spell.fortune +- spell.frenzy +- spell.frostRing +- spell.haste +- spell.hypnotize +- spell.iceBolt +- spell.implosion +- spell.inferno +- spell.infernoMoat +- spell.infernoMoatTrigger +- spell.landMine +- spell.landMineTrigger +- spell.lightningBolt +- spell.magicArrow +- spell.magicMirror +- spell.meteorShower +- spell.mirth +- spell.misfortune +- spell.necropolisMoat +- spell.necropolisMoatTrigger +- spell.paralyze +- spell.poison +- spell.prayer +- spell.precision +- spell.protectAir +- spell.protectEarth +- spell.protectFire +- spell.protectWater +- spell.quicksand +- spell.rampartMoat +- spell.rampartMoatTrigger +- spell.removeObstacle +- spell.resurrection +- spell.sacrifice +- spell.scuttleBoat +- spell.shield +- spell.slayer +- spell.slow +- spell.sorrow +- spell.stoneGaze +- spell.stoneSkin +- spell.strongholdMoat +- spell.strongholdMoatTrigger +- spell.summonBoat +- spell.summonDemons +- spell.teleport +- spell.thunderbolt +- spell.titanBolt +- spell.towerMoat +- spell.townPortal +- spell.viewAir +- spell.viewEarth +- spell.visions +- spell.waterElemental +- spell.waterWalk +- spell.weakness + +### spellSchool + +- spellSchool.air +- spellSchool.any +- spellSchool.earth +- spellSchool.fire +- spellSchool.water + +### terrain + +- terrain.dirt +- terrain.grass +- terrain.lava +- terrain.rock +- terrain.rough +- terrain.sand +- terrain.snow +- terrain.subterra +- terrain.swamp +- terrain.water diff --git a/docs/modders/Map_Objects/Boat.md b/docs/modders/Map_Objects/Boat.md index 30404ce4c..92b5057ef 100644 --- a/docs/modders/Map_Objects/Boat.md +++ b/docs/modders/Map_Objects/Boat.md @@ -1 +1,30 @@ -TODO \ No newline at end of file +< [Documentation](../../Readme.md) / [Modding](../Readme.md) / [Map Object Format](../Map_Object_Format.md) / Boat + +``` javascript +{ + // Layer on which this boat moves. Possible values: + // "land" - same rules as movement of hero on land + // "sail" - object can move on water and interact with objects on water + // "water" - object can walk on water but can not interact with objects + // "air" - object can fly across any terrain but can not interact with objects + "layer" : "", + + // If set to true, it is possible to attack wandering monsters from boat + "onboardAssaultAllowed" : true; + + // If set to true, it is possible to visit object (e.g. pick up resources) from boat + "onboardVisitAllowed" : true; + + // Path to file that contains animated boat movement + "actualAnimation" : "", + + // Path to file that contains animated overlay animation, such as waves around boat + "overlayAnimation" : "", + + // Path to 8 files that contain animated flag of the boat. 8 files, one per each player + "flagAnimations" : ["", "" ], + + // 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/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 1694b7e7a..245719060 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -4,6 +4,7 @@ - [Base object definition](#base-object-definition) - [Configurable object definition](#configurable-object-definition) - [Base object definition](#base-object-definition) +- [Variables Parameters definition](#variables-parameters-definition) - [Reset Parameters definition](#reset-parameters-definition) - [Appear Chance definition](#appear-chance-definition) - [Configurable Properties](#configurable-properties) @@ -17,12 +18,18 @@ - - [Movement Percentage](#movement-percentage) - - [Primary Skills](#primary-skills) - - [Secondary Skills](#secondary-skills) +- - [Can learn skills](#can-learn-skills) - - [Bonus System](#bonus-system) - - [Artifacts](#artifacts) - - [Spells](#spells) +- - [Can learn spells](#can-learn-spells) - - [Creatures](#creatures) - - [Creatures Change](#creatures-change) - - [Spell cast](#spell-cast) +- - [Fog of War](#fog-of-war) +- - [Player color](#player-color) +- - [Hero types](#hero-types) +- - [Hero classes](#hero-classes) ## Base object definition Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode. @@ -72,7 +79,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid if any of these conditions are true "anyOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] @@ -80,32 +87,59 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid only if none of these conditions are true "noneOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } // message that will be shown if this is the only available award "message": "{Warehouse of Crystal}" - // object will be disappeared after taking reward is set to true - "removeObject": false + // Alternative object description that will be used in place of generic description after player visits this object and reveals its content + // For example, Tree of Knowledge will display cost of levelup (gems or gold) only after object has been visited once + "description" : "", - // See "Configurable Properties" section for additiona parameters + // object will be disappeared after taking reward is set to true + "removeObject": false, + + // See "Configurable Properties" section for additional parameters } ], +/// List of variables shared between all rewards and limiters +/// See "Variables" section for description +"variables" : { +} + // If true, hero can not move to visitable tile of the object and will access this object from adjacent tile (e.g. Treasure Chest) "blockedVisitable" : true, // Message that will be shown if there are no applicable awards "onEmptyMessage": "", +// Object description that will be shown when player right-clicks object +"description" : "", + +// If set to true, right-clicking previously visited object would show preview of its content. For example, Witch Hut will show icon with provided skill +"showScoutedPreview" : true, + +// Text that should be used if hero has not visited this object. If not specified, game will use standard "(Not visited)" text +"notVisitedTooltip" : "", + +// Text that should be used if hero has already visited this object. If not specified, game will use standard "(Already visited)" text +"visitedTooltip" : "", + +// Used only if visitMode is set to "limiter" +// Hero that passes this limiter will be considered to have visited this object +// Note that if player or his allies have never visited this object, it will always show up as "not visited" +"visitLimiter" : { +}, + // Alternatively, rewards for empty state: // Format is identical to "rewards" section, allowing to fine-tune behavior in this case, including giving awards or different messages to explain why object is "empty". For example, Tree of Knowledge will give different messages depending on whether it asks for gold or crystals "onEmpty" : [ @@ -133,6 +167,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // determines who can revisit object before reset // "once", - object can only be visited once. First visitor takes it all. // "hero", - object can be visited if this hero has not visited it before +// "limiter", - object can be visited if hero fails to fulfill provided limiter // "player", - object can be visited if this player has not visited it before // "bonus" - object can be visited if hero no longer has bonus from this object (including any other object of the same type) // "unlimited" - no restriction on revisiting. @@ -147,6 +182,39 @@ Rewardable object is defined similarly to other objects, with key difference bei } ``` +## Variables Parameters definition + +This property allows defining "variables" that are shared between all rewards and limiters of this object. +Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut). + +Example of creation of a variable named "gainedSkill" of type "secondarySkill": +```json +"variables" : { + "secondarySkill" : { + "gainedSkill" : { + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } +} +``` + +Possible variable types: +- number: can be used in any place that expects a number +- artifact +- spell +- primarySkill +- secondarySkill + +To reference variable in limiter prepend variable name with '@' symbol: +```json +"secondary" : { + "@gainedSkill" : 1 +}, +``` + ## Reset Parameters definition This property describes how object state should be reset. Objects without this field will never reset its state. - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month. @@ -220,7 +288,7 @@ Keep in mind, that all randomization is performed on map load and on object rese ```jsonc "resources": [ { - "list" : [ "wood", "ore" ], + "anyOf" : [ "wood", "ore" ], "amount" : 10 }, { @@ -349,13 +417,21 @@ Keep in mind, that all randomization is performed on map load and on object rese ] ``` +### Can learn skills + +- Can be used as limiter. Hero must have free skill slot to pass limiter + +```json + "canLearnSkills" : true +``` + ### Bonus System - Can be used as reward, to grant bonus to player - if present, MORALE and LUCK bonus will add corresponding image component to UI. - Note that unlike most values, parameter of bonuses can NOT be randomized - Description can be string or number of corresponding string from `arraytxt.txt` -```jsonc +```json "bonuses" : [ { "type" : "MORALE", @@ -414,6 +490,22 @@ Keep in mind, that all randomization is performed on map load and on object rese ], ``` +### Can learn spells + +- Can be used as limiter. Hero must be able to learn spell to pass the limiter +- Hero is considered to not able to learn the spell if: +- - he already has specified spell +- - he does not have a spellbook +- - he does not have sufficient Wisdom level for this spell + +```json + "canLearnSpells" : [ + "magicArrow" +], +``` + +canLearnSpells + ### Creatures - Can be used as limiter - Can be used as reward, to give new creatures to a hero @@ -445,9 +537,55 @@ Keep in mind, that all randomization is performed on map load and on object rese - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards - School level possible values: 1 (basic), 2 (advanced), 3 (expert) -```jsonc +```json "spellCast" : { "spell" : "townPortal", "schoolLevel": 3 } +``` + +### Fog of War + +- Can NOT be used as limiter +- Can be used as reward, to reveal or hide affected tiles +- If radius is not specified, then all matching tiles on the map will be affected +- It is possible to specify which terrain classes should be affected. Tile will be affected if sum of values its classes is positive. For example, `"water" : 1` will affect all water tiles, while `"surface" : 1, "subterra" : -1` will include terrains that have "surface" flag but do not have "subterra" flag +- If 'hide' is set to true, then instead of revealing terrain, game will hide affected tiles for all other players + +```json +"revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true +} +``` + +### Player color +- Can be used as limiter +- Can NOT be used as reward +- Only players with specific color can pass the limiter + +```jsonc +"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 +"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 +"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 cb47e5a98..2b507974d 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -30,8 +30,13 @@ "version" : "1.2.3" // Type of mod, list of all possible values: - // "Translation", "Town", "Test", "Templates", "Spells", "Music", "Sounds", "Skills", "Other", "Objects", - // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Artifacts", "AI" + // "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", + // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Artifacts", "AI" + // + // Some mod types have additional effects on your mod: + // Translation: mod of this type is only active if player uses base language of this mod. See "language" property. + // Additionally, if such type is used for submod it will be hidden in UI and automatically activated if player uses base language of this mod. This allows to provide locale-specific resources for a mod + // Compatibility: mods of this type are hidden in UI and will be automatically activated if all mod dependencies are active. Intended to be used to provide compatibility patches between mods "modType" : "Graphical", // Base language of the mod, before applying localizations. By default vcmi assumes English @@ -178,6 +183,39 @@ These are fields that are present only in local mod.json file } ``` +## Translation fields + +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 + +``` + "" : { + "name" : "", + "description" : "", + "author" : "", + "translations" : [ + "config//.json" + ] + }, +``` + +## Mod repository fields + +These are fields that are present only in remote repository and are generally not used in mod.json + +```jsonc +{ + // URL to mod.json that describes this mod + "mod" : "https://raw.githubusercontent.com/vcmi-mods/vcmi-extras/vcmi-1.4/mod.json", + + // URL that player can use to download mod + "download" : "https://github.com/vcmi-mods/vcmi-extras/archive/refs/heads/vcmi-1.4.zip", + + // Approximate size of download, megabytes + "downloadSize" : 4.496 +} +``` + ## Notes For mod description it is possible to use certain subset of HTML as diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 886ce5414..0e31d9da7 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -6,9 +6,8 @@ /// Unique template name "Triangle" : { - //optional name - useful to have several template variations with same name (since 0.99) + //Optional name - useful to have several template variations with same name "name" : "Custom template name", - "description" : "Brief description of template, recommended setting or rules". /// Minimal and maximal size of the map. Possible formats: /// Size code: s, m, l or xl for size with optional suffix "+u" for underground @@ -19,8 +18,8 @@ /// Number of players that will be present on map (human or AI) "players" : "2-4", - /// Number of AI-only players - "cpu" : "2", + /// Since 1.4.0 - Optional, number of human-only players (as in original templates) + "humans" : "1-4", ///Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified "allowedWaterContent" : ["none", "normal", "islands"] @@ -37,9 +36,9 @@ { "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" - //"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 + //"type" can be "guarded" (default), "wide", "fictive" or "repulsive" + //"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 ] } ``` @@ -48,38 +47,71 @@ ``` javascript { - "type" : "playerStart", //"cpuStart" "treasure" "junction" - "size" : 2, //relative size of zone - "owner" : 1, //player owned this zone + // Type of this zone. Possible values are: + // "playerStart", "cpuStart", "treasure", "junction" + "type" : "playerStart", + + // relative size of zone + "size" : 2, + + // index of player that owns this zone + "owner" : 1, + + // castles and towns owned by player in this zone "playerTowns" : { "castles" : 1 - //"towns" : 1 + "towns" : 1 }, + + // castles and towns that are neutral on game start in this zone "neutralTowns" : { //"castles" : 1 "towns" : 1 }, + + // if true, all towns generated in this zone will belong to the same faction "townsAreSameType" : true, - "monsters" : "normal", //"weak" "strong", "none" - All treasures will be unguarded + + //"weak" "strong", "none" - All treasures will be unguarded + "monsters" : "normal", - "terrainTypes" : [ "sand" ], //possible terrain types. All terrains will be available if not specified - "bannedTerrains" : ["lava", "asphalt"] //optional + //possible terrain types. All terrains will be available if not specified + "terrainTypes" : [ "sand" ], + + //optional, list of explicitly banned terrain types + "bannedTerrains" : ["lava", "asphalt"] - "matchTerrainToTown" : false, //if true, terrain for this zone will match native terrain of player faction + // if true, terrain for this zone will match native terrain of player faction. Used only in owned zones + "matchTerrainToTown" : false, + + // Mines will have same configuration as in linked zone "minesLikeZone" : 1, + + // Treasures will have same configuration as in linked zone "treasureLikeZone" : 1 + + // Terrain type will have same configuration as in linked zone "terrainTypeLikeZone" : 3 - "allowedMonsters" : ["inferno", "necropolis"] //factions of monsters allowed on this zone - "bannedMonsters" : ["fortress", "stronghold", "conflux"] //These monsers will never appear in the zone - "allowedTowns" : ["castle", "tower", "rampart"] //towns allowed on this terrain - "bannedTowns" : ["necropolis"] //towns will never spawn on this terrain + // factions of monsters allowed on this zone + "allowedMonsters" : ["inferno", "necropolis"] + + // These monsers will never appear in the zone + "bannedMonsters" : ["fortress", "stronghold", "conflux"] + + // towns allowed on this terrain + "allowedTowns" : ["castle", "tower", "rampart"] + + // towns will never spawn on this terrain + "bannedTowns" : ["necropolis"] + // List of mines that will be added to this zone "mines" : { "wood" : 1, "ore" : 1, }, + // List of treasures that will be placed in this zone "treasure" : [ { "min" : 2100, diff --git a/docs/modders/Translations.md b/docs/modders/Translations.md index 40f62f6ac..23b378797 100644 --- a/docs/modders/Translations.md +++ b/docs/modders/Translations.md @@ -1,64 +1,139 @@ < [Documentation](../Readme.md) / [Modding](Readme.md) / Translations -## For translators -### Adding new languages -New languages require minor changes in the source code. Please contact someone from vcmi team if you wish to translate game into a new language +# Translation system -### Translation of Launcher and Map Editor -TODO: write me +## List of currently supported languages -### Translation of game content -TODO: write me +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. -## For modders +- Czech +- Chinese (Simplified) +- English +- Finnish +- French +- German +- Hungarian +- Italian +- Korean +- Polish +- Portuguese (Brazilian) +- Russian +- Spanish +- Swedish +- Turkish +- Ukrainian +- Vietnamese -### Adding new translation to mod -TODO: write me +## Translating Heroes III data -### Translation of mod information +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 +- 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 + +If you have already existing Heroes III translation you can: + +- Install VCMI and select your localized Heroes III data files for VCMI data files +- Launch VCMI_Client.exe directly from game install directory +- In console window, type `convert txt` + +This will export all strings from game into `Documents/My Games/VCMI/VCMI_Client_log.txt` which you can then use to update json files in your translation + +## Translating VCMI data + +VCMI contains several new strings, to cover functionality not existing in Heroes III. It can be roughly split into following parts: + +- In-game texts, most noticeably - in-game settings menu. +- Game Launcher +- Map Editor +- Android Launcher + +Before you start, make sure that you have copy of VCMI source code. If you are not familiar with git, you can use Github Desktop to clone VCMI repository. + +### 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. + +After this, you can set language in Launcher to your language and start game. All translated strings should show up in your language. + +### Translation of Launcher and Editor + +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 +- 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. + +Translation of Map Editor is identical, except for location of translation files. Open `/editor/translation/` instead to translate Map Editor + +TODO: how to test translation locally + +### Translation of Android Launcher + +TODO +see https://github.com/vcmi/vcmi/blob/develop/android/vcmi-app/src/main/res/values/strings.xml + +### 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 + +If everything is OK, your changes will be accepted and will be part of next release. + +## Translating mods + +### Exporting translation + +TODO + +### Translating mod information In order to display information in Launcher in language selected by user add following block into your mod.json: ``` "" : { "name" : "", "description" : "", "author" : "", - "modType" : "", + "translations" : [ + "config//.json" + ] }, ``` -List of currently supported values for language parameter: -- english -- german -- polish -- russian -- ukrainian - 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. -### Translation of mod content -TODO: write me +# Developers documentation -### Searching for missing translation strings -TODO: write me - -## For developers ### 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 Launcher UI drop-down selector and name new string using such format: -`" ()"` -- Add new language into array of possible values for said selector +- 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 Also, make full search for a name of an existing language to ensure that there are not other places not referenced here + ### 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. 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 ``` lupdate -no-obsolete * -ts translation/*.ts ``` -This will remove any no longer existing lines from translation and add any new lines for all translations. + +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` \ No newline at end of file diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index c54299e5a..e92f674b6 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -131,6 +131,5 @@ Below a list of supported commands, with their arguments wrapped in `<>` `activate <0/1/2>` - activate game windows (no current use, apparently broken long ago) `redraw` - force full graphical redraw `screen` - show value of screenBuf variable, which prints "screen" when adventure map has current focus, "screen2" otherwise, and dumps values of both screen surfaces to .bmp files -`unlock pim` - unlocks specific mutex known in VCMI code as "pim" `not dialog` - set the state indicating if dialog box is active to "no" `tell hs ` - write what artifact is present on artifact slot with specified ID for hero with specified ID. (must be called during gameplay) diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index c4c045e68..556caae7e 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -2,23 +2,58 @@ # 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. +- In-game GUI can be freely scaled +- Adventure map can be freely zoomed + +Assets from Heroes of Might & Magic III HD - Remake released by Ubisoft in 2015 - are **not** supported. + +## Extended engine limits + Some of game features have already been extended in comparison to Shadow of Death: - Support for 32-bit graphics with alpha channel. Supported formats are .def, .bmp, .png and .tga - Support for maps of any size, including rectangular shapes - No limit of number of map objects, such as dwellings and stat boosters - Hero experience capacity currently at 2^64, which equals 199 levels with typical progression -- Heroes can have primary stats up to 2^16 -- Unlimited backpack -- Support for Stack Experience +- Heroes can have primary stats up to 2^16. +- Unlimited backpack (by default). This can be toggled off to restore original 64-slot backpack limit. The list of implemented cheat codes and console commands is [here](Cheat_codes.md). +# New mechanics (Optional) + +## 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. + +Stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: + +- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). +- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. +- [Stack Artifact](#stack-artifact-module). You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. + +## Commanders module + +VCMI offers native support for Commanders. Commanders are part of WoG mod for VCMI and require it to be enabled. However, once this is done, any new faction can use its own Commander, too. + +## Mithril module + +VCMI natively supports Mithril resource known from WoG. However, it is not currently used by any mod. + +## Stack Artifact module + +In original WoG, there is one available Stack Artifact - Warlord's Banner, which is related directly to stack experience. VCMI natively supports any number of Stack Artifacts regardless if of Stack Experience module is enabled or not. However, currently no mods make use of this feature and it hasn't been tested for many years. + # List of bugs fixed in VCMI These bugs were present in original Shadow of Death game, however the team decided to fix them to bring back desired behaviour: -# List of game mechanics changes +## List of game mechanics changes Some of H3 mechanics can't be straight considered as bug, but default VCMI behaviour is different: @@ -27,9 +62,48 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - Battles. Spells from artifacts like AOTD are autocasted on beginning of the battle, not beginning of turn. - Spells. Dimension Door spell doesn't allow to teleport to unexplored tiles. -# List of extended game functionality +# List of extended GUI features -## Quick Army Management +## Adventure map + +### New Shortcuts + +- [LCtrl] + [R] - Quick restart of current scenario. +- [LCtrl] + Arrows - scrolls Adventure Map behind an open window. +- [LCtrl] pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). +- NumPad 5 - centers view on selected hero. +- NumPad Enter functions same as normal Enter in the game (it didn't in H3). +- [LCtrl] + LClick – perform a normal click (same as no hero is selected). This make it possible to select other hero instead of changing path of current hero. + +## Pathfinder + +VCMI introduces improved pathfinder, which may find the way on adventure map using ships,Subterranean Gates and Monoliths. Simply click your destination anywhere on adventure map and it will find shortest path, if if target position is reachable. + +### Quest log + +VCMI itroduces custom Quest Log window. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. + +### 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. + +### Minor GUI features + +Some windows and dialogs now display extra info and images to make game more accessible for new players. This can be turned off, if desired. + +## Battles + +### Stack Queue + +Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press [Q] during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. + +### Attack range + +In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. + +## Town Screen + +### Quick Army Management - [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. @@ -37,11 +111,60 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - [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] + [LShift] + LClick - dismiss selected stack` +- Directly type numbers in the Split Stack window to split them in any way you wish -## Quick Recruitment +### Interface Shortcuts + +It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitment and Marketplace (click on gold) from various places: + +- Town screen (left bottom) +- Kingdom overview for each town +- Infobox (only if info box army management is enabled) + +### Quick Recruitment Mouse click on castle icon in the town screen open quick recruitment window, where we can purhase in fast way units. +## Pregame - Scenario / Saved Game list + +- Mouse wheel - scroll through the Scenario list. +- [Home] - move to the top of the list. +- [End] - move to the bottom of the list. +- NumPad keys can be used in the Save Game screen (they didn't work in H3). + +## Fullscreen + +- [F4] - Toggle fullscreen mode on/off. + +## FPS counter + +It's the new feature meant for testing game performance on various platforms. + +## Color support in game text + +Additional color are supported for text fields (e.g. map description). Uses HTML color syntax (e.g. #abcdef) / HTML predefined colors (e.g. green). + +##### Original Heroes III Support + +`This is white` + +This is white + +`{This is yellow}` + +This is yellow + +##### New + +`{#ff0000|This is red}` + +This is red + +`{green|This is green}` + +This is green + + # 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. +- https://heroes.thelazy.net/index.php/Main_Page Wiki that aims to be a complete reference to Heroes of Might and Magic III. diff --git a/docs/players/Installation_Android.md b/docs/players/Installation_Android.md index 0c1fa2ac9..be77110ab 100644 --- a/docs/players/Installation_Android.md +++ b/docs/players/Installation_Android.md @@ -23,7 +23,7 @@ Installation is a two step process, at first you need to install game, then you ### I imported game data files, but music in game is missing -**Solution:** Try to run data import again or copy Mp3 folder from Heroes III manually to Android/data/is.xyz.vcmi/files/vcmi-data/Mp3 +**Solution:** Try to run data import again or place Mp3 folder from Heroes III manually to Android/data/is.xyz.vcmi/files/vcmi-data/Mp3 ### I installed google play version, but have a problem while installing daily builds @@ -39,4 +39,4 @@ Installation is a two step process, at first you need to install game, then you ## Other problems -Please report about gameplay problem: [Github](https://github.com/vcmi/vcmi/issues), [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) or [Discord](https://discord.gg/chBT42V). Make sure to specify your device and used version of Android. \ No newline at end of file +Please report about gameplay problem: [Github](https://github.com/vcmi/vcmi/issues), [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) or [Discord](https://discord.gg/chBT42V). Make sure to specify your device and used version of Android. diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index ef650d545..009522776 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -63,7 +63,7 @@ If you are interested in providing builds for other distributions, please let us ## Compiling from source -Please check following developer guide: [How to build VCMI (Linux)]((../developers/Building_Linux.md)) +Please check following developer guide: [How to build VCMI (Linux)](../developers/Building_Linux.md) # Step 2: Installing Heroes III data files @@ -101,7 +101,7 @@ innoextract --output-dir=~/Downloads/HoMM3 "setup_heroes_of_might_and_magic_3_co ``` (note that installer file name might be different) -Once innoextract completes, start VCMI Launcher and choose to copy existing files. Select the ~/Downloads/HoMM3 directory. Once copy is complete, you can delete both offline installer files as well as ~/Downloads/HoMM3. +Once innoextract completes, start VCMI Launcher and choose to place existing files. Select the ~/Downloads/HoMM3 directory. Once placing is complete, you can delete both offline installer files as well as ~/Downloads/HoMM3. ## Install manually using existing Heroes III data @@ -118,4 +118,4 @@ Or, to start game directly avoiding Launcher: `vcmiclient` # Reporting bugs -Please report any issues with packages according to [Bug Reporting Guidelines](Bug_Reporting_Guidelines.md) \ No newline at end of file +Please report any issues with packages according to [Bug Reporting Guidelines](Bug_Reporting_Guidelines.md) diff --git a/docs/players/Installation_Windows.md b/docs/players/Installation_Windows.md index c8857b940..900b9e711 100644 --- a/docs/players/Installation_Windows.md +++ b/docs/players/Installation_Windows.md @@ -17,7 +17,7 @@ Install one of following into new folder, same as when installing new game: **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. -- Copy "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` +- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` Create this folder if it doesnt exist yet diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index 40e889697..a57eef464 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -16,6 +16,10 @@ have the following options: - [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. +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: + + /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa + ## 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. @@ -28,7 +32,7 @@ To play the game, you need to upload HoMM3 data files - **Data**, **Maps** and * 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. -Move or copy **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders. +Place **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders. ### Step 2.c: Installing data files with Xcode on macOS diff --git a/docs/players/Installation_macOS.md b/docs/players/Installation_macOS.md index 6965c3c3a..9a9adcd97 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -16,4 +16,4 @@ Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi # Step 2: Installing Heroes III data files 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. Copy (or symlink) **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` +2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` diff --git a/docs/players/Manual.md b/docs/players/Manual.md deleted file mode 100644 index e5f613c8f..000000000 --- a/docs/players/Manual.md +++ /dev/null @@ -1,168 +0,0 @@ -< [Documentation](../Readme.md) / Manual - -# Introduction - -The purpose of VCMI project is to rewrite entire HoMM3: WoG engine from scratch, giving it new and extended possibilities. We are hoping to support mods and new towns already made by fans, but abandoned because of game code limitations. -VCMI is a fan-made open-source project in progress. We already allow support for maps of any sizes, higher resolutions and extended engine limits. However, although working, the game is not finished. There are still many features and functionalities to add, both old and brand new. - -# Installation - -VCMI requires Heroes of Might & Magic 3 complete installation and will not run properly without their files. - -## Windows - -To install VCMI, simply unzip downloaded archive to main HoMM3 directory. To launch it, click `VCMI_client` icon. Server mode is inactive yet. - -## Linux - -Visit [Installation on Linux](Installation_Linux.md) for Linux packages and installation guidelines. - -# New features - -A number of enchancements had been introduced thorough new versions of VCMI. In this section you can learn about all of them. - -## High resolutions - -VCMI supports resolutions higher than original 800x600. -Switching resolution may not only change visible area of map, but also alters some interface features such as [Stack Queue.](#Stack_Queue) -To change resolution or full screen mode use System Options menu when in game. Fullscreen mode can be toggled anytime using F4 hotkey. - -## Stack Experience - -In 0.85, new stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: - -- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). -- Stack Artifact. As yet creature artifacts are not handled, so this place is unused. You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. -- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. - -## Commanders - -VCMI offers native support for Commanders. By default, they resemble original WoG behaviour with basic "Commanders" script enabled. - -## Stack Queue - -Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press 'Q' during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. - -## Pathfinder - -VCMI introduces improved pathfinder, which may find the way on adventure map using ships and subterranean gates. Simply click your destination on another island or level and the proposed path will be displayed. - -## Quest log - -In 0.9 new quest log was introduced. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. - -## Attack range - -In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. - -## Power rating - -When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to selected hero. This feature has been introduced in Heroes of Might and Magic V and is planned to be extended to all kinds of armed objects. - -## FPS counter - -VCMI 0.85 introduces new feature for testing, the FPS counter. - -## Minor improvements - -## New controls - -VCMI introduces several minor improvements and new keybinds in user -interface. - -### Pregame - Scenario / Saved Game list - -- Mouse wheel - scroll through the Scenario list. -- Home - move to the top of the list. -- End - move to the bottom of the list. -- NumPad keys can be used in the Save Game screen (they didn't work in H3). - -### Adventure Map - -- CTRL + R - Quick restart of current scenario. -- CTRL + Arrows - scrolls Adventure Map behind an open window. -- CTRL pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). -- NumPad 5 - centers view on selected hero. -- NumPad Enter functions same as normal Enter in the game (it didn't in H3). - -### Miscellaneous - -- Numbers for components in selection window - for example Treasure Chest, skill choice dialog and more yet to come. -- Type numbers in the Split Stack screen (for example 25 will split the stacks as such that there are 25 creatures in the second stack). -- 'Q' - Toggles the [Stack Queue](#Stack_Queue) display (so it can be enabled/disabled with single key press). -- During Tactics phase, click on any of your stack to instantly activate it. No need to scroll trough entire army anymore. - -## Cheat codes - -Following cheat codes have been implemented in VCMI. Type them in console: - -- `vcmiistari` - Gives all spells and 999 mana to currently selected hero -- `vcmiainur` - Gives 5 Archangels to every empty slot of currently selected hero -- `vcmiangband` - Gives 10 Black Knights into each slot -- `vcmiarmenelos` - Build all structures in currently selected town -- `vcminoldor` - All war machines -- `vcminahar` - 1000000 movement points -- `vcmiformenos` - Give resources (100 wood, ore and rare resources and 20000 gold) -- `vcmieagles` - Reveals fog of war -- `vcmiglorfindel` - Advances currently selected hero to the next level -- `vcmisilmaril` - Player wins -- `vcmimelkor` - Player loses -- `vcmiforgeofnoldorking` - Hero gets all artifacts except spell book, spell scrolls and war machines - -# Feedback - -Our project is open and its sources are available for everyone to browse and download. We do our best to inform community of Heroes fans with all the details and development progress. We also look forward to your comments, support and advice.\ -A good place to start is [VCMI Documentation](../Readme.md) which contains all necessary information for developers, testers and the people who would like to get familiar with our project. If you want to report a bug, use [Mantis Bugtracker](http://bugs.vcmi.eu/bug_report_advanced_page.php). -Make sure the issue is not already mentioned on [the list](http://bugs.vcmi.eu/view_all_bug_page.php) unless you can provide additional details for it. Please do not report as bugs features not yet implemented. For proposing new ideas and requests, visit [our board](http://forum.vcmi.eu/index.php). -VCMI comes with its own bug handlers: the console which prints game log `(server_log, VCMI_Client_log, VCMI_Server_log)` and memory dump file (`.dmp`) created on crash on Windows systems. These may be very helpful when the nature of bug is not obvious, please attach them if necessary. -To resolve an issue, we must be able to reproduce it on our computers. Please put down all circumstances in which a bug occurred and what did you do before, especially if it happens rarely or is not clearly visible. The better report, the better chance to track the bug quickly. - -# FAQ - -### What does "VCMI" stand for? - -VCMI is an acronym of the [Quenya](https://en.wikipedia.org/wiki/Quenya) phrase "Vinyar Callor Meletya Ingole", meaning "New Heroes of Might and Magic". ([Source](https://forum.vcmi.eu/t/what-vcmi-stands-for/297/4)) - -## How to turn off creature queue panel in battles? - -Hotkey to switch this panel is "Q" - -### Is it possible to add town X to vcmi? - -This depends on town authors or anyone else willing to port it to vcmi. Aim of VCMI is to provide *support* for such features. - -## When will the final version be released? - -When it is finished, which is another year at least. Exact date is impossible to predict. Development tempo depends mostly on free time of active programmers and community members, there is no exact shedule. You may expect new version every three months. Of course, joining the project will speed things up. - -## Are you going to add / change X? - -VCMI recreates basic H3:SoD engine and does not add new content or modify original mechanics by default. Only engine and interface improvements are likely to be supported now. If you want something specific to be done, please present detailed project on [our board](http://forum.vcmi.eu/index.php). Of course you are free to contribute with anything you can do. - -## Will it be possible to do Y? - -Removing engine restrictions and allowing flexible modding of game is the main aim of the project. As yet modification of game is not supported. - -## The game is not working, it crashes and I get strange console messages. - -Report your bug. Details are described [here](#Feedback). The sooner you tell the team about the problem, the sooner it will be resolved. Many problems come just from improper installation or system settings. - -## What is the current status of the project? - -Check [Documentation](../Readme.md), [release notes](http://forum.vcmi.eu/viewforum.php?f=1) or [changelog](https://github.com/vcmi/vcmi/blob/develop/ChangeLog). The best place to watch current changes as they are committed to the develop branch is the [Github commits page](https://github.com/vcmi/vcmi/commits/develop). The game is quite playable by now, although many important features are missing. - -## I have a great idea! - -Share it on [VCMI forum](http://forum.vcmi.eu/index.php) so all team members can see it and share their thoughts. Remember, brainstorming is good for your health. - -## Are you going to support Horn of The Abyss / Wog 3.59 / Grove Town etc.? - -Yes, of course. VCMI is designed as a base for any further mods and uses own executables, so the compatibility is not an issue. The team is not going to compete, but to cooperate with the community of creative modders. - -## Can I help VCMI Project in any way? - -If you are C++ programmer, graphican, tester or just have tons of ideas, do not hesistate - your help is needed. The game is huge and many different ares of activity are still waiting for someone like you. - -## I would like to join development team. - -You are always welcome. Contact the core team via [our board](http://forum.vcmi.eu/index.php). The usual way to join the team is to post your patch for review on our board. If the patch is positively rated by core team members, you will be given access to SVN repository. diff --git a/include/vcmi/Creature.h b/include/vcmi/Creature.h index 736d16043..645d144ab 100644 --- a/include/vcmi/Creature.h +++ b/include/vcmi/Creature.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CreatureID; class ResourceSet; -enum class EGameResID : int8_t; +class GameResID; /// Base class for creatures and battle stacks class DLL_LINKAGE ACreature: public AFactionMember @@ -63,7 +63,7 @@ public: virtual int32_t getBaseSpeed() const = 0; virtual int32_t getBaseShots() const = 0; - virtual int32_t getRecruitCost(Identifier resIndex) const = 0; + virtual int32_t getRecruitCost(GameResID resIndex) const = 0; virtual ResourceSet getFullRecruitCost() const = 0; virtual bool hasUpgrades() const = 0; diff --git a/include/vcmi/Entity.h b/include/vcmi/Entity.h index f91130d7b..eded843b7 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -14,8 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class IBonusBearer; class FactionID; -enum class ETerrainId; -template class Identifier; +class TerrainId; class DLL_LINKAGE IConstBonusProvider { @@ -26,9 +25,9 @@ public: class DLL_LINKAGE INativeTerrainProvider { public: - virtual Identifier getNativeTerrain() const = 0; + virtual TerrainId getNativeTerrain() const = 0; virtual FactionID getFaction() const = 0; - virtual bool isNativeTerrain(Identifier terrain) const; + virtual bool isNativeTerrain(TerrainId terrain) const; }; class DLL_LINKAGE Entity @@ -51,6 +50,8 @@ template class DLL_LINKAGE EntityT : public Entity { public: + using IdentifierType = IdType; + virtual IdType getId() const = 0; }; diff --git a/include/vcmi/Environment.h b/include/vcmi/Environment.h index 8c456bca2..b127e57b3 100644 --- a/include/vcmi/Environment.h +++ b/include/vcmi/Environment.h @@ -16,6 +16,7 @@ class Services; class IGameInfoCallback; class IBattleInfoCallback; +class BattleID; namespace events { @@ -31,7 +32,7 @@ public: virtual ~Environment() = default; virtual const Services * services() const = 0; - virtual const BattleCb * battle() const = 0; + virtual const BattleCb * battle(const BattleID & battleID) const = 0; virtual const GameCb * game() const = 0; virtual vstd::CLoggerBase * logger() const = 0; virtual events::EventBus * eventBus() const = 0; diff --git a/include/vcmi/Faction.h b/include/vcmi/Faction.h index 8d98ae075..6a1afe58f 100644 --- a/include/vcmi/Faction.h +++ b/include/vcmi/Faction.h @@ -15,15 +15,15 @@ VCMI_LIB_NAMESPACE_BEGIN class FactionID; -enum class EAlignment : uint8_t; -enum class EBoatId : int32_t; +enum class EAlignment : int8_t; +class BoatId; class DLL_LINKAGE Faction : public EntityT, public INativeTerrainProvider { public: virtual bool hasTown() const = 0; virtual EAlignment getAlignment() const = 0; - virtual EBoatId getBoatType() const = 0; + virtual BoatId getBoatType() const = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index 722ac7117..c87049ecb 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -15,11 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BonusList; - -namespace PrimarySkill -{ - enum PrimarySkill : int8_t; -} +class PrimarySkill; class DLL_LINKAGE AFactionMember: public IConstBonusProvider, public INativeTerrainProvider { @@ -27,7 +23,7 @@ public: /** Returns native terrain considering some terrain bonuses. */ - virtual Identifier getNativeTerrain() const; + virtual TerrainId getNativeTerrain() const; /** Returns magic resistance considering some bonuses. */ @@ -51,7 +47,7 @@ public: /** Returns primskill of creature or hero. */ - int getPrimSkillLevel(PrimarySkill::PrimarySkill id) const; + int getPrimSkillLevel(PrimarySkill id) const; /** Returns morale of creature or hero. Taking absolute bonuses into account. For now, uses range from EGameSettings @@ -71,4 +67,4 @@ public: int luckValAndBonusList(std::shared_ptr & bonusList) const; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/include/vcmi/events/PlayerGotTurn.h b/include/vcmi/events/PlayerGotTurn.h index a75380cca..278e03c88 100644 --- a/include/vcmi/events/PlayerGotTurn.h +++ b/include/vcmi/events/PlayerGotTurn.h @@ -29,7 +29,7 @@ public: using ExecHandler = Sub::ExecHandler; static Sub * getRegistry(); - static void defaultExecute(const EventBus * bus, const ExecHandler & execHandler, PlayerColor & player); + static void defaultExecute(const EventBus * bus, const PlayerColor & player); virtual PlayerColor getPlayer() const = 0; virtual void setPlayer(const PlayerColor & value) = 0; diff --git a/include/vcmi/spells/Caster.h b/include/vcmi/spells/Caster.h index 0d779a2a4..4158c0c53 100644 --- a/include/vcmi/spells/Caster.h +++ b/include/vcmi/spells/Caster.h @@ -17,6 +17,7 @@ class MetaString; class ServerCallback; class CGHeroInstance; class Spell; +class SpellSchool; namespace battle { @@ -38,7 +39,7 @@ public: /// returns level on which given spell would be cast by this(0 - none, 1 - basic etc); /// caster may not know this spell at all /// optionally returns number of selected school by arg - 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic - virtual int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const = 0; + virtual int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const = 0; ///default spell school level for effect calculation virtual int32_t getEffectLevel(const Spell * spell) const = 0; diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index ba16ad105..60e7b8023 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -15,17 +15,16 @@ VCMI_LIB_NAMESPACE_BEGIN class SpellID; -enum class ESpellSchool: int8_t; +class SpellSchool; namespace spells { -struct SchoolInfo; class Caster; class DLL_LINKAGE Spell: public EntityT { public: - using SchoolCallback = std::function; + using SchoolCallback = std::function; ///calculate spell damage on stack taking caster`s secondary skills into account virtual int64_t calculateDamage(const Caster * caster) const = 0; @@ -44,9 +43,8 @@ public: virtual bool isSpecial() const = 0; virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind) - virtual bool hasSchool(ESpellSchool school) const = 0; + virtual bool hasSchool(SpellSchool school) const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; - virtual const std::string & getCastSound() const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; virtual int32_t getBasePower() const = 0; diff --git a/include/vstd/DateUtils.h b/include/vstd/DateUtils.h new file mode 100644 index 000000000..d080e96b1 --- /dev/null +++ b/include/vstd/DateUtils.h @@ -0,0 +1,13 @@ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt); + DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt); + +} + +VCMI_LIB_NAMESPACE_END diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8a77fc06d..5ab52821f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -18,6 +18,7 @@ set(launcher_SRCS lobby/lobby.cpp lobby/lobby_moc.cpp lobby/lobbyroomrequest_moc.cpp + lobby/chat_moc.cpp ) set(launcher_HEADERS @@ -39,6 +40,7 @@ set(launcher_HEADERS lobby/lobby.h lobby/lobby_moc.h lobby/lobbyroomrequest_moc.h + lobby/chat_moc.h main.h ) @@ -52,6 +54,7 @@ set(launcher_FORMS updatedialog_moc.ui lobby/lobby_moc.ui lobby/lobbyroomrequest_moc.ui + lobby/chat_moc.ui ) set(launcher_TS @@ -63,6 +66,7 @@ set(launcher_TS translation/russian.ts translation/spanish.ts translation/ukrainian.ts + translation/vietnamese.ts ) if(APPLE_IOS) @@ -170,12 +174,11 @@ if(APPLE_IOS) else() set(RESOURCES_DESTINATION ${DATA_DIR}/launcher) - # Copy to build directory for easier debugging + # Link to build directory for easier debugging add_custom_command(TARGET vcmilauncher POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation - + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation ) install(TARGETS vcmilauncher DESTINATION ${BIN_DIR}) diff --git a/launcher/StdInc.cpp b/launcher/StdInc.cpp index b64b59be5..6237e6e6f 100644 --- a/launcher/StdInc.cpp +++ b/launcher/StdInc.cpp @@ -1 +1 @@ -#include "StdInc.h" +#include "StdInc.h" diff --git a/launcher/StdInc.h b/launcher/StdInc.h index e97483820..def7e8ba4 100644 --- a/launcher/StdInc.h +++ b/launcher/StdInc.h @@ -1,31 +1,31 @@ -#pragma once - -#include "../Global.h" - -#include -#include -#include -#include -#include -#include -#include - -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 -} +#pragma once + +#include "../Global.h" + +#include +#include +#include +#include +#include +#include +#include + +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/aboutProject/aboutproject_moc.h b/launcher/aboutProject/aboutproject_moc.h index 386565a60..aaad48d18 100644 --- a/launcher/aboutProject/aboutproject_moc.h +++ b/launcher/aboutProject/aboutproject_moc.h @@ -23,7 +23,7 @@ class AboutProjectView : public QWidget void changeEvent(QEvent *event) override; public: - explicit AboutProjectView(QWidget * parent = 0); + explicit AboutProjectView(QWidget * parent = nullptr); public slots: diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 97687abaa..26ef16ef5 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -17,21 +17,38 @@
  • Random map generator that supports objects added by mods
  • Note: In order to play the game using VCMI you need to own data files for Heroes of Might and Magic III: The Shadow of Death.

    -

    VCMI is an open-source project released under the GNU General Public License version 2 or later.

    If you want help, please check our forum, bug tracker or GitHub page.

    - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_1.png + https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_2.png + https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_3.png + https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_4.png + https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440 + + + https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440 + + + https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440 + + + https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440 + + + https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440 + + + https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440 + + + https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440 https://vcmi.eu/ @@ -39,7 +56,7 @@ https://vcmi.eu/faq/ https://github.com/vcmi/vcmi/blob/master/docs/Readme.md https://github.com/vcmi/vcmi/blob/master/docs/modders/Translations.md - https://slack.vcmi.eu/ + https://discord.gg/chBT42V https://github.com/vcmi/vcmi keyboard @@ -51,6 +68,7 @@ StrategyGame + diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index 2eb9e0cef..7d09dd688 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -150,7 +150,7 @@ void FirstLaunchView::activateTabModPreset() void FirstLaunchView::exitSetup() { - if(auto * mainWindow = dynamic_cast(qApp->activeWindow())) + if(auto * mainWindow = dynamic_cast(QApplication::activeWindow())) mainWindow->exitSetup(); } @@ -160,7 +160,7 @@ void FirstLaunchView::languageSelected(const QString & selectedLanguage) Settings node = settings.write["general"]["language"]; node->String() = selectedLanguage.toStdString(); - if(auto * mainWindow = dynamic_cast(qApp->activeWindow())) + if(auto * mainWindow = dynamic_cast(QApplication::activeWindow())) mainWindow->updateTranslation(); } @@ -230,8 +230,8 @@ bool FirstLaunchView::heroesDataDetect() CResourceHandler::load("config/filesystem.json"); // use file from lod archive to check presence of H3 data. Very rough estimate, but will work in majority of cases - bool heroesDataFoundROE = CResourceHandler::get()->existsResource(ResourceID("DATA/GENRLTXT.TXT")); - bool heroesDataFoundSOD = CResourceHandler::get()->existsResource(ResourceID("DATA/TENTCOLR.TXT")); + bool heroesDataFoundROE = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT")); + bool heroesDataFoundSOD = CResourceHandler::get()->existsResource(ResourcePath("DATA/TENTCOLR.TXT")); return heroesDataFoundROE && heroesDataFoundSOD; } @@ -398,7 +398,7 @@ bool FirstLaunchView::checkCanInstallExtras() CModListView * FirstLaunchView::getModView() { - auto * mainWindow = dynamic_cast(qApp->activeWindow()); + auto * mainWindow = dynamic_cast(QApplication::activeWindow()); assert(mainWindow); if (!mainWindow) diff --git a/launcher/firstLaunch/firstlaunch_moc.h b/launcher/firstLaunch/firstlaunch_moc.h index c45bd2b18..dd773c365 100644 --- a/launcher/firstLaunch/firstlaunch_moc.h +++ b/launcher/firstLaunch/firstlaunch_moc.h @@ -67,7 +67,7 @@ class FirstLaunchView : public QWidget void installMod(const QString & modID); public: - explicit FirstLaunchView(QWidget * parent = 0); + explicit FirstLaunchView(QWidget * parent = nullptr); public slots: diff --git a/launcher/icons/menu-game.png b/launcher/icons/menu-game.png index cc345dd51..71ae6ca54 100644 Binary files a/launcher/icons/menu-game.png and b/launcher/icons/menu-game.png differ diff --git a/launcher/jsonutils.cpp b/launcher/jsonutils.cpp index 6906da1dc..895eee540 100644 --- a/launcher/jsonutils.cpp +++ b/launcher/jsonutils.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "jsonutils.h" -#include "../lib/filesystem/FileStream.h" static QVariantMap JsonToMap(const JsonMap & json) { @@ -114,7 +113,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp new file mode 100644 index 000000000..a260ba312 --- /dev/null +++ b/launcher/lobby/chat_moc.cpp @@ -0,0 +1,173 @@ +/* + * chat_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 "chat_moc.h" +#include "ui_chat_moc.h" + +Chat::Chat(QWidget *parent) : + QWidget(parent), + ui(new Ui::Chat) +{ + ui->setupUi(this); + + namesCompleter.setModel(ui->listUsers->model()); + namesCompleter.setCompletionMode(QCompleter::InlineCompletion); + + ui->messageEdit->setCompleter(&namesCompleter); + + for([[maybe_unused]] auto i : {GLOBAL, ROOM}) + chatDocuments.push_back(new QTextDocument(this)); + + setChatId(GLOBAL); +} + +Chat::~Chat() +{ + delete ui; +} + +void Chat::setUsername(const QString & user) +{ + username = user; +} + +void Chat::setSession(const QString & s) +{ + session = s; + + on_chatSwitch_clicked(); +} + +void Chat::setChannel(const QString & channel) +{ + static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; + + setChatId(chatNames.value(channel)); +} + +void Chat::addUser(const QString & user) +{ + ui->listUsers->addItem(new QListWidgetItem("@" + user)); +} + +void Chat::clearUsers() +{ + ui->listUsers->clear(); +} + +void Chat::chatMessage(const QString & title, const QString & channel, QString body, bool isSystem) +{ + const QTextCharFormat regularFormat; + const QString boldHtml = "%1"; + const QString colorHtml = "%2"; + bool meMentioned = false; + bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); + + static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; + QTextDocument * doc = ui->chat->document(); + if(chatNames.contains(channel)) + doc = chatDocuments[chatNames.value(channel)]; + + QTextCursor curs(doc); + curs.movePosition(QTextCursor::End); + + QString titleColor = "Olive"; + if(isSystem || title == "System") + titleColor = "ForestGreen"; + if(title == username) + titleColor = "Gold"; + + curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); + + QRegularExpression mentionRe("@[\\w\\d]+"); + auto subBody = body; + int mem = 0; + for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) + { + body.insert(mem + match.capturedEnd(), QChar(-1)); + body.insert(mem + match.capturedStart(), QChar(-1)); + mem += match.capturedEnd() + 2; + subBody = body.right(body.size() - mem); + } + auto pieces = body.split(QChar(-1)); + for(auto & block : pieces) + { + if(block.startsWith("@")) + { + if(block == "@" + username) + { + meMentioned = true; + curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); + } + else + curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); + } + else + { + if(isSystem) + curs.insertHtml(colorHtml.arg("ForestGreen", block)); + else + curs.insertText(block, regularFormat); + } + } + curs.insertText("\n", regularFormat); + + if(doc == ui->chat->document() && (meMentioned || isScrollBarBottom)) + { + ui->chat->ensureCursorVisible(); + ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); + } +} + +void Chat::chatMessage(const QString & title, QString body, bool isSystem) +{ + chatMessage(title, "", body, isSystem); +} + +void Chat::sysMessage(QString body) +{ + chatMessage("System", body, true); +} + +void Chat::sendMessage() +{ + QString msg(ui->messageEdit->text()); + ui->messageEdit->clear(); + emit messageSent(msg); +} + +void Chat::on_messageEdit_returnPressed() +{ + sendMessage(); +} + +void Chat::on_sendButton_clicked() +{ + sendMessage(); +} + +void Chat::on_chatSwitch_clicked() +{ + static const QMap chatNames{{GLOBAL, "global"}, {ROOM, "room"}}; + + if(chatId == GLOBAL && !session.isEmpty()) + emit channelSwitch(chatNames[ROOM]); + else + emit channelSwitch(chatNames[GLOBAL]); +} + +void Chat::setChatId(ChatId _chatId) +{ + static const QMap chatNames{{GLOBAL, "Global"}, {ROOM, "Room"}}; + + chatId = _chatId; + ui->chatSwitch->setText(chatNames[chatId] + " chat"); + ui->chat->setDocument(chatDocuments[chatId]); +} diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h new file mode 100644 index 000000000..d5a735f67 --- /dev/null +++ b/launcher/lobby/chat_moc.h @@ -0,0 +1,69 @@ +/* + * chat_moc.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 + +namespace Ui { +class Chat; +} + +class Chat : public QWidget +{ + Q_OBJECT + + enum ChatId + { + GLOBAL = 0, + ROOM + }; + + QCompleter namesCompleter; + QString username, session; + ChatId chatId = GLOBAL; + + QVector chatDocuments; + +private: + void setChatId(ChatId); + void sendMessage(); + +public: + explicit Chat(QWidget *parent = nullptr); + ~Chat(); + + void setUsername(const QString &); + void setSession(const QString &); + void setChannel(const QString &); + + void clearUsers(); + void addUser(const QString & user); + + void chatMessage(const QString & title, const QString & channel, QString body, bool isSystem = false); + void chatMessage(const QString & title, QString body, bool isSystem = false); + +signals: + void messageSent(QString); + void channelSwitch(QString); + +public slots: + void sysMessage(QString body); + +private slots: + void on_messageEdit_returnPressed(); + + void on_sendButton_clicked(); + + void on_chatSwitch_clicked(); + +private: + Ui::Chat *ui; +}; diff --git a/launcher/lobby/chat_moc.ui b/launcher/lobby/chat_moc.ui new file mode 100644 index 000000000..a3208d982 --- /dev/null +++ b/launcher/lobby/chat_moc.ui @@ -0,0 +1,121 @@ + + + Chat + + + + 0 + 0 + 465 + 413 + + + + Form + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + Users in lobby + + + -1 + + + + + + + Global chat + + + + + + + + + + 0 + 0 + + + + + 16777215 + 96 + + + + 0 + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + QListView::SinglePass + + + + + + + + + + -1 + + + 0 + + + + + + + + type you message + + + + + + + send + + + + + + + + + + diff --git a/launcher/lobby/lobby.h b/launcher/lobby/lobby.h index 2ff03f721..84fe67e22 100644 --- a/launcher/lobby/lobby.h +++ b/launcher/lobby/lobby.h @@ -12,7 +12,7 @@ #include #include -const unsigned int ProtocolVersion = 4; +const unsigned int ProtocolVersion = 5; const std::string ProtocolEncoding = "utf8"; class ProtocolError: public std::runtime_error @@ -24,10 +24,10 @@ public: enum ProtocolConsts { //client consts - GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, + GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, SETCHANNEL, //server consts - SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE + SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, CHATCHANNEL, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE, CHANNEL }; const QMap ProtocolStrings @@ -88,6 +88,10 @@ const QMap ProtocolStrings //host sets game mode (new game or load game) //%1: game mode - 0 for new game, 1 for load game {HOSTMODE, "%1"}, + + //set new chat channel + //%1: channel name + {SETCHANNEL, "%1"}, //=== server commands === //server commands are started from :>>, arguments are enumerated by : symbol @@ -149,9 +153,16 @@ const QMap ProtocolStrings //received chat message //arg[0]: sender username - //arg[1]: message text + //arg[1]: channel + //arg[2]: message text {CHAT, "MSG"}, + //received chat message to specific channel + //arg[0]: sender username + //arg[1]: channel + //arg[2]: message text + {CHATCHANNEL, "MSGCH"}, + //list of users currently in lobby //arg[0]: amount of players, following arguments depend on it //arg[x]: username @@ -164,6 +175,10 @@ const QMap ProtocolStrings //game mode (new game or load game) set by host //arg[0]: game mode {GAMEMODE, "GAMEMODE"}, + + //chat channel changed + //arg[0]: channel name + {CHANNEL, "CHANNEL"}, }; class ServerCommand @@ -179,7 +194,7 @@ class SocketLobby : public QObject { Q_OBJECT public: - explicit SocketLobby(QObject *parent = 0); + explicit SocketLobby(QObject *parent = nullptr); void connectServer(const QString & host, int port, const QString & username, int timeout); void disconnectServer(); void requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods); diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index 727cbcab1..308148d3c 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -34,9 +34,11 @@ Lobby::Lobby(QWidget *parent) : { ui->setupUi(this); - connect(&socketLobby, SIGNAL(text(QString)), this, SLOT(sysMessage(QString))); + connect(&socketLobby, SIGNAL(text(QString)), ui->chatWidget, SLOT(sysMessage(QString))); connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString))); connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected())); + connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString))); + connect(ui->chatWidget, SIGNAL(channelSwitch(QString)), this, SLOT(onChannelSwitch(QString))); QString hostString("%1:%2"); hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); @@ -110,7 +112,7 @@ void Lobby::serverCommand(const ServerCommand & command) try { case SRVERROR: protocolAssert(args.size()); - chatMessage("System error", args[0], true); + ui->chatWidget->chatMessage("System error", args[0], true); if(authentificationStatus == AuthStatus::AUTH_NONE) authentificationStatus = AuthStatus::AUTH_ERROR; break; @@ -119,7 +121,7 @@ void Lobby::serverCommand(const ServerCommand & command) try protocolAssert(args.size()); hostSession = args[0]; session = args[0]; - sysMessage("new session started"); + ui->chatWidget->setSession(session); break; case SESSIONS: @@ -151,15 +153,15 @@ void Lobby::serverCommand(const ServerCommand & command) try case JOINED: case KICKED: protocolAssert(args.size() == 2); - joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); - if(args[1] == username) { hostModsMap.clear(); + session = ""; + ui->chatWidget->setSession(session); ui->buttonReady->setText("Ready"); ui->optNewGame->setChecked(true); - sysMessage(joinStr.arg("you", args[0])); session = args[0]; + ui->chatWidget->setSession(session); bool isHost = command.command == JOINED && hostSession == session; ui->optNewGame->setEnabled(isHost); ui->optLoadGame->setEnabled(isHost); @@ -167,7 +169,8 @@ void Lobby::serverCommand(const ServerCommand & command) try } else { - sysMessage(joinStr.arg(args[1], args[0])); + joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); + ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0])); } break; @@ -186,10 +189,14 @@ void Lobby::serverCommand(const ServerCommand & command) try case CLIENTMODS: { protocolAssert(args.size() >= 1); + auto & clientModsMap = clientsModsMap[args[0]]; amount = args[1].toInt(); protocolAssert(amount * 2 == (args.size() - 2)); tagPoint = 2; + for(int i = 0; i < amount; ++i, tagPoint += 2) + clientModsMap[args[tagPoint]] = args[tagPoint + 1]; + break; } @@ -244,7 +251,22 @@ void Lobby::serverCommand(const ServerCommand & command) try QString msg; for(int i = 1; i < args.size(); ++i) msg += args[i]; - chatMessage(args[0], msg); + ui->chatWidget->chatMessage(args[0], msg); + break; + } + + case CHATCHANNEL: { + protocolAssert(args.size() > 2); + QString msg; + for(int i = 2; i < args.size(); ++i) + msg += args[i]; + ui->chatWidget->chatMessage(args[0], args[1], msg); + break; + } + + case CHANNEL: { + protocolAssert(args.size() == 1); + ui->chatWidget->setChannel(args[0]); break; } @@ -258,10 +280,10 @@ void Lobby::serverCommand(const ServerCommand & command) try amount = args[0].toInt(); protocolAssert(amount == (args.size() - 1)); - ui->listUsers->clear(); + ui->chatWidget->clearUsers(); for(int i = 0; i < amount; ++i) { - ui->listUsers->addItem(new QListWidgetItem(args[i + 1])); + ui->chatWidget->addUser(args[i + 1]); } break; } @@ -277,7 +299,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } default: - sysMessage("Unknown server command"); + ui->chatWidget->sysMessage("Unknown server command"); } if(authentificationStatus == AuthStatus::AUTH_ERROR) @@ -292,7 +314,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } catch(const ProtocolError & e) { - chatMessage("System error", e.what(), true); + ui->chatWidget->chatMessage("System error", e.what(), true); } void Lobby::dispatchMessage(QString txt) try @@ -316,12 +338,15 @@ void Lobby::dispatchMessage(QString txt) try } catch(const ProtocolError & e) { - chatMessage("System error", e.what(), true); + ui->chatWidget->chatMessage("System error", e.what(), true); } void Lobby::onDisconnected() { authentificationStatus = AuthStatus::AUTH_NONE; + session = ""; + ui->chatWidget->setSession(session); + ui->chatWidget->setChannel("global"); ui->stackedWidget->setCurrentWidget(ui->sessionsPage); ui->connectButton->setChecked(false); ui->serverEdit->setEnabled(true); @@ -331,37 +356,12 @@ void Lobby::onDisconnected() ui->sessionsTable->setRowCount(0); } -void Lobby::chatMessage(QString title, QString body, bool isSystem) -{ - QTextCharFormat fmtBody, fmtTitle; - fmtTitle.setFontWeight(QFont::Bold); - if(isSystem) - fmtBody.setFontWeight(QFont::DemiBold); - - QTextCursor curs(ui->chat->document()); - curs.movePosition(QTextCursor::End); - curs.insertText(title + ": ", fmtTitle); - curs.insertText(body + "\n", fmtBody); - ui->chat->ensureCursorVisible(); -} - -void Lobby::sysMessage(QString body) -{ - chatMessage("System", body, true); -} - void Lobby::protocolAssert(bool expr) { if(!expr) throw ProtocolError("Protocol error"); } -void Lobby::on_messageEdit_returnPressed() -{ - socketLobby.send(ProtocolStrings[MESSAGE].arg(ui->messageEdit->text())); - ui->messageEdit->clear(); -} - void Lobby::on_connectButton_toggled(bool checked) { if(checked) @@ -369,6 +369,7 @@ void Lobby::on_connectButton_toggled(bool checked) ui->connectButton->setText(tr("Disconnect")); authentificationStatus = AuthStatus::AUTH_NONE; username = ui->userEdit->text(); + ui->chatWidget->setUsername(username); const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer(); auto serverStrings = ui->serverEdit->text().split(":"); @@ -389,9 +390,9 @@ void Lobby::on_connectButton_toggled(bool checked) ui->serverEdit->setEnabled(false); ui->userEdit->setEnabled(false); - sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); + ui->chatWidget->sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); //show text immediately - ui->chat->repaint(); + ui->chatWidget->repaint(); qApp->processEvents(); socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout); @@ -401,7 +402,7 @@ void Lobby::on_connectButton_toggled(bool checked) ui->connectButton->setText(tr("Connect")); ui->serverEdit->setEnabled(true); ui->userEdit->setEnabled(true); - ui->listUsers->clear(); + ui->chatWidget->clearUsers(); hostModsMap.clear(); updateMods(); socketLobby.disconnectServer(); @@ -521,7 +522,14 @@ void Lobby::on_kickButton_clicked() void Lobby::on_buttonResolve_clicked() { QStringList toEnableList, toDisableList; - for(auto * item : ui->modsList->selectedItems()) + auto items = ui->modsList->selectedItems(); + if(items.empty()) + { + for(int i = 0; i < ui->modsList->count(); ++i) + items.push_back(ui->modsList->item(i)); + } + + for(auto * item : items) { auto modName = item->data(ModResolutionRoles::ModNameRole); if(modName.isNull()) @@ -564,3 +572,12 @@ void Lobby::on_optLoadGame_toggled(bool checked) } } +void Lobby::onMessageSent(QString message) +{ + socketLobby.send(ProtocolStrings[MESSAGE].arg(message)); +} + +void Lobby::onChannelSwitch(QString channel) +{ + socketLobby.send(ProtocolStrings[SETCHANNEL].arg(channel)); +} diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h index c372b6204..23a8ddac1 100644 --- a/launcher/lobby/lobby_moc.h +++ b/launcher/lobby/lobby_moc.h @@ -33,12 +33,10 @@ public slots: void updateMods(); private slots: - void on_messageEdit_returnPressed(); - - void chatMessage(QString title, QString body, bool isSystem = false); - void sysMessage(QString body); void dispatchMessage(QString); void serverCommand(const ServerCommand &); + void onMessageSent(QString message); + void onChannelSwitch(QString channel); void on_connectButton_toggled(bool checked); @@ -76,6 +74,7 @@ private: QString username; QStringList gameArgs; QMap hostModsMap; + QMap> clientsModsMap; enum AuthStatus { diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui index 8001d3c8a..07406b877 100644 --- a/launcher/lobby/lobby_moc.ui +++ b/launcher/lobby/lobby_moc.ui @@ -14,6 +14,9 @@ + + 0 + @@ -62,69 +65,33 @@ + + -1 + 0 + + 0 + + + 10 + 0 - - - Players in lobby - - - - - + - + 0 0 - - - 16777215 - 96 - - - - 0 - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - true - - - QListView::SinglePass - - - - - Lobby chat - - - - - - - true - - - - - - @@ -140,6 +107,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -210,6 +189,21 @@ + + 0 + + + 0 + + + 0 + + + 0 + + + -1 + @@ -304,6 +298,14 @@ + + + Chat + QWidget +
    lobby/chat_moc.h
    + 1 +
    +
    diff --git a/launcher/main.cpp b/launcher/main.cpp index 3b626346b..cb0b38ac1 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -1,96 +1,96 @@ -/* - * main.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 "main.h" -#include "mainwindow_moc.h" - -#include -#include -#include -#include "../lib/VCMIDirs.h" - -// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332 -#ifdef VCMI_IOS -#if __has_include("QIOSIntegrationPlugin.h") -#include "QIOSIntegrationPlugin.h" -#endif -int argcForClient; -char ** argvForClient; -#endif - -int main(int argc, char * argv[]) -{ - int result; -#ifdef VCMI_IOS - { -#endif - QApplication vcmilauncher(argc, argv); - - MainWindow mainWindow; - mainWindow.show(); - result = vcmilauncher.exec(); -#ifdef VCMI_IOS - } - if (result == 0) - launchGame(argcForClient, argvForClient); -#endif - return result; -} - -void startGame(const QStringList & args) -{ - logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString()); - -#ifdef Q_OS_IOS - static const char clientName[] = "vcmiclient"; - argcForClient = args.size() + 1; //first argument is omitted - argvForClient = new char*[argcForClient]; - argvForClient[0] = new char[strlen(clientName)+1]; - strcpy(argvForClient[0], clientName); - for(int i = 1; i < argcForClient; ++i) - { - std::string s = args.at(i - 1).toStdString(); - argvForClient[i] = new char[s.size() + 1]; - strcpy(argvForClient[i], s.c_str()); - } - qApp->quit(); -#else - startExecutable(pathToQString(VCMIDirs::get().clientPath()), args); -#endif -} - -void startEditor(const QStringList & args) -{ -#ifdef ENABLE_EDITOR - startExecutable(pathToQString(VCMIDirs::get().mapEditorPath()), args); -#endif -} - -#ifndef Q_OS_IOS -void startExecutable(QString name, const QStringList & args) -{ - QProcess process; - - // Start the executable - if(process.startDetached(name, args)) - { - qApp->quit(); - } - else - { - QMessageBox::critical(qApp->activeWindow(), - "Error starting executable", - "Failed to start " + name + "\n" - "Reason: " + process.errorString(), - QMessageBox::Ok, - QMessageBox::Ok); - } -} -#endif +/* + * main.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 "main.h" +#include "mainwindow_moc.h" + +#include +#include +#include +#include "../lib/VCMIDirs.h" + +// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332 +#ifdef VCMI_IOS +#if __has_include("QIOSIntegrationPlugin.h") +#include "QIOSIntegrationPlugin.h" +#endif +int argcForClient; +char ** argvForClient; +#endif + +int main(int argc, char * argv[]) +{ + int result; +#ifdef VCMI_IOS + { +#endif + QApplication vcmilauncher(argc, argv); + + MainWindow mainWindow; + mainWindow.show(); + result = vcmilauncher.exec(); +#ifdef VCMI_IOS + } + if (result == 0) + launchGame(argcForClient, argvForClient); +#endif + return result; +} + +void startGame(const QStringList & args) +{ + logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString()); + +#ifdef Q_OS_IOS + static const char clientName[] = "vcmiclient"; + argcForClient = args.size() + 1; //first argument is omitted + argvForClient = new char*[argcForClient]; + argvForClient[0] = new char[strlen(clientName)+1]; + strcpy(argvForClient[0], clientName); + for(int i = 1; i < argcForClient; ++i) + { + std::string s = args.at(i - 1).toStdString(); + argvForClient[i] = new char[s.size() + 1]; + strcpy(argvForClient[i], s.c_str()); + } + qApp->quit(); +#else + startExecutable(pathToQString(VCMIDirs::get().clientPath()), args); +#endif +} + +void startEditor(const QStringList & args) +{ +#ifdef ENABLE_EDITOR + startExecutable(pathToQString(VCMIDirs::get().mapEditorPath()), args); +#endif +} + +#ifndef Q_OS_IOS +void startExecutable(QString name, const QStringList & args) +{ + QProcess process; + + // Start the executable + if(process.startDetached(name, args)) + { + qApp->quit(); + } + else + { + QMessageBox::critical(qApp->activeWindow(), + "Error starting executable", + "Failed to start " + name + "\n" + "Reason: " + process.errorString(), + QMessageBox::Ok, + QMessageBox::Ok); + } +} +#endif diff --git a/launcher/main.h b/launcher/main.h index ee7f1ea3f..566394363 100644 --- a/launcher/main.h +++ b/launcher/main.h @@ -1,19 +1,19 @@ -/* - * main.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 - -void startGame(const QStringList & args); -void startEditor(const QStringList & args); - -#ifdef VCMI_IOS -extern "C" void launchGame(int argc, char * argv[]); -#else -void startExecutable(QString name, const QStringList & args); -#endif +/* + * main.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 + +void startGame(const QStringList & args); +void startEditor(const QStringList & args); + +#ifdef VCMI_IOS +extern "C" void launchGame(int argc, char * argv[]); +#else +void startExecutable(QString name, const QStringList & args); +#endif diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 99db438c9..55389163d 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -45,7 +45,7 @@ void MainWindow::load() QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "icons")); #endif - settings.init(); + settings.init("config/settings.json", "vcmi:settings"); } void MainWindow::computeSidePanelSizes() @@ -110,7 +110,7 @@ MainWindow::MainWindow(QWidget * parent) computeSidePanelSizes(); - bool h3DataFound = CResourceHandler::get()->existsResource(ResourceID("DATA/GENRLTXT.TXT")); + bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT")); if (h3DataFound && setupCompleted) ui->tabListWidget->setCurrentIndex(TabRows::MODS); diff --git a/launcher/mainwindow_moc.ui b/launcher/mainwindow_moc.ui index 302b6b9e2..1208bbe04 100644 --- a/launcher/mainwindow_moc.ui +++ b/launcher/mainwindow_moc.ui @@ -7,7 +7,7 @@ 0 0 900 - 536 + 640
    @@ -62,8 +62,8 @@ - 60 - 60 + 64 + 64 @@ -112,8 +112,8 @@ - 60 - 60 + 64 + 64 @@ -162,8 +162,8 @@ - 60 - 60 + 64 + 64 @@ -212,8 +212,8 @@ - 30 - 30 + 32 + 32 @@ -281,8 +281,8 @@ - 30 - 30 + 32 + 32 @@ -334,8 +334,8 @@ - 60 - 60 + 64 + 64 diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index a190bf757..a0e3eddaa 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -18,13 +18,13 @@ CDownloadManager::CDownloadManager() SLOT(downloadFinished(QNetworkReply *))); } -void CDownloadManager::downloadFile(const QUrl & url, const QString & file) +void CDownloadManager::downloadFile(const QUrl & url, const QString & file, qint64 bytesTotal) { QNetworkRequest request(url); FileEntry entry; entry.file.reset(new QFile(CLauncherDirs::get().downloadsPath() + '/' + file)); entry.bytesReceived = 0; - entry.totalSize = 0; + entry.totalSize = bytesTotal; entry.filename = file; if(entry.file->open(QIODevice::WriteOnly | QIODevice::Truncate)) @@ -79,7 +79,7 @@ void CDownloadManager::downloadFinished(QNetworkReply * reply) break; } } - downloadFile(qurl, filename); + downloadFile(qurl, filename, file.totalSize); return; } @@ -131,7 +131,8 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte entry.file->write(entry.reply->readAll()); entry.bytesReceived = bytesReceived; - entry.totalSize = bytesTotal; + if(bytesTotal > entry.totalSize) + entry.totalSize = bytesTotal; quint64 total = 0; for(auto & entry : currentDownloads) @@ -140,11 +141,14 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte quint64 received = 0; for(auto & entry : currentDownloads) received += entry.bytesReceived > 0 ? entry.bytesReceived : 0; + + if(received > total) + total = received; emit downloadProgress(received, total); } -bool CDownloadManager::downloadInProgress(const QUrl & url) +bool CDownloadManager::downloadInProgress(const QUrl & url) const { for(auto & entry : currentDownloads) { diff --git a/launcher/modManager/cdownloadmanager_moc.h b/launcher/modManager/cdownloadmanager_moc.h index 6d33ba8b3..8c0e8d664 100644 --- a/launcher/modManager/cdownloadmanager_moc.h +++ b/launcher/modManager/cdownloadmanager_moc.h @@ -48,10 +48,10 @@ public: // returns true if download with such URL is in progress/queued // FIXME: not sure what's right place for "mod download in progress" check - bool downloadInProgress(const QUrl & url); + bool downloadInProgress(const QUrl & url) const; // returns network reply so caller can connect to required signals - void downloadFile(const QUrl & url, const QString & file); + void downloadFile(const QUrl & url, const QString & file, qint64 bytesTotal = 0); public slots: void downloadFinished(QNetworkReply * reply); diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index f04c485e2..b36d4b198 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -14,63 +14,13 @@ #include "../../lib/JsonNode.h" #include "../../lib/filesystem/CFileInputStream.h" #include "../../lib/GameConstants.h" - -namespace -{ -bool isCompatible(const QString & verMin, const QString & verMax) -{ - QVersionNumber vcmiVersion(VCMI_VERSION_MAJOR, - VCMI_VERSION_MINOR, - VCMI_VERSION_PATCH); - - auto versionMin = QVersionNumber::fromString(verMin); - auto versionMax = QVersionNumber::fromString(verMax); - - auto buildVersion = [](QVersionNumber & ver) - { - const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch - - if(ver.segmentCount() < maxSections) - { - auto segments = ver.segments(); - for(int i = segments.size(); i < maxSections; ++i) - segments.append(0); - ver = QVersionNumber(segments); - } - }; - - if(!versionMin.isNull()) - { - buildVersion(versionMin); - if(vcmiVersion < versionMin) - return false; - } - - if(!versionMax.isNull()) - { - buildVersion(versionMax); - if(vcmiVersion > versionMax) - return false; - } - return true; -} -} - -bool CModEntry::compareVersions(QString lesser, QString greater) -{ - auto versionLesser = QVersionNumber::fromString(lesser); - auto versionGreater = QVersionNumber::fromString(greater); - return versionLesser < versionGreater; -} +#include "../../lib/modding/CModVersion.h" QString CModEntry::sizeToString(double size) { - static const QString sizes[] = - { - /*"%1 B", */ "%1 KiB", "%1 MiB", "%1 GiB", "%1 TiB" - }; + static const std::array sizes { "%1 B", "%1 KiB", "%1 MiB", "%1 GiB", "%1 TiB" }; size_t index = 0; - while(size > 1024 && index < 4) + while(size > 1024 && index < sizes.size()) { size /= 1024; index++; @@ -110,18 +60,24 @@ bool CModEntry::isUpdateable() const if(!isInstalled()) return false; - QString installedVer = localData["installedVersion"].toString(); - QString availableVer = repository["latestVersion"].toString(); + auto installedVer = localData["installedVersion"].toString().toStdString(); + auto availableVer = repository["latestVersion"].toString().toStdString(); - if(compareVersions(installedVer, availableVer)) - return true; - return false; + 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 { - auto compatibility = localData["compatibility"].toMap(); - return ::isCompatible(compatibility["min"].toString(), compatibility["max"].toString()); + return ::isCompatible(localData["compatibility"].toMap()); } bool CModEntry::isEssential() const @@ -134,14 +90,32 @@ bool CModEntry::isInstalled() const return !localData.isEmpty(); } -bool CModEntry::isValid() const +bool CModEntry::isVisible() const { - return !localData.isEmpty() || !repository.isEmpty(); + if (getBaseValue("modType").toString() == "Compatibility") + { + if (isSubmod()) + return false; + } + + if (getBaseValue("modType").toString() == "Translation") + { + // 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().toLower() == "translation"; + return getBaseValue("modType").toString() == "Translation"; +} + +bool CModEntry::isSubmod() const +{ + return getName().contains('.'); } int CModEntry::getModStatus() const @@ -167,6 +141,22 @@ 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); @@ -186,10 +176,10 @@ QVariant CModEntry::getValueImpl(QString value, bool localized) const if(repository.contains(value) && localData.contains(value)) { // value is present in both repo and locally installed. Select one from latest version - QString installedVer = localData["installedVersion"].toString(); - QString availableVer = repository["latestVersion"].toString(); + auto installedVer = localData["installedVersion"].toString().toStdString(); + auto availableVer = repository["latestVersion"].toString().toStdString(); - useRepositoryData = compareVersions(installedVer, availableVer); + useRepositoryData = CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer); } auto & storage = useRepositoryData ? repository : localData; @@ -207,7 +197,7 @@ QVariant CModEntry::getValueImpl(QString value, bool localized) const return QVariant(); } -QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) +QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) const { QVariantMap renamed; @@ -310,10 +300,8 @@ CModEntry CModList::getMod(QString modname) const if(settings.value("active").toBool()) { - auto compatibility = local.value("compatibility").toMap(); - if(compatibility["min"].isValid() || compatibility["max"].isValid()) - if(!isCompatible(compatibility["min"].toString(), compatibility["max"].toString())) - settings["active"] = false; + if(!::isCompatible(local.value("compatibility").toMap())) + settings["active"] = false; } for(auto entry : repositories) @@ -322,14 +310,16 @@ CModEntry CModList::getMod(QString modname) const if(repoVal.isValid()) { auto repoValMap = repoVal.toMap(); - auto compatibility = repoValMap["compatibility"].toMap(); - if(isCompatible(compatibility["min"].toString(), compatibility["max"].toString())) + if(::isCompatible(repoValMap["compatibility"].toMap())) { - if(repo.empty() || CModEntry::compareVersions(repo["version"].toString(), repoValMap["version"].toString())) + if(repo.empty() + || CModVersion::fromString(repo["version"].toString().toStdString()) + < CModVersion::fromString(repoValMap["version"].toString().toStdString())) { - //take valid download link and screenshots before assignment + //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()) { @@ -337,6 +327,8 @@ CModEntry CModList::getMod(QString modname) const if(repo.value("screenshots").isNull()) //taking screenshot from the downloadable version repo["screenshots"] = screenshots; } + if(repo.value("downloadSize").isNull()) + repo["downloadSize"] = size; } } } @@ -365,8 +357,8 @@ QStringList CModList::getRequirements(QString modname) { auto mod = getMod(modname); - for(auto entry : mod.getValue("depends").toStringList()) - ret += getRequirements(entry); + for(auto entry : mod.getDependencies()) + ret += getRequirements(entry.toLower()); } ret += modname; diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index b8a9276ae..05aaa4f6b 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -60,10 +60,12 @@ public: bool isEssential() const; // checks if verison is compatible with vcmi bool isCompatible() const; - // returns if has any data - bool isValid() const; + // returns true if mod should be visible in Launcher + bool isVisible() const; // installed and enabled bool isTranslation() const; + // returns true if this is a submod + bool isSubmod() const; // see ModStatus enum int getModStatus() const; @@ -74,8 +76,8 @@ public: QVariant getValue(QString value) const; QVariant getBaseValue(QString value) const; - // returns true if less < greater comparing versions section by section - static bool compareVersions(QString lesser, QString greater); + QStringList getDependencies() const; + QStringList getConflicts() const; static QString sizeToString(double size); }; @@ -86,7 +88,7 @@ class CModList QVariantMap localModList; QVariantMap modSettings; - QVariantMap copyField(QVariantMap data, QString from, QString to); + QVariantMap copyField(QVariantMap data, QString from, QString to) const; public: virtual void resetRepositories(); diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/cmodlistmodel_moc.cpp index 9bde9860e..cece26b53 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/cmodlistmodel_moc.cpp @@ -45,6 +45,7 @@ QString CModListModel::modTypeName(QString modTypeID) const {"Templates", tr("Templates") }, {"Spells", tr("Spells") }, {"Music", tr("Music") }, + {"Maps", tr("Maps") }, {"Sounds", tr("Sounds") }, {"Skills", tr("Skills") }, {"Other", tr("Other") }, @@ -58,6 +59,7 @@ QString CModListModel::modTypeName(QString modTypeID) const {"Graphical", tr("Graphical") }, {"Expansion", tr("Expansion") }, {"Creatures", tr("Creatures") }, + {"Compatibility", tr("Compatibility") }, {"Artifacts", tr("Artifacts") }, {"AI", tr("AI") }, }; @@ -257,7 +259,6 @@ bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const { CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString()); return (mod.getModStatus() & filterMask) == filteredType && - mod.isValid() && QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); } @@ -265,6 +266,10 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc { 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; diff --git a/launcher/modManager/cmodlistmodel_moc.h b/launcher/modManager/cmodlistmodel_moc.h index 65a905409..5125b61b8 100644 --- a/launcher/modManager/cmodlistmodel_moc.h +++ b/launcher/modManager/cmodlistmodel_moc.h @@ -56,7 +56,7 @@ class CModListModel : public QAbstractItemModel, public CModList QVariant getIcon(const CModEntry & mod, int field) const; public: - explicit CModListModel(QObject * parent = 0); + explicit CModListModel(QObject * parent = nullptr); /// CModListContainer overrides void resetRepositories() override; @@ -93,5 +93,5 @@ class CModFilterModel : public QSortFilterProxyModel public: void setTypeFilter(int filteredType, int filterMask); - CModFilterModel(CModListModel * model, QObject * parent = 0); + CModFilterModel(CModListModel * model, QObject * parent = nullptr); }; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index f0cf46c16..23789707d 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -25,6 +25,12 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/Languages.h" +#include "../../lib/modding/CModVersion.h" + +static double mbToBytes(double mb) +{ + return mb * 1024 * 1024; +} void CModListView::setupModModel() { @@ -191,7 +197,7 @@ QString CModListView::genChangelogText(CModEntry & mod) std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater) { - return CModEntry::compareVersions(lesser, greater); + return CModVersion::fromString(lesser.toStdString()) < CModVersion::fromString(greater.toStdString()); }); std::reverse(versions.begin(), versions.end()); @@ -212,12 +218,12 @@ QStringList CModListView::getModNames(QStringList input) for(const auto & modID : input) { - auto mod = modModel->getMod(modID); + auto mod = modModel->getMod(modID.toLower()); QString modName = mod.getValue("name").toString(); if (modName.isEmpty()) - result += modID; + result += modID.toLower(); else result += modName; } @@ -243,8 +249,11 @@ QString CModListView::genModInfoText(CModEntry & mod) result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); - if(mod.getValue("size").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble()), lineTemplate.arg(tr("Download size"))); + 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"))); + result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); if(mod.getValue("licenseURL").isValid()) @@ -302,8 +311,8 @@ QString CModListView::genModInfoText(CModEntry & mod) if(needToShowSupportedLanguages) result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("depends").toStringList()), lineTemplate.arg(tr("Required mods"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("conflicts").toStringList()), lineTemplate.arg(tr("Conflicting mods"))); + result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); + result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); result += replaceIfNotEmpty(getModNames(mod.getValue("description").toStringList()), textTemplate.arg(tr("Description"))); result += "

    "; // to get some empty space @@ -323,7 +332,7 @@ QString CModListView::genModInfoText(CModEntry & mod) if(mod.isInstalled()) notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods)); - if(mod.getName().contains('.')) + if(mod.isSubmod()) notes += noteTemplate.arg(thisIsSubmod); if(notes.size()) @@ -365,8 +374,8 @@ void CModListView::selectMod(const QModelIndex & index) ui->disableButton->setVisible(mod.isEnabled()); ui->enableButton->setVisible(mod.isDisabled()); - ui->installButton->setVisible(mod.isAvailable() && !mod.getName().contains('.')); - ui->uninstallButton->setVisible(mod.isInstalled() && !mod.getName().contains('.')); + ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod()); + ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod()); ui->updateButton->setVisible(mod.isUpdateable()); // Block buttons if action is not allowed at this time @@ -455,7 +464,7 @@ QStringList CModListView::findBlockingMods(QString modUnderTest) if(mod.isEnabled()) { // one of enabled mods have requirement (or this mod) marked as conflict - for(auto conflict : mod.getValue("conflicts").toStringList()) + for(auto conflict : mod.getConflicts()) { if(required.contains(conflict)) ret.push_back(name); @@ -476,7 +485,7 @@ QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) if(!current.isInstalled()) continue; - if(current.getValue("depends").toStringList().contains(mod)) + if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) { if(!(current.isDisabled() && excludeDisabled)) ret += modName; @@ -535,7 +544,7 @@ void CModListView::on_updateButton_clicked() auto mod = modModel->getMod(name); // update required mod, install missing (can be new dependency) if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } @@ -565,11 +574,11 @@ void CModListView::on_installButton_clicked() { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } -void CModListView::downloadFile(QString file, QString url, QString description) +void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) { if(!dlManager) { @@ -581,31 +590,34 @@ void CModListView::downloadFile(QString file, QString url, QString description) connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)), this, SLOT(downloadFinished(QStringList,QStringList,QStringList))); + connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), + this, SLOT(downloadProgress(qint64,qint64))); connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); - - QString progressBarFormat = "Downloading %s%. %p% (%v KB out of %m KB) finished"; + + QString progressBarFormat = tr("Downloading %s%. %p% (%v MB out of %m MB) finished"); progressBarFormat.replace("%s%", description); ui->progressBar->setFormat(progressBarFormat); } - dlManager->downloadFile(QUrl(url), file); + dlManager->downloadFile(QUrl(url), file, size); } void CModListView::downloadProgress(qint64 current, qint64 max) { - // display progress, in kilobytes - ui->progressBar->setMaximum(max / 1024); - ui->progressBar->setValue(current / 1024); + // display progress, in megabytes + ui->progressBar->setVisible(true); + ui->progressBar->setMaximum(max / (1024 * 1024)); + ui->progressBar->setValue(current / (1024 * 1024)); } void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors) { - QString title = "Download failed"; - QString firstLine = "Unable to download all files.\n\nEncountered errors:\n\n"; - QString lastLine = "\n\nInstall successfully downloaded?"; + QString title = tr("Download failed"); + QString firstLine = tr("Unable to download all files.\n\nEncountered errors:\n\n"); + QString lastLine = tr("\n\nInstall successfully downloaded?"); bool doInstallFiles = false; // if all files were d/loaded there should be no errors. And on failure there must be an error @@ -631,15 +643,16 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi doInstallFiles = true; } - // remove progress bar after some delay so user can see that download was complete and not interrupted. - QTimer::singleShot(1000, this, SLOT(hideProgressBar())); - dlManager->deleteLater(); dlManager = nullptr; + + ui->progressBar->setMaximum(0); + ui->progressBar->setValue(0); if(doInstallFiles) installFiles(savedFiles); + hideProgressBar(); emit modsChanged(); } @@ -742,7 +755,10 @@ void CModListView::installMods(QStringList archives) } for(int i = 0; i < modNames.size(); i++) + { + ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i])); manager->installMod(modNames[i], archives[i]); + } std::function enableMod; @@ -792,8 +808,8 @@ void CModListView::checkManagerErrors() QString errors = manager->getErrors().join('\n'); if(errors.size() != 0) { - QString title = "Operation failed"; - QString description = "Encountered errors:\n" + errors; + QString title = tr("Operation failed"); + QString description = tr("Encountered errors:\n") + errors; QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok); } } @@ -859,7 +875,7 @@ void CModListView::doInstallMod(const QString & modName) { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } @@ -892,3 +908,51 @@ QString CModListView::getTranslationModName(const QString & language) return QString(); } + +void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) +{ + if(!index.isValid()) + return; + + auto modName = index.data(ModRoles::ModNameRole).toString(); + auto mod = modModel->getMod(modName); + + bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); + bool hasBlockingMods = !findBlockingMods(modName).empty(); + bool hasDependentMods = !findDependentMods(modName, true).empty(); + + if(!hasInvalidDeps && mod.isAvailable() && !mod.isSubmod()) + { + on_installButton_clicked(); + return; + } + + if(!hasInvalidDeps && !hasDependentMods && mod.isUpdateable() && index.column() == ModFields::STATUS_UPDATE) + { + on_updateButton_clicked(); + return; + } + + if(index.column() == ModFields::NAME) + { + if(ui->allModsView->isExpanded(index)) + ui->allModsView->collapse(index); + else + ui->allModsView->expand(index); + + return; + } + + if(!hasBlockingMods && !hasInvalidDeps && mod.isDisabled()) + { + on_enableButton_clicked(); + return; + } + + if(!hasDependentMods && !mod.isEssential() && mod.isEnabled()) + { + on_disableButton_clicked(); + return; + } +} + diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index e6ede36c1..cadfb0a51 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -51,7 +51,7 @@ class CModListView : public QWidget // find mods that depend on this one QStringList findDependentMods(QString mod, bool excludeDisabled); - void downloadFile(QString file, QString url, QString description); + void downloadFile(QString file, QString url, QString description, qint64 size = 0); void installMods(QStringList archives); void installFiles(QStringList mods); @@ -64,7 +64,7 @@ signals: void modsChanged(); public: - explicit CModListView(QWidget * parent = 0); + explicit CModListView(QWidget * parent = nullptr); ~CModListView(); void loadScreenshots(); @@ -126,6 +126,8 @@ private slots: void on_screenshotsList_clicked(const QModelIndex & index); + void on_allModsView_doubleClicked(const QModelIndex &index); + private: Ui::CModListView * ui; }; diff --git a/launcher/modManager/cmodlistview_moc.ui b/launcher/modManager/cmodlistview_moc.ui index 72961f923..d5bfa0b34 100644 --- a/launcher/modManager/cmodlistview_moc.ui +++ b/launcher/modManager/cmodlistview_moc.ui @@ -119,6 +119,9 @@ Qt::Horizontal + + 12 + QAbstractItemView::SingleSelection diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 656560f75..9559630ff 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -13,22 +13,24 @@ #include "../../lib/VCMIDirs.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CZipLoader.h" -#include "../../lib/CModHandler.h" +#include "../../lib/modding/CModHandler.h" +#include "../../lib/modding/CModInfo.h" +#include "../../lib/modding/IdentifierStorage.h" #include "../jsonutils.h" #include "../launcherdirs.h" namespace { -QString detectModArchive(QString path, QString modName) +QString detectModArchive(QString path, QString modName, std::vector & filesToExtract) { - auto files = ZipArchive::listFiles(qstringToPath(path)); + filesToExtract = ZipArchive::listFiles(qstringToPath(path)); QString modDirName; for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root { - for(auto file : files) + for(auto file : filesToExtract) { QString filename = QString::fromUtf8(file.c_str()); modDirName = filename.section('/', 0, folderLevel); @@ -42,7 +44,7 @@ QString detectModArchive(QString path, QString modName) logGlobal->error("Failed to detect mod path in archive!"); logGlobal->debug("List of file in archive:"); - for(auto file : files) + for(auto file : filesToExtract) logGlobal->debug("%s", file.c_str()); return ""; @@ -89,18 +91,26 @@ void CModManager::loadMods() for(auto modname : installedMods) { - ResourceID resID(CModInfo::getModFile(modname)); + auto resID = CModInfo::getModFile(modname); if(CResourceHandler::get()->existsResource(resID)) { - boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); - auto mod = JsonUtils::JsonFromFile(pathToQString(name)); - if(!name.is_absolute()) + //calculate mod size + qint64 total = 0; + ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); + if(CResourceHandler::get()->existsResource(resDir)) { - auto json = JsonUtils::toJson(mod); - json["storedLocaly"].Bool() = true; - mod = JsonUtils::toVariant(json); + 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); } } @@ -144,7 +154,7 @@ bool CModManager::canInstallMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.getName().contains('.')) + if(mod.isSubmod()) return addError(modname, "Can not install submod"); if(mod.isInstalled()) @@ -159,7 +169,7 @@ bool CModManager::canUninstallMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.getName().contains('.')) + if(mod.isSubmod()) return addError(modname, "Can not uninstall submod"); if(!mod.isInstalled()) @@ -182,7 +192,7 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isCompatible()) return addError(modname, "Mod is not compatible, please update VCMI and checkout latest mod revisions"); - for(auto modEntry : mod.getValue("depends").toStringList()) + for(auto modEntry : mod.getDependencies()) { if(!modList->hasMod(modEntry)) // required mod is not available return addError(modname, QString("Required mod %1 is missing").arg(modEntry)); @@ -195,11 +205,11 @@ bool CModManager::canEnableMod(QString modname) auto mod = modList->getMod(modEntry); // "reverse conflict" - enabled mod has this one as conflict - if(mod.isEnabled() && mod.getValue("conflicts").toStringList().contains(modname)) + if(mod.isEnabled() && mod.getConflicts().contains(modname)) return addError(modname, QString("This mod conflicts with %1").arg(modEntry)); } - for(auto modEntry : mod.getValue("conflicts").toStringList()) + for(auto modEntry : mod.getConflicts()) { // check if conflicting mod installed and enabled if(modList->hasMod(modEntry) && modList->getMod(modEntry).isEnabled()) @@ -222,7 +232,7 @@ bool CModManager::canDisableMod(QString modname) { auto current = modList->getMod(modEntry); - if(current.getValue("depends").toStringList().contains(modname) && current.isEnabled()) + if(current.getDependencies().contains(modname) && current.isEnabled()) return addError(modname, QString("This mod is needed to run %1").arg(modEntry)); } return true; @@ -270,11 +280,23 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) if(localMods.contains(modname)) return addError(modname, "Mod with such name is already installed"); - QString modDirName = ::detectModArchive(archivePath, modname); + std::vector filesToExtract; + QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract); if(!modDirName.size()) return addError(modname, "Mod archive is invalid or corrupted"); - - if(!ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir))) + + auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesToExtract]() + { + return ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract); + }); + + while(futureExtract.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) + { + emit extractionProgress(0, 0); + qApp->processEvents(); + } + + if(!futureExtract.get()) { removeModDir(destDir + modDirName); return addError(modname, "Failed to extract mod data"); @@ -300,7 +322,7 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) bool CModManager::doUninstallMod(QString modname) { - ResourceID resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); + 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)); diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/cmodmanager.h index d2bc6271d..987d1a580 100644 --- a/launcher/modManager/cmodmanager.h +++ b/launcher/modManager/cmodmanager.h @@ -53,4 +53,7 @@ public: bool canUninstallMod(QString mod); bool canEnableMod(QString mod); bool canDisableMod(QString mod); + +signals: + void extractionProgress(qint64 currentAmount, qint64 maxAmount); }; diff --git a/launcher/modManager/imageviewer_moc.h b/launcher/modManager/imageviewer_moc.h index 771a4e9a4..5c0c1e674 100644 --- a/launcher/modManager/imageviewer_moc.h +++ b/launcher/modManager/imageviewer_moc.h @@ -23,12 +23,12 @@ class ImageViewer : public QDialog void changeEvent(QEvent *event) override; public: - explicit ImageViewer(QWidget * parent = 0); + explicit ImageViewer(QWidget * parent = nullptr); ~ImageViewer(); void setPixmap(QPixmap & pixmap); - static void showPixmap(QPixmap & pixmap, QWidget * parent = 0); + static void showPixmap(QPixmap & pixmap, QWidget * parent = nullptr); protected: void mousePressEvent(QMouseEvent * event) override; diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 9f00a369f..e0c6bb635 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -83,12 +83,16 @@ void CSettingsView::loadSettings() ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float()); + ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); + ui->checkBoxVSync->setChecked(settings["video"]["vsync"].Bool()); ui->spinBoxReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100)); ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String())); ui->comboBoxNeutralAI->setCurrentText(QString::fromStdString(settings["server"]["neutralAI"].String())); ui->comboBoxEnemyAI->setCurrentText(QString::fromStdString(settings["server"]["enemyAI"].String())); + ui->comboBoxEnemyPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["playerAI"].String())); + ui->comboBoxAlliedPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["alliedAI"].String())); ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Integer()); @@ -494,6 +498,13 @@ void CSettingsView::on_spinBoxFramerateLimit_valueChanged(int arg1) node->Float() = arg1; } +void CSettingsView::on_checkBoxVSync_stateChanged(int arg1) +{ + Settings node = settings.write["video"]["vsync"]; + node->Bool() = arg1; + ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); +} + void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1) { Settings node = settings.write["server"]["playerAI"]; diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 5cc14c505..0a144f7f5 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -20,7 +20,7 @@ class CSettingsView : public QWidget Q_OBJECT public: - explicit CSettingsView(QWidget * parent = 0); + explicit CSettingsView(QWidget * parent = nullptr); ~CSettingsView(); void loadSettings(); @@ -62,6 +62,8 @@ private slots: void on_spinBoxFramerateLimit_valueChanged(int arg1); + void on_checkBoxVSync_stateChanged(int arg1); + void on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1); void on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1); diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 186689a6c..deae0833c 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -42,7 +42,6 @@ - 75 true @@ -107,20 +106,208 @@ 0 - -197 + -356 610 - 768 + 873 - - + + + + Heroes III Translation + + + + + + + + true + + + + General + + + + + + + Fullscreen + + + + + + + Interface Scaling + + + + + + + Autosave prefix + + + + + + + Adventure Map Allies + + + + + + + + true + + + + Video + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + + true + + + + Artificial Intelligence + + + + + + + empty = map name prefix + + + + + + + true + + + + + + true + + + + + + + + + Autosave limit (0 = off) + + + + + + + + + + Additional repository + + + + + + + % + + + 0 + + + 25 + + + 1 + + + 0 + + + + + + + Show intro + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Enemy AI in battles + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + false @@ -140,470 +327,13 @@ - - - - Display index - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - Autosave limit (0 = off) - - - - - - - Fullscreen - - - - - - - Network port - - - - - - - - 75 - true - - - - General - - - - - - - Autosave - - - - - - - - Hardware - - - - - Software - - - - - - - - Show intro - - - - - - - - - - true - - - - - - - true - - - - - - true - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - - - - Framerate Limit - - - - - - - - - - Adventure Map Enemies - - - - - - - Default repository - - - - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - - 75 - true - - - - Video - - - - - - - Autosave prefix - - - - - - - Resolution - - - - + - - - - - - - true - - - - - - - Cursor - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - Refresh now - - - - - - - - - - - - - - Heroes III Translation - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - Enemy AI in battles - - - - - - - - 75 - true - - - - Mod Repositories - - - - - - - Additional repository - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - 50 - - - 400 - - - 10 - - - - - - - Heroes III Data Language - - - - - - - - - - Neutral AI in battles - - - - - - - Interface Scaling - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - - - - - - - - Friendly AI in battles - - - - - - - VCMI Language - - - - - - - - - - Check on startup - - - - - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - Adventure Map Allies - - - - - - - - 75 - true - - - - Artificial Intelligence - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - empty = map name prefix - - - @@ -635,6 +365,266 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
    + + + + + + + Display index + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + + + + + + + + Default repository + + + + + + + Heroes III Data Language + + + + + + + + + + true + + + + + + + Framerate Limit + + + + + + + Friendly AI in battles + + + + + + + VCMI Language + + + + + + + + + + true + + + + + + + + + + 50 + + + 400 + + + 10 + + + + + + + + + + + true + + + + Mod Repositories + + + + + + + + + + + + + + Resolution + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + Autosave + + + + + + + Cursor + + + + + + + + Hardware + + + + + Software + + + + + + + + + + + Network port + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Refresh now + + + + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Check on startup + + + + + + + Neutral AI in battles + + + @@ -642,22 +632,41 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - % + + + + Adventure Map Enemies - - 0 - - - 25 - - + + + + + 1 - - 0 + + + Off + + + + + On + + + + + + + + VSync + + + + + + + diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 6bdd419ce..3b65d01a2 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -120,81 +120,91 @@ + Maps + + + + Sounds 音效 - + Skills 技能 - - + + Other 其他 - + Objects 物件 - + Mechanics 无法确定是否分类是游戏机制或者是游戏中的战争器械 机制 - + Interface 界面 - + Heroes 英雄 - + Graphical 图像 - + Expansion 扩展包 - + Creatures 生物 - + + Compatibility + 兼容性 + + + Artifacts 宝物 - + AI AI - + Name 名称 - + Type 类型 - + Version 版本 @@ -242,164 +252,211 @@ 下载并刷新仓库 - - + + Description 详细介绍 - + Changelog 修改日志 - + Screenshots 截图 - + Uninstall 卸载 - + Enable 激活 - + Disable 禁用 - + Update 更新 - + Install 安装 - + %p% (%v KB out of %m KB) %p% (%v KB 完成,总共 %m KB) - + Abort 终止 - + Mod name MOD名称 - + Installed version 已安装的版本 - + Latest version 最新版本 - + + Size + + + + Download size 下载大小 - + Authors 作者 - + License 授权许可 - + Contact 联系方式 - + Compatibility 兼容性 - - + + Required VCMI version 需要VCMI版本 - + Supported VCMI version 支持的VCMI版本 - + Supported VCMI versions 支持的VCMI版本 - + Languages 语言 - + Required mods 前置MODs - + Conflicting mods 冲突的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 is a submod and it cannot be installed or uninstalled separately from its parent mod 这是一个附属模组它无法在所属模组外被直接被安装或者卸载 - + Notes 笔记注释 - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 截图 %1 - + Mod is incompatible MOD不兼容 @@ -407,123 +464,123 @@ CSettingsView - - - + + + Off 关闭 - - + + Artificial Intelligence 人工智能 - - + + Mod Repositories 模组仓库 - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + 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 @@ -534,104 +591,137 @@ 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 安装 + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -938,88 +1028,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect 连接 - + Username 用户名 - + Server 服务器 - - Lobby chat - 大厅聊天 - - - + Session 会话 - + Players 玩家 - + Resolve 解决 - + New game 新游戏 - + Load game 加载游戏 - + New room 新房间 - - Players in lobby - 大厅中的玩家 - - - + Join room 加入房间 - + Ready 准备 - + Mods mismatch MODs不匹配 - + Leave 离开 - + Kick player 踢出玩家 - + Players in the room 大厅中的玩家 @@ -1029,7 +1109,7 @@ Heroes® of Might and Magic® III HD is currently not supported! 断开 - + No issues detected 没有发现问题 diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 4d6ad1deb..691c1b400 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -120,80 +120,90 @@ - Sounds + Maps - Skills + Sounds - - Other + Skills - Objects + + Other + Objects + + + + Mechanics - + Interface - + Heroes - + Graphical - + Expansion - + Creatures - + + Compatibility + + + + Artifacts - + AI - + Name - + Type - + Version @@ -241,164 +251,211 @@ - - + + Description - + Changelog - + Screenshots - + Uninstall - + Enable - + Disable - + Update - + Install - + %p% (%v KB out of %m KB) - + Abort - + Mod name - + Installed version - + Latest version - + + Size + + + + Download size - + Authors - + License - + Contact - + Compatibility - - + + Required VCMI version - + Supported VCMI version - + 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 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 is a submod and it cannot be installed or uninstalled separately from its parent mod - + Notes - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 - + Mod is incompatible @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off - - + + Artificial Intelligence - - + + Mod Repositories - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -533,103 +590,136 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Reserved screen area - + Hardware - + Software - + 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 + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -930,88 +1020,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect - + Username - + Server - - Lobby chat - - - - + Session - + Players - + Resolve - + New game - + Load game - + New room - - Players in lobby - - - - + Join room - + Ready - + Mods mismatch - + Leave - + Kick player - + Players in the room @@ -1021,7 +1101,7 @@ Heroes® of Might and Magic® III HD is currently not supported! - + No issues detected diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index e59b15e82..1f1949470 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Sons - + Skills Compétences - - + + Other Autre - + Objects Objets - + Mechanics Mécaniques - + Interface Interface - + Heroes Héros - + Graphical Graphisme - + Expansion Extension - + Creatures Créatures - - Artifacts - Artéfacts + + Compatibility + Compatibilité - + + Artifacts + Artefacts + + + AI IA - + Name Nom - + Type Type - + Version Version @@ -241,169 +251,216 @@ Télécharger et rafraîchir les dépôts - - + + Description Description - + Changelog Journal - + Screenshots Impressions écran - + %p% (%v KB out of %m KB) %p% (%v Ko sur %m Ko) - + 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 + + + + 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 - + 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 - + 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 - + 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 - + 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 - + 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 - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -411,43 +468,48 @@ 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 + + + + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -464,183 +526,211 @@ 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 - + Neutral AI in battles IA neutre dans les batailles - + Autosave limit (0 = off) - + Adventure Map Enemies Ennemis de la carte d"aventure - + Autosave prefix - + empty = map name prefix - + 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 - + 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 - + 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 + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -947,88 +1037,78 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Lobby - + Username Nom d'utilisateur - - + + Connect Connecter - + Server Serveur - - Players in lobby - Joueurs à la salle d'attente - - - - Lobby chat - Discussion de salle d'attente - - - + New room Nouveau salon - + Join room Rejoindre le salon - + Session Session - + Players Joueurs - + Kick player Jeter le joueur - + Players in the room Joueurs dans le salon - + Leave Quitter - + Mods mismatch Incohérence de mods - + Ready Prêt - + Resolve Résoudre - + New game Nouvelle partie - + Load game Charger une partie @@ -1038,7 +1118,7 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Déconnecter - + No issues detected Pas de problème détecté diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index fd803264f..b9f875973 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -120,80 +120,90 @@ + 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 - + Name Name - + Type Typ - + Version Version @@ -241,164 +251,218 @@ Repositories herunterladen && aktualisieren - - + + Description Beschreibung - + Changelog Änderungslog - + Screenshots Screenshots - + Uninstall Deinstallieren - + Enable Aktivieren - + Disable Deaktivieren - + Update Aktualisieren - + Install Installieren - + %p% (%v KB out of %m KB) %p% (%v КB von %m КB) - + 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 - + 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 - + 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 - + 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 - + 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 - + 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 - + + 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: + + + Es konnten nicht alle Dateien heruntergeladen werden. + +Es sind Fehler aufgetreten: + + + + + + + +Install successfully downloaded? + + +Installation erfolgreich heruntergeladen? + + + + Installing mod %1 + Installation von Mod %1 + + + + Operation failed + Operation fehlgeschlagen + + + + Encountered errors: + + Aufgetretene Fehler: + + + + Screenshot %1 Screenshot %1 - + Mod is incompatible Mod ist inkompatibel @@ -406,123 +470,123 @@ 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 - + Adventure Map Allies Abenteuerkarte Verbündete - + Adventure Map Enemies Abenteuerkarte Feinde - + 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 - + empty = map name prefix leer = Kartenname als Präfix - + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - - - + + + 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 @@ -539,103 +603,136 @@ 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 + + Chat + + + Form + Formular + + + + Users in lobby + Benutzer in der Lobby + + + + Global chat + Globaler Chat + + + + type you message + Nachricht eingeben + + + + send + senden + + FirstLaunchView @@ -942,88 +1039,78 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Lobby - - + + Connect Verbinden - + Username Benutzername - + Server Server - - Lobby chat - Lobby-Chat - - - + Session Sitzung - + Players Spieler - + Resolve Auflösen - + New game Neues Spiel - + Load game Spiel laden - + New room Neuer Raum - - Players in lobby - Spieler in der Lobby - - - + Join room Raum beitreten - + Ready Bereit - + Mods mismatch Mods stimmen nicht überein - + Leave Verlassen - + Kick player Spieler kicken - + Players in the room Spieler im Raum @@ -1033,7 +1120,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Verbindung trennen - + No issues detected Keine Probleme festgestellt diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 75ea0498f..554fab79f 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -120,80 +120,90 @@ + Maps + Mapy + + + Sounds Dźwięki - + Skills Umiejętności - - + + Other Inne - + Objects Obiekty - + Mechanics Mechaniki - + Interface Interfejs - + Heroes Bohaterowie - + Graphical Graficzny - + Expansion Dodatek - + Creatures Stworzenia - + + Compatibility + Kompatybilność + + + Artifacts Artefakty - + AI AI - + Name Nazwa - + Type Typ - + Version Wersja @@ -241,164 +251,218 @@ Pobierz i odśwież repozytoria - - + + Description Opis - + Changelog Lista zmian - + Screenshots Zrzuty ekranu - + 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) - + 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 - + 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 - + 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 - + 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 - + 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 - + 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 - + + 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: + + + Nie udało się pobrać wszystkich plików. + +Napotkane błędy: + + + + + + + +Install successfully downloaded? + + +Zainstalować pomyślnie pobrane? + + + + Installing mod %1 + Instalowanie modyfikacji %1 + + + + Operation failed + Operacja nieudana + + + + Encountered errors: + + Napotkane błędy: + + + + Screenshot %1 Zrzut ekranu %1 - + Mod is incompatible Mod jest niekompatybilny @@ -406,123 +470,123 @@ 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 - + Adventure Map Allies AI sojuszników mapy przygody - + Adventure Map Enemies AI wrogów mapy przygody - + 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 - + empty = map name prefix puste = przedrostek z nazwy mapy - + Refresh now Odśwież - + Default repository Domyślne repozytorium - - - + + + 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 @@ -539,103 +603,136 @@ 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 + + Chat + + + Form + Okno + + + + Users in lobby + Gracze w lobby + + + + Global chat + Globalny czat + + + + type you message + napisz wiadomość + + + + send + wyślij + + FirstLaunchView @@ -836,87 +933,87 @@ Heroes III: HD Edition nie jest obecnie wspierane! Czech - + Czeski Chinese - + Chiński English - + Angielski Finnish - + Fiński French - + Francuski German - + Niemiecki Hungarian - + Węgierski Italian - + Włoski Korean - + Koreański Polish - + Polski Portuguese - + Portugalski Russian - + Rosyjski Spanish - + Hiszpański Swedish - + Szwedzki Turkish - + Turecki Ukrainian - + Ukraiński Vietnamese - + Wietnamski @@ -936,94 +1033,84 @@ Heroes III: HD Edition nie jest obecnie wspierane! Auto (%1) - + Lobby - - + + Connect Połącz - + Username Nazwa użytkownika - + Server Serwer - - Lobby chat - Czat lobby - - - + Session Sesja - + Players Gracze - + Resolve Rozwiąż - + New game Nowa gra - + Load game Wczytaj grę - + New room Nowy pokój - - Players in lobby - Gracze w lobby - - - + Join room Dołącz - + Ready Zgłoś gotowość - + Mods mismatch Niezgodność modów - + Leave Wyjdź - + Kick player Wyrzuć gracza - + Players in the room Gracze w pokoju @@ -1033,7 +1120,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Rozłącz - + No issues detected Nie znaleziono problemów diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index a5ef0dd5c..c421df14b 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Звуки - + Skills Навыки - - + + Other Иное - + Objects Объекты - + Mechanics Механика - + Interface Интерфейс - + Heroes Герои - + Graphical Графика - + Expansion Дополнение - + Creatures Существа - + + Compatibility + Совместимость + + + Artifacts Артефакт - + AI ИИ - + Name Название - + Type Тип - + Version Версия @@ -241,164 +251,211 @@ Обновить репозиторий - - + + Description Описание - + Changelog Изменения - + Screenshots Скриншоты - + Uninstall Удалить - + Enable Включить - + Disable Отключить - + Update Обновить - + Install Установить - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) - + Abort Отмена - + Mod name Название мода - + Installed version Установленная версия - + Latest version Последняя версия - + + Size + + + + Download size Размер загрузки - + Authors Авторы - + License Лицензия - + Contact Контакты - + Compatibility Совместимость - - + + Required VCMI version Требуемая версия VCMI - + Supported VCMI version Поддерживаемая версия VCMI - + 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 is a submod and it cannot be installed or uninstalled separately from its parent mod Это вложенный мод, он не может быть установлен или удален отдельно от родительского - + Notes Замечания - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим @@ -406,149 +463,154 @@ 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 - + + VSync + + + + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Reserved screen area - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Default repository - + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -559,77 +621,105 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Аппаратный - + Software Программный - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - - + + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -936,88 +1026,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect Подключиться - + Username Имя пользователя - + Server Сервер - - Lobby chat - Чат лобби - - - + Session Сессия - + Players Игроки - + Resolve Скорректировать - + New game Новая игра - + Load game Загрузить игру - + New room Создать комнату - - Players in lobby - Люди в лобби - - - + Join room Присоединиться к комнате - + Ready Готово - + Mods mismatch Моды не совпадают - + Leave Выйти - + Kick player Выгнать игрока - + Players in the room Игроки в комнате @@ -1027,7 +1107,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Отключиться - + No issues detected Проблем не обнаружено diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 04499cedf..2dba9fdbe 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -120,80 +120,90 @@ + Maps + + + + 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 - + Name Nombre - + Type Tipo - + Version Versión @@ -241,164 +251,211 @@ Descargar y actualizar repositorios - - + + Description Descripción - + Changelog Registro de cambios - + Screenshots Capturas de pantalla - + Uninstall Desinstalar - + Enable Activar - + Disable Desactivar - + Update Actualizar - + Install Instalar - + %p% (%v KB out of %m KB) %p% (%v KB de %m KB) - + Abort Cancelar - + Mod name Nombre del mod - + Installed version Versión instalada - + Latest version Última versión - + + Size + + + + 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 - + 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 - + 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 - + 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 - + 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 - + 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 - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Captura de pantalla %1 - + Mod is incompatible El mod es incompatible @@ -406,170 +463,175 @@ CSettingsView - - - + + + Off Desactivado - - + + Artificial Intelligence Inteligencia Artificial - - + + Mod Repositories Repositorios de Mods - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On Encendido - + Cursor Cursor - + Heroes III Translation Traducción de Heroes III - + Reserved screen area - + Fullscreen Pantalla completa - - + + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + + VSync + + + + 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 @@ -580,56 +642,84 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + 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 + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -936,88 +1026,78 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Lobby - - + + Connect Conectar - + Username Nombre de usuario - + Server Servidor - - Lobby chat - Charlar en la sala - - - + Session Sesión - + Players Jugadores - + Resolve Resolver - + New game Nueva partida - + Load game Cargar partida - + New room Nueva sala - - Players in lobby - Jugadores en la sala - - - + Join room Unirse a la sala - + Ready Listo - + Mods mismatch No coinciden los mods - + Leave Salir - + Kick player Expulsar jugador - + Players in the room Jugadores en la sala @@ -1027,7 +1107,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Desconectar - + No issues detected No se han detectado problemas diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index d3015e426..971474a49 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -120,80 +120,90 @@ + Maps + Мапи + + + Sounds Звуки - + Skills Вміння - - + + Other Інше - + Objects Об'єкти - + Mechanics Механіки - + Interface Інтерфейс - + Heroes Герої - + Graphical Графічний - + Expansion Розширення - + Creatures Істоти - + + Compatibility + Сумісність + + + Artifacts Артефакти - + AI ШІ - + Name Назва - + Type Тип - + Version Версія @@ -241,164 +251,218 @@ Оновити репозиторії - - + + Description Опис - + Changelog Зміни - + Screenshots Знімки - + Uninstall Видалити - + Enable Активувати - + Disable Деактивувати - + Update Оновити - + Install Встановити - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) - + Abort Відмінити - + Mod name Назва модифікації - + Installed version Встановлена версія - + Latest version Найновіша версія - + + Size + Розмір + + + Download size Розмір для завантаження - + Authors Автори - + License Ліцензія - + Contact Контакти - + Compatibility Сумісність - - + + Required VCMI version Необхідна версія VCMI - + Supported VCMI version Підтримувана версія VCMI - + 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 is a submod and it cannot be installed or uninstalled separately from its parent mod Це вкладена модифікація, і її не можна встановити або видалити окремо від батьківської модифікації - + Notes Примітки - + + Downloading %s%. %p% (%v MB out of %m MB) finished + Завантажуємо %s%. %p% (%v МБ з %m Мб) виконано + + + + Download failed + Помилка завантаження + + + + Unable to download all files. + +Encountered errors: + + + Не вдалося завантажити усі файли. + +Виникли помилки: + + + + + + + +Install successfully downloaded? + + +Встановити успішно завантажені? + + + + Installing mod %1 + Встановлення модифікації %1 + + + + Operation failed + Операція завершилася невдало + + + + Encountered errors: + + Виникли помилки: + + + + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -406,123 +470,123 @@ CSettingsView - - - + + + Off Вимкнено - - + + Artificial Intelligence Штучний інтелект - - + + Mod Repositories Репозиторії модифікацій - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - + Adventure Map Allies Союзники на мапі пригод - + Adventure Map Enemies Вороги на мапі пригод - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + Autosave limit (0 = off) Кількість автозбережень - + Friendly AI in battles Дружній ШІ в боях - + Framerate Limit Обмеження частоти кадрів - + Autosave prefix Префікс назв автозбережень - + empty = map name prefix (використовувати назву карти) - + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - - - + + + On Увімкнено - + Cursor Курсор - + Heroes III Data Language Мова Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,103 +603,136 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + 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 Встановити + + Chat + + + Form + + + + + Users in lobby + Гравців у лобі + + + + Global chat + Загальний чат + + + + type you message + введіть повідомлення + + + + send + Відправити + + FirstLaunchView @@ -942,88 +1039,78 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Lobby - - + + Connect Підключитися - + Username Ім'я користувача - + Server Сервер - - Lobby chat - Лобі чат - - - + Session Сесія - + Players Гравці - + Resolve Розв'язати - + New game Нова гра - + Load game Завантажити гру - + New room Створити кімнату - - Players in lobby - Гравці у лобі - - - + Join room Приєднатися до кімнати - + Ready Готовність - + Mods mismatch Модифікації, що не збігаються - + Leave Вийти з кімнати - + Kick player Виключити гравця - + Players in the room Гравці у кімнаті @@ -1033,7 +1120,7 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Від'єднатися - + No issues detected Проблем не виявлено diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts new file mode 100644 index 000000000..a5a1c99c1 --- /dev/null +++ b/launcher/translation/vietnamese.ts @@ -0,0 +1,1200 @@ + + + + + AboutProjectView + + + VCMI on Discord + VCMI trên Discord + + + + 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! + + + + VCMI on Github + VCMI trên Github + + + + Our Community + Cộng đồng + + + + VCMI on Slack + VCMI trên Slack + + + + 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 + + + + Project homepage + Trang chủ + + + + Report a bug + Báo lỗi + + + + CModListModel + + + 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 + + + + Name + Tên + + + + Type + Loại + + + + Version + Phiên bản + + + + CModListView + + + Filter + Bộ lọc + + + + All mods + Tất cả + + + + Downloadable + Có thể tải về + + + + Installed + Đã cài đặt + + + + Updatable + Cập nhật mới + + + + Active + Bật + + + + Inactive + Tắt + + + + Download && refresh repositories + Tải lại + + + + + Description + Mô tả + + + + Changelog + Các thay đổi + + + + Screenshots + Hình ảnh + + + + 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) + + + + 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 + + + + 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 + + + + 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 + + + + 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 + + + + 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 + + + + 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ú + + + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + + Screenshot %1 + Hình ảnh %1 + + + + Mod is incompatible + Bản sửa đổi này không tương thích + + + + 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 + + + + Adventure Map Allies + Máy liên minh ở bản đồ phiêu lưu + + + + Adventure Map Enemies + Máy đối thủ ở bản đồ phiêu lưu + + + + 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 + + + + empty = map name prefix + Rỗng = tên bản đồ + + + + Refresh now + Làm mới + + + + Default repository + Nguồn mặc định + + + + + + 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 + +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ị + +Cửa sổ - Trò chơi chạy trong 1 cửa sổ + +Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung độ phân giải hiện tại + +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 + + + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + + + FirstLaunchView + + + Language + Ngôn ngữ + + + + Heroes III Data + Dữ liệu Heroes III + + + + Mods Preset + Bản thiết lập trước + + + + Select your language + 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. + +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! + +Trước khi bắt đầu, còn vài bước cần hoàn thành. + +Để chạy VCMI, bạn cần có dữ liệu gốc Heroes® of Might and Magic® III: Complete hoặc The Shadow of Death. + +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 + + + + If you don't have a copy of Heroes III installed, you can use our automatic installation tool 'vcmibuilder', which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions. + Nếu không có bản sao Heroes III, bạn có thể sử dụng 'vcmibuilder', cái mà chỉ cần bản cài GoG.com Heroes III. Tham khảo trang wiki của chúng tôi để biết thêm chi tiết. + + + + 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 + + + + 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 + + + + 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 + + + + Finish + Hoàn thành + + + + VCMI on Github + VCMI trên Github + + + + VCMI on Slack + VCMI trên Slack + + + + VCMI on Discord + VCMI trên Discord + + + + + Next + Tiếp theo + + + + Open help in browser + Mở trợ giúp trên trình duyệt + + + + 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. + + + + 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 + + + + ImageViewer + + + Image Viewer + Trình xem ảnh + + + + Language + + + Czech + Tiếng Séc + + + + Chinese + Tiếng Trung + + + + English + Tiếng Anh + + + + Finnish + Tiếng Phần Lan + + + + French + Tiếng Pháp + + + + German + Tiếng Đức + + + + Hungarian + Tiếng Hungary + + + + Italian + Tiếng Ý + + + + Korean + Tiếng Hàn + + + + Polish + Tiếng Ba Lan + + + + Portuguese + Tiếng Bồ Đào Nha + + + + Russian + Tiếng Nga + + + + Spanish + Tiếng Tây Ban Nha + + + + Swedish + Tiếng Thụy Điển + + + + Turkish + Tiếng Thổ Nhĩ Kì + + + + Ukrainian + Tiếng Ukraina + + + + 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) + Tự động (%1) + + + + Lobby + + + + Connect + Kết nối + + + + Username + Tên đăng nhập + + + + Server + Máy chủ + + + + Session + Phiên + + + + Players + Người chơi + + + + Resolve + Phân tích + + + + New game + Tạo mới + + + + Load game + Tải lại + + + + New room + Tạo phòng + + + + Join room + Vào phòng + + + + Ready + Sẵn sàng + + + + Mods mismatch + Bản sửa đổi chưa giống + + + + Leave + Rời khỏi + + + + Kick player + Mời ra + + + + Players in the room + Người chơi trong phòng + + + + Disconnect + Thoát + + + + No issues detected + Không có vấn đề + + + + LobbyRoomRequest + + + Room settings + Cài đặt phòng + + + + Room name + Tên phòng + + + + Maximum players + Số người chơi tối đa + + + + Password (optional) + Mật khẩu (tùy chọn) + + + + MainWindow + + + VCMI Launcher + VCMI Launcher + + + + Settings + Cài đặt + + + + Help + + + + + Map Editor + Tạo bản đồ + + + + Start game + Chơi ngay + + + + Lobby + Sảnh + + + + Mods + Bản sửa đổi + + + + UpdateDialog + + + You have the latest version + + + + + Close + Đóng + + + + Check for updates on startup + + + + diff --git a/lib/AI_Base.h b/lib/AI_Base.h index cf4bf9c13..a6761c38f 100644 --- a/lib/AI_Base.h +++ b/lib/AI_Base.h @@ -1,14 +1,14 @@ -/* - * AI_Base.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 "CGameInterface.h" - -#define AI_INTERFACE_VER 1 +/* + * AI_Base.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 "CGameInterface.h" + +#define AI_INTERFACE_VER 1 diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index b56f2c7be..bfe46894c 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -33,16 +33,17 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid) { const auto * art = aid.toArtifact(); - if(art->canBePutAt(target, GameConstants::BACKPACK_START)) - { - return GameConstants::BACKPACK_START; - } + if(target->bearerType() == ArtBearer::HERO) + if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) + { + return ArtifactPosition::BACKPACK_START; + } return ArtifactPosition::PRE_FIRST; } -DLL_LINKAGE const std::vector & ArtifactUtils::unmovableSlots() +DLL_LINKAGE const std::vector & ArtifactUtils::unmovableSlots() { - static const std::vector positions = + static const std::vector positions = { ArtifactPosition::SPELLBOOK, ArtifactPosition::MACH4 @@ -51,9 +52,9 @@ DLL_LINKAGE const std::vector & ArtifactUti return positions; } -DLL_LINKAGE const std::vector & ArtifactUtils::constituentWornSlots() +DLL_LINKAGE const std::vector & ArtifactUtils::constituentWornSlots() { - static const std::vector positions = + static const std::vector positions = { ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, @@ -74,6 +75,49 @@ DLL_LINKAGE const std::vector & ArtifactUti return positions; } +DLL_LINKAGE const std::vector & ArtifactUtils::allWornSlots() +{ + static const std::vector positions = + { + ArtifactPosition::HEAD, + ArtifactPosition::SHOULDERS, + ArtifactPosition::NECK, + ArtifactPosition::RIGHT_HAND, + ArtifactPosition::LEFT_HAND, + ArtifactPosition::TORSO, + ArtifactPosition::RIGHT_RING, + ArtifactPosition::LEFT_RING, + ArtifactPosition::FEET, + ArtifactPosition::MISC1, + ArtifactPosition::MISC2, + ArtifactPosition::MISC3, + ArtifactPosition::MISC4, + ArtifactPosition::MISC5, + ArtifactPosition::MACH1, + ArtifactPosition::MACH2, + ArtifactPosition::MACH3, + ArtifactPosition::MACH4, + ArtifactPosition::SPELLBOOK + }; + + return positions; +} + +DLL_LINKAGE const std::vector & ArtifactUtils::commanderSlots() +{ + static const std::vector positions = + { + ArtifactPosition::COMMANDER1, + ArtifactPosition::COMMANDER2, + ArtifactPosition::COMMANDER3, + ArtifactPosition::COMMANDER4, + ArtifactPosition::COMMANDER5, + ArtifactPosition::COMMANDER6 + }; + + return positions; +} + DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair & slot) { return slot.second.artifact @@ -87,10 +131,9 @@ DLL_LINKAGE bool ArtifactUtils::checkSpellbookIsNeeded(const CGHeroInstance * he // Titan's Thunder creates new spellbook on equip if(artID == ArtifactID::TITANS_THUNDER && slot == ArtifactPosition::RIGHT_HAND) { - if(heroPtr) + if(heroPtr && !heroPtr->hasSpellbook()) { - if(heroPtr && !heroPtr->hasSpellbook()) - return true; + return true; } } return false; @@ -98,12 +141,12 @@ DLL_LINKAGE bool ArtifactUtils::checkSpellbookIsNeeded(const CGHeroInstance * he DLL_LINKAGE bool ArtifactUtils::isSlotBackpack(const ArtifactPosition & slot) { - return slot >= GameConstants::BACKPACK_START; + return slot >= ArtifactPosition::BACKPACK_START; } DLL_LINKAGE bool ArtifactUtils::isSlotEquipment(const ArtifactPosition & slot) { - return slot < GameConstants::BACKPACK_START && slot >= 0; + return slot < ArtifactPosition::BACKPACK_START && slot >= ArtifactPosition(0); } DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots) @@ -116,7 +159,7 @@ DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, } DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( - const CArtifactSet * artSet, const ArtifactID & aid, bool equipped) + const CArtifactSet * artSet, const ArtifactID & aid) { std::vector arts; const auto * art = aid.toArtifact(); @@ -130,23 +173,10 @@ DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( for(const auto constituent : artifact->getConstituents()) //check if all constituents are available { - if(equipped) + if(!artSet->hasArt(constituent->getId(), false, false, false)) { - // Search for equipped arts - if(!artSet->hasArt(constituent->getId(), true, false, false)) - { - possible = false; - break; - } - } - else - { - // Search in backpack - if(!artSet->hasArtBackpack(constituent->getId())) - { - possible = false; - break; - } + possible = false; + break; } } if(possible) @@ -159,7 +189,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) { auto ret = new CArtifactInstance(VLC->arth->objects[ArtifactID::SPELL_SCROLL]); auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, - BonusSource::ARTIFACT_INSTANCE, -1, ArtifactID::SPELL_SCROLL, sid); + BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(sid)); ret->addNewBonus(bonus); return ret; } @@ -171,7 +201,6 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifa auto * artInst = new CArtifactInstance(art); if(art->isCombined()) { - assert(art->isCombined()); for(const auto & part : art->getConstituents()) artInst->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); } @@ -187,21 +216,21 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifa DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const ArtifactID & aid) { - return ArtifactUtils::createNewArtifactInstance(VLC->arth->objects[aid]); + return ArtifactUtils::createNewArtifactInstance((*VLC->arth)[aid]); } -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, int spellID) +DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID) { CArtifactInstance * art = nullptr; - if(aid >= 0) + if(aid.getNum() >= 0) { - if(spellID < 0) + if(spellID == SpellID::NONE) { art = ArtifactUtils::createNewArtifactInstance(aid); } else { - art = ArtifactUtils::createScroll(SpellID(spellID)); + art = ArtifactUtils::createScroll(spellID); } } else @@ -226,10 +255,13 @@ DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description // However other language versions don't have name placeholder at all, so we have to be careful auto nameStart = description.find_first_of('['); auto nameEnd = description.find_first_of(']', nameStart); - if(sid.getNum() >= 0) + + if(nameStart != std::string::npos && nameEnd != std::string::npos) { - if(nameStart != std::string::npos && nameEnd != std::string::npos) - description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toSpell(VLC->spells())->getNameTranslated()); + if(sid.getNum() >= 0) + description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toEntity(VLC->spells())->getNameTranslated()); + else + description = description.erase(nameStart, nameEnd - nameStart + 2); // erase "[spell name] " - including space } } diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index c7b893dee..b4af3b401 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -29,18 +29,20 @@ namespace ArtifactUtils DLL_LINKAGE ArtifactPosition getArtAnyPosition(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(); - DLL_LINKAGE const std::vector & constituentWornSlots(); + DLL_LINKAGE const std::vector & unmovableSlots(); + DLL_LINKAGE const std::vector & constituentWornSlots(); + DLL_LINKAGE const std::vector & allWornSlots(); + DLL_LINKAGE const std::vector & commanderSlots(); DLL_LINKAGE bool isArtRemovable(const std::pair & slot); DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, const ArtifactID & artID, const ArtifactPosition & slot); 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, bool equipped); + DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid); DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & sid); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(CArtifact * art); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid); - DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, int spellID = -1); + DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, 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 37579a53f..a6e8d9466 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -33,14 +33,13 @@ bool INativeTerrainProvider::isNativeTerrain(TerrainId terrain) const TerrainId AFactionMember::getNativeTerrain() const { - constexpr auto any = TerrainId(ETerrainId::ANY_TERRAIN); - const std::string cachingStringNoTerrainPenalty = "type_NO_TERRAIN_PENALTY_sANY"; - static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, any); + const std::string cachingStringNoTerrainPenalty = "type_TERRAIN_NATIVE_NONE"; + static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::TERRAIN_NATIVE, BonusSubtypeID()); //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) - ? any : VLC->factions()->getById(getFaction())->getNativeTerrain(); + ? TerrainId::ANY_TERRAIN : VLC->factions()->getById(getFaction())->getNativeTerrain(); } int32_t AFactionMember::magicResistance() const @@ -54,7 +53,7 @@ int AFactionMember::getAttack(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_ATTACK"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -63,7 +62,7 @@ int AFactionMember::getDefense(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_DEFENSE"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -71,23 +70,23 @@ int AFactionMember::getDefense(bool ranged) const int AFactionMember::getMinDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_1"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } int AFactionMember::getMaxDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_2"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } -int AFactionMember::getPrimSkillLevel(PrimarySkill::PrimarySkill id) const +int AFactionMember::getPrimSkillLevel(PrimarySkill id) const { static const CSelector selectorAllSkills = Selector::type()(BonusType::PRIMARY_SKILL); static const std::string keyAllSkills = "type_PRIMARY_SKILL"; auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills); - auto ret = allSkills->valOfBonuses(Selector::subtype()(id)); + auto ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id))); auto minSkillValue = (id == PrimarySkill::SPELL_POWER || id == PrimarySkill::KNOWLEDGE) ? 1 : 0; return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves } @@ -111,7 +110,7 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const bonusList = getBonusBearer()->getBonuses(moraleSelector, cachingStrMor); int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); - int32_t maxBadMorale = -VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); + int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); return std::clamp(bonusList->totalValue(), maxBadMorale, maxGoodMorale); } @@ -130,7 +129,7 @@ int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const bonusList = getBonusBearer()->getBonuses(luckSelector, cachingStrLuck); int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); - int32_t maxBadLuck = -VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); + int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); return std::clamp(bonusList->totalValue(), maxBadLuck, maxGoodLuck); } @@ -183,4 +182,4 @@ bool ACreature::isLiving() const //TODO: theoreticaly there exists "LIVING" bonu } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index 2f6df594b..9b64a5336 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -21,7 +21,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto * info = new BattleFieldInfo(BattleField(index), identifier); - info->graphics = json["graphics"].String(); + info->graphics = ImagePath::fromJson(json["graphics"]); info->icon = json["icon"].String(); info->name = json["name"].String(); for(const auto & b : json["bonuses"].Vector()) @@ -29,7 +29,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto bonus = JsonUtils::parseBonus(b); bonus->source = BonusSource::TERRAIN_OVERLAY; - bonus->sid = info->getIndex(); + bonus->sid = BonusSourceID(info->getId()); bonus->duration = BonusDuration::ONE_BATTLE; info->bonuses.push_back(bonus); @@ -54,11 +54,6 @@ const std::vector & BattleFieldHandler::getTypeNames() const return types; } -std::vector BattleFieldHandler::getDefaultAllowed() const -{ - return std::vector(); -} - int32_t BattleFieldInfo::getIndex() const { return battlefield.getNum(); diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index a3e550485..ec5535bc0 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -15,6 +15,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "battle/BattleHex.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,7 +25,7 @@ public: BattleField battlefield; std::vector> bonuses; bool isSpecial; - std::string graphics; + ImagePath graphics; std::string name; std::string identifier; std::string icon; @@ -52,19 +53,6 @@ public: std::string getNameTranslated() const override; void registerIcons(const IconRegistar & cb) const override; BattleField getId() const override; - - template void serialize(Handler & h, const int version) - { - h & name; - h & identifier; - h & isSpecial; - h & graphics; - h & icon; - h & iconIndex; - h & battlefield; - h & impassableHexes; - - } }; class DLL_LINKAGE BattleFieldService : public EntityServiceT @@ -83,12 +71,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index d39030400..64bbe63d7 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -1,1201 +1,1096 @@ -/* - * CArtHandler.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 "CArtHandler.h" - -#include "ArtifactUtils.h" -#include "CGeneralTextHandler.h" -#include "CModHandler.h" -#include "GameSettings.h" -#include "mapObjects/MapObjects.h" -#include "StringConstants.h" - -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "serializer/JsonSerializeFormat.h" - -// Note: list must match entries in ArtTraits.txt -#define ART_POS_LIST \ - ART_POS(SPELLBOOK) \ - ART_POS(MACH4) \ - ART_POS(MACH3) \ - ART_POS(MACH2) \ - ART_POS(MACH1) \ - ART_POS(MISC5) \ - ART_POS(MISC4) \ - ART_POS(MISC3) \ - ART_POS(MISC2) \ - ART_POS(MISC1) \ - ART_POS(FEET) \ - ART_POS(LEFT_RING) \ - ART_POS(RIGHT_RING) \ - ART_POS(TORSO) \ - ART_POS(LEFT_HAND) \ - ART_POS(RIGHT_HAND) \ - ART_POS(NECK) \ - ART_POS(SHOULDERS) \ - ART_POS(HEAD) - -VCMI_LIB_NAMESPACE_BEGIN - -bool CCombinedArtifact::isCombined() const -{ - return !(constituents.empty()); -} - -const std::vector & CCombinedArtifact::getConstituents() const -{ - return constituents; -} - -const std::vector & CCombinedArtifact::getPartOf() const -{ - return partOf; -} - -bool CScrollArtifact::isScroll() const -{ - return static_cast(this)->getId() == ArtifactID::SPELL_SCROLL; -} - -bool CGrowingArtifact::isGrowing() const -{ - return !bonusesPerLevel.empty() || !thresholdBonuses.empty(); -} - -std::vector > & CGrowingArtifact::getBonusesPerLevel() -{ - return bonusesPerLevel; -} - -const std::vector > & CGrowingArtifact::getBonusesPerLevel() const -{ - return bonusesPerLevel; -} - -std::vector > & CGrowingArtifact::getThresholdBonuses() -{ - return thresholdBonuses; -} - -const std::vector > & CGrowingArtifact::getThresholdBonuses() const -{ - return thresholdBonuses; -} - -int32_t CArtifact::getIndex() const -{ - return id.toEnum(); -} - -int32_t CArtifact::getIconIndex() const -{ - return iconIndex; -} - -std::string CArtifact::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -void CArtifact::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "ARTIFACT", image); - cb(getIconIndex(), 0, "ARTIFACTLARGE", large); -} - -ArtifactID CArtifact::getId() const -{ - return id; -} - -const IBonusBearer * CArtifact::getBonusBearer() const -{ - return this; -} - -std::string CArtifact::getDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getDescriptionTextID()); -} - -std::string CArtifact::getEventTranslated() const -{ - return VLC->generaltexth->translate(getEventTextID()); -} - -std::string CArtifact::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CArtifact::getDescriptionTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "description").get(); -} - -std::string CArtifact::getEventTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "event").get(); -} - -std::string CArtifact::getNameTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "name").get(); -} - -uint32_t CArtifact::getPrice() const -{ - return price; -} - -CreatureID CArtifact::getWarMachine() const -{ - return warMachine; -} - -bool CArtifact::isBig() const -{ - return warMachine != CreatureID::NONE; -} - -bool CArtifact::isTradable() const -{ - switch(id) - { - case ArtifactID::SPELLBOOK: - case ArtifactID::GRAIL: - return false; - default: - return !isBig(); - } -} - -bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const -{ - auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool - { - if(ArtifactUtils::isSlotBackpack(slot)) - { - if(isBig() || !ArtifactUtils::isBackpackFreeSlots(artSet)) - return false; - return true; - } - - if(!vstd::contains(possibleSlots.at(artSet->bearerType()), slot)) - return false; - - return artSet->isPositionFree(slot, assumeDestRemoved); - }; - - auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool - { - if(isCombined()) - { - if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved)) - return false; - if(ArtifactUtils::isSlotBackpack(slot)) - return true; - - CArtifactFittingSet fittingSet(artSet->bearerType()); - fittingSet.artifactsWorn = artSet->artifactsWorn; - if(assumeDestRemoved) - fittingSet.removeArtifact(slot); - - for(const auto art : constituents) - { - auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId()); - if(ArtifactUtils::isSlotEquipment(possibleSlot)) - { - fittingSet.setNewArtSlot(possibleSlot, nullptr, true); - } - else - { - return false; - } - } - return true; - } - else - { - return simpleArtCanBePutAt(artSet, slot, assumeDestRemoved); - } - }; - - if(slot == ArtifactPosition::TRANSITION_POS) - return true; - - if(slot == ArtifactPosition::FIRST_AVAILABLE) - { - for(const auto & slot : possibleSlots.at(artSet->bearerType())) - { - if(artCanBePutAt(artSet, slot, assumeDestRemoved)) - return true; - } - return artCanBePutAt(artSet, GameConstants::BACKPACK_START, assumeDestRemoved); - } - else if(ArtifactUtils::isSlotBackpack(slot)) - { - return artCanBePutAt(artSet, GameConstants::BACKPACK_START, assumeDestRemoved); - } - else - { - return artCanBePutAt(artSet, slot, assumeDestRemoved); - } -} - -CArtifact::CArtifact() - : iconIndex(ArtifactID::NONE), - price(0) -{ - setNodeType(ARTIFACT); - possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty - possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty - possibleSlots[ArtBearer::COMMANDER]; -} - -//This destructor should be placed here to avoid side effects -CArtifact::~CArtifact() = default; - -int CArtifact::getArtClassSerial() const -{ - if(id == ArtifactID::SPELL_SCROLL) - return 4; - switch(aClass) - { - case ART_TREASURE: - return 0; - case ART_MINOR: - return 1; - case ART_MAJOR: - return 2; - case ART_RELIC: - return 3; - case ART_SPECIAL: - return 5; - } - - return -1; -} - -std::string CArtifact::nodeName() const -{ - return "Artifact: " + getNameTranslated(); -} - -void CArtifact::addNewBonus(const std::shared_ptr& b) -{ - b->source = BonusSource::ARTIFACT; - b->duration = BonusDuration::PERMANENT; - b->description = getNameTranslated(); - CBonusSystemNode::addNewBonus(b); -} - -const std::map> & CArtifact::getPossibleSlots() const -{ - return possibleSlots; -} - -void CArtifact::updateFrom(const JsonNode& data) -{ - //TODO:CArtifact::updateFrom -} - -void CArtifact::setImage(int32_t iconIndex, std::string image, std::string large) -{ - this->iconIndex = iconIndex; - this->image = image; - this->large = large; -} - -CArtHandler::~CArtHandler() = default; - -std::vector CArtHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ARTIFACT); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - #define ART_POS(x) #x , - const std::vector artSlots = { ART_POS_LIST }; - #undef ART_POS - - static std::map classes = - {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; - - CLegacyConfigParser parser("DATA/ARTRAITS.TXT"); - CLegacyConfigParser events("DATA/ARTEVENT.TXT"); - - parser.endLine(); // header - parser.endLine(); - - for (size_t i = 0; i < dataSize; i++) - { - JsonNode artData; - - artData["text"]["name"].String() = parser.readString(); - artData["text"]["event"].String() = events.readString(); - artData["value"].Float() = parser.readNumber(); - - for(const auto & artSlot : artSlots) - { - if(parser.readString() == "x") - { - artData["slot"].Vector().push_back(JsonNode()); - artData["slot"].Vector().back().String() = artSlot; - } - } - artData["class"].String() = classes[parser.readString()[0]]; - artData["text"]["description"].String() = parser.readString(); - - parser.endLine(); - events.endLine(); - h3Data.push_back(artData); - } - return h3Data; -} - -void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) -{ - auto * object = loadFromJson(scope, data, name, objects.size()); - - object->iconIndex = object->getIndex() + 5; - - objects.emplace_back(object); - - registerObject(scope, "artifact", name, object->id); -} - -void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto * object = loadFromJson(scope, data, name, index); - - object->iconIndex = object->getIndex(); - - assert(objects[index] == nullptr); // ensure that this id was not loaded before - objects[index] = object; - - registerObject(scope, "artifact", name, object->id); -} - -const std::vector & CArtHandler::getTypeNames() const -{ - static const std::vector typeNames = { "artifact" }; - return typeNames; -} - -CArtifact * 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(); - if(!node["growing"].isNull()) - { - for(auto bonus : node["growing"]["bonusesPerLevel"].Vector()) - { - art->bonusesPerLevel.emplace_back(static_cast(bonus["level"].Float()), Bonus()); - JsonUtils::parseBonus(bonus["bonus"], &art->bonusesPerLevel.back().second); - } - for(auto bonus : node["growing"]["thresholdBonuses"].Vector()) - { - art->thresholdBonuses.emplace_back(static_cast(bonus["level"].Float()), Bonus()); - JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second); - } - } - art->id = ArtifactID(index); - art->identifier = identifier; - art->modScope = scope; - - const JsonNode & text = node["text"]; - - VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String()); - VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String()); - VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String()); - - const JsonNode & graphics = node["graphics"]; - art->image = graphics["image"].String(); - - if(!graphics["large"].isNull()) - art->large = graphics["large"].String(); - else - art->large = art->image; - - art->advMapDef = graphics["map"].String(); - - art->price = static_cast(node["value"].Float()); - art->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); - - loadSlots(art, node); - loadClass(art, node); - loadType(art, node); - loadComponents(art, node); - - for(const auto & b : node["bonuses"].Vector()) - { - auto bonus = JsonUtils::parseBonus(b); - art->addNewBonus(bonus); - } - - const JsonNode & warMachine = node["warMachine"]; - if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) - { - VLC->modh->identifiers.requestIdentifier("creature", warMachine, [=](si32 id) - { - art->warMachine = CreatureID(id); - - //this assumes that creature object is stored before registration - VLC->creh->objects.at(id)->warMachine = art->id; - }); - } - - VLC->modh->identifiers.requestIdentifier(scope, "object", "artifact", [=](si32 index) - { - JsonNode conf; - conf.setMeta(scope); - - VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex()); - - if(!art->advMapDef.empty()) - { - JsonNode templ; - templ["animation"].String() = art->advMapDef; - templ.setMeta(scope); - - // add new template. - // Necessary for objects added via mods that don't have any templates in H3 - VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); - } - // object does not have any templates - this is not usable object (e.g. pseudo-art like lock) - if(VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::ARTIFACT, art->getIndex()); - }); - - return art; -} - -ArtifactPosition::ArtifactPosition(std::string slotName): - num(ArtifactPosition::PRE_FIRST) -{ -#define ART_POS(x) { #x, ArtifactPosition::x }, - static const std::map artifactPositionMap = { ART_POS_LIST }; -#undef ART_POS - auto it = artifactPositionMap.find (slotName); - if (it != artifactPositionMap.end()) - num = it->second; - else - logMod->warn("Warning! Artifact slot %s not recognized!", slotName); -} - -void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const -{ - static const std::vector miscSlots = - { - ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 - }; - - static const std::vector ringSlots = - { - ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING - }; - - if (slotID == "MISC") - { - vstd::concatenate(art->possibleSlots[ArtBearer::HERO], miscSlots); - } - else if (slotID == "RING") - { - vstd::concatenate(art->possibleSlots[ArtBearer::HERO], ringSlots); - } - else - { - auto slot = ArtifactPosition(slotID); - if (slot != ArtifactPosition::PRE_FIRST) - art->possibleSlots[ArtBearer::HERO].push_back(slot); - } -} - -void CArtHandler::loadSlots(CArtifact * art, const JsonNode & node) const -{ - if (!node["slot"].isNull()) //we assume non-hero slots are irrelevant? - { - if (node["slot"].getType() == JsonNode::JsonType::DATA_STRING) - addSlot(art, node["slot"].String()); - else - { - for (const JsonNode & slot : node["slot"].Vector()) - addSlot(art, slot.String()); - } - std::sort(art->possibleSlots.at(ArtBearer::HERO).begin(), art->possibleSlots.at(ArtBearer::HERO).end()); - } -} - -CArtifact::EartClass CArtHandler::stringToClass(const std::string & className) -{ - static const std::map artifactClassMap = - { - {"TREASURE", CArtifact::ART_TREASURE}, - {"MINOR", CArtifact::ART_MINOR}, - {"MAJOR", CArtifact::ART_MAJOR}, - {"RELIC", CArtifact::ART_RELIC}, - {"SPECIAL", CArtifact::ART_SPECIAL} - }; - - auto it = artifactClassMap.find (className); - if (it != artifactClassMap.end()) - return it->second; - - logMod->warn("Warning! Artifact rarity %s not recognized!", className); - return CArtifact::ART_SPECIAL; -} - -void CArtHandler::loadClass(CArtifact * art, const JsonNode & node) const -{ - art->aClass = stringToClass(node["class"].String()); -} - -void CArtHandler::loadType(CArtifact * art, const JsonNode & node) const -{ -#define ART_BEARER(x) { #x, ArtBearer::x }, - static const std::map artifactBearerMap = { ART_BEARER_LIST }; -#undef ART_BEARER - - for (const JsonNode & b : node["type"].Vector()) - { - auto it = artifactBearerMap.find (b.String()); - if (it != artifactBearerMap.end()) - { - int bearerType = it->second; - switch (bearerType) - { - case ArtBearer::HERO://TODO: allow arts having several possible bearers - break; - case ArtBearer::COMMANDER: - makeItCommanderArt (art); //original artifacts should have only one bearer type - break; - case ArtBearer::CREATURE: - makeItCreatureArt (art); - break; - } - } - else - logMod->warn("Warning! Artifact type %s not recognized!", b.String()); - } -} - -void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) -{ - if (!node["components"].isNull()) - { - for(const auto & component : node["components"].Vector()) - { - VLC->modh->identifiers.requestIdentifier("artifact", component, [=](si32 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(objects[id]); - objects[id]->partOf.push_back(art); - }); - } - } -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) -{ - auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) - { - if (arts->empty()) //restock available arts - fillList(*arts, flag); - - for (auto & arts_i : *arts) - { - if (accepts(arts_i->id)) - { - CArtifact *art = arts_i; - out.emplace_back(art); - } - } - }; - - auto getAllowed = [&](std::vector > &out) - { - if (flags & CArtifact::ART_TREASURE) - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - if (flags & CArtifact::ART_MINOR) - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - if (flags & CArtifact::ART_MAJOR) - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - if (flags & CArtifact::ART_RELIC) - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - if(out.empty()) //no artifact of specified rarity, we need to take another one - { - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - } - if(out.empty()) //no arts are available at all - { - out.resize (64); - std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully) - } - }; - - std::vector > out; - getAllowed(out); - ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; - erasePickedArt(artID); - return artID; -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) -{ - return pickRandomArtifact(rand, 0xff, std::move(accepts)); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) -{ - return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); -} - -void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) -{ - if (onlyCreature) - { - a->possibleSlots[ArtBearer::HERO].clear(); - a->possibleSlots[ArtBearer::COMMANDER].clear(); - } - a->possibleSlots[ArtBearer::CREATURE].push_back(ArtifactPosition::CREATURE_SLOT); -} - -void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) -{ - if (onlyCommander) - { - a->possibleSlots[ArtBearer::HERO].clear(); - a->possibleSlots[ArtBearer::CREATURE].clear(); - } - for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i) - a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i)); -} - -bool CArtHandler::legalArtifact(const ArtifactID & id) -{ - auto art = objects[id]; - //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components - - if(art->isCombined()) - return false; //no combo artifacts spawning - - if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) - return false; // invalid class - - if(!art->possibleSlots[ArtBearer::HERO].empty()) - return true; - - if(!art->possibleSlots[ArtBearer::CREATURE].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) - return true; - - if(!art->possibleSlots[ArtBearer::COMMANDER].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) - return true; - - return false; -} - -void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) -{ - allowedArtifacts.clear(); - treasures.clear(); - minors.clear(); - majors.clear(); - relics.clear(); - - for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) - { - if (allowed[i] && legalArtifact(ArtifactID(i))) - allowedArtifacts.push_back(objects[i]); - //keep im mind that artifact can be worn by more than one type of bearer - } -} - -std::vector CArtHandler::getDefaultAllowed() const -{ - std::vector allowedArtifacts; - allowedArtifacts.resize(127, true); - allowedArtifacts.resize(141, false); - allowedArtifacts.resize(size(), true); - return allowedArtifacts; -} - -void CArtHandler::erasePickedArt(const ArtifactID & id) -{ - CArtifact *art = objects[id]; - - std::vector * artifactList = nullptr; - switch(art->aClass) - { - case CArtifact::ART_TREASURE: - artifactList = &treasures; - break; - case CArtifact::ART_MINOR: - artifactList = &minors; - break; - case CArtifact::ART_MAJOR: - artifactList = &majors; - break; - case CArtifact::ART_RELIC: - artifactList = &relics; - break; - default: - logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated()); - return; - } - - if(artifactList->empty()) - fillList(*artifactList, art->aClass); - - auto itr = vstd::find(*artifactList, art); - if(itr != artifactList->end()) - { - artifactList->erase(itr); - } - else - logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated()); -} - -void CArtHandler::fillList( std::vector &listToBeFilled, CArtifact::EartClass artifactClass ) -{ - assert(listToBeFilled.empty()); - for (auto & elem : allowedArtifacts) - { - if (elem->aClass == artifactClass) - listToBeFilled.push_back(elem); - } -} - -void CArtHandler::afterLoadFinalization() -{ - //All artifacts have their id, so we can properly update their bonuses' source ids. - for(auto &art : objects) - { - for(auto &bonus : art->getExportedBonusList()) - { - assert(art == objects[art->id]); - assert(bonus->source == BonusSource::ARTIFACT); - bonus->sid = art->id; - } - } - CBonusSystemNode::treeHasChanged(); -} - -CArtifactSet::~CArtifactSet() = default; - -const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const -{ - if(const ArtSlotInfo * si = getSlot(pos)) - { - if(si->artifact && (!excludeLocked || !si->locked)) - return si->artifact; - } - - 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]; -} - -ArtifactPosition CArtifactSet::getArtBackpackPos(const ArtifactID & aid) const -{ - const auto result = getBackpackArtPositions(aid); - 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 = GameConstants::BACKPACK_START; - for(const auto & artInfo : artifactsInBackpack) - { - const auto * art = artInfo.getArt(); - if(art && art->artType->getId() == aid) - result.emplace_back(backpackPosition); - backpackPosition++; - } - 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(GameConstants::BACKPACK_START + i); - - return ArtifactPosition::PRE_FIRST; -} - -const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const -{ - for(auto i : artifactsWorn) - if(i.second.artifact->getId() == artInstId) - return i.second.artifact; - - for(auto i : artifactsInBackpack) - if(i.artifact->getId() == artInstId) - return i.artifact; - - return nullptr; -} - -const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * artInst) const -{ - if(artInst) - { - for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType())) - if(getArt(slot) == artInst) - return slot; - - auto backpackSlot = GameConstants::BACKPACK_START; - for(auto & slotInfo : artifactsInBackpack) - { - if(slotInfo.getArt() == artInst) - return backpackSlot; - backpackSlot = ArtifactPosition(backpackSlot + 1); - } - } - return ArtifactPosition::PRE_FIRST; -} - -bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const -{ - return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0; -} - -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; -} - -void CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) -{ - setNewArtSlot(slot, art, false); - if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) - { - const CArtifactInstance * mainPart = nullptr; - for(const auto & part : art->getPartsInfo()) - if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot) - && (part.slot == ArtifactPosition::PRE_FIRST)) - { - mainPart = part.art; - break; - } - - for(auto & part : art->getPartsInfo()) - { - if(part.art != mainPart) - { - if(!part.art->artType->canBePutAt(this, part.slot)) - part.slot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); - - assert(ArtifactUtils::isSlotEquipment(part.slot)); - setNewArtSlot(part.slot, part.art, true); - } - } - } -} - -void CArtifactSet::removeArtifact(ArtifactPosition slot) -{ - auto art = getArt(slot, false); - if(art) - { - if(art->isCombined()) - { - for(auto & part : art->getPartsInfo()) - { - if(getArt(part.slot, false)) - eraseArtSlot(part.slot); - } - } - eraseArtSlot(slot); - } -} - -std::pair CArtifactSet::searchForConstituent(const ArtifactID & aid) const -{ - for(const auto & slot : artifactsInBackpack) - { - auto art = slot.artifact; - if(art->isCombined()) - { - for(auto & ci : art->getPartsInfo()) - { - if(ci.art->getTypeId() == aid) - { - return {art, ci.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; -} - -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()); - } - if(vstd::contains(artifactsWorn, pos)) - return &artifactsWorn.at(pos); - if(pos >= ArtifactPosition::AFTER_LAST ) - { - int backpackPos = (int)pos - GameConstants::BACKPACK_START; - if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) - return nullptr; - else - return &artifactsInBackpack[backpackPos]; - } - - return nullptr; -} - -bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const -{ - if(const ArtSlotInfo *s = getSlot(pos)) - return (onlyLockCheck || !s->artifact) && !s->locked; - - return true; //no slot means not used -} - -void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, CArtifactInstance * 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 - GameConstants::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 - GameConstants::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) - if(elem.second.artifact && !elem.second.locked) - node->attachTo(*elem.second.artifact); -} - -void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map) -{ - //todo: creature and commander artifacts - if(handler.saving && artifactsInBackpack.empty() && artifactsWorn.empty()) - return; - - if(!handler.saving) - { - assert(map); - artifactsInBackpack.clear(); - artifactsWorn.clear(); - } - - auto s = handler.enterStruct(fieldName); - - switch(bearerType()) - { - case ArtBearer::HERO: - serializeJsonHero(handler, map); - break; - case ArtBearer::CREATURE: - serializeJsonCreature(handler, map); - break; - case ArtBearer::COMMANDER: - serializeJsonCommander(handler, map); - break; - default: - assert(false); - break; - } -} - -void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) -{ - for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1)) - { - serializeJsonSlot(handler, ap, map); - } - - std::vector backpackTemp; - - if(handler.saving) - { - backpackTemp.reserve(artifactsInBackpack.size()); - for(const ArtSlotInfo & info : artifactsInBackpack) - backpackTemp.push_back(info.artifact->getTypeId()); - } - handler.serializeIdArray(NArtifactPosition::backpack, backpackTemp); - if(!handler.saving) - { - for(const ArtifactID & artifactID : backpackTemp) - { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + (si32)artifactsInBackpack.size()); - if(artifact->artType->canBePutAt(this, slot)) - putArtifact(slot, artifact); - } - } -} - -void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler, CMap * map) -{ - logGlobal->error("CArtifactSet::serializeJsonCreature not implemented"); -} - -void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler, CMap * map) -{ - logGlobal->error("CArtifactSet::serializeJsonCommander not implemented"); -} - -void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map) -{ - ArtifactID artifactID; - - if(handler.saving) - { - const ArtSlotInfo * info = getSlot(slot); - - if(info != nullptr && !info->locked) - { - artifactID = info->artifact->getTypeId(); - handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); - } - } - else - { - handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); - - if(artifactID != ArtifactID::NONE) - { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); - - if(artifact->artType->canBePutAt(this, slot)) - { - putArtifact(slot, artifact); - } - else - { - logGlobal->debug("Artifact can't be put at the specified location."); //TODO add more debugging information - } - } - } -} - -CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer Bearer): - Bearer(Bearer) -{ -} - -ArtBearer::ArtBearer CArtifactFittingSet::bearerType() const -{ - return this->Bearer; -} - -VCMI_LIB_NAMESPACE_END +/* + * CArtHandler.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 "ArtifactUtils.h" +#include "CGeneralTextHandler.h" +#include "GameSettings.h" +#include "mapObjects/MapObjects.h" +#include "constants/StringConstants.h" + +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "serializer/JsonSerializeFormat.h" + +// Note: list must match entries in ArtTraits.txt +#define ART_POS_LIST \ + ART_POS(SPELLBOOK) \ + ART_POS(MACH4) \ + ART_POS(MACH3) \ + ART_POS(MACH2) \ + ART_POS(MACH1) \ + ART_POS(MISC5) \ + ART_POS(MISC4) \ + ART_POS(MISC3) \ + ART_POS(MISC2) \ + ART_POS(MISC1) \ + ART_POS(FEET) \ + ART_POS(LEFT_RING) \ + ART_POS(RIGHT_RING) \ + ART_POS(TORSO) \ + ART_POS(LEFT_HAND) \ + ART_POS(RIGHT_HAND) \ + ART_POS(NECK) \ + ART_POS(SHOULDERS) \ + ART_POS(HEAD) + +VCMI_LIB_NAMESPACE_BEGIN + +bool CCombinedArtifact::isCombined() const +{ + return !(constituents.empty()); +} + +const std::vector & CCombinedArtifact::getConstituents() const +{ + return constituents; +} + +const std::vector & CCombinedArtifact::getPartOf() const +{ + return partOf; +} + +bool CScrollArtifact::isScroll() const +{ + return static_cast(this)->getId() == ArtifactID::SPELL_SCROLL; +} + +bool CGrowingArtifact::isGrowing() const +{ + return !bonusesPerLevel.empty() || !thresholdBonuses.empty(); +} + +std::vector > & CGrowingArtifact::getBonusesPerLevel() +{ + return bonusesPerLevel; +} + +const std::vector > & CGrowingArtifact::getBonusesPerLevel() const +{ + return bonusesPerLevel; +} + +std::vector > & CGrowingArtifact::getThresholdBonuses() +{ + return thresholdBonuses; +} + +const std::vector > & CGrowingArtifact::getThresholdBonuses() const +{ + return thresholdBonuses; +} + +int32_t CArtifact::getIndex() const +{ + return id.toEnum(); +} + +int32_t CArtifact::getIconIndex() const +{ + return iconIndex; +} + +std::string CArtifact::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +void CArtifact::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "ARTIFACT", image); + cb(getIconIndex(), 0, "ARTIFACTLARGE", large); +} + +ArtifactID CArtifact::getId() const +{ + return id; +} + +const IBonusBearer * CArtifact::getBonusBearer() const +{ + return this; +} + +std::string CArtifact::getDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getDescriptionTextID()); +} + +std::string CArtifact::getEventTranslated() const +{ + return VLC->generaltexth->translate(getEventTextID()); +} + +std::string CArtifact::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CArtifact::getDescriptionTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "description").get(); +} + +std::string CArtifact::getEventTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "event").get(); +} + +std::string CArtifact::getNameTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "name").get(); +} + +uint32_t CArtifact::getPrice() const +{ + return price; +} + +CreatureID CArtifact::getWarMachine() const +{ + return warMachine; +} + +bool CArtifact::isBig() const +{ + return warMachine != CreatureID::NONE; +} + +bool CArtifact::isTradable() const +{ + switch(id.toEnum()) + { + case ArtifactID::SPELLBOOK: + case ArtifactID::GRAIL: + return false; + default: + return !isBig(); + } +} + +bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const +{ + auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool + { + if(ArtifactUtils::isSlotBackpack(slot)) + { + if(isBig() || !ArtifactUtils::isBackpackFreeSlots(artSet)) + return false; + return true; + } + + if(!vstd::contains(possibleSlots.at(artSet->bearerType()), slot)) + return false; + + return artSet->isPositionFree(slot, assumeDestRemoved); + }; + + auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool + { + if(isCombined()) + { + if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved)) + return false; + if(ArtifactUtils::isSlotBackpack(slot)) + return true; + + CArtifactFittingSet fittingSet(artSet->bearerType()); + fittingSet.artifactsWorn = artSet->artifactsWorn; + if(assumeDestRemoved) + fittingSet.removeArtifact(slot); + + for(const auto art : constituents) + { + auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId()); + if(ArtifactUtils::isSlotEquipment(possibleSlot)) + { + fittingSet.setNewArtSlot(possibleSlot, nullptr, true); + } + else + { + return false; + } + } + return true; + } + else + { + return simpleArtCanBePutAt(artSet, slot, assumeDestRemoved); + } + }; + + if(slot == ArtifactPosition::TRANSITION_POS) + return true; + + if(slot == ArtifactPosition::FIRST_AVAILABLE) + { + for(const auto & slot : possibleSlots.at(artSet->bearerType())) + { + if(artCanBePutAt(artSet, slot, assumeDestRemoved)) + return true; + } + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); + } + else if(ArtifactUtils::isSlotBackpack(slot)) + { + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); + } + else + { + return artCanBePutAt(artSet, slot, assumeDestRemoved); + } +} + +CArtifact::CArtifact() + : iconIndex(ArtifactID::NONE), + price(0) +{ + setNodeType(ARTIFACT); + possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty + possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty + possibleSlots[ArtBearer::COMMANDER]; +} + +//This destructor should be placed here to avoid side effects +CArtifact::~CArtifact() = default; + +int CArtifact::getArtClassSerial() const +{ + if(id == ArtifactID::SPELL_SCROLL) + return 4; + switch(aClass) + { + case ART_TREASURE: + return 0; + case ART_MINOR: + return 1; + case ART_MAJOR: + return 2; + case ART_RELIC: + return 3; + case ART_SPECIAL: + return 5; + } + + return -1; +} + +std::string CArtifact::nodeName() const +{ + return "Artifact: " + getNameTranslated(); +} + +void CArtifact::addNewBonus(const std::shared_ptr& b) +{ + b->source = BonusSource::ARTIFACT; + b->duration = BonusDuration::PERMANENT; + b->description = getNameTranslated(); + CBonusSystemNode::addNewBonus(b); +} + +const std::map> & CArtifact::getPossibleSlots() const +{ + return possibleSlots; +} + +void CArtifact::updateFrom(const JsonNode& data) +{ + //TODO:CArtifact::updateFrom +} + +void CArtifact::setImage(int32_t iconIndex, std::string image, std::string large) +{ + this->iconIndex = iconIndex; + this->image = image; + this->large = large; +} + +CArtHandler::~CArtHandler() = default; + +std::vector CArtHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ARTIFACT); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + #define ART_POS(x) #x , + const std::vector artSlots = { ART_POS_LIST }; + #undef ART_POS + + static std::map classes = + {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; + + CLegacyConfigParser parser(TextPath::builtin("DATA/ARTRAITS.TXT")); + CLegacyConfigParser events(TextPath::builtin("DATA/ARTEVENT.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for (size_t i = 0; i < dataSize; i++) + { + JsonNode artData; + + artData["text"]["name"].String() = parser.readString(); + artData["text"]["event"].String() = events.readString(); + artData["value"].Float() = parser.readNumber(); + + for(const auto & artSlot : artSlots) + { + if(parser.readString() == "x") + { + artData["slot"].Vector().push_back(JsonNode()); + artData["slot"].Vector().back().String() = artSlot; + } + } + artData["class"].String() = classes[parser.readString()[0]]; + artData["text"]["description"].String() = parser.readString(); + + parser.endLine(); + events.endLine(); + h3Data.push_back(artData); + } + return h3Data; +} + +void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) +{ + auto * object = loadFromJson(scope, data, name, objects.size()); + + object->iconIndex = object->getIndex() + 5; + + objects.emplace_back(object); + + registerObject(scope, "artifact", name, object->id.getNum()); +} + +void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto * object = loadFromJson(scope, data, name, index); + + object->iconIndex = object->getIndex(); + + assert(objects[index] == nullptr); // ensure that this id was not loaded before + objects[index] = object; + + registerObject(scope, "artifact", name, object->id.getNum()); +} + +const std::vector & CArtHandler::getTypeNames() const +{ + static const std::vector typeNames = { "artifact" }; + return typeNames; +} + +CArtifact * 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(); + if(!node["growing"].isNull()) + { + for(auto bonus : node["growing"]["bonusesPerLevel"].Vector()) + { + art->bonusesPerLevel.emplace_back(static_cast(bonus["level"].Float()), Bonus()); + JsonUtils::parseBonus(bonus["bonus"], &art->bonusesPerLevel.back().second); + } + for(auto bonus : node["growing"]["thresholdBonuses"].Vector()) + { + art->thresholdBonuses.emplace_back(static_cast(bonus["level"].Float()), Bonus()); + JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second); + } + } + art->id = ArtifactID(index); + art->identifier = identifier; + art->modScope = scope; + + const JsonNode & text = node["text"]; + + VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String()); + VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String()); + VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String()); + + const JsonNode & graphics = node["graphics"]; + art->image = graphics["image"].String(); + + if(!graphics["large"].isNull()) + art->large = graphics["large"].String(); + else + art->large = art->image; + + art->advMapDef = graphics["map"].String(); + + art->price = static_cast(node["value"].Float()); + art->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); + + loadSlots(art, node); + loadClass(art, node); + loadType(art, node); + loadComponents(art, node); + + for(const auto & b : node["bonuses"].Vector()) + { + auto bonus = JsonUtils::parseBonus(b); + art->addNewBonus(bonus); + } + + const JsonNode & warMachine = node["warMachine"]; + if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) + { + VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id) + { + art->warMachine = CreatureID(id); + + //this assumes that creature object is stored before registration + VLC->creh->objects.at(id)->warMachine = art->id; + }); + } + + VLC->identifiers()->requestIdentifier(scope, "object", "artifact", [=](si32 index) + { + JsonNode conf; + conf.setMeta(scope); + + VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex()); + + if(!art->advMapDef.empty()) + { + JsonNode templ; + templ["animation"].String() = art->advMapDef; + templ.setMeta(scope); + + // add new template. + // Necessary for objects added via mods that don't have any templates in H3 + VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); + } + }); + + return art; +} + +int32_t ArtifactPositionBase::decode(const std::string & slotName) +{ +#define ART_POS(x) { #x, ArtifactPosition::x }, + static const std::map artifactPositionMap = { ART_POS_LIST }; +#undef ART_POS + auto it = artifactPositionMap.find (slotName); + if (it != artifactPositionMap.end()) + return it->second; + else + return PRE_FIRST; +} + +void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const +{ + static const std::vector miscSlots = + { + ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 + }; + + static const std::vector ringSlots = + { + ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING + }; + + if (slotID == "MISC") + { + vstd::concatenate(art->possibleSlots[ArtBearer::HERO], miscSlots); + } + else if (slotID == "RING") + { + vstd::concatenate(art->possibleSlots[ArtBearer::HERO], ringSlots); + } + else + { + auto slot = ArtifactPosition::decode(slotID); + if (slot != ArtifactPosition::PRE_FIRST) + art->possibleSlots[ArtBearer::HERO].push_back(slot); + } +} + +void CArtHandler::loadSlots(CArtifact * art, const JsonNode & node) const +{ + if (!node["slot"].isNull()) //we assume non-hero slots are irrelevant? + { + if (node["slot"].getType() == JsonNode::JsonType::DATA_STRING) + addSlot(art, node["slot"].String()); + else + { + for (const JsonNode & slot : node["slot"].Vector()) + addSlot(art, slot.String()); + } + std::sort(art->possibleSlots.at(ArtBearer::HERO).begin(), art->possibleSlots.at(ArtBearer::HERO).end()); + } +} + +CArtifact::EartClass CArtHandler::stringToClass(const std::string & className) +{ + static const std::map artifactClassMap = + { + {"TREASURE", CArtifact::ART_TREASURE}, + {"MINOR", CArtifact::ART_MINOR}, + {"MAJOR", CArtifact::ART_MAJOR}, + {"RELIC", CArtifact::ART_RELIC}, + {"SPECIAL", CArtifact::ART_SPECIAL} + }; + + auto it = artifactClassMap.find (className); + if (it != artifactClassMap.end()) + return it->second; + + logMod->warn("Warning! Artifact rarity %s not recognized!", className); + return CArtifact::ART_SPECIAL; +} + +void CArtHandler::loadClass(CArtifact * art, const JsonNode & node) const +{ + art->aClass = stringToClass(node["class"].String()); +} + +void CArtHandler::loadType(CArtifact * art, const JsonNode & node) const +{ +#define ART_BEARER(x) { #x, ArtBearer::x }, + static const std::map artifactBearerMap = { ART_BEARER_LIST }; +#undef ART_BEARER + + for (const JsonNode & b : node["type"].Vector()) + { + auto it = artifactBearerMap.find (b.String()); + if (it != artifactBearerMap.end()) + { + int bearerType = it->second; + switch (bearerType) + { + case ArtBearer::HERO://TODO: allow arts having several possible bearers + break; + case ArtBearer::COMMANDER: + makeItCommanderArt (art); //original artifacts should have only one bearer type + break; + case ArtBearer::CREATURE: + makeItCreatureArt (art); + break; + } + } + else + logMod->warn("Warning! Artifact type %s not recognized!", b.String()); + } +} + +void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) +{ + if (!node["components"].isNull()) + { + for(const auto & component : node["components"].Vector()) + { + VLC->identifiers()->requestIdentifier("artifact", component, [=](si32 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(objects[id]); + objects[id]->partOf.push_back(art); + }); + } + } +} + +void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) +{ + if (onlyCreature) + { + a->possibleSlots[ArtBearer::HERO].clear(); + a->possibleSlots[ArtBearer::COMMANDER].clear(); + } + a->possibleSlots[ArtBearer::CREATURE].push_back(ArtifactPosition::CREATURE_SLOT); +} + +void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) +{ + if (onlyCommander) + { + a->possibleSlots[ArtBearer::HERO].clear(); + a->possibleSlots[ArtBearer::CREATURE].clear(); + } + for(const auto & slot : ArtifactUtils::commanderSlots()) + a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot)); +} + +bool CArtHandler::legalArtifact(const ArtifactID & id) +{ + auto art = id.toArtifact(); + //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components + + if(art->isCombined()) + return false; //no combo artifacts spawning + + if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) + return false; // invalid class + + 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)) + return true; + + if(art->possibleSlots.count(ArtBearer::COMMANDER) && !art->possibleSlots.at(ArtBearer::COMMANDER).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + return true; + + return false; +} + +void CArtHandler::initAllowedArtifactsList(const std::set & allowed) +{ + allowedArtifacts.clear(); + + for (ArtifactID i : allowed) + { + if (legalArtifact(ArtifactID(i))) + allowedArtifacts.push_back(i.toArtifact()); + //keep im mind that artifact can be worn by more than one type of bearer + } +} + +std::set CArtHandler::getDefaultAllowed() const +{ + std::set allowedArtifacts; + + for (auto artifact : objects) + { + if (!artifact->isCombined()) + allowedArtifacts.insert(artifact->getId()); + } + return allowedArtifacts; +} + +void CArtHandler::afterLoadFinalization() +{ + //All artifacts have their id, so we can properly update their bonuses' source ids. + for(auto &art : objects) + { + for(auto &bonus : art->getExportedBonusList()) + { + assert(bonus->source == BonusSource::ARTIFACT); + bonus->sid = BonusSourceID(art->id); + } + } + CBonusSystemNode::treeHasChanged(); +} + +CArtifactSet::~CArtifactSet() = default; + +const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const +{ + if(const ArtSlotInfo * si = getSlot(pos)) + { + if(si->artifact && (!excludeLocked || !si->locked)) + return si->artifact; + } + + 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) + { + const auto * art = artInfo.getArt(); + if(art && art->artType->getId() == aid) + result.emplace_back(backpackPosition); + backpackPosition++; + } + 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) + if(i.second.artifact->getId() == artInstId) + return i.second.artifact; + + for(auto i : artifactsInBackpack) + if(i.artifact->getId() == artInstId) + return i.artifact; + + return nullptr; +} + +const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * artInst) const +{ + if(artInst) + { + for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType())) + if(getArt(slot) == artInst) + return slot; + + ArtifactPosition backpackSlot = ArtifactPosition::BACKPACK_START; + for(auto & slotInfo : artifactsInBackpack) + { + if(slotInfo.getArt() == artInst) + return backpackSlot; + backpackSlot = ArtifactPosition(backpackSlot + 1); + } + } + return ArtifactPosition::PRE_FIRST; +} + +bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const +{ + return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0; +} + +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) +{ + ArtPlacementMap resArtPlacement; + + setNewArtSlot(slot, art, false); + if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) + { + const CArtifactInstance * mainPart = nullptr; + for(const auto & part : art->getPartsInfo()) + if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot) + && (part.slot == ArtifactPosition::PRE_FIRST)) + { + mainPart = part.art; + break; + } + + for(const auto & part : art->getPartsInfo()) + { + if(part.art != mainPart) + { + auto partSlot = part.slot; + if(!part.art->artType->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)); + } + else + { + resArtPlacement.emplace(std::make_pair(part.art, part.slot)); + } + } + } + return resArtPlacement; +} + +void CArtifactSet::removeArtifact(ArtifactPosition slot) +{ + auto art = getArt(slot, false); + if(art) + { + if(art->isCombined()) + { + for(auto & part : art->getPartsInfo()) + { + if(getArt(part.slot, false)) + eraseArtSlot(part.slot); + } + } + eraseArtSlot(slot); + } +} + +std::pair CArtifactSet::searchForConstituent(const ArtifactID & aid) const +{ + for(const auto & slot : artifactsInBackpack) + { + auto art = slot.artifact; + if(art->isCombined()) + { + for(auto & ci : art->getPartsInfo()) + { + if(ci.art->getTypeId() == aid) + { + return {art, ci.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; +} + +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()); + } + if(vstd::contains(artifactsWorn, pos)) + return &artifactsWorn.at(pos); + if(ArtifactUtils::isSlotBackpack(pos)) + { + auto backpackPos = pos - ArtifactPosition::BACKPACK_START; + if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) + return nullptr; + else + return &artifactsInBackpack[backpackPos]; + } + + return nullptr; +} + +bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const +{ + if(const ArtSlotInfo *s = getSlot(pos)) + return (onlyLockCheck || !s->artifact) && !s->locked; + + 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) + if(elem.second.artifact && !elem.second.locked) + node->attachTo(*elem.second.artifact); +} + +void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map) +{ + //todo: creature and commander artifacts + if(handler.saving && artifactsInBackpack.empty() && artifactsWorn.empty()) + return; + + if(!handler.saving) + { + assert(map); + artifactsInBackpack.clear(); + artifactsWorn.clear(); + } + + auto s = handler.enterStruct(fieldName); + + switch(bearerType()) + { + case ArtBearer::HERO: + serializeJsonHero(handler, map); + break; + case ArtBearer::CREATURE: + serializeJsonCreature(handler, map); + break; + case ArtBearer::COMMANDER: + serializeJsonCommander(handler, map); + break; + default: + assert(false); + break; + } +} + +void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) +{ + for(const auto & slot : ArtifactUtils::allWornSlots()) + { + serializeJsonSlot(handler, slot, map); + } + + std::vector backpackTemp; + + if(handler.saving) + { + backpackTemp.reserve(artifactsInBackpack.size()); + for(const ArtSlotInfo & info : artifactsInBackpack) + backpackTemp.push_back(info.artifact->getTypeId()); + } + handler.serializeIdArray(NArtifactPosition::backpack, backpackTemp); + if(!handler.saving) + { + for(const ArtifactID & artifactID : backpackTemp) + { + auto * artifact = ArtifactUtils::createArtifact(map, artifactID); + auto slot = ArtifactPosition::BACKPACK_START + artifactsInBackpack.size(); + if(artifact->artType->canBePutAt(this, slot)) + { + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); + } + } + } +} + +void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler, CMap * map) +{ + logGlobal->error("CArtifactSet::serializeJsonCreature not implemented"); +} + +void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler, CMap * map) +{ + logGlobal->error("CArtifactSet::serializeJsonCommander not implemented"); +} + +void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map) +{ + ArtifactID artifactID; + + if(handler.saving) + { + const ArtSlotInfo * info = getSlot(slot); + + if(info != nullptr && !info->locked) + { + artifactID = info->artifact->getTypeId(); + handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); + } + } + else + { + handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); + + if(artifactID != ArtifactID::NONE) + { + auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); + + if(artifact->artType->canBePutAt(this, slot)) + { + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); + } + else + { + logGlobal->debug("Artifact can't be put at the specified location."); //TODO add more debugging information + } + } + } +} + +CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer Bearer): + Bearer(Bearer) +{ +} + +ArtBearer::ArtBearer CArtifactFittingSet::bearerType() const +{ + return this->Bearer; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index dad6e88e5..957d2b1c5 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -1,313 +1,261 @@ -/* - * CArtHandler.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 "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "GameConstants.h" -#include "IHandlerBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CArtHandler; -class CGHeroInstance; -class CArtifactSet; -class CArtifactInstance; -class CRandomGenerator; -class CMap; -class JsonSerializeFormat; - -#define ART_BEARER_LIST \ - ART_BEARER(HERO)\ - ART_BEARER(CREATURE)\ - ART_BEARER(COMMANDER) - -namespace ArtBearer -{ - enum ArtBearer - { -#define ART_BEARER(x) x, - ART_BEARER_LIST -#undef ART_BEARER - }; -} - -class DLL_LINKAGE CCombinedArtifact -{ -protected: - CCombinedArtifact() = default; - - 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 -public: - bool isCombined() const; - const std::vector & getConstituents() const; - const std::vector & getPartOf() const; - - template void serialize(Handler & h, const int version) - { - h & constituents; - h & partOf; - } -}; - -class DLL_LINKAGE CScrollArtifact -{ -protected: - CScrollArtifact() = default; -public: - bool isScroll() const; -}; - -class DLL_LINKAGE CGrowingArtifact -{ -protected: - CGrowingArtifact() = default; - - std::vector > bonusesPerLevel; // Bonus given each n levels - std::vector > thresholdBonuses; // After certain level they will be added once -public: - bool isGrowing() const; - - std::vector > & getBonusesPerLevel(); - const std::vector > & getBonusesPerLevel() const; - std::vector > & getThresholdBonuses(); - const std::vector > & getThresholdBonuses() const; - - template void serialize(Handler & h, const int version) - { - h & bonusesPerLevel; - h & thresholdBonuses; - } -}; - -// Container for artifacts. Not for instances. -class DLL_LINKAGE CArtifact - : public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact -{ - ArtifactID id; - std::string image; - std::string large; // big image for custom artifacts, used in drag & drop - std::string advMapDef; // used for adventure map object - std::string modScope; - std::string identifier; - int32_t iconIndex; - uint32_t price; - CreatureID warMachine; - // Bearer Type => ids of slots where artifact can be placed - std::map> possibleSlots; - -public: - enum EartClass {ART_SPECIAL=1, ART_TREASURE=2, ART_MINOR=4, ART_MAJOR=8, ART_RELIC=16}; //artifact classes - - EartClass aClass = ART_SPECIAL; - bool onlyOnWaterMap; - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - ArtifactID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; - - std::string getDescriptionTranslated() const override; - std::string getEventTranslated() const override; - std::string getNameTranslated() const override; - - std::string getDescriptionTextID() const override; - std::string getEventTextID() const override; - std::string getNameTextID() const override; - - uint32_t getPrice() const override; - CreatureID getWarMachine() const override; - bool isBig() const override; - bool isTradable() const override; - - int getArtClassSerial() const; //0 - treasure, 1 - minor, 2 - major, 3 - relic, 4 - spell scroll, 5 - other - std::string nodeName() const override; - void addNewBonus(const std::shared_ptr& b) override; - const std::map> & getPossibleSlots() const; - - virtual bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, - bool assumeDestRemoved = false) const; - void updateFrom(const JsonNode & data); - // Is used for testing purposes only - void setImage(int32_t iconIndex, std::string image, std::string large); - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & image; - h & large; - h & advMapDef; - h & iconIndex; - h & price; - h & possibleSlots; - h & aClass; - h & id; - h & modScope; - h & identifier; - h & warMachine; - h & onlyOnWaterMap; - } - - CArtifact(); - ~CArtifact(); - - friend class CArtHandler; -}; - -class DLL_LINKAGE CArtHandler : public CHandlerBase -{ -public: - std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! - - std::vector allowedArtifacts; - std::set growingArtifacts; - - void addBonuses(CArtifact *art, const JsonNode &bonusList); - - void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects - - static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor - - /// Gets a artifact ID randomly and removes the selected artifact from this handler. - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); - - bool legalArtifact(const ArtifactID & id); - void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed - static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); - static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); - - ~CArtHandler(); - - 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 afterLoadFinalization() override; - - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & objects; - h & allowedArtifacts; - h & treasures; - h & minors; - h & majors; - h & relics; - h & growingArtifacts; - } - -protected: - const std::vector & getTypeNames() const override; - CArtifact * 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; - void loadSlots(CArtifact * art, const JsonNode & node) const; - void loadClass(CArtifact * art, const JsonNode & node) const; - void loadType(CArtifact * art, const JsonNode & node) const; - void loadComponents(CArtifact * art, const JsonNode & node); - - void erasePickedArt(const ArtifactID & id); -}; - -struct DLL_LINKAGE ArtSlotInfo -{ - ConstTransitivePtr artifact; - ui8 locked; //if locked, then artifact points to the combined artifact - - ArtSlotInfo() : locked(false) {} - const CArtifactInstance * getArt() const; - - template void serialize(Handler & h, const int version) - { - h & artifact; - h & locked; - } -}; - -class DLL_LINKAGE CArtifactSet -{ -public: - 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, CArtifactInstance * art, bool locked); - void eraseArtSlot(const ArtifactPosition & slot); - - const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const; - const CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; //nullptr - no artifact - CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true); //nullptr - no artifact - /// Looks for equipped artifact with given ID and returns its slot ID or -1 if none - /// (if more than one such artifact lower ID is returned) - ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; - ArtifactPosition getArtPos(const CArtifactInstance *art) const; - ArtifactPosition getArtBackpackPos(const ArtifactID & aid) const; - std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; - std::vector getBackpackArtPositions(const ArtifactID & aid) const; - const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; - 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 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 void putArtifact(ArtifactPosition slot, CArtifactInstance * art); - virtual void removeArtifact(ArtifactPosition slot); - virtual ~CArtifactSet(); - - template void serialize(Handler &h, const int version) - { - h & artifactsInBackpack; - h & artifactsWorn; - } - - void artDeserializationFix(CBonusSystemNode *node); - - void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map); -protected: - std::pair searchForConstituent(const ArtifactID & aid) const; - -private: - void serializeJsonHero(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCommander(JsonSerializeFormat & handler, CMap * map); - - void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map);//normal slots -}; - -// Used to try on artifacts before the claimed changes have been applied -class DLL_LINKAGE CArtifactFittingSet : public CArtifactSet -{ -public: - CArtifactFittingSet(ArtBearer::ArtBearer Bearer); - ArtBearer::ArtBearer bearerType() const override; - -protected: - ArtBearer::ArtBearer Bearer; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CArtHandler.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 "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "GameConstants.h" +#include "IHandlerBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArtHandler; +class CGHeroInstance; +class CArtifactSet; +class CArtifactInstance; +class CRandomGenerator; +class CMap; +class JsonSerializeFormat; + +#define ART_BEARER_LIST \ + ART_BEARER(HERO)\ + ART_BEARER(CREATURE)\ + ART_BEARER(COMMANDER) + +namespace ArtBearer +{ + enum ArtBearer + { +#define ART_BEARER(x) x, + ART_BEARER_LIST +#undef ART_BEARER + }; +} + +class DLL_LINKAGE CCombinedArtifact +{ +protected: + CCombinedArtifact() = default; + + 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 +public: + bool isCombined() const; + const std::vector & getConstituents() const; + const std::vector & getPartOf() const; +}; + +class DLL_LINKAGE CScrollArtifact +{ +protected: + CScrollArtifact() = default; +public: + bool isScroll() const; +}; + +class DLL_LINKAGE CGrowingArtifact +{ +protected: + CGrowingArtifact() = default; + + std::vector > bonusesPerLevel; // Bonus given each n levels + std::vector > thresholdBonuses; // After certain level they will be added once +public: + bool isGrowing() const; + + std::vector > & getBonusesPerLevel(); + const std::vector > & getBonusesPerLevel() const; + std::vector > & getThresholdBonuses(); + const std::vector > & getThresholdBonuses() const; +}; + +// Container for artifacts. Not for instances. +class DLL_LINKAGE CArtifact + : public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact +{ + ArtifactID id; + std::string image; + std::string large; // big image for custom artifacts, used in drag & drop + std::string advMapDef; // used for adventure map object + std::string modScope; + std::string identifier; + int32_t iconIndex; + uint32_t price; + CreatureID warMachine; + // Bearer Type => ids of slots where artifact can be placed + std::map> possibleSlots; + +public: + enum EartClass {ART_SPECIAL=1, ART_TREASURE=2, ART_MINOR=4, ART_MAJOR=8, ART_RELIC=16}; //artifact classes + + EartClass aClass = ART_SPECIAL; + bool onlyOnWaterMap; + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + ArtifactID getId() const override; + virtual const IBonusBearer * getBonusBearer() const override; + + std::string getDescriptionTranslated() const override; + std::string getEventTranslated() const override; + std::string getNameTranslated() const override; + + std::string getDescriptionTextID() const override; + std::string getEventTextID() const override; + std::string getNameTextID() const override; + + uint32_t getPrice() const override; + CreatureID getWarMachine() const override; + bool isBig() const override; + bool isTradable() const override; + + int getArtClassSerial() const; //0 - treasure, 1 - minor, 2 - major, 3 - relic, 4 - spell scroll, 5 - other + std::string nodeName() const override; + void addNewBonus(const std::shared_ptr& b) override; + const std::map> & getPossibleSlots() const; + + virtual bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, + bool assumeDestRemoved = false) const; + void updateFrom(const JsonNode & data); + // Is used for testing purposes only + void setImage(int32_t iconIndex, std::string image, std::string large); + + CArtifact(); + ~CArtifact(); + + friend class CArtHandler; +}; + +class DLL_LINKAGE CArtHandler : public CHandlerBase +{ +public: + /// List of artifacts allowed on the map + std::vector allowedArtifacts; + + void addBonuses(CArtifact *art, const JsonNode &bonusList); + + static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor + + bool legalArtifact(const ArtifactID & id); + void initAllowedArtifactsList(const std::set & allowed); + static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); + static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); + + ~CArtHandler(); + + 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 afterLoadFinalization() override; + + std::set getDefaultAllowed() const; + +protected: + const std::vector & getTypeNames() const override; + CArtifact * 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; + void loadSlots(CArtifact * art, const JsonNode & node) const; + void loadClass(CArtifact * art, const JsonNode & node) const; + void loadType(CArtifact * art, const JsonNode & node) const; + void loadComponents(CArtifact * art, const JsonNode & node); +}; + +struct DLL_LINKAGE ArtSlotInfo +{ + ConstTransitivePtr artifact; + ui8 locked; //if locked, then artifact points to the combined artifact + + ArtSlotInfo() : locked(false) {} + const CArtifactInstance * getArt() const; + + template void serialize(Handler & h, const int version) + { + h & artifact; + h & locked; + } +}; + +class DLL_LINKAGE CArtifactSet +{ +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); + + 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) + ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; + ArtifactPosition getArtPos(const CArtifactInstance *art) const; + std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; + std::vector getBackpackArtPositions(const ArtifactID & aid) const; + const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; + 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 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(); + + template void serialize(Handler &h, const int version) + { + h & artifactsInBackpack; + h & artifactsWorn; + } + + void artDeserializationFix(CBonusSystemNode *node); + + void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map); +protected: + std::pair searchForConstituent(const ArtifactID & aid) const; + +private: + void serializeJsonHero(JsonSerializeFormat & handler, CMap * map); + void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map); + void serializeJsonCommander(JsonSerializeFormat & handler, CMap * map); + + void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map);//normal slots +}; + +// Used to try on artifacts before the claimed changes have been applied +class DLL_LINKAGE CArtifactFittingSet : public CArtifactSet +{ +public: + CArtifactFittingSet(ArtBearer::ArtBearer Bearer); + ArtBearer::ArtBearer bearerType() const override; + +protected: + ArtBearer::ArtBearer Bearer; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 81d475ad6..c790ca379 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -13,7 +13,7 @@ #include "ArtifactUtils.h" #include "CArtHandler.h" -#include "NetPacksBase.h" +#include "networkPacks/ArtifactLocation.h" VCMI_LIB_NAMESPACE_BEGIN @@ -44,17 +44,21 @@ bool CCombinedArtifactInstance::isPart(const CArtifactInstance * supposedPart) c return false; } -std::vector & CCombinedArtifactInstance::getPartsInfo() -{ - // TODO romove this func. encapsulation violation - return partsInfo; -} - const std::vector & CCombinedArtifactInstance::getPartsInfo() const { return partsInfo; } +void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap) +{ + if(!placementMap.empty()) + for(auto & part : partsInfo) + { + assert(placementMap.find(part.art) != placementMap.end()); + part.slot = placementMap.at(part.art); + } +} + SpellID CScrollArtifactInstance::getScrollSpellID() const { auto artInst = static_cast(this); @@ -64,7 +68,7 @@ SpellID CScrollArtifactInstance::getScrollSpellID() const logMod->warn("Warning: %s doesn't bear any spell!", artInst->nodeName()); return SpellID::NONE; } - return SpellID(bonus->subtype); + return bonus->subtype.as(); } void CGrowingArtifactInstance::growingUp() @@ -151,9 +155,9 @@ void CArtifactInstance::setId(ArtifactInstanceID id) this->id = id; } -bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const +bool CArtifactInstance::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const { - return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved); + return artType->canBePutAt(artSet, slot, assumeDestRemoved); } bool CArtifactInstance::isCombined() const @@ -161,14 +165,15 @@ bool CArtifactInstance::isCombined() const return artType->isCombined(); } -void CArtifactInstance::putAt(const ArtifactLocation & al) +void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot) { - al.getHolderArtSet()->putArtifact(al.slot, this); + auto placementMap = set.putArtifact(slot, this); + addPlacementMap(placementMap); } -void CArtifactInstance::removeFrom(const ArtifactLocation & al) +void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot) { - al.getHolderArtSet()->removeArtifact(al.slot); + set.removeArtifact(slot); for(auto & part : partsInfo) { if(part.slot != ArtifactPosition::PRE_FIRST) @@ -176,10 +181,10 @@ void CArtifactInstance::removeFrom(const ArtifactLocation & al) } } -void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst) +void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot) { - removeFrom(src); - putAt(dst); + removeFrom(srcSet, srcSlot); + putAt(dstSet, dstSlot); } void CArtifactInstance::deserializationFix() diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 079db214f..d72397afe 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -11,6 +11,8 @@ #include "bonuses/CBonusSystemNode.h" #include "GameConstants.h" +#include "ConstTransitivePtr.h" +#include "CArtHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,8 +38,8 @@ 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; - std::vector & getPartsInfo(); const std::vector & getPartsInfo() const; + void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap); template void serialize(Handler & h, const int version) { @@ -82,11 +84,12 @@ public: ArtifactInstanceID getId() const; void setId(ArtifactInstanceID id); - bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const; + bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, + bool assumeDestRemoved = false) const; bool isCombined() const; - void putAt(const ArtifactLocation & al); - void removeFrom(const ArtifactLocation & al); - void move(const ArtifactLocation & src, const ArtifactLocation & dst); + 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, const int version) diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 4d5479dd2..b5cce121f 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -1,252 +1,246 @@ -/* - * CBonusTypeHandler.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" - -#define INSTANTIATE_CBonusTypeHandler_HERE - -#include "CBonusTypeHandler.h" - -#include "JsonNode.h" -#include "filesystem/Filesystem.h" - -#include "GameConstants.h" -#include "CCreatureHandler.h" -#include "CGeneralTextHandler.h" -#include "spells/CSpellHandler.h" - -template class std::vector; - -VCMI_LIB_NAMESPACE_BEGIN - -///CBonusType - -CBonusType::CBonusType(): - hidden(true) -{} - -std::string CBonusType::getNameTextID() const -{ - return TextIdentifier( "core", "bonus", identifier, "name").get(); -} - -std::string CBonusType::getDescriptionTextID() const -{ - return TextIdentifier( "core", "bonus", identifier, "description").get(); -} - -///CBonusTypeHandler - -CBonusTypeHandler::CBonusTypeHandler() -{ - //register predefined bonus types - - #define BONUS_NAME(x) \ - do { \ - bonusTypes.push_back(CBonusType()); \ - } while(0); - - - BONUS_LIST; - #undef BONUS_NAME - - load(); -} - -CBonusTypeHandler::~CBonusTypeHandler() -{ - //dtor -} - -std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const -{ - const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; - if(bt.hidden) - return ""; - - std::string textID = description ? bt.getDescriptionTextID() : bt.getNameTextID(); - std::string text = VLC->generaltexth->translate(textID); - - if (text.find("${val}") != std::string::npos) - boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); - - if (text.find("${subtype.creature}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.creature}", CreatureID(bonus->subtype).toCreature()->getNamePluralTranslated()); - - if (text.find("${subtype.spell}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.spell}", SpellID(bonus->subtype).toSpell()->getNameTranslated()); - - return text; -} - -std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const -{ - std::string fileName; - bool fullPath = false; - - switch(bonus->type) - { - case BonusType::SPELL_IMMUNITY: - { - fullPath = true; - const CSpell * sp = SpellID(bonus->subtype).toSpell(); - fileName = sp->getIconImmune(); - break; - } - case BonusType::FIRE_IMMUNITY: - switch(bonus->subtype) - { - case 0: - fileName = "E_SPFIRE.bmp"; - break;//all - case 1: - fileName = "E_SPFIRE1.bmp"; - break;//not positive - case 2: - fileName = "E_FIRE.bmp"; - break;//direct damage - } - break; - case BonusType::WATER_IMMUNITY: - switch(bonus->subtype) - { - case 0: - fileName = "E_SPWATER.bmp"; - break;//all - case 1: - fileName = "E_SPWATER1.bmp"; - break;//not positive - case 2: - fileName = "E_SPCOLD.bmp"; - break;//direct damage - } - break; - case BonusType::AIR_IMMUNITY: - switch(bonus->subtype) - { - case 0: - fileName = "E_SPAIR.bmp"; - break;//all - case 1: - fileName = "E_SPAIR1.bmp"; - break;//not positive - case 2: - fileName = "E_LIGHT.bmp"; - break;//direct damage - } - break; - case BonusType::EARTH_IMMUNITY: - switch(bonus->subtype) - { - case 0: - fileName = "E_SPEATH.bmp"; - break;//all - case 1: - case 2://no specific icon for direct damage immunity - fileName = "E_SPEATH1.bmp"; - break;//not positive - } - break; - case BonusType::LEVEL_SPELL_IMMUNITY: - { - if(vstd::iswithin(bonus->val, 1, 5)) - { - fileName = "E_SPLVL" + std::to_string(bonus->val) + ".bmp"; - } - break; - } - case BonusType::KING: - { - if(vstd::iswithin(bonus->val, 0, 3)) - { - fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp"; - } - break; - } - case BonusType::GENERAL_DAMAGE_REDUCTION: - { - switch(bonus->subtype) - { - case 0: - fileName = "DamageReductionMelee.bmp"; - break; - case 1: - fileName = "DamageReductionRanged.bmp"; - break; - } - break; - } - - default: - { - const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; - fileName = bt.icon; - fullPath = true; - } - break; - } - - if(!fileName.empty() && !fullPath) - fileName = "zvs/Lib1.res/" + fileName; - return fileName; -} - -void CBonusTypeHandler::load() -{ - const JsonNode gameConf(ResourceID("config/gameConfig.json")); - const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); - load(config); -} - -void CBonusTypeHandler::load(const JsonNode & config) -{ - for(const auto & node : config.Struct()) - { - auto it = bonusNameMap.find(node.first); - - if(it == bonusNameMap.end()) - { - //TODO: new bonus -// CBonusType bt; -// loadItem(node.second, bt); -// -// auto new_id = bonusTypes.size(); -// -// bonusTypes.push_back(bt); - - logBonus->warn("Unrecognized bonus name! (%s)", node.first); - } - else - { - CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)]; - - loadItem(node.second, bt, node.first); - logBonus->trace("Loaded bonus type %s", node.first); - } - } -} - -void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const -{ - dest.identifier = name; - dest.hidden = source["hidden"].Bool(); //Null -> false - - if (!dest.hidden) - { - VLC->generaltexth->registerString( "core", dest.getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString( "core", dest.getDescriptionTextID(), source["description"].String()); - } - - const JsonNode & graphics = source["graphics"]; - - if(!graphics.isNull()) - dest.icon = graphics["icon"].String(); -} - -VCMI_LIB_NAMESPACE_END +/* + * CBonusTypeHandler.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" + +#define INSTANTIATE_CBonusTypeHandler_HERE + +#include "CBonusTypeHandler.h" + +#include "JsonNode.h" +#include "filesystem/Filesystem.h" + +#include "GameConstants.h" +#include "CCreatureHandler.h" +#include "CGeneralTextHandler.h" +#include "spells/CSpellHandler.h" + +template class std::vector; + +VCMI_LIB_NAMESPACE_BEGIN + +///CBonusType + +CBonusType::CBonusType(): + hidden(true) +{} + +std::string CBonusType::getNameTextID() const +{ + return TextIdentifier( "core", "bonus", identifier, "name").get(); +} + +std::string CBonusType::getDescriptionTextID() const +{ + return TextIdentifier( "core", "bonus", identifier, "description").get(); +} + +///CBonusTypeHandler + +CBonusTypeHandler::CBonusTypeHandler() +{ + //register predefined bonus types + + #define BONUS_NAME(x) \ + do { \ + bonusTypes.push_back(CBonusType()); \ + } while(0); + + + BONUS_LIST; + #undef BONUS_NAME + + load(); +} + +CBonusTypeHandler::~CBonusTypeHandler() +{ + //dtor +} + +std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const +{ + const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; + if(bt.hidden) + return ""; + + std::string textID = description ? bt.getDescriptionTextID() : bt.getNameTextID(); + std::string text = VLC->generaltexth->translate(textID); + + if (text.find("${val}") != std::string::npos) + boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); + + if (text.find("${subtype.creature}") != std::string::npos) + boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as().toCreature()->getNamePluralTranslated()); + + if (text.find("${subtype.spell}") != std::string::npos) + boost::algorithm::replace_all(text, "${subtype.spell}", bonus->subtype.as().toSpell()->getNameTranslated()); + + return text; +} + +ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const +{ + std::string fileName; + bool fullPath = false; + + switch(bonus->type) + { + case BonusType::SPELL_IMMUNITY: + { + fullPath = true; + const CSpell * sp = bonus->subtype.as().toSpell(); + fileName = sp->getIconImmune(); + break; + } + case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools + { + if (bonus->subtype.as() == SpellSchool::ANY) + fileName = "E_GOLEM.bmp"; + + if (bonus->subtype.as() == SpellSchool::AIR) + fileName = "E_LIGHT.bmp"; + + if (bonus->subtype.as() == SpellSchool::FIRE) + fileName = "E_FIRE.bmp"; + + if (bonus->subtype.as() == SpellSchool::WATER) + fileName = "E_COLD.bmp"; + + if (bonus->subtype.as() == SpellSchool::EARTH) + fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage + + break; + } + case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school + { + if (bonus->subtype.as() == SpellSchool::AIR) + fileName = "E_SPAIR.bmp"; + + if (bonus->subtype.as() == SpellSchool::FIRE) + fileName = "E_SPFIRE.bmp"; + + if (bonus->subtype.as() == SpellSchool::WATER) + fileName = "E_SPWATER.bmp"; + + if (bonus->subtype.as() == SpellSchool::EARTH) + fileName = "E_SPEATH.bmp"; + + break; + } + case BonusType::NEGATIVE_EFFECTS_IMMUNITY: + { + if (bonus->subtype.as() == SpellSchool::AIR) + fileName = "E_SPAIR1.bmp"; + + if (bonus->subtype.as() == SpellSchool::FIRE) + fileName = "E_SPFIRE1.bmp"; + + if (bonus->subtype.as() == SpellSchool::WATER) + fileName = "E_SPWATER1.bmp"; + + if (bonus->subtype.as() == SpellSchool::EARTH) + fileName = "E_SPEATH1.bmp"; + + break; + } + case BonusType::LEVEL_SPELL_IMMUNITY: + { + if(vstd::iswithin(bonus->val, 1, 5)) + { + fileName = "E_SPLVL" + std::to_string(bonus->val) + ".bmp"; + } + break; + } + case BonusType::KING: + { + if(vstd::iswithin(bonus->val, 0, 3)) + { + fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp"; + } + break; + } + case BonusType::GENERAL_DAMAGE_REDUCTION: + { + if (bonus->subtype == BonusCustomSubtype::damageTypeMelee) + fileName = "DamageReductionMelee.bmp"; + + if (bonus->subtype == BonusCustomSubtype::damageTypeRanged) + fileName = "DamageReductionRanged.bmp"; + + break; + } + + default: + { + const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; + fileName = bt.icon; + fullPath = true; + } + break; + } + + if(!fileName.empty() && !fullPath) + fileName = "zvs/Lib1.res/" + fileName; + return ImagePath::builtinTODO(fileName); +} + +void CBonusTypeHandler::load() +{ + const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); + const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); + load(config); +} + +void CBonusTypeHandler::load(const JsonNode & config) +{ + for(const auto & node : config.Struct()) + { + auto it = bonusNameMap.find(node.first); + + if(it == bonusNameMap.end()) + { + //TODO: new bonus +// CBonusType bt; +// loadItem(node.second, bt); +// +// auto new_id = bonusTypes.size(); +// +// bonusTypes.push_back(bt); + + logBonus->warn("Unrecognized bonus name! (%s)", node.first); + } + else + { + CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)]; + + loadItem(node.second, bt, node.first); + logBonus->trace("Loaded bonus type %s", node.first); + } + } +} + +void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const +{ + dest.identifier = name; + dest.hidden = source["hidden"].Bool(); //Null -> false + + if (!dest.hidden) + { + VLC->generaltexth->registerString( "core", dest.getNameTextID(), source["name"].String()); + VLC->generaltexth->registerString( "core", dest.getDescriptionTextID(), source["description"].String()); + } + + const JsonNode & graphics = source["graphics"]; + + if(!graphics.isNull()) + dest.icon = graphics["icon"].String(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBonusTypeHandler.h b/lib/CBonusTypeHandler.h index d747b8d98..2ddecfb30 100644 --- a/lib/CBonusTypeHandler.h +++ b/lib/CBonusTypeHandler.h @@ -1,75 +1,75 @@ -/* - * CBonusTypeHandler.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 "IBonusTypeHandler.h" -#include "IHandlerBase.h" -#include "bonuses/Bonus.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -class JsonNode; - -class DLL_LINKAGE CBonusType -{ -public: - CBonusType(); - - std::string getNameTextID() const; - std::string getDescriptionTextID() const; - - template void serialize(Handler & h, const int version) - { - h & icon; - h & identifier; - h & hidden; - - } - -private: - friend class CBonusTypeHandler; - - std::string icon; - std::string identifier; - - bool hidden; -}; - -class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler -{ -public: - CBonusTypeHandler(); - virtual ~CBonusTypeHandler(); - - std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; - std::string bonusToGraphics(const std::shared_ptr & bonus) const override; - - template void serialize(Handler & h, const int version) - { - //for now always use up to date configuration - //once modded bonus type will be implemented, serialize only them - std::vector ignore; - h & ignore; - } -private: - void load(); - void load(const JsonNode & config); - void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const; - - std::vector bonusTypes; //index = BonusType -}; - -VCMI_LIB_NAMESPACE_END - -#ifndef INSTANTIATE_CBonusTypeHandler_HERE -extern template class std::vector; -#endif +/* + * CBonusTypeHandler.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 "IBonusTypeHandler.h" +#include "IHandlerBase.h" +#include "bonuses/Bonus.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +class JsonNode; + +class DLL_LINKAGE CBonusType +{ +public: + CBonusType(); + + std::string getNameTextID() const; + std::string getDescriptionTextID() const; + + template void serialize(Handler & h, const int version) + { + h & icon; + h & identifier; + h & hidden; + + } + +private: + friend class CBonusTypeHandler; + + std::string icon; + std::string identifier; + + bool hidden; +}; + +class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler +{ +public: + CBonusTypeHandler(); + virtual ~CBonusTypeHandler(); + + std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const override; + + template void serialize(Handler & h, const int version) + { + //for now always use up to date configuration + //once modded bonus type will be implemented, serialize only them + std::vector ignore; + h & ignore; + } +private: + void load(); + void load(const JsonNode & config); + void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const; + + std::vector bonusTypes; //index = BonusType +}; + +VCMI_LIB_NAMESPACE_END + +#ifndef INSTANTIATE_CBonusTypeHandler_HERE +extern template class std::vector; +#endif diff --git a/lib/CBuildingHandler.cpp b/lib/CBuildingHandler.cpp index d5539ebac..be986ee8b 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/CBuildingHandler.cpp @@ -1,81 +1,85 @@ -/* - * CBuildingHandler.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 "CBuildingHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set & builtBuildings) -{ - static const std::vector campToERMU = - { - BuildingID::TOWN_HALL, BuildingID::CITY_HALL, - BuildingID::CAPITOL, BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::TAVERN, - BuildingID::BLACKSMITH, BuildingID::MARKETPLACE, BuildingID::RESOURCE_SILO, BuildingID::NONE, - BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, - BuildingID::MAGES_GUILD_5, - BuildingID::SHIPYARD, BuildingID::GRAIL, - BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4 - }; //creature generators with banks - handled separately - - if (camp < campToERMU.size()) - { - return campToERMU[camp]; - } - - static const std::vector hordeLvlsPerTType[GameConstants::F_NUMBER] = - { - {2}, {1}, {1,4}, {0,2}, {0}, {0}, {0}, {0}, {0} - }; - - int curPos = static_cast(campToERMU.size()); - for (int i=0; i 1) - { - if(vstd::contains(builtBuildings, 37 + hordeLvlsPerTType[townType][1])) //if upgraded dwelling is built - return BuildingID::HORDE_2_UPGR; - else //upgraded dwelling not presents - return BuildingID::HORDE_2; - } - } - } - } - curPos++; - } - } - assert(0); - return BuildingID::NONE; //not found -} - - -VCMI_LIB_NAMESPACE_END +/* + * CBuildingHandler.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 "CBuildingHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std::set & builtBuildings) +{ + static const std::vector campToERMU = + { + BuildingID::TOWN_HALL, BuildingID::CITY_HALL, + BuildingID::CAPITOL, BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::TAVERN, + BuildingID::BLACKSMITH, BuildingID::MARKETPLACE, BuildingID::RESOURCE_SILO, BuildingID::NONE, + BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, + BuildingID::MAGES_GUILD_5, + BuildingID::SHIPYARD, BuildingID::GRAIL, + BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4 + }; //creature generators with banks - handled separately + + if (camp < campToERMU.size()) + { + return campToERMU[camp]; + } + + static const std::vector hordeLvlsPerTType[GameConstants::F_NUMBER] = + { + {2}, {1}, {1,4}, {0,2}, {0}, {0}, {0}, {0}, {0} + }; + + int curPos = static_cast(campToERMU.size()); + for (int i=0; i 1) + { + BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][1]); + + if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built + return BuildingID::HORDE_2_UPGR; + else //upgraded dwelling not presents + return BuildingID::HORDE_2; + } + } + } + } + curPos++; + } + } + assert(0); + return BuildingID::NONE; //not found +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBuildingHandler.h b/lib/CBuildingHandler.h index 04d5e9400..059b9f087 100644 --- a/lib/CBuildingHandler.h +++ b/lib/CBuildingHandler.h @@ -1,22 +1,22 @@ -/* - * CBuildingHandler.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 DLL_LINKAGE CBuildingHandler -{ -public: - static BuildingID campToERMU(int camp, int townType, const std::set & builtBuildings); -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBuildingHandler.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 DLL_LINKAGE CBuildingHandler +{ +public: + static BuildingID campToERMU(int camp, FactionID townType, const std::set & builtBuildings); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 52fd9d3e9..5df984740 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -1,184 +1,195 @@ -/* - * CConfigHandler.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 "CConfigHandler.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileStream.h" -#include "../lib/GameConstants.h" -#include "../lib/VCMIDirs.h" - -VCMI_LIB_NAMESPACE_BEGIN - -SettingsStorage settings; - -template -SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): - parent(_parent), - path(std::move(_path)) -{ -} - -template -SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator[](const std::string & nextNode) const -{ - std::vector newPath = path; - newPath.push_back(nextNode); - return NodeAccessor(parent, newPath); -} - -template -SettingsStorage::NodeAccessor::operator Accessor() const -{ - return Accessor(parent, path); -} - -template -SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator () (std::vector _path) const -{ - std::vector newPath = path; - newPath.insert( newPath.end(), _path.begin(), _path.end()); - return NodeAccessor(parent, newPath); -} - -SettingsStorage::SettingsStorage(): - write(NodeAccessor(*this, std::vector() )), - listen(NodeAccessor(*this, std::vector() )) -{ -} - -void SettingsStorage::init() -{ - std::string confName = "config/settings.json"; - - JsonUtils::assembleFromFiles(confName).swap(config); - - // Probably new install. Create config file to save settings to - if (!CResourceHandler::get("local")->existsResource(ResourceID(confName))) - CResourceHandler::get("local")->createResource(confName); - - JsonUtils::maximize(config, "vcmi:settings"); - JsonUtils::validate(config, "vcmi:settings", "settings"); -} - -void SettingsStorage::invalidateNode(const std::vector &changedPath) -{ - for(SettingsListener * listener : listeners) - listener->nodeInvalidated(changedPath); - - JsonNode savedConf = config; - savedConf.Struct().erase("session"); - JsonUtils::minimize(savedConf, "vcmi:settings"); - - FileStream file(*CResourceHandler::get()->getResourceName(ResourceID("config/settings.json")), std::ofstream::out | std::ofstream::trunc); - file << savedConf.toJson(); -} - -JsonNode & SettingsStorage::getNode(const std::vector & path) -{ - JsonNode *node = &config; - for(const std::string & value : path) - node = &(*node)[value]; - - return *node; -} - -Settings SettingsStorage::get(const std::vector & path) -{ - return Settings(*this, path); -} - -const JsonNode & SettingsStorage::operator[](const std::string & value) const -{ - return config[value]; -} - -const JsonNode & SettingsStorage::toJsonNode() const -{ - return config; -} - -SettingsListener::SettingsListener(SettingsStorage & _parent, std::vector _path): - parent(_parent), - path(std::move(_path)) -{ - parent.listeners.insert(this); -} - -SettingsListener::SettingsListener(const SettingsListener &sl): - parent(sl.parent), - path(sl.path), - callback(sl.callback) -{ - parent.listeners.insert(this); -} - -SettingsListener::~SettingsListener() -{ - parent.listeners.erase(this); -} - -void SettingsListener::nodeInvalidated(const std::vector &changedPath) -{ - if (!callback) - return; - - size_t min = std::min(path.size(), changedPath.size()); - size_t mismatch = std::mismatch(path.begin(), path.begin()+min, changedPath.begin()).first - path.begin(); - - if (min == mismatch) - callback(parent.getNode(path)); -} - -void SettingsListener::operator() (std::function _callback) -{ - callback = std::move(_callback); -} - -Settings::Settings(SettingsStorage &_parent, const std::vector &_path): - parent(_parent), - path(_path), - node(_parent.getNode(_path)), - copy(_parent.getNode(_path)) -{ -} - -Settings::~Settings() -{ - if (node != copy) - parent.invalidateNode(path); -} - -JsonNode* Settings::operator -> () -{ - return &node; -} - -const JsonNode* Settings::operator ->() const -{ - return &node; -} - -const JsonNode & Settings::operator[](const std::string & value) const -{ - return node[value]; -} - -JsonNode & Settings::operator[](const std::string & value) -{ - return node[value]; -} - -// Force instantiation of the SettingsStorage::NodeAccessor class template. -// That way method definitions can sit in the cpp file -template struct SettingsStorage::NodeAccessor; -template struct SettingsStorage::NodeAccessor; - -VCMI_LIB_NAMESPACE_END +/* + * CConfigHandler.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 "CConfigHandler.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/GameConstants.h" +#include "../lib/VCMIDirs.h" + +VCMI_LIB_NAMESPACE_BEGIN + +SettingsStorage settings; +SettingsStorage persistentStorage; + +template +SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): + parent(_parent), + path(std::move(_path)) +{ +} + +template +SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator[](const std::string & nextNode) const +{ + std::vector newPath = path; + newPath.push_back(nextNode); + return NodeAccessor(parent, newPath); +} + +template +SettingsStorage::NodeAccessor::operator Accessor() const +{ + return Accessor(parent, path); +} + +template +SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator () (std::vector _path) const +{ + std::vector newPath = path; + newPath.insert( newPath.end(), _path.begin(), _path.end()); + return NodeAccessor(parent, newPath); +} + +SettingsStorage::SettingsStorage(): + write(NodeAccessor(*this, std::vector() )), + listen(NodeAccessor(*this, std::vector() )) +{ +} + +void SettingsStorage::init(const std::string & dataFilename, const std::string & schema) +{ + this->dataFilename = dataFilename; + this->schema = schema; + + JsonPath confName = JsonPath::builtin(dataFilename); + + config = JsonUtils::assembleFromFiles(confName.getOriginalName()); + + // Probably new install. Create config file to save settings to + if (!CResourceHandler::get("local")->existsResource(confName)) + { + CResourceHandler::get("local")->createResource(dataFilename); + if(schema.empty()) + invalidateNode(std::vector()); + } + + if(!schema.empty()) + { + JsonUtils::maximize(config, schema); + JsonUtils::validate(config, schema, "settings"); + } +} + +void SettingsStorage::invalidateNode(const std::vector &changedPath) +{ + for(SettingsListener * listener : listeners) + listener->nodeInvalidated(changedPath); + + JsonNode savedConf = config; + savedConf.Struct().erase("session"); + if(!schema.empty()) + JsonUtils::minimize(savedConf, schema); + + std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); + file << savedConf.toJson(); +} + +JsonNode & SettingsStorage::getNode(const std::vector & path) +{ + JsonNode *node = &config; + for(const std::string & value : path) + node = &(*node)[value]; + + return *node; +} + +Settings SettingsStorage::get(const std::vector & path) +{ + return Settings(*this, path); +} + +const JsonNode & SettingsStorage::operator[](const std::string & value) const +{ + return config[value]; +} + +const JsonNode & SettingsStorage::toJsonNode() const +{ + return config; +} + +SettingsListener::SettingsListener(SettingsStorage & _parent, std::vector _path): + parent(_parent), + path(std::move(_path)) +{ + parent.listeners.insert(this); +} + +SettingsListener::SettingsListener(const SettingsListener &sl): + parent(sl.parent), + path(sl.path), + callback(sl.callback) +{ + parent.listeners.insert(this); +} + +SettingsListener::~SettingsListener() +{ + parent.listeners.erase(this); +} + +void SettingsListener::nodeInvalidated(const std::vector &changedPath) +{ + if (!callback) + return; + + size_t min = std::min(path.size(), changedPath.size()); + size_t mismatch = std::mismatch(path.begin(), path.begin()+min, changedPath.begin()).first - path.begin(); + + if (min == mismatch) + callback(parent.getNode(path)); +} + +void SettingsListener::operator() (std::function _callback) +{ + callback = std::move(_callback); +} + +Settings::Settings(SettingsStorage &_parent, const std::vector &_path): + parent(_parent), + path(_path), + node(_parent.getNode(_path)), + copy(_parent.getNode(_path)) +{ +} + +Settings::~Settings() +{ + if (node != copy) + parent.invalidateNode(path); +} + +JsonNode* Settings::operator -> () +{ + return &node; +} + +const JsonNode* Settings::operator ->() const +{ + return &node; +} + +const JsonNode & Settings::operator[](const std::string & value) const +{ + return node[value]; +} + +JsonNode & Settings::operator[](const std::string & value) +{ + return node[value]; +} + +// Force instantiation of the SettingsStorage::NodeAccessor class template. +// That way method definitions can sit in the cpp file +template struct SettingsStorage::NodeAccessor; +template struct SettingsStorage::NodeAccessor; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConfigHandler.h b/lib/CConfigHandler.h index 6a526f512..a446d8e5c 100644 --- a/lib/CConfigHandler.h +++ b/lib/CConfigHandler.h @@ -1,117 +1,121 @@ -/* - * CConfigHandler.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/JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Settings; -class SettingsListener; - -/// Main storage of game settings -class DLL_LINKAGE SettingsStorage -{ - //Helper struct to access specific node either via chain of operator[] or with one operator() (vector) - template - struct DLL_LINKAGE NodeAccessor - { - SettingsStorage & parent; - std::vector path; - - NodeAccessor(SettingsStorage & _parent, std::vector _path); - NodeAccessor operator[](const std::string & nextNode) const; - NodeAccessor operator () (std::vector _path) const; - operator Accessor() const; - }; - - std::set listeners; - JsonNode config; - - JsonNode & getNode(const std::vector & path); - - // Calls all required listeners - void invalidateNode(const std::vector &changedPath); - - Settings get(const std::vector & path); - -public: - // Initialize config structure - SettingsStorage(); - void init(); - - // Get write access to config node at path - const NodeAccessor write; - - // Get access to listener at path - const NodeAccessor listen; - - //Read access, see JsonNode::operator[] - const JsonNode & operator[](const std::string & value) const; - const JsonNode & toJsonNode() const; - - friend class SettingsListener; - friend class Settings; -}; - -/// Class for listening changes in specific part of configuration (e.g. change of music volume) -class DLL_LINKAGE SettingsListener -{ - SettingsStorage &parent; - // Path to this node - std::vector path; - // Callback - std::function callback; - - SettingsListener(SettingsStorage & _parent, std::vector _path); - - // Executes callback if changedpath begins with path - void nodeInvalidated(const std::vector & changedPath); - -public: - SettingsListener(const SettingsListener &sl); - ~SettingsListener(); - - // assign callback function - void operator()(std::function _callback); - - friend class SettingsStorage; -}; - -/// System options, provides write access to config tree with auto-saving on change -class DLL_LINKAGE Settings -{ - SettingsStorage &parent; - //path to this node - std::vector path; - JsonNode &node; - JsonNode copy; - - //Get access to node pointed by path - Settings(SettingsStorage &_parent, const std::vector &_path); - -public: - //Saves config if it was modified - ~Settings(); - - //Returns node selected during construction - JsonNode* operator ->(); - const JsonNode* operator ->() const; - - //Helper, replaces JsonNode::operator[] - JsonNode & operator[](const std::string & value); - const JsonNode & operator[](const std::string & value) const; - - friend class SettingsStorage; -}; - -extern DLL_LINKAGE SettingsStorage settings; - -VCMI_LIB_NAMESPACE_END +/* + * CConfigHandler.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/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Settings; +class SettingsListener; + +/// Main storage of game settings +class DLL_LINKAGE SettingsStorage +{ + //Helper struct to access specific node either via chain of operator[] or with one operator() (vector) + template + struct DLL_LINKAGE NodeAccessor + { + SettingsStorage & parent; + std::vector path; + + NodeAccessor(SettingsStorage & _parent, std::vector _path); + NodeAccessor operator[](const std::string & nextNode) const; + NodeAccessor operator () (std::vector _path) const; + operator Accessor() const; + }; + + std::set listeners; + JsonNode config; + + std::string dataFilename; + std::string schema; + + JsonNode & getNode(const std::vector & path); + + // Calls all required listeners + void invalidateNode(const std::vector &changedPath); + + Settings get(const std::vector & path); + +public: + // Initialize config structure + SettingsStorage(); + void init(const std::string & dataFilename, const std::string & schema); + + // Get write access to config node at path + const NodeAccessor write; + + // Get access to listener at path + const NodeAccessor listen; + + //Read access, see JsonNode::operator[] + const JsonNode & operator[](const std::string & value) const; + const JsonNode & toJsonNode() const; + + friend class SettingsListener; + friend class Settings; +}; + +/// Class for listening changes in specific part of configuration (e.g. change of music volume) +class DLL_LINKAGE SettingsListener +{ + SettingsStorage &parent; + // Path to this node + std::vector path; + // Callback + std::function callback; + + SettingsListener(SettingsStorage & _parent, std::vector _path); + + // Executes callback if changedpath begins with path + void nodeInvalidated(const std::vector & changedPath); + +public: + SettingsListener(const SettingsListener &sl); + ~SettingsListener(); + + // assign callback function + void operator()(std::function _callback); + + friend class SettingsStorage; +}; + +/// System options, provides write access to config tree with auto-saving on change +class DLL_LINKAGE Settings +{ + SettingsStorage &parent; + //path to this node + std::vector path; + JsonNode &node; + JsonNode copy; + + //Get access to node pointed by path + Settings(SettingsStorage &_parent, const std::vector &_path); + +public: + //Saves config if it was modified + ~Settings(); + + //Returns node selected during construction + JsonNode* operator ->(); + const JsonNode* operator ->() const; + + //Helper, replaces JsonNode::operator[] + JsonNode & operator[](const std::string & value); + const JsonNode & operator[](const std::string & value) const; + + friend class SettingsStorage; +}; + +extern DLL_LINKAGE SettingsStorage settings; +extern DLL_LINKAGE SettingsStorage persistentStorage; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 595de23b3..72de08baa 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -1,296 +1,296 @@ -/* - * CConsoleHandler.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 "CConsoleHandler.h" -#include "CConfigHandler.h" - -#include "CThreadHelper.h" - -VCMI_LIB_NAMESPACE_BEGIN - -std::mutex CConsoleHandler::smx; - -DLL_LINKAGE CConsoleHandler * console = nullptr; - -VCMI_LIB_NAMESPACE_END - -#ifndef VCMI_WINDOWS - using TColor = std::string; - #define CONSOLE_GREEN "\x1b[1;32m" - #define CONSOLE_RED "\x1b[1;31m" - #define CONSOLE_MAGENTA "\x1b[1;35m" - #define CONSOLE_YELLOW "\x1b[1;33m" - #define CONSOLE_WHITE "\x1b[1;37m" - #define CONSOLE_GRAY "\x1b[1;30m" - #define CONSOLE_TEAL "\x1b[1;36m" -#else - #include - #include -#ifndef __MINGW32__ - #pragma comment(lib, "dbghelp.lib") -#endif - typedef WORD TColor; - HANDLE handleIn; - HANDLE handleOut; - HANDLE handleErr; - #define CONSOLE_GREEN FOREGROUND_GREEN | FOREGROUND_INTENSITY - #define CONSOLE_RED FOREGROUND_RED | FOREGROUND_INTENSITY - #define CONSOLE_MAGENTA FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY - #define CONSOLE_YELLOW FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY - #define CONSOLE_WHITE FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY - #define CONSOLE_GRAY FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE - #define CONSOLE_TEAL FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY - - static TColor defErrColor; -#endif - -static TColor defColor; - -VCMI_LIB_NAMESPACE_BEGIN - -#ifdef VCMI_WINDOWS - -void printWinError() -{ - //Get error code - int error = GetLastError(); - if(!error) - { - logGlobal->error("No Win error information set."); - return; - } - logGlobal->error("Error %d encountered:", error); - - //Get error description - char* pTemp = nullptr; - FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, - nullptr, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&pTemp, 1, nullptr); - logGlobal->error(pTemp); - LocalFree( pTemp ); -} - -const char* exceptionName(DWORD exc) -{ -#define EXC_CASE(EXC) case EXCEPTION_##EXC : return "EXCEPTION_" #EXC - switch (exc) - { - EXC_CASE(ACCESS_VIOLATION); - EXC_CASE(DATATYPE_MISALIGNMENT); - EXC_CASE(BREAKPOINT); - EXC_CASE(SINGLE_STEP); - EXC_CASE(ARRAY_BOUNDS_EXCEEDED); - EXC_CASE(FLT_DENORMAL_OPERAND); - EXC_CASE(FLT_DIVIDE_BY_ZERO); - EXC_CASE(FLT_INEXACT_RESULT); - EXC_CASE(FLT_INVALID_OPERATION); - EXC_CASE(FLT_OVERFLOW); - EXC_CASE(FLT_STACK_CHECK); - EXC_CASE(FLT_UNDERFLOW); - EXC_CASE(INT_DIVIDE_BY_ZERO); - EXC_CASE(INT_OVERFLOW); - EXC_CASE(PRIV_INSTRUCTION); - EXC_CASE(IN_PAGE_ERROR); - EXC_CASE(ILLEGAL_INSTRUCTION); - EXC_CASE(NONCONTINUABLE_EXCEPTION); - EXC_CASE(STACK_OVERFLOW); - EXC_CASE(INVALID_DISPOSITION); - EXC_CASE(GUARD_PAGE); - EXC_CASE(INVALID_HANDLE); - default: - return "UNKNOWN EXCEPTION"; - } -#undef EXC_CASE -} - - - -LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) -{ - logGlobal->error("Disaster happened."); - - PEXCEPTION_RECORD einfo = exception->ExceptionRecord; - logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); - - if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) - { - logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); - } - const DWORD threadId = ::GetCurrentThreadId(); - logGlobal->error("Thread ID: %d", threadId); - - //exception info to be placed in the dump - MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; - - //create file where dump will be placed - char *mname = nullptr; - char buffer[MAX_PATH + 1]; - HMODULE hModule = nullptr; - GetModuleFileNameA(hModule, buffer, MAX_PATH); - mname = strrchr(buffer, '\\'); - if (mname != 0) - mname++; - else - mname = buffer; - - strcat(mname, "_crashinfo.dmp"); - HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); - logGlobal->error("Crash info will be put in %s", mname); - - // flush loggers - std::string padding(1000, '@'); - - logGlobal->error(padding); - logAi->error(padding); - logNetwork->error(padding); - - auto dumpType = MiniDumpWithDataSegs; - - if(settings["general"]["extraDump"].Bool()) - { - dumpType = (MINIDUMP_TYPE)( - MiniDumpWithFullMemory - | MiniDumpWithFullMemoryInfo - | MiniDumpWithHandleData - | MiniDumpWithUnloadedModules - | MiniDumpWithThreadInfo); - } - - MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, &meinfo, 0, 0); - MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); - return EXCEPTION_EXECUTE_HANDLER; -} -#endif - - -void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) -{ - TColor colorCode; - switch(color) - { - case EConsoleTextColor::DEFAULT: - colorCode = defColor; - break; - case EConsoleTextColor::GREEN: - colorCode = CONSOLE_GREEN; - break; - case EConsoleTextColor::RED: - colorCode = CONSOLE_RED; - break; - case EConsoleTextColor::MAGENTA: - colorCode = CONSOLE_MAGENTA; - break; - case EConsoleTextColor::YELLOW: - colorCode = CONSOLE_YELLOW; - break; - case EConsoleTextColor::WHITE: - colorCode = CONSOLE_WHITE; - break; - case EConsoleTextColor::GRAY: - colorCode = CONSOLE_GRAY; - break; - case EConsoleTextColor::TEAL: - colorCode = CONSOLE_TEAL; - break; - default: - colorCode = defColor; - break; - } -#ifdef VCMI_WINDOWS - SetConsoleTextAttribute(handleOut, colorCode); - if (color == EConsoleTextColor::DEFAULT) - colorCode = defErrColor; - SetConsoleTextAttribute(handleErr, colorCode); -#else - std::cout << colorCode; -#endif -} - -int CConsoleHandler::run() const -{ - setThreadName("CConsoleHandler::run"); - //disabling sync to make in_avail() work (othervice always returns 0) - { - TLockGuard _(smx); - std::ios::sync_with_stdio(false); - } - std::string buffer; - - while ( std::cin.good() ) - { -#ifndef VCMI_WINDOWS - //check if we have some unreaded symbols - if (std::cin.rdbuf()->in_avail()) - { - if ( getline(std::cin, buffer).good() ) - if ( cb && *cb ) - (*cb)(buffer, false); - } - else - boost::this_thread::sleep(boost::posix_time::millisec(100)); - - boost::this_thread::interruption_point(); -#else - std::getline(std::cin, buffer); - if ( cb && *cb ) - (*cb)(buffer, false); -#endif - } - return -1; -} -CConsoleHandler::CConsoleHandler(): - cb(new std::function), - thread(nullptr) -{ -#ifdef VCMI_WINDOWS - handleIn = GetStdHandle(STD_INPUT_HANDLE); - handleOut = GetStdHandle(STD_OUTPUT_HANDLE); - handleErr = GetStdHandle(STD_ERROR_HANDLE); - - CONSOLE_SCREEN_BUFFER_INFO csbi; - GetConsoleScreenBufferInfo(handleOut,&csbi); - defColor = csbi.wAttributes; - - GetConsoleScreenBufferInfo(handleErr, &csbi); - defErrColor = csbi.wAttributes; -#ifndef _DEBUG - SetUnhandledExceptionFilter(onUnhandledException); -#endif -#else - defColor = "\x1b[0m"; -#endif -} -CConsoleHandler::~CConsoleHandler() -{ - logGlobal->info("Killing console..."); - end(); - delete cb; - logGlobal->info("Killing console... done!"); -} -void CConsoleHandler::end() -{ - if (thread) - { -#ifndef VCMI_WINDOWS - thread->interrupt(); -#else - TerminateThread(thread->native_handle(),0); -#endif - thread->join(); - delete thread; - thread = nullptr; - } -} - -void CConsoleHandler::start() -{ - thread = new boost::thread(std::bind(&CConsoleHandler::run,console)); -} - -VCMI_LIB_NAMESPACE_END +/* + * CConsoleHandler.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 "CConsoleHandler.h" +#include "CConfigHandler.h" + +#include "CThreadHelper.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::mutex CConsoleHandler::smx; + +DLL_LINKAGE CConsoleHandler * console = nullptr; + +VCMI_LIB_NAMESPACE_END + +#ifndef VCMI_WINDOWS + using TColor = std::string; + #define CONSOLE_GREEN "\x1b[1;32m" + #define CONSOLE_RED "\x1b[1;31m" + #define CONSOLE_MAGENTA "\x1b[1;35m" + #define CONSOLE_YELLOW "\x1b[1;33m" + #define CONSOLE_WHITE "\x1b[1;37m" + #define CONSOLE_GRAY "\x1b[1;30m" + #define CONSOLE_TEAL "\x1b[1;36m" +#else + #include + #include +#ifndef __MINGW32__ + #pragma comment(lib, "dbghelp.lib") +#endif + typedef WORD TColor; + HANDLE handleIn; + HANDLE handleOut; + HANDLE handleErr; + #define CONSOLE_GREEN FOREGROUND_GREEN | FOREGROUND_INTENSITY + #define CONSOLE_RED FOREGROUND_RED | FOREGROUND_INTENSITY + #define CONSOLE_MAGENTA FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY + #define CONSOLE_YELLOW FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY + #define CONSOLE_WHITE FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY + #define CONSOLE_GRAY FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE + #define CONSOLE_TEAL FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY + + static TColor defErrColor; +#endif + +static TColor defColor; + +VCMI_LIB_NAMESPACE_BEGIN + +#ifdef VCMI_WINDOWS + +void printWinError() +{ + //Get error code + int error = GetLastError(); + if(!error) + { + logGlobal->error("No Win error information set."); + return; + } + logGlobal->error("Error %d encountered:", error); + + //Get error description + char* pTemp = nullptr; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&pTemp, 1, nullptr); + logGlobal->error(pTemp); + LocalFree( pTemp ); +} + +const char* exceptionName(DWORD exc) +{ +#define EXC_CASE(EXC) case EXCEPTION_##EXC : return "EXCEPTION_" #EXC + switch (exc) + { + EXC_CASE(ACCESS_VIOLATION); + EXC_CASE(DATATYPE_MISALIGNMENT); + EXC_CASE(BREAKPOINT); + EXC_CASE(SINGLE_STEP); + EXC_CASE(ARRAY_BOUNDS_EXCEEDED); + EXC_CASE(FLT_DENORMAL_OPERAND); + EXC_CASE(FLT_DIVIDE_BY_ZERO); + EXC_CASE(FLT_INEXACT_RESULT); + EXC_CASE(FLT_INVALID_OPERATION); + EXC_CASE(FLT_OVERFLOW); + EXC_CASE(FLT_STACK_CHECK); + EXC_CASE(FLT_UNDERFLOW); + EXC_CASE(INT_DIVIDE_BY_ZERO); + EXC_CASE(INT_OVERFLOW); + EXC_CASE(PRIV_INSTRUCTION); + EXC_CASE(IN_PAGE_ERROR); + EXC_CASE(ILLEGAL_INSTRUCTION); + EXC_CASE(NONCONTINUABLE_EXCEPTION); + EXC_CASE(STACK_OVERFLOW); + EXC_CASE(INVALID_DISPOSITION); + EXC_CASE(GUARD_PAGE); + EXC_CASE(INVALID_HANDLE); + default: + return "UNKNOWN EXCEPTION"; + } +#undef EXC_CASE +} + + + +LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) +{ + logGlobal->error("Disaster happened."); + + PEXCEPTION_RECORD einfo = exception->ExceptionRecord; + logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); + + if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) + { + logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); + } + const DWORD threadId = ::GetCurrentThreadId(); + logGlobal->error("Thread ID: %d", threadId); + + //exception info to be placed in the dump + MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; + + //create file where dump will be placed + char *mname = nullptr; + char buffer[MAX_PATH + 1]; + HMODULE hModule = nullptr; + GetModuleFileNameA(hModule, buffer, MAX_PATH); + mname = strrchr(buffer, '\\'); + if (mname != nullptr) + mname++; + else + mname = buffer; + + strcat(mname, "_crashinfo.dmp"); + HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); + logGlobal->error("Crash info will be put in %s", mname); + + // flush loggers + std::string padding(1000, '@'); + + logGlobal->error(padding); + logAi->error(padding); + logNetwork->error(padding); + + auto dumpType = MiniDumpWithDataSegs; + + if(settings["general"]["extraDump"].Bool()) + { + dumpType = (MINIDUMP_TYPE)( + MiniDumpWithFullMemory + | MiniDumpWithFullMemoryInfo + | MiniDumpWithHandleData + | MiniDumpWithUnloadedModules + | MiniDumpWithThreadInfo); + } + + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, &meinfo, 0, 0); + MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); + return EXCEPTION_EXECUTE_HANDLER; +} +#endif + + +void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) +{ + TColor colorCode; + switch(color) + { + case EConsoleTextColor::DEFAULT: + colorCode = defColor; + break; + case EConsoleTextColor::GREEN: + colorCode = CONSOLE_GREEN; + break; + case EConsoleTextColor::RED: + colorCode = CONSOLE_RED; + break; + case EConsoleTextColor::MAGENTA: + colorCode = CONSOLE_MAGENTA; + break; + case EConsoleTextColor::YELLOW: + colorCode = CONSOLE_YELLOW; + break; + case EConsoleTextColor::WHITE: + colorCode = CONSOLE_WHITE; + break; + case EConsoleTextColor::GRAY: + colorCode = CONSOLE_GRAY; + break; + case EConsoleTextColor::TEAL: + colorCode = CONSOLE_TEAL; + break; + default: + colorCode = defColor; + break; + } +#ifdef VCMI_WINDOWS + SetConsoleTextAttribute(handleOut, colorCode); + if (color == EConsoleTextColor::DEFAULT) + colorCode = defErrColor; + SetConsoleTextAttribute(handleErr, colorCode); +#else + std::cout << colorCode; +#endif +} + +int CConsoleHandler::run() const +{ + setThreadName("consoleHandler"); + //disabling sync to make in_avail() work (othervice always returns 0) + { + TLockGuard _(smx); + std::ios::sync_with_stdio(false); + } + std::string buffer; + + while ( std::cin.good() ) + { +#ifndef VCMI_WINDOWS + //check if we have some unreaded symbols + if (std::cin.rdbuf()->in_avail()) + { + if ( getline(std::cin, buffer).good() ) + if ( cb && *cb ) + (*cb)(buffer, false); + } + else + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + + boost::this_thread::interruption_point(); +#else + std::getline(std::cin, buffer); + if ( cb && *cb ) + (*cb)(buffer, false); +#endif + } + return -1; +} +CConsoleHandler::CConsoleHandler(): + cb(new std::function), + thread(nullptr) +{ +#ifdef VCMI_WINDOWS + handleIn = GetStdHandle(STD_INPUT_HANDLE); + handleOut = GetStdHandle(STD_OUTPUT_HANDLE); + handleErr = GetStdHandle(STD_ERROR_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(handleOut,&csbi); + defColor = csbi.wAttributes; + + GetConsoleScreenBufferInfo(handleErr, &csbi); + defErrColor = csbi.wAttributes; +#ifndef _DEBUG + SetUnhandledExceptionFilter(onUnhandledException); +#endif +#else + defColor = "\x1b[0m"; +#endif +} +CConsoleHandler::~CConsoleHandler() +{ + logGlobal->info("Killing console..."); + end(); + delete cb; + logGlobal->info("Killing console... done!"); +} +void CConsoleHandler::end() +{ + if (thread) + { +#ifndef VCMI_WINDOWS + thread->interrupt(); +#else + TerminateThread(thread->native_handle(),0); +#endif + thread->join(); + delete thread; + thread = nullptr; + } +} + +void CConsoleHandler::start() +{ + thread = new boost::thread(std::bind(&CConsoleHandler::run,console)); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConsoleHandler.h b/lib/CConsoleHandler.h index 28ee0ad0b..aef086329 100644 --- a/lib/CConsoleHandler.h +++ b/lib/CConsoleHandler.h @@ -1,98 +1,98 @@ -/* - * CConsoleHandler.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 - -namespace EConsoleTextColor -{ -/** The color enum is used for colored text console output. */ -enum EConsoleTextColor -{ - DEFAULT = -1, - GREEN, - RED, - MAGENTA, - YELLOW, - WHITE, - GRAY, - TEAL = -2 -}; -} - -/// Class which wraps the native console. It can print text based on -/// the chosen color -class DLL_LINKAGE CConsoleHandler -{ -public: - CConsoleHandler(); - ~CConsoleHandler(); - void start(); //starts listening thread - - template void print(const T &data, bool addNewLine = false, EConsoleTextColor::EConsoleTextColor color = EConsoleTextColor::DEFAULT, bool printToStdErr = false) - { - TLockGuard _(smx); -#ifndef VCMI_WINDOWS - // with love from ffmpeg - library is trying to print some warnings from separate thread - // this results in broken console on Linux. Lock stdout to print all our data at once - flockfile(stdout); -#endif - if(color != EConsoleTextColor::DEFAULT) setColor(color); - if(printToStdErr) - { - std::cerr << data; - if(addNewLine) - { - std::cerr << std::endl; - } - else - { - std::cerr << std::flush; - } - } - else - { - std::cout << data; - if(addNewLine) - { - std::cout << std::endl; - } - else - { - std::cout << std::flush; - } - } - - if(color != EConsoleTextColor::DEFAULT) setColor(EConsoleTextColor::DEFAULT); -#ifndef VCMI_WINDOWS - funlockfile(stdout); -#endif - } - //function to be called when message is received - string: message, bool: whether call was made from in-game console - std::function *cb; - -private: - int run() const; - - void end(); //kills listening thread - - 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:) - /// Without static, application will crash complaining about mutex deleted. In short: CConsoleHandler gets deleted before - /// the logging system. - static std::mutex smx; - - boost::thread * thread; -}; - -extern DLL_LINKAGE CConsoleHandler * console; - -VCMI_LIB_NAMESPACE_END +/* + * CConsoleHandler.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 + +namespace EConsoleTextColor +{ +/** The color enum is used for colored text console output. */ +enum EConsoleTextColor +{ + DEFAULT = -1, + GREEN, + RED, + MAGENTA, + YELLOW, + WHITE, + GRAY, + TEAL = -2 +}; +} + +/// Class which wraps the native console. It can print text based on +/// the chosen color +class DLL_LINKAGE CConsoleHandler +{ +public: + CConsoleHandler(); + ~CConsoleHandler(); + void start(); //starts listening thread + + template void print(const T &data, bool addNewLine = false, EConsoleTextColor::EConsoleTextColor color = EConsoleTextColor::DEFAULT, bool printToStdErr = false) + { + TLockGuard _(smx); +#ifndef VCMI_WINDOWS + // with love from ffmpeg - library is trying to print some warnings from separate thread + // this results in broken console on Linux. Lock stdout to print all our data at once + flockfile(stdout); +#endif + if(color != EConsoleTextColor::DEFAULT) setColor(color); + if(printToStdErr) + { + std::cerr << data; + if(addNewLine) + { + std::cerr << std::endl; + } + else + { + std::cerr << std::flush; + } + } + else + { + std::cout << data; + if(addNewLine) + { + std::cout << std::endl; + } + else + { + std::cout << std::flush; + } + } + + if(color != EConsoleTextColor::DEFAULT) setColor(EConsoleTextColor::DEFAULT); +#ifndef VCMI_WINDOWS + funlockfile(stdout); +#endif + } + //function to be called when message is received - string: message, bool: whether call was made from in-game console + std::function *cb; + +private: + int run() const; + + void end(); //kills listening thread + + 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:) + /// Without static, application will crash complaining about mutex deleted. In short: CConsoleHandler gets deleted before + /// the logging system. + static std::mutex smx; + + boost::thread * thread; +}; + +extern DLL_LINKAGE CConsoleHandler * console; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index d83138b54..3b72d3719 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -1,1392 +1,1364 @@ -/* - * CCreatureHandler.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 "CCreatureHandler.h" - -#include "CGeneralTextHandler.h" -#include "ResourceSet.h" -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" -#include "CTownHandler.h" -#include "CModHandler.h" -#include "GameSettings.h" -#include "StringConstants.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "serializer/JsonDeserializer.h" -#include "serializer/JsonUpdater.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const std::map CCreature::creatureQuantityRanges = -{ - {CCreature::CreatureQuantityId::FEW, "1-4"}, - {CCreature::CreatureQuantityId::SEVERAL, "5-9"}, - {CCreature::CreatureQuantityId::PACK, "10-19"}, - {CCreature::CreatureQuantityId::LOTS, "20-49"}, - {CCreature::CreatureQuantityId::HORDE, "50-99"}, - {CCreature::CreatureQuantityId::THRONG, "100-249"}, - {CCreature::CreatureQuantityId::SWARM, "250-499"}, - {CCreature::CreatureQuantityId::ZOUNDS, "500-999"}, - {CCreature::CreatureQuantityId::LEGION, "1000+"} -}; - -int32_t CCreature::getIndex() const -{ - return idNumber.toEnum(); -} - -int32_t CCreature::getIconIndex() const -{ - return iconIndex; -} - -std::string CCreature::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -void CCreature::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "CPRSMALL", smallIconName); - cb(getIconIndex(), 0, "TWCRPORT", largeIconName); -} - -CreatureID CCreature::getId() const -{ - return idNumber; -} - -const IBonusBearer * CCreature::getBonusBearer() const -{ - return this; -} - -int32_t CCreature::getAdvMapAmountMin() const -{ - return ammMin; -} - -int32_t CCreature::getAdvMapAmountMax() const -{ - return ammMax; -} - -int32_t CCreature::getAIValue() const -{ - return AIValue; -} - -int32_t CCreature::getFightValue() const -{ - return fightValue; -} - -int32_t CCreature::getLevel() const -{ - return level; -} - -int32_t CCreature::getGrowth() const -{ - return growth; -} - -int32_t CCreature::getHorde() const -{ - return hordeGrowth; -} - -FactionID CCreature::getFaction() const -{ - return FactionID(faction); -} - -int32_t CCreature::getBaseAttack() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDefense() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDamageMin() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDamageMax() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseHitPoints() const -{ - static const auto SELECTOR = Selector::type()(BonusType::STACK_HEALTH).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseSpellPoints() const -{ - static const auto SELECTOR = Selector::type()(BonusType::CASTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseSpeed() const -{ - static const auto SELECTOR = Selector::type()(BonusType::STACKS_SPEED).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseShots() const -{ - static const auto SELECTOR = Selector::type()(BonusType::SHOTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getRecruitCost(GameResID resIndex) const -{ - if(resIndex >= 0 && resIndex < cost.size()) - return cost[resIndex]; - else - return 0; -} - -TResources CCreature::getFullRecruitCost() const -{ - return cost; -} - -bool CCreature::hasUpgrades() const -{ - return !upgrades.empty(); -} - -std::string CCreature::getNameTranslated() const -{ - return getNameSingularTranslated(); -} - -std::string CCreature::getNamePluralTranslated() const -{ - return VLC->generaltexth->translate(getNamePluralTextID()); -} - -std::string CCreature::getNameSingularTranslated() const -{ - return VLC->generaltexth->translate(getNameSingularTextID()); -} - -std::string CCreature::getNameTextID() const -{ - return getNameSingularTextID(); -} - -std::string CCreature::getNamePluralTextID() const -{ - return TextIdentifier("creatures", modScope, identifier, "name", "plural" ).get(); -} - -std::string CCreature::getNameSingularTextID() const -{ - return TextIdentifier("creatures", modScope, identifier, "name", "singular" ).get(); -} - -CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity) -{ - if (quantity<5) - return CCreature::CreatureQuantityId::FEW; - if (quantity<10) - return CCreature::CreatureQuantityId::SEVERAL; - if (quantity<20) - return CCreature::CreatureQuantityId::PACK; - if (quantity<50) - return CCreature::CreatureQuantityId::LOTS; - if (quantity<100) - return CCreature::CreatureQuantityId::HORDE; - if (quantity<250) - return CCreature::CreatureQuantityId::THRONG; - if (quantity<500) - return CCreature::CreatureQuantityId::SWARM; - if (quantity<1000) - return CCreature::CreatureQuantityId::ZOUNDS; - - return CCreature::CreatureQuantityId::LEGION; -} - -std::string CCreature::getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId) -{ - if(creatureQuantityRanges.find(quantityId) != creatureQuantityRanges.end()) - return creatureQuantityRanges.at(quantityId); - - logGlobal->error("Wrong quantityId: %d", (int)quantityId); - assert(0); - return "[ERROR]"; -} - -int CCreature::estimateCreatureCount(ui32 countID) -{ - static const int creature_count[] = { 0, 3, 8, 15, 35, 75, 175, 375, 750, 2500 }; - - if(countID > 9) - { - logGlobal->error("Wrong countID %d!", countID); - return 0; - } - else - return creature_count[countID]; -} - -bool CCreature::isDoubleWide() const -{ - return doubleWide; -} - -/** - * Determines if the creature is of a good alignment. - * @return true if the creture is good, false otherwise. - */ -bool CCreature::isGood () const -{ - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD; -} - -/** - * Determines if the creature is of an evil alignment. - * @return true if the creature is evil, false otherwise. - */ -bool CCreature::isEvil () const -{ - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL; -} - -si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought -{ - int ret = 2147483645; - int resAmnt = static_cast(std::min(res.size(),cost.size())); - for(int i=0;i(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, getIndex(), subtype, BonusValueType::BASE_NUMBER); - addNewBonus(added); - } - else - { - std::shared_ptr b = existing[0]; - b->val = val; - } -} - -bool CCreature::isMyUpgrade(const CCreature *anotherCre) const -{ - //TODO upgrade of upgrade? - return vstd::contains(upgrades, anotherCre->getId()); -} - -bool CCreature::valid() const -{ - return this == VLC->creh->objects[idNumber]; -} - -std::string CCreature::nodeName() const -{ - return "\"" + getNamePluralTextID() + "\""; -} - -void CCreature::updateFrom(const JsonNode & data) -{ - JsonUpdater handler(nullptr, data); - - { - auto configScope = handler.enterStruct("config"); - - const JsonNode & configNode = handler.getCurrent(); - - serializeJson(handler); - - if(!configNode["hitPoints"].isNull()) - addBonus(configNode["hitPoints"].Integer(), BonusType::STACK_HEALTH); - - if(!configNode["speed"].isNull()) - addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); - - if(!configNode["attack"].isNull()) - addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); - - if(!configNode["defense"].isNull()) - addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); - - if(!configNode["damage"]["min"].isNull()) - addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); - - if(!configNode["damage"]["max"].isNull()) - addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); - - if(!configNode["shots"].isNull()) - addBonus(configNode["shots"].Integer(), BonusType::SHOTS); - - if(!configNode["spellPoints"].isNull()) - addBonus(configNode["spellPoints"].Integer(), BonusType::CASTS); - } - - - handler.serializeBonuses("bonuses", this); -} - -void CCreature::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("fightValue", fightValue); - handler.serializeInt("aiValue", AIValue); - handler.serializeInt("growth", growth); - handler.serializeInt("horde", hordeGrowth);// Needed at least until configurable buildings - - { - auto advMapNode = handler.enterStruct("advMapAmount"); - handler.serializeInt("min", ammMin); - handler.serializeInt("max", ammMax); - } - - if(handler.updating) - { - cost.serializeJson(handler, "cost"); - handler.serializeInt("faction", faction);//TODO: unify with deferred resolve - } - - handler.serializeInt("level", level); - handler.serializeBool("doubleWide", doubleWide); - - if(!handler.saving) - { - if(ammMin > ammMax) - logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); - } -} - -CCreatureHandler::CCreatureHandler() - : expAfterUpgrade(0) -{ - VLC->creh = this; - loadCommanders(); -} - -const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const -{ - std::optional index = VLC->modh->identifiers.getIdentifier(scope, "creature", identifier); - - if(!index) - throw std::runtime_error("Creature not found "+identifier); - - return objects[*index]; -} - -void CCreatureHandler::loadCommanders() -{ - ResourceID configResource("config/commanders.json"); - - std::string modSource = VLC->modh->findResourceOrigin(configResource); - JsonNode data(configResource); - data.setMeta(modSource); - - const JsonNode & config = data; // switch to const data accessors - - for (auto bonus : config["bonusPerLevel"].Vector()) - { - commanderLevelPremy.push_back(JsonUtils::parseBonus(bonus.Vector())); - } - - int i = 0; - for (auto skill : config["skillLevels"].Vector()) - { - skillLevels.emplace_back(); - for (auto skillLevel : skill["levels"].Vector()) - { - skillLevels[i].push_back(static_cast(skillLevel.Float())); - } - ++i; - } - - for (auto ability : config["abilityRequirements"].Vector()) - { - std::pair , std::pair > a; - a.first = JsonUtils::parseBonus (ability["ability"].Vector()); - a.second.first = static_cast(ability["skills"].Vector()[0].Float()); - a.second.second = static_cast(ability["skills"].Vector()[1].Float()); - skillRequirements.push_back (a); - } -} - -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); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser parser("DATA/CRTRAITS.TXT"); - - parser.endLine(); // header - - // this file is a bit different in some of Russian localisations: - //ENG: Singular Plural Wood ... - //RUS: Singular Plural Plural2 Wood ... - // Try to detect which version this is by header - // TODO: use 3rd name? Stand for "whose", e.g. pikemans' - size_t namesCount = 2; - { - if ( parser.readString() != "Singular" || parser.readString() != "Plural" ) - throw std::runtime_error("Incorrect format of CrTraits.txt"); - - if (parser.readString() == "Plural2") - namesCount = 3; - - parser.endLine(); - } - - for (size_t i=0; iidNumber = CreatureID(index); - cre->iconIndex = cre->getIndex() + 2; - cre->identifier = identifier; - cre->modScope = scope; - - JsonDeserializer handler(nullptr, node); - cre->serializeJson(handler); - - 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()); - - cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); - cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); - cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); - cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); - - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); - - assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); - - if(!node["shots"].isNull()) - cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); - - loadStackExperience(cre, node["stackExperience"]); - loadJsonAnimation(cre, node["graphics"]); - loadCreatureJson(cre, node); - - for(const auto & extraName : node["extraNames"].Vector()) - { - for(const auto & type_name : getTypeNames()) - registerObject(scope, type_name, extraName.String(), cre->getIndex()); - } - - JsonNode advMapFile = node["graphics"]["map"]; - JsonNode advMapMask = node["graphics"]["mapMask"]; - - VLC->modh->identifiers.requestIdentifier(scope, "object", "monster", [=](si32 index) - { - JsonNode conf; - conf.setMeta(scope); - - VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); - if (!advMapFile.isNull()) - { - JsonNode templ; - templ["animation"] = advMapFile; - if (!advMapMask.isNull()) - templ["mask"] = advMapMask; - templ.setMeta(scope); - - // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead - VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); - VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ); - } - - // object does not have any templates - this is not usable object (e.g. pseudo-creature like Arrow Tower) - if (VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num); - }); - - return cre; -} - -const std::vector & CCreatureHandler::getTypeNames() const -{ - static const std::vector typeNames = { "creature" }; - return typeNames; -} - -std::vector CCreatureHandler::getDefaultAllowed() const -{ - std::vector ret; - - ret.reserve(objects.size()); - for(const CCreature * crea : objects) - { - ret.push_back(crea ? !crea->special : false); - } - return ret; -} - -void CCreatureHandler::loadCrExpMod() -{ - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values - { - //Calculate rank exp values, formula appears complicated bu no parsing needed - expRanks.resize(8); - int dif = 0; - int it = 8000; //ignore name of this variable - expRanks[0].push_back(it); - for (int j = 1; j < 10; ++j) //used for tiers 8-10, and all other probably - { - expRanks[0].push_back(expRanks[0][j-1] + it + dif); - dif += it/5; - } - for (int i = 1; i < 8; ++i) //used for tiers 1-7 - { - dif = 0; - it = 1000 * i; - expRanks[i].push_back(it); - for (int j = 1; j < 10; ++j) - { - expRanks[i].push_back(expRanks[i][j-1] + it + dif); - dif += it/5; - } - } - - CLegacyConfigParser expBonParser("DATA/CREXPMOD.TXT"); - - expBonParser.endLine(); //header - - maxExpPerBattle.resize(8); - for (int i = 1; i < 8; ++i) - { - expBonParser.readString(); //index - expBonParser.readString(); //float multiplier -> hardcoded - expBonParser.readString(); //ignore upgrade mod? ->hardcoded - expBonParser.readString(); //already calculated - - maxExpPerBattle[i] = static_cast(expBonParser.readNumber()); - expRanks[i].push_back(expRanks[i].back() + static_cast(expBonParser.readNumber())); - - expBonParser.endLine(); - } - //skeleton gets exp penalty - objects[56].get()->addBonus(-50, BonusType::EXP_MULTIPLIER, -1); - objects[57].get()->addBonus(-50, BonusType::EXP_MULTIPLIER, -1); - //exp for tier >7, rank 11 - expRanks[0].push_back(147000); - expAfterUpgrade = 75; //percent - maxExpPerBattle[0] = maxExpPerBattle[7]; - } -} - - -void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) -{ - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses - { - logGlobal->debug("\tLoading stack experience bonuses"); - auto addBonusForAllCreatures = [&](std::shared_ptr b) { - auto limiter = std::make_shared(); - b->addLimiter(limiter); - globalEffects.addNewBonus(b); - }; - 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 limiter = std::make_shared(tier, max); - b->addLimiter(limiter); - globalEffects.addNewBonus(b); - }; - - CLegacyConfigParser parser("DATA/CREXPBON.TXT"); - - Bonus b; //prototype with some default properties - b.source = BonusSource::STACK_EXPERIENCE; - b.duration = BonusDuration::PERMANENT; - b.valType = BonusValueType::ADDITIVE_VALUE; - b.effectRange = BonusLimitEffect::NO_LIMIT; - b.additionalInfo = 0; - b.turnsRemain = 0; - BonusList bl; - - parser.endLine(); - - parser.readString(); //ignore index - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForAllCreatures(b); //health bonus is common for all - parser.endLine(); - - for (int i = 1; i < 7; ++i) - { - for (int j = 0; j < 4; ++j) //four modifiers common for tiers - { - parser.readString(); //ignore index - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForTier(i, b); - parser.endLine(); - } - } - for (int j = 0; j < 4; ++j) //tier 7 - { - parser.readString(); //ignore index - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForTier(7, b); - parser.endLine(); - } - do //parse everything that's left - { - auto sid = static_cast(parser.readNumber()); //id = this particular creature ID - - b.sid = sid; - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - { - objects[sid]->addNewBonus(b); //add directly to CCreature Node - } - } - while (parser.endLine()); - - }//end of Stack Experience -} - -void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const -{ - CLegacyConfigParser parser("DATA/CRANIM.TXT"); - - parser.endLine(); // header - parser.endLine(); - - for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) - { - while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines - ; - - loadUnitAnimInfo(h3Data[dd]["graphics"], parser); - parser.endLine(); - } -} - -void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser & parser) const -{ - graphics["timeBetweenFidgets"].Float() = parser.readNumber(); - - JsonNode & animationTime = graphics["animationTime"]; - animationTime["walk"].Float() = parser.readNumber(); - animationTime["attack"].Float() = parser.readNumber(); - parser.readNumber(); // unused value "Flight animation time" - H3 actually uses "Walk animation time" even for flying creatures - animationTime["idle"].Float() = 10.0; - - JsonNode & missile = graphics["missile"]; - JsonNode & offsets = missile["offset"]; - - offsets["upperX"].Float() = parser.readNumber(); - offsets["upperY"].Float() = parser.readNumber(); - offsets["middleX"].Float() = parser.readNumber(); - offsets["middleY"].Float() = parser.readNumber(); - offsets["lowerX"].Float() = parser.readNumber(); - offsets["lowerY"].Float() = parser.readNumber(); - - for(int i=0; i<12; i++) - { - JsonNode entry; - entry.Float() = parser.readNumber(); - missile["frameAngles"].Vector().push_back(entry); - } - - graphics["troopCountLocationOffset"].Float() = parser.readNumber(); - - missile["attackClimaxFrame"].Float() = parser.readNumber(); - - // assume that creature is not a shooter and should not have whole missile field - if (missile["frameAngles"].Vector()[0].Float() == 0 && - missile["attackClimaxFrame"].Float() == 0) - graphics.Struct().erase("missile"); -} - -void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const -{ - cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); - cre->animation.troopCountLocationOffset = static_cast(graphics["troopCountLocationOffset"].Float()); - - const JsonNode & animationTime = graphics["animationTime"]; - cre->animation.walkAnimationTime = animationTime["walk"].Float(); - cre->animation.idleAnimationTime = animationTime["idle"].Float(); - cre->animation.attackAnimationTime = animationTime["attack"].Float(); - - 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.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); - cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); - - cre->smallIconName = graphics["iconSmall"].String(); - cre->largeIconName = graphics["iconLarge"].String(); -} - -void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const -{ - creature->animDefName = config["graphics"]["animation"].String(); - - //FIXME: MOD COMPATIBILITY - if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) - { - for(const auto & ability : config["abilities"].Struct()) - { - if (!ability.second.isNull()) - { - auto b = JsonUtils::parseBonus(ability.second); - b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); - b->duration = BonusDuration::PERMANENT; - creature->addNewBonus(b); - } - } - } - else - { - for(const JsonNode &ability : config["abilities"].Vector()) - { - if(ability.getType() == JsonNode::JsonType::DATA_VECTOR) - { - logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey()); - } - else - { - auto b = JsonUtils::parseBonus(ability); - b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); - b->duration = BonusDuration::PERMANENT; - creature->addNewBonus(b); - } - } - } - - VLC->modh->identifiers.requestIdentifier("faction", config["faction"], [=](si32 faction) - { - creature->faction = FactionID(faction); - }); - - for(const JsonNode &value : config["upgrades"].Vector()) - { - VLC->modh->identifiers.requestIdentifier("creature", value, [=](si32 identifier) - { - creature->upgrades.insert(CreatureID(identifier)); - }); - } - - creature->animation.projectileImageName = config["graphics"]["missile"]["projectile"].String(); - - for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) - { - CCreature::CreatureAnimation::RayColor color; - - color.start.r = value["start"].Vector()[0].Integer(); - color.start.g = value["start"].Vector()[1].Integer(); - color.start.b = value["start"].Vector()[2].Integer(); - color.start.a = value["start"].Vector()[3].Integer(); - - color.end.r = value["end"].Vector()[0].Integer(); - color.end.g = value["end"].Vector()[1].Integer(); - color.end.b = value["end"].Vector()[2].Integer(); - color.end.a = value["end"].Vector()[3].Integer(); - - creature->animation.projectileRay.push_back(color); - } - - creature->special = config["special"].Bool() || config["disabled"].Bool(); - - const JsonNode & sounds = config["sound"]; - -#define GET_SOUND_VALUE(value_name) creature->sounds.value_name = sounds[#value_name].String() - GET_SOUND_VALUE(attack); - GET_SOUND_VALUE(defend); - GET_SOUND_VALUE(killed); - GET_SOUND_VALUE(move); - GET_SOUND_VALUE(shoot); - GET_SOUND_VALUE(wince); - GET_SOUND_VALUE(startMoving); - GET_SOUND_VALUE(endMoving); -#undef GET_SOUND_VALUE -} - -void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const -{ - for (const JsonNode &exp : input.Vector()) - { - const JsonVector &values = exp["values"].Vector(); - int lowerLimit = 1;//, upperLimit = 255; - if (values[0].getType() == JsonNode::JsonType::DATA_BOOL) - { - for (const JsonNode &val : values) - { - if(val.Bool()) - { - // parse each bonus separately - // we can not create copies since identifiers resolution does not tracks copies - // leading to unset identifier values in copies - auto bonus = JsonUtils::parseBonus (exp["bonus"]); - bonus->source = BonusSource::STACK_EXPERIENCE; - bonus->duration = BonusDuration::PERMANENT; - bonus->limiter = std::make_shared(RankRangeLimiter(lowerLimit)); - creature->addNewBonus (bonus); - break; //TODO: allow bonuses to turn off? - } - ++lowerLimit; - } - } - else - { - int lastVal = 0; - for (const JsonNode &val : values) - { - if (val.Float() != lastVal) - { - JsonNode bonusInput = exp["bonus"]; - bonusInput["val"].Float() = static_cast(val.Float()) - lastVal; - - auto bonus = JsonUtils::parseBonus (bonusInput); - bonus->source = BonusSource::STACK_EXPERIENCE; - bonus->duration = BonusDuration::PERMANENT; - bonus->limiter.reset (new RankRangeLimiter(lowerLimit)); - creature->addNewBonus (bonus); - } - lastVal = static_cast(val.Float()); - ++lowerLimit; - } - } - } -} - -void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const//help function for parsing CREXPBON.txt -{ - bool enable = false; //some bonuses are activated with values 2 or 1 - std::string buf = parser.readString(); - std::string mod = parser.readString(); - - switch (buf[0]) - { - case 'H': - b.type = BonusType::STACK_HEALTH; - b.valType = BonusValueType::PERCENT_TO_BASE; - break; - case 'A': - b.type = BonusType::PRIMARY_SKILL; - b.subtype = PrimarySkill::ATTACK; - break; - case 'D': - b.type = BonusType::PRIMARY_SKILL; - b.subtype = PrimarySkill::DEFENSE; - break; - case 'M': //Max damage - b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 2; - break; - case 'm': //Min damage - b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 1; - break; - case 'S': - b.type = BonusType::STACKS_SPEED; break; - case 'O': - b.type = BonusType::SHOTS; break; - case 'b': - b.type = BonusType::ENEMY_DEFENCE_REDUCTION; break; - case 'C': - b.type = BonusType::CHANGES_SPELL_COST_FOR_ALLY; break; - case 'd': - b.type = BonusType::DEFENSIVE_STANCE; break; - case 'e': - b.type = BonusType::DOUBLE_DAMAGE_CHANCE; - b.subtype = 0; - break; - case 'E': - b.type = BonusType::DEATH_STARE; - b.subtype = 0; //Gorgon - break; - case 'F': - b.type = BonusType::FEAR; break; - case 'g': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); - break; - case 'P': - b.type = BonusType::CASTS; break; - case 'R': - b.type = BonusType::ADDITIONAL_RETALIATION; break; - case 'W': - b.type = BonusType::MAGIC_RESISTANCE; - b.subtype = 0; //otherwise creature window goes crazy - break; - case 'f': //on-off skill - enable = true; //sometimes format is: 2 -> 0, 1 -> 1 - switch (mod[0]) - { - case 'A': - b.type = BonusType::ATTACKS_ALL_ADJACENT; break; - case 'b': - b.type = BonusType::RETURN_AFTER_STRIKE; break; - case 'B': - b.type = BonusType::TWO_HEX_ATTACK_BREATH; break; - case 'c': - b.type = BonusType::JOUSTING; - b.val = 5; - break; - case 'D': - b.type = BonusType::ADDITIONAL_ATTACK; break; - case 'f': - b.type = BonusType::FEARLESS; break; - case 'F': - b.type = BonusType::FLYING; break; - case 'm': - b.type = BonusType::MORALE; - b.val = 1; - b.valType = BonusValueType::INDEPENDENT_MAX; - break; - case 'M': - b.type = BonusType::NO_MORALE; break; - case 'p': //Mind spells - case 'P': - b.type = BonusType::MIND_IMMUNITY; break; - case 'r': - b.type = BonusType::REBIRTH; //on/off? makes sense? - b.subtype = 0; - b.val = 20; //arbitrary value - break; - case 'R': - b.type = BonusType::BLOCKS_RETALIATION; break; - case 's': - b.type = BonusType::FREE_SHOOTING; break; - case 'u': - b.type = BonusType::SPELL_RESISTANCE_AURA; break; - case 'U': - b.type = BonusType::UNDEAD; break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - break; - } - break; - case 'w': //specific spell immunities, enabled/disabled - enable = true; - switch (mod[0]) - { - case 'B': //Blind - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BLIND; - b.additionalInfo = 0;//normal immunity - break; - case 'H': //Hypnotize - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::HYPNOTIZE; - b.additionalInfo = 0;//normal immunity - break; - case 'I': //Implosion - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::IMPLOSION; - b.additionalInfo = 0;//normal immunity - break; - case 'K': //Berserk - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BERSERK; - b.additionalInfo = 0;//normal immunity - break; - case 'M': //Meteor Shower - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::METEOR_SHOWER; - b.additionalInfo = 0;//normal immunity - break; - case 'N': //dispell beneficial spells - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::DISPEL_HELPFUL_SPELLS; - b.additionalInfo = 0;//normal immunity - break; - case 'R': //Armageddon - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::ARMAGEDDON; - b.additionalInfo = 0;//normal immunity - break; - case 'S': //Slow - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::SLOW; - b.additionalInfo = 0;//normal immunity - break; - case '6': - case '7': - case '8': - case '9': - b.type = BonusType::LEVEL_SPELL_IMMUNITY; - b.val = std::atoi(mod.c_str()) - 5; - break; - case ':': - b.type = BonusType::LEVEL_SPELL_IMMUNITY; - b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? - break; - case 'F': - b.type = BonusType::FIRE_IMMUNITY; - b.subtype = 1; //not positive - break; - case 'O': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::FIRE); - b.val = 100; //Full damage immunity - break; - case 'f': - b.type = BonusType::FIRE_IMMUNITY; - b.subtype = 0; //all - break; - case 'C': - b.type = BonusType::WATER_IMMUNITY; - b.subtype = 1; //not positive - break; - case 'W': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::WATER); - b.val = 100; //Full damage immunity - break; - case 'w': - b.type = BonusType::WATER_IMMUNITY; - b.subtype = 0; //all - break; - case 'E': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::EARTH); - b.val = 100; //Full damage immunity - break; - case 'e': - b.type = BonusType::EARTH_IMMUNITY; - b.subtype = 0; //all - break; - case 'A': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::AIR); - b.val = 100; //Full damage immunity - break; - case 'a': - b.type = BonusType::AIR_IMMUNITY; - b.subtype = 0; //all - break; - case 'D': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); - b.val = 100; //Full damage immunity - break; - case '0': - b.type = BonusType::RECEPTIVE; - break; - case 'm': - b.type = BonusType::MIND_IMMUNITY; - break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - } - break; - - case 'i': - enable = true; - b.type = BonusType::NO_DISTANCE_PENALTY; - break; - case 'o': - enable = true; - b.type = BonusType::NO_WALL_PENALTY; - break; - - case 'a': - case 'c': - case 'K': - case 'k': - b.type = BonusType::SPELL_AFTER_ATTACK; - b.subtype = stringToNumber(mod); - break; - case 'h': - b.type = BonusType::HATE; - b.subtype = stringToNumber(mod); - break; - case 'p': - case 'J': - b.type = BonusType::SPELL_BEFORE_ATTACK; - b.subtype = stringToNumber(mod); - b.additionalInfo = 3; //always expert? - break; - case 'r': - b.type = BonusType::HP_REGENERATION; - b.val = stringToNumber(mod); - break; - case 's': - b.type = BonusType::ENCHANTED; - b.subtype = stringToNumber(mod); - b.valType = BonusValueType::INDEPENDENT_MAX; - break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - break; - } - switch (mod[0]) - { - case '+': - case '=': //should we allow percent values to stack or pick highest? - b.valType = BonusValueType::ADDITIVE_VALUE; - break; - } - - //limiters, range - si32 lastVal; - si32 curVal; - si32 lastLev = 0; - - if (enable) //0 and 2 means non-active, 1 - active - { - if (b.type != BonusType::REBIRTH) - b.val = 0; //on-off ability, no value specified - parser.readNumber(); // 0 level is never active - for (int i = 1; i < 11; ++i) - { - curVal = static_cast(parser.readNumber()); - if (curVal == 1) - { - b.limiter.reset (new RankRangeLimiter(i)); - bl.push_back(std::make_shared(b)); - break; //never turned off it seems - } - } - } - else - { - lastVal = static_cast(parser.readNumber()); - if (b.type == BonusType::HATE) - lastVal *= 10; //odd fix - //FIXME: value for zero level should be stored in our config files (independent of stack exp) - for (int i = 1; i < 11; ++i) - { - curVal = static_cast(parser.readNumber()); - if (b.type == BonusType::HATE) - curVal *= 10; //odd fix - if (curVal > lastVal) //threshold, add new bonus - { - b.val = curVal - lastVal; - lastVal = curVal; - b.limiter.reset (new RankRangeLimiter(i)); - bl.push_back(std::make_shared(b)); - lastLev = i; //start new range from here, i = previous rank - } - else if (curVal < lastVal) - { - b.val = lastVal; - b.limiter.reset (new RankRangeLimiter(lastLev, i)); - } - } - } -} - -int CCreatureHandler::stringToNumber(std::string & s) const -{ - boost::algorithm::replace_first(s,"#",""); //drop hash character - return std::atoi(s.c_str()); -} - -CCreatureHandler::~CCreatureHandler() -{ - for(auto & p : skillRequirements) - p.first = nullptr; -} - -CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const -{ - int r = 0; - if(tier == -1) //pick any allowed creature - { - do - { - r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId(); - } while (objects[r] && objects[r]->special); // find first "not special" creature - } - else - { - assert(vstd::iswithin(tier, 1, 7)); - std::vector allowed; - for(const auto & creature : objects) - { - if(!creature->special && creature->level == tier) - allowed.push_back(creature->getId()); - } - - if(allowed.empty()) - { - logGlobal->warn("Cannot pick a random creature of tier %d!", tier); - return CreatureID::NONE; - } - - return *RandomGeneratorUtil::nextItem(allowed, rand); - } - assert (r >= 0); //should always be, but it crashed - return CreatureID(r); -} - - -void CCreatureHandler::afterLoadFinalization() -{ - -} - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureHandler.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 "CCreatureHandler.h" + +#include "CGeneralTextHandler.h" +#include "ResourceSet.h" +#include "filesystem/Filesystem.h" +#include "VCMI_Lib.h" +#include "CTownHandler.h" +#include "GameSettings.h" +#include "constants/StringConstants.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.h" +#include "serializer/JsonDeserializer.h" +#include "serializer/JsonUpdater.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/CModHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const std::map CCreature::creatureQuantityRanges = +{ + {CCreature::CreatureQuantityId::FEW, "1-4"}, + {CCreature::CreatureQuantityId::SEVERAL, "5-9"}, + {CCreature::CreatureQuantityId::PACK, "10-19"}, + {CCreature::CreatureQuantityId::LOTS, "20-49"}, + {CCreature::CreatureQuantityId::HORDE, "50-99"}, + {CCreature::CreatureQuantityId::THRONG, "100-249"}, + {CCreature::CreatureQuantityId::SWARM, "250-499"}, + {CCreature::CreatureQuantityId::ZOUNDS, "500-999"}, + {CCreature::CreatureQuantityId::LEGION, "1000+"} +}; + +int32_t CCreature::getIndex() const +{ + return idNumber.toEnum(); +} + +int32_t CCreature::getIconIndex() const +{ + return iconIndex; +} + +std::string CCreature::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +void CCreature::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "CPRSMALL", smallIconName); + cb(getIconIndex(), 0, "TWCRPORT", largeIconName); +} + +CreatureID CCreature::getId() const +{ + return idNumber; +} + +const IBonusBearer * CCreature::getBonusBearer() const +{ + return this; +} + +int32_t CCreature::getAdvMapAmountMin() const +{ + return ammMin; +} + +int32_t CCreature::getAdvMapAmountMax() const +{ + return ammMax; +} + +int32_t CCreature::getAIValue() const +{ + return AIValue; +} + +int32_t CCreature::getFightValue() const +{ + return fightValue; +} + +int32_t CCreature::getLevel() const +{ + return level; +} + +int32_t CCreature::getGrowth() const +{ + return growth; +} + +int32_t CCreature::getHorde() const +{ + return hordeGrowth; +} + +FactionID CCreature::getFaction() const +{ + return FactionID(faction); +} + +int32_t CCreature::getBaseAttack() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDefense() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDamageMin() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDamageMax() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseHitPoints() const +{ + static const auto SELECTOR = Selector::type()(BonusType::STACK_HEALTH).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseSpellPoints() const +{ + static const auto SELECTOR = Selector::type()(BonusType::CASTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseSpeed() const +{ + static const auto SELECTOR = Selector::type()(BonusType::STACKS_SPEED).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseShots() const +{ + static const auto SELECTOR = Selector::type()(BonusType::SHOTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getRecruitCost(GameResID resIndex) const +{ + if(resIndex.getNum() >= 0 && resIndex.getNum() < cost.size()) + return cost[resIndex]; + else + return 0; +} + +TResources CCreature::getFullRecruitCost() const +{ + return cost; +} + +bool CCreature::hasUpgrades() const +{ + return !upgrades.empty(); +} + +std::string CCreature::getNameTranslated() const +{ + return getNameSingularTranslated(); +} + +std::string CCreature::getNamePluralTranslated() const +{ + return VLC->generaltexth->translate(getNamePluralTextID()); +} + +std::string CCreature::getNameSingularTranslated() const +{ + return VLC->generaltexth->translate(getNameSingularTextID()); +} + +std::string CCreature::getNameTextID() const +{ + return getNameSingularTextID(); +} + +std::string CCreature::getNamePluralTextID() const +{ + return TextIdentifier("creatures", modScope, identifier, "name", "plural" ).get(); +} + +std::string CCreature::getNameSingularTextID() const +{ + return TextIdentifier("creatures", modScope, identifier, "name", "singular" ).get(); +} + +CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity) +{ + if (quantity<5) + return CCreature::CreatureQuantityId::FEW; + if (quantity<10) + return CCreature::CreatureQuantityId::SEVERAL; + if (quantity<20) + return CCreature::CreatureQuantityId::PACK; + if (quantity<50) + return CCreature::CreatureQuantityId::LOTS; + if (quantity<100) + return CCreature::CreatureQuantityId::HORDE; + if (quantity<250) + return CCreature::CreatureQuantityId::THRONG; + if (quantity<500) + return CCreature::CreatureQuantityId::SWARM; + if (quantity<1000) + return CCreature::CreatureQuantityId::ZOUNDS; + + return CCreature::CreatureQuantityId::LEGION; +} + +std::string CCreature::getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId) +{ + if(creatureQuantityRanges.find(quantityId) != creatureQuantityRanges.end()) + return creatureQuantityRanges.at(quantityId); + + logGlobal->error("Wrong quantityId: %d", (int)quantityId); + assert(0); + return "[ERROR]"; +} + +int CCreature::estimateCreatureCount(ui32 countID) +{ + static const int creature_count[] = { 0, 3, 8, 15, 35, 75, 175, 375, 750, 2500 }; + + if(countID > 9) + { + logGlobal->error("Wrong countID %d!", countID); + return 0; + } + else + return creature_count[countID]; +} + +bool CCreature::isDoubleWide() const +{ + return doubleWide; +} + +/** + * Determines if the creature is of a good alignment. + * @return true if the creture is good, false otherwise. + */ +bool CCreature::isGood () const +{ + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::GOOD; +} + +/** + * Determines if the creature is of an evil alignment. + * @return true if the creature is evil, false otherwise. + */ +bool CCreature::isEvil () const +{ + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::EVIL; +} + +si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought +{ + int ret = 2147483645; + int resAmnt = static_cast(std::min(res.size(),cost.size())); + for(int i=0;i(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, BonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER); + addNewBonus(added); + } + else + { + std::shared_ptr b = existing[0]; + b->val = val; + } +} + +bool CCreature::isMyUpgrade(const CCreature *anotherCre) const +{ + //TODO upgrade of upgrade? + return vstd::contains(upgrades, anotherCre->getId()); +} + +bool CCreature::valid() const +{ + return this == (*VLC->creh)[idNumber]; +} + +std::string CCreature::nodeName() const +{ + return "\"" + getNamePluralTextID() + "\""; +} + +void CCreature::updateFrom(const JsonNode & data) +{ + JsonUpdater handler(nullptr, data); + + { + auto configScope = handler.enterStruct("config"); + + const JsonNode & configNode = handler.getCurrent(); + + serializeJson(handler); + + if(!configNode["hitPoints"].isNull()) + addBonus(configNode["hitPoints"].Integer(), BonusType::STACK_HEALTH); + + if(!configNode["speed"].isNull()) + addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); + + if(!configNode["attack"].isNull()) + addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); + + if(!configNode["defense"].isNull()) + addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); + + if(!configNode["damage"]["min"].isNull()) + addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + + if(!configNode["damage"]["max"].isNull()) + addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + + if(!configNode["shots"].isNull()) + addBonus(configNode["shots"].Integer(), BonusType::SHOTS); + + if(!configNode["spellPoints"].isNull()) + addBonus(configNode["spellPoints"].Integer(), BonusType::CASTS); + } + + + handler.serializeBonuses("bonuses", this); +} + +void CCreature::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("fightValue", fightValue); + handler.serializeInt("aiValue", AIValue); + handler.serializeInt("growth", growth); + handler.serializeInt("horde", hordeGrowth);// Needed at least until configurable buildings + + { + auto advMapNode = handler.enterStruct("advMapAmount"); + handler.serializeInt("min", ammMin); + handler.serializeInt("max", ammMax); + } + + if(handler.updating) + { + cost.serializeJson(handler, "cost"); + handler.serializeId("faction", faction); + } + + handler.serializeInt("level", level); + handler.serializeBool("doubleWide", doubleWide); + + if(!handler.saving) + { + if(ammMin > ammMax) + logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); + } +} + +CCreatureHandler::CCreatureHandler() + : expAfterUpgrade(0) +{ + VLC->creh = this; + loadCommanders(); +} + +const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const +{ + std::optional index = VLC->identifiers()->getIdentifier(scope, "creature", identifier); + + if(!index) + throw std::runtime_error("Creature not found "+identifier); + + return objects[*index]; +} + +void CCreatureHandler::loadCommanders() +{ + auto configResource = JsonPath::builtin("config/commanders.json"); + + std::string modSource = VLC->modh->findResourceOrigin(configResource); + JsonNode data(configResource); + data.setMeta(modSource); + + const JsonNode & config = data; // switch to const data accessors + + for (auto bonus : config["bonusPerLevel"].Vector()) + { + commanderLevelPremy.push_back(JsonUtils::parseBonus(bonus.Vector())); + } + + int i = 0; + for (auto skill : config["skillLevels"].Vector()) + { + skillLevels.emplace_back(); + for (auto skillLevel : skill["levels"].Vector()) + { + skillLevels[i].push_back(static_cast(skillLevel.Float())); + } + ++i; + } + + for (auto ability : config["abilityRequirements"].Vector()) + { + std::pair , std::pair > a; + a.first = JsonUtils::parseBonus (ability["ability"].Vector()); + a.second.first = static_cast(ability["skills"].Vector()[0].Float()); + a.second.second = static_cast(ability["skills"].Vector()[1].Float()); + skillRequirements.push_back (a); + } +} + +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); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser parser(TextPath::builtin("DATA/CRTRAITS.TXT")); + + parser.endLine(); // header + + // this file is a bit different in some of Russian localisations: + //ENG: Singular Plural Wood ... + //RUS: Singular Plural Plural2 Wood ... + // Try to detect which version this is by header + // TODO: use 3rd name? Stand for "whose", e.g. pikemans' + size_t namesCount = 2; + { + if ( parser.readString() != "Singular" || parser.readString() != "Plural" ) + throw std::runtime_error("Incorrect format of CrTraits.txt"); + + if (parser.readString() == "Plural2") + namesCount = 3; + + parser.endLine(); + } + + for (size_t i=0; iidNumber = CreatureID(index); + cre->iconIndex = cre->getIndex() + 2; + cre->identifier = identifier; + cre->modScope = scope; + + JsonDeserializer handler(nullptr, node); + cre->serializeJson(handler); + + 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()); + + cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); + cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); + cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); + cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); + + cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + + assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); + + if(!node["shots"].isNull()) + cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); + + loadStackExperience(cre, node["stackExperience"]); + loadJsonAnimation(cre, node["graphics"]); + loadCreatureJson(cre, node); + + for(const auto & extraName : node["extraNames"].Vector()) + { + for(const auto & type_name : getTypeNames()) + registerObject(scope, type_name, extraName.String(), cre->getIndex()); + } + + JsonNode advMapFile = node["graphics"]["map"]; + JsonNode advMapMask = node["graphics"]["mapMask"]; + + VLC->identifiers()->requestIdentifier(scope, "object", "monster", [=](si32 index) + { + JsonNode conf; + conf.setMeta(scope); + + VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); + if (!advMapFile.isNull()) + { + JsonNode templ; + templ["animation"] = advMapFile; + if (!advMapMask.isNull()) + templ["mask"] = advMapMask; + templ.setMeta(scope); + + // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead + VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); + VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ); + } + + // object does not have any templates - this is not usable object (e.g. pseudo-creature like Arrow Tower) + if (VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->getTemplates().empty()) + VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num); + }); + + return cre; +} + +const std::vector & CCreatureHandler::getTypeNames() const +{ + static const std::vector typeNames = { "creature" }; + return typeNames; +} + +void CCreatureHandler::loadCrExpMod() +{ + if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values + { + //Calculate rank exp values, formula appears complicated bu no parsing needed + expRanks.resize(8); + int dif = 0; + int it = 8000; //ignore name of this variable + expRanks[0].push_back(it); + for (int j = 1; j < 10; ++j) //used for tiers 8-10, and all other probably + { + expRanks[0].push_back(expRanks[0][j-1] + it + dif); + dif += it/5; + } + for (int i = 1; i < 8; ++i) //used for tiers 1-7 + { + dif = 0; + it = 1000 * i; + expRanks[i].push_back(it); + for (int j = 1; j < 10; ++j) + { + expRanks[i].push_back(expRanks[i][j-1] + it + dif); + dif += it/5; + } + } + + CLegacyConfigParser expBonParser(TextPath::builtin("DATA/CREXPMOD.TXT")); + + expBonParser.endLine(); //header + + maxExpPerBattle.resize(8); + for (int i = 1; i < 8; ++i) + { + expBonParser.readString(); //index + expBonParser.readString(); //float multiplier -> hardcoded + expBonParser.readString(); //ignore upgrade mod? ->hardcoded + expBonParser.readString(); //already calculated + + maxExpPerBattle[i] = static_cast(expBonParser.readNumber()); + expRanks[i].push_back(expRanks[i].back() + static_cast(expBonParser.readNumber())); + + expBonParser.endLine(); + } + //exp for tier >7, rank 11 + expRanks[0].push_back(147000); + expAfterUpgrade = 75; //percent + maxExpPerBattle[0] = maxExpPerBattle[7]; + } +} + + +void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) +{ + if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses + { + logGlobal->debug("\tLoading stack experience bonuses"); + auto addBonusForAllCreatures = [&](std::shared_ptr b) { + auto limiter = std::make_shared(); + b->addLimiter(limiter); + globalEffects.addNewBonus(b); + }; + 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 limiter = std::make_shared(tier, max); + b->addLimiter(limiter); + globalEffects.addNewBonus(b); + }; + + CLegacyConfigParser parser(TextPath::builtin("DATA/CREXPBON.TXT")); + + Bonus b; //prototype with some default properties + b.source = BonusSource::STACK_EXPERIENCE; + b.duration = BonusDuration::PERMANENT; + b.valType = BonusValueType::ADDITIVE_VALUE; + b.effectRange = BonusLimitEffect::NO_LIMIT; + b.additionalInfo = 0; + b.turnsRemain = 0; + BonusList bl; + + parser.endLine(); + + parser.readString(); //ignore index + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForAllCreatures(b); //health bonus is common for all + parser.endLine(); + + for (int i = 1; i < 7; ++i) + { + for (int j = 0; j < 4; ++j) //four modifiers common for tiers + { + parser.readString(); //ignore index + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForTier(i, b); + parser.endLine(); + } + } + for (int j = 0; j < 4; ++j) //tier 7 + { + parser.readString(); //ignore index + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForTier(7, b); + parser.endLine(); + } + do //parse everything that's left + { + CreatureID sid = parser.readNumber(); //id = this particular creature ID + + b.sid = BonusSourceID(sid); + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + (*this)[sid]->addNewBonus(b); //add directly to CCreature Node + } + while (parser.endLine()); + + }//end of Stack Experience +} + +void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const +{ + CLegacyConfigParser parser(TextPath::builtin("DATA/CRANIM.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) + { + while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines + ; + + loadUnitAnimInfo(h3Data[dd]["graphics"], parser); + parser.endLine(); + } +} + +void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser & parser) const +{ + graphics["timeBetweenFidgets"].Float() = parser.readNumber(); + + JsonNode & animationTime = graphics["animationTime"]; + animationTime["walk"].Float() = parser.readNumber(); + animationTime["attack"].Float() = parser.readNumber(); + parser.readNumber(); // unused value "Flight animation time" - H3 actually uses "Walk animation time" even for flying creatures + animationTime["idle"].Float() = 10.0; + + JsonNode & missile = graphics["missile"]; + JsonNode & offsets = missile["offset"]; + + offsets["upperX"].Float() = parser.readNumber(); + offsets["upperY"].Float() = parser.readNumber(); + offsets["middleX"].Float() = parser.readNumber(); + offsets["middleY"].Float() = parser.readNumber(); + offsets["lowerX"].Float() = parser.readNumber(); + offsets["lowerY"].Float() = parser.readNumber(); + + for(int i=0; i<12; i++) + { + JsonNode entry; + entry.Float() = parser.readNumber(); + missile["frameAngles"].Vector().push_back(entry); + } + + // Unused property "troopCountLocationOffset" + parser.readNumber(); + + missile["attackClimaxFrame"].Float() = parser.readNumber(); + + // assume that creature is not a shooter and should not have whole missile field + if (missile["frameAngles"].Vector()[0].Float() == 0 && + missile["attackClimaxFrame"].Float() == 0) + graphics.Struct().erase("missile"); +} + +void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const +{ + cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); + + const JsonNode & animationTime = graphics["animationTime"]; + cre->animation.walkAnimationTime = animationTime["walk"].Float(); + cre->animation.idleAnimationTime = animationTime["idle"].Float(); + cre->animation.attackAnimationTime = animationTime["attack"].Float(); + + 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.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); + cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); + + cre->smallIconName = graphics["iconSmall"].String(); + cre->largeIconName = graphics["iconLarge"].String(); +} + +void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const +{ + creature->animDefName = AnimationPath::fromJson(config["graphics"]["animation"]); + + //FIXME: MOD COMPATIBILITY + if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) + { + for(const auto & ability : config["abilities"].Struct()) + { + if (!ability.second.isNull()) + { + auto b = JsonUtils::parseBonus(ability.second); + b->source = BonusSource::CREATURE_ABILITY; + b->sid = BonusSourceID(creature->getId()); + b->duration = BonusDuration::PERMANENT; + creature->addNewBonus(b); + } + } + } + else + { + for(const JsonNode &ability : config["abilities"].Vector()) + { + if(ability.getType() == JsonNode::JsonType::DATA_VECTOR) + { + logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey()); + } + else + { + auto b = JsonUtils::parseBonus(ability); + b->source = BonusSource::CREATURE_ABILITY; + b->sid = BonusSourceID(creature->getId()); + b->duration = BonusDuration::PERMANENT; + creature->addNewBonus(b); + } + } + } + + VLC->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction) + { + creature->faction = FactionID(faction); + }); + + for(const JsonNode &value : config["upgrades"].Vector()) + { + VLC->identifiers()->requestIdentifier("creature", value, [=](si32 identifier) + { + creature->upgrades.insert(CreatureID(identifier)); + }); + } + + creature->animation.projectileImageName = AnimationPath::fromJson(config["graphics"]["missile"]["projectile"]); + + for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) + { + CCreature::CreatureAnimation::RayColor color; + + color.start.r = value["start"].Vector()[0].Integer(); + color.start.g = value["start"].Vector()[1].Integer(); + color.start.b = value["start"].Vector()[2].Integer(); + color.start.a = value["start"].Vector()[3].Integer(); + + color.end.r = value["end"].Vector()[0].Integer(); + color.end.g = value["end"].Vector()[1].Integer(); + color.end.b = value["end"].Vector()[2].Integer(); + color.end.a = value["end"].Vector()[3].Integer(); + + creature->animation.projectileRay.push_back(color); + } + + creature->special = config["special"].Bool() || config["disabled"].Bool(); + + const JsonNode & sounds = config["sound"]; + creature->sounds.attack = AudioPath::fromJson(sounds["attack"]); + creature->sounds.defend = AudioPath::fromJson(sounds["defend"]); + creature->sounds.killed = AudioPath::fromJson(sounds["killed"]); + creature->sounds.move = AudioPath::fromJson(sounds["move"]); + creature->sounds.shoot = AudioPath::fromJson(sounds["shoot"]); + creature->sounds.wince = AudioPath::fromJson(sounds["wince"]); + creature->sounds.startMoving = AudioPath::fromJson(sounds["startMoving"]); + creature->sounds.endMoving = AudioPath::fromJson(sounds["endMoving"]); +} + +void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const +{ + for (const JsonNode &exp : input.Vector()) + { + const JsonVector &values = exp["values"].Vector(); + int lowerLimit = 1;//, upperLimit = 255; + if (values[0].getType() == JsonNode::JsonType::DATA_BOOL) + { + for (const JsonNode &val : values) + { + if(val.Bool()) + { + // parse each bonus separately + // we can not create copies since identifiers resolution does not tracks copies + // leading to unset identifier values in copies + auto bonus = JsonUtils::parseBonus (exp["bonus"]); + bonus->source = BonusSource::STACK_EXPERIENCE; + bonus->duration = BonusDuration::PERMANENT; + bonus->limiter = std::make_shared(RankRangeLimiter(lowerLimit)); + creature->addNewBonus (bonus); + break; //TODO: allow bonuses to turn off? + } + ++lowerLimit; + } + } + else + { + int lastVal = 0; + for (const JsonNode &val : values) + { + if (val.Float() != lastVal) + { + JsonNode bonusInput = exp["bonus"]; + bonusInput["val"].Float() = static_cast(val.Float()) - lastVal; + + auto bonus = JsonUtils::parseBonus (bonusInput); + bonus->source = BonusSource::STACK_EXPERIENCE; + bonus->duration = BonusDuration::PERMANENT; + bonus->limiter.reset (new RankRangeLimiter(lowerLimit)); + creature->addNewBonus (bonus); + } + lastVal = static_cast(val.Float()); + ++lowerLimit; + } + } + } +} + +void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const//help function for parsing CREXPBON.txt +{ + bool enable = false; //some bonuses are activated with values 2 or 1 + std::string buf = parser.readString(); + std::string mod = parser.readString(); + + switch (buf[0]) + { + case 'H': + b.type = BonusType::STACK_HEALTH; + b.valType = BonusValueType::PERCENT_TO_BASE; + break; + case 'A': + b.type = BonusType::PRIMARY_SKILL; + b.subtype = BonusSubtypeID(PrimarySkill::ATTACK); + break; + case 'D': + b.type = BonusType::PRIMARY_SKILL; + b.subtype = BonusSubtypeID(PrimarySkill::DEFENSE); + break; + case 'M': //Max damage + b.type = BonusType::CREATURE_DAMAGE; + b.subtype = BonusCustomSubtype::creatureDamageMax; + break; + case 'm': //Min damage + b.type = BonusType::CREATURE_DAMAGE; + b.subtype = BonusCustomSubtype::creatureDamageMin; + break; + case 'S': + b.type = BonusType::STACKS_SPEED; break; + case 'O': + b.type = BonusType::SHOTS; break; + case 'b': + b.type = BonusType::ENEMY_DEFENCE_REDUCTION; break; + case 'C': + b.type = BonusType::CHANGES_SPELL_COST_FOR_ALLY; break; + case 'd': + b.type = BonusType::DEFENSIVE_STANCE; break; + case 'e': + b.type = BonusType::DOUBLE_DAMAGE_CHANCE; + break; + case 'E': + b.type = BonusType::DEATH_STARE; + b.subtype = BonusCustomSubtype::deathStareGorgon; + break; + case 'F': + b.type = BonusType::FEAR; break; + case 'g': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::ANY); + break; + case 'P': + b.type = BonusType::CASTS; break; + case 'R': + b.type = BonusType::ADDITIONAL_RETALIATION; break; + case 'W': + b.type = BonusType::MAGIC_RESISTANCE; + break; + case 'f': //on-off skill + enable = true; //sometimes format is: 2 -> 0, 1 -> 1 + switch (mod[0]) + { + case 'A': + b.type = BonusType::ATTACKS_ALL_ADJACENT; break; + case 'b': + b.type = BonusType::RETURN_AFTER_STRIKE; break; + case 'B': + b.type = BonusType::TWO_HEX_ATTACK_BREATH; break; + case 'c': + b.type = BonusType::JOUSTING; + b.val = 5; + break; + case 'D': + b.type = BonusType::ADDITIONAL_ATTACK; break; + case 'f': + b.type = BonusType::FEARLESS; break; + case 'F': + b.type = BonusType::FLYING; break; + case 'm': + b.type = BonusType::MORALE; + b.val = 1; + b.valType = BonusValueType::INDEPENDENT_MAX; + break; + case 'M': + b.type = BonusType::NO_MORALE; break; + case 'p': //Mind spells + case 'P': + b.type = BonusType::MIND_IMMUNITY; break; + case 'r': + b.type = BonusType::REBIRTH; //on/off? makes sense? + b.subtype = BonusCustomSubtype::rebirthRegular; + b.val = 20; //arbitrary value + break; + case 'R': + b.type = BonusType::BLOCKS_RETALIATION; break; + case 's': + b.type = BonusType::FREE_SHOOTING; break; + case 'u': + b.type = BonusType::SPELL_RESISTANCE_AURA; break; + case 'U': + b.type = BonusType::UNDEAD; break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + break; + } + break; + case 'w': //specific spell immunities, enabled/disabled + enable = true; + switch (mod[0]) + { + case 'B': //Blind + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::BLIND)); + b.additionalInfo = 0;//normal immunity + break; + case 'H': //Hypnotize + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::HYPNOTIZE)); + b.additionalInfo = 0;//normal immunity + break; + case 'I': //Implosion + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::IMPLOSION)); + b.additionalInfo = 0;//normal immunity + break; + case 'K': //Berserk + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::BERSERK)); + b.additionalInfo = 0;//normal immunity + break; + case 'M': //Meteor Shower + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::METEOR_SHOWER)); + b.additionalInfo = 0;//normal immunity + break; + case 'N': //dispell beneficial spells + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); + b.additionalInfo = 0;//normal immunity + break; + case 'R': //Armageddon + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::ARMAGEDDON)); + b.additionalInfo = 0;//normal immunity + break; + case 'S': //Slow + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellID(SpellID::SLOW)); + b.additionalInfo = 0;//normal immunity + break; + case '6': + case '7': + case '8': + case '9': + b.type = BonusType::LEVEL_SPELL_IMMUNITY; + b.val = std::atoi(mod.c_str()) - 5; + break; + case ':': + b.type = BonusType::LEVEL_SPELL_IMMUNITY; + b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? + break; + case 'F': + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::FIRE); + break; + case 'O': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::FIRE); + b.val = 100; //Full damage immunity + break; + case 'f': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::FIRE); + break; + case 'C': + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::WATER); + break; + case 'W': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::WATER); + b.val = 100; //Full damage immunity + break; + case 'w': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::WATER); + break; + case 'E': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::EARTH); + b.val = 100; //Full damage immunity + break; + case 'e': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::EARTH); + break; + case 'A': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::AIR); + b.val = 100; //Full damage immunity + break; + case 'a': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = BonusSubtypeID(SpellSchool::AIR); + break; + case 'D': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = BonusSubtypeID(SpellSchool::ANY); + b.val = 100; //Full damage immunity + break; + case '0': + b.type = BonusType::RECEPTIVE; + break; + case 'm': + b.type = BonusType::MIND_IMMUNITY; + break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + } + break; + + case 'i': + enable = true; + b.type = BonusType::NO_DISTANCE_PENALTY; + break; + case 'o': + enable = true; + b.type = BonusType::NO_WALL_PENALTY; + break; + + case 'a': + case 'c': + case 'K': + case 'k': + b.type = BonusType::SPELL_AFTER_ATTACK; + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); + break; + case 'h': + b.type = BonusType::HATE; + b.subtype = BonusSubtypeID(CreatureID(stringToNumber(mod))); + break; + case 'p': + case 'J': + b.type = BonusType::SPELL_BEFORE_ATTACK; + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); + b.additionalInfo = 3; //always expert? + break; + case 'r': + b.type = BonusType::HP_REGENERATION; + b.val = stringToNumber(mod); + break; + case 's': + b.type = BonusType::ENCHANTED; + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); + b.valType = BonusValueType::INDEPENDENT_MAX; + break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + break; + } + switch (mod[0]) + { + case '+': + case '=': //should we allow percent values to stack or pick highest? + b.valType = BonusValueType::ADDITIVE_VALUE; + break; + } + + //limiters, range + si32 lastVal; + si32 curVal; + si32 lastLev = 0; + + if (enable) //0 and 2 means non-active, 1 - active + { + if (b.type != BonusType::REBIRTH) + b.val = 0; //on-off ability, no value specified + parser.readNumber(); // 0 level is never active + for (int i = 1; i < 11; ++i) + { + curVal = static_cast(parser.readNumber()); + if (curVal == 1) + { + b.limiter.reset (new RankRangeLimiter(i)); + bl.push_back(std::make_shared(b)); + break; //never turned off it seems + } + } + } + else + { + lastVal = static_cast(parser.readNumber()); + if (b.type == BonusType::HATE) + lastVal *= 10; //odd fix + //FIXME: value for zero level should be stored in our config files (independent of stack exp) + for (int i = 1; i < 11; ++i) + { + curVal = static_cast(parser.readNumber()); + if (b.type == BonusType::HATE) + curVal *= 10; //odd fix + if (curVal > lastVal) //threshold, add new bonus + { + b.val = curVal - lastVal; + lastVal = curVal; + b.limiter.reset (new RankRangeLimiter(i)); + bl.push_back(std::make_shared(b)); + lastLev = i; //start new range from here, i = previous rank + } + else if (curVal < lastVal) + { + b.val = lastVal; + b.limiter.reset (new RankRangeLimiter(lastLev, i)); + } + } + } +} + +int CCreatureHandler::stringToNumber(std::string & s) const +{ + boost::algorithm::replace_first(s,"#",""); //drop hash character + return std::atoi(s.c_str()); +} + +CCreatureHandler::~CCreatureHandler() +{ + for(auto & p : skillRequirements) + p.first = nullptr; +} + +CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const +{ + std::vector allowed; + for(const auto & creature : objects) + { + if(creature->special) + continue; + + if (creature->level == tier || tier == -1) + allowed.push_back(creature->getId()); + } + + if(allowed.empty()) + { + logGlobal->warn("Cannot pick a random creature of tier %d!", tier); + return CreatureID::NONE; + } + + return *RandomGeneratorUtil::nextItem(allowed, rand); +} + + +void CCreatureHandler::afterLoadFinalization() +{ + +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 0ef2a26df..6d821e0c7 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -1,327 +1,243 @@ -/* - * CCreatureHandler.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 "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "ConstTransitivePtr.h" -#include "ResourceSet.h" -#include "GameConstants.h" -#include "JsonNode.h" -#include "IHandlerBase.h" -#include "CRandomGenerator.h" -#include "Color.h" - -#include -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class CLegacyConfigParser; -class CCreatureHandler; -class CCreature; -class JsonSerializeFormat; - -class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode -{ - friend class CCreatureHandler; - std::string modScope; - std::string identifier; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - - CreatureID idNumber; - - FactionID faction = FactionID::NEUTRAL; - ui8 level = 0; // 0 - unknown; 1-7 for "usual" creatures - - //stats that are not handled by bonus system - ui32 fightValue, AIValue, growth, hordeGrowth; - - bool doubleWide = false; - - TResources cost; //cost[res_id] - amount of that resource required to buy creature from dwelling - -public: - ui32 ammMin, ammMax; // initial size of stack of these creatures on adventure map (if not set in editor) - - bool special = true; // Creature is not available normally (war machines, commanders, several unused creatures, etc - - std::set upgrades; // IDs of creatures to which this creature can be upgraded - - std::string animDefName; // creature animation used during battles - - si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now. - /// names of files with appropriate icons. Used only during loading - std::string smallIconName; - std::string largeIconName; - - enum class CreatureQuantityId - { - FEW = 1, - SEVERAL, - PACK, - LOTS, - HORDE, - THRONG, - SWARM, - ZOUNDS, - LEGION - }; - - struct CreatureAnimation - { - struct RayColor { - ColorRGBA start; - ColorRGBA end; - - template void serialize(Handler &h, const int version) - { - h & start & end; - } - }; - - double timeBetweenFidgets, idleAnimationTime, - walkAnimationTime, attackAnimationTime; - int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX, - upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; - - std::vector missleFrameAngles; - int troopCountLocationOffset, attackClimaxFrame; - - std::string projectileImageName; - std::vector projectileRay; - //bool projectileSpin; //if true, appropriate projectile is spinning during flight - - template void serialize(Handler &h, const int version) - { - h & timeBetweenFidgets; - h & idleAnimationTime; - h & walkAnimationTime; - h & attackAnimationTime; - - if (version < 814) - { - float unused = 0.f; - h & unused; - } - - h & upperRightMissleOffsetX; - h & rightMissleOffsetX; - h & lowerRightMissleOffsetX; - h & upperRightMissleOffsetY; - h & rightMissleOffsetY; - h & lowerRightMissleOffsetY; - h & missleFrameAngles; - h & troopCountLocationOffset; - h & attackClimaxFrame; - h & projectileImageName; - h & projectileRay; - } - } animation; - - //sound info - struct CreatureBattleSounds - { - std::string attack; - std::string defend; - std::string killed; // was killed or died - std::string move; - std::string shoot; // range attack - std::string wince; // attacked but did not die - std::string startMoving; - std::string endMoving; - - template void serialize(Handler &h, const int version) - { - h & attack; - h & defend; - h & killed; - h & move; - h & shoot; - h & wince; - h & startMoving; - h & endMoving; - } - } sounds; - - ArtifactID warMachine; - - std::string getNamePluralTranslated() const override; - std::string getNameSingularTranslated() const override; - - std::string getNamePluralTextID() const override; - std::string getNameSingularTextID() const override; - - FactionID getFaction() const override; - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - CreatureID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; - - int32_t getAdvMapAmountMin() const override; - int32_t getAdvMapAmountMax() const override; - int32_t getAIValue() const override; - int32_t getFightValue() const override; - int32_t getLevel() const override; - int32_t getGrowth() const override; - int32_t getHorde() const override; - - int32_t getBaseAttack() const override; - int32_t getBaseDefense() const override; - int32_t getBaseDamageMin() const override; - int32_t getBaseDamageMax() const override; - int32_t getBaseHitPoints() const override; - int32_t getBaseSpellPoints() const override; - int32_t getBaseSpeed() const override; - int32_t getBaseShots() const override; - - int32_t getRecruitCost(GameResID resIndex) const override; - TResources getFullRecruitCost() const override; - bool isDoubleWide() const override; //returns true if unit is double wide on battlefield - bool hasUpgrades() const override; - - bool isGood () const; - bool isEvil () const; - si32 maxAmount(const TResources &res) const; //how many creatures can be bought - static CCreature::CreatureQuantityId getQuantityID(const int & quantity); - static std::string getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId); - 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, int subtype = -1); - std::string nodeName() const override; - - template - int getRandomAmount(RanGen ranGen) const - { - if(ammMax == ammMin) - return ammMax; - else - return ammMin + (ranGen() % (ammMax - ammMin)); - } - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & cost; - h & upgrades; - h & fightValue; - h & AIValue; - h & growth; - h & hordeGrowth; - h & ammMin; - h & ammMax; - h & level; - h & animDefName; - h & iconIndex; - h & smallIconName; - h & largeIconName; - - h & idNumber; - h & faction; - h & sounds; - h & animation; - - h & doubleWide; - h & special; - h & identifier; - h & modScope; - h & warMachine; - } - - CCreature(); - -private: - static const std::map creatureQuantityRanges; -}; - -class DLL_LINKAGE CCreatureHandler : public CHandlerBase -{ -private: - void loadJsonAnimation(CCreature * creature, const JsonNode & graphics) const; - 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(); - /// load creature from json structure - void load(std::string creatureID, const JsonNode & node); - /// read cranim.txt file from H3 - void loadAnimationInfo(std::vector & h3Data) const; - /// read one line from cranim.txt - void loadUnitAnimInfo(JsonNode & unit, CLegacyConfigParser & parser) const; - /// parse crexpbon.txt file from H3 - void loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const; - /// help function for parsing CREXPBON.txt - int stringToNumber(std::string & s) const; - -protected: - const std::vector & getTypeNames() const override; - CCreature * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; - -public: - std::set doubledCreatures; //they get double week - - //stack exp - std::vector > expRanks; // stack experience needed for certain rank, index 0 for other tiers (?) - std::vector maxExpPerBattle; //%, tiers same as above - si8 expAfterUpgrade;//multiplier in % - - //Commanders - BonusList commanderLevelPremy; //bonus values added with each level-up - 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 - - const CCreature * getCreature(const std::string & scope, const std::string & identifier) const; - - CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any - - CCreatureHandler(); - ~CCreatureHandler(); - - /// load all stack experience bonuses from H3 files - void loadCrExpBon(CBonusSystemNode & globalEffects); - - /// load all stack modifier bonuses from H3 files. TODO: move this to json - void loadCrExpMod(); - - void afterLoadFinalization() override; - - std::vector loadLegacyData() override; - - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - //TODO: should be optimized, not all these informations needs to be serialized (same for ccreature) - h & doubledCreatures; - h & objects; - h & expRanks; - h & maxExpPerBattle; - h & expAfterUpgrade; - h & skillLevels; - h & skillRequirements; - h & commanderLevelPremy; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureHandler.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 "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "ConstTransitivePtr.h" +#include "ResourceSet.h" +#include "GameConstants.h" +#include "JsonNode.h" +#include "IHandlerBase.h" +#include "CRandomGenerator.h" +#include "Color.h" +#include "filesystem/ResourcePath.h" + +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class CLegacyConfigParser; +class CCreatureHandler; +class CCreature; +class JsonSerializeFormat; + +class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode +{ + friend class CCreatureHandler; + std::string modScope; + std::string identifier; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + + CreatureID idNumber; + + FactionID faction = FactionID::NEUTRAL; + ui8 level = 0; // 0 - unknown; 1-7 for "usual" creatures + + //stats that are not handled by bonus system + ui32 fightValue, AIValue, growth, hordeGrowth; + + bool doubleWide = false; + + TResources cost; //cost[res_id] - amount of that resource required to buy creature from dwelling + +public: + ui32 ammMin, ammMax; // initial size of stack of these creatures on adventure map (if not set in editor) + + bool special = true; // Creature is not available normally (war machines, commanders, several unused creatures, etc + + std::set upgrades; // IDs of creatures to which this creature can be upgraded + + AnimationPath animDefName; // creature animation used during battles + + si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now. + /// names of files with appropriate icons. Used only during loading + std::string smallIconName; + std::string largeIconName; + + enum class CreatureQuantityId + { + FEW = 1, + SEVERAL, + PACK, + LOTS, + HORDE, + THRONG, + SWARM, + ZOUNDS, + LEGION + }; + + struct CreatureAnimation + { + struct RayColor { + ColorRGBA start; + ColorRGBA end; + }; + + double timeBetweenFidgets, idleAnimationTime, + walkAnimationTime, attackAnimationTime; + int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX, + upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; + + std::vector missleFrameAngles; + int attackClimaxFrame; + + AnimationPath projectileImageName; + std::vector projectileRay; + + } animation; + + //sound info + struct CreatureBattleSounds + { + AudioPath attack; + AudioPath defend; + AudioPath killed; // was killed or died + AudioPath move; + AudioPath shoot; // range attack + AudioPath wince; // attacked but did not die + AudioPath startMoving; + AudioPath endMoving; + } sounds; + + ArtifactID warMachine; + + std::string getNamePluralTranslated() const override; + std::string getNameSingularTranslated() const override; + + std::string getNamePluralTextID() const override; + std::string getNameSingularTextID() const override; + + FactionID getFaction() const override; + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + CreatureID getId() const override; + virtual const IBonusBearer * getBonusBearer() const override; + + int32_t getAdvMapAmountMin() const override; + int32_t getAdvMapAmountMax() const override; + int32_t getAIValue() const override; + int32_t getFightValue() const override; + int32_t getLevel() const override; + int32_t getGrowth() const override; + int32_t getHorde() const override; + + int32_t getBaseAttack() const override; + int32_t getBaseDefense() const override; + int32_t getBaseDamageMin() const override; + int32_t getBaseDamageMax() const override; + int32_t getBaseHitPoints() const override; + int32_t getBaseSpellPoints() const override; + int32_t getBaseSpeed() const override; + int32_t getBaseShots() const override; + + int32_t getRecruitCost(GameResID resIndex) const override; + TResources getFullRecruitCost() const override; + bool isDoubleWide() const override; //returns true if unit is double wide on battlefield + bool hasUpgrades() const override; + + bool isGood () const; + bool isEvil () const; + si32 maxAmount(const TResources &res) const; //how many creatures can be bought + static CCreature::CreatureQuantityId getQuantityID(const int & quantity); + static std::string getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId); + 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; + + template + int getRandomAmount(RanGen ranGen) const + { + if(ammMax == ammMin) + return ammMax; + else + return ammMin + (ranGen() % (ammMax - ammMin)); + } + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + CCreature(); + +private: + static const std::map creatureQuantityRanges; +}; + +class DLL_LINKAGE CCreatureHandler : public CHandlerBase +{ +private: + void loadJsonAnimation(CCreature * creature, const JsonNode & graphics) const; + 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(); + /// load creature from json structure + void load(std::string creatureID, const JsonNode & node); + /// read cranim.txt file from H3 + void loadAnimationInfo(std::vector & h3Data) const; + /// read one line from cranim.txt + void loadUnitAnimInfo(JsonNode & unit, CLegacyConfigParser & parser) const; + /// parse crexpbon.txt file from H3 + void loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const; + /// help function for parsing CREXPBON.txt + int stringToNumber(std::string & s) const; + +protected: + const std::vector & getTypeNames() const override; + CCreature * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; + +public: + std::set doubledCreatures; //they get double week + + //stack exp + std::vector > expRanks; // stack experience needed for certain rank, index 0 for other tiers (?) + std::vector maxExpPerBattle; //%, tiers same as above + si8 expAfterUpgrade;//multiplier in % + + //Commanders + BonusList commanderLevelPremy; //bonus values added with each level-up + 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 + + const CCreature * getCreature(const std::string & scope, const std::string & identifier) const; + + CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any + + CCreatureHandler(); + ~CCreatureHandler(); + + /// load all stack experience bonuses from H3 files + void loadCrExpBon(CBonusSystemNode & globalEffects); + + /// load all stack modifier bonuses from H3 files. TODO: move this to json + void loadCrExpMod(); + + void afterLoadFinalization() override; + + std::vector loadLegacyData() override; + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index a11ede6ab..913ec72f6 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1,1070 +1,1079 @@ -/* - * CCreatureSet.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 "CCreatureSet.h" - -#include "ArtifactUtils.h" -#include "CConfigHandler.h" -#include "CCreatureHandler.h" -#include "VCMI_Lib.h" -#include "CModHandler.h" -#include "GameSettings.h" -#include "mapObjects/CGHeroInstance.h" -#include "IGameCallback.h" -#include "CGeneralTextHandler.h" -#include "spells/CSpellHandler.h" -#include "CHeroHandler.h" -#include "IBonusTypeHandler.h" -#include "serializer/JsonSerializeFormat.h" -#include "NetPacksBase.h" - -#include -#include - -VCMI_LIB_NAMESPACE_BEGIN - - -bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs) -{ - return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting -} - -const CStackInstance & CCreatureSet::operator[](const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return *i->second; - else - throw std::runtime_error("That slot is empty!"); -} - -const CCreature * CCreatureSet::getCreature(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->type; - else - return nullptr; -} - -bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) /*slots 0 to 6 */ -{ - if(!slot.validSlot()) - { - logGlobal->error("Cannot set slot %d", slot.getNum()); - return false; - } - if(!quantity) - { - logGlobal->warn("Using set creature to delete stack?"); - eraseStack(slot); - return true; - } - - if(hasStackAtSlot(slot)) //remove old creature - eraseStack(slot); - - auto * armyObj = castToArmyObj(); - bool isHypotheticArmy = armyObj ? armyObj->isHypothetic() : false; - - putStack(slot, new CStackInstance(type, quantity, isHypotheticArmy)); - return true; -} - -SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ -{ - return getSlotFor(VLC->creh->objects[creature], slotsAmount); -} - -SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const -{ - assert(c && c->valid()); - for(const auto & elem : stacks) - { - assert(elem.second->type->valid()); - if(elem.second->type == c) - { - return elem.first; //if there is already such creature we return its slot id - } - } - return getFreeSlot(slotsAmount); -} - -bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) const -{ - assert(c && c->valid()); - for(const auto & elem : stacks) // elem is const - { - if(elem.first == exclude) // Check slot - continue; - - if(!elem.second || !elem.second->type) // Check creature - continue; - - assert(elem.second->type->valid()); - - if(elem.second->type == c) - return true; - } - return false; -} - -std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount) const -{ - assert(c && c->valid()); - std::vector result; - - for(const auto & elem : stacks) - { - if(elem.first == exclude) - continue; - - if(!elem.second || !elem.second->type || elem.second->type != c) - continue; - - if(elem.second->count == ignoreAmount || elem.second->count < 1) - continue; - - assert(elem.second->type->valid()); - result.push_back(elem.first); - } - return result; -} - -bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const -{ - assert(c && c->valid()); - TQuantity max = 0; - TQuantity min = std::numeric_limits::max(); - - for(const auto & elem : stacks) - { - if(!elem.second || !elem.second->type || elem.second->type != c) - continue; - - const auto count = elem.second->count; - - if(count == ignoreAmount || count < 1) - continue; - - assert(elem.second->type->valid()); - - if(count > max) - max = count; - if(count < min) - min = count; - if(max - min > 1) - return false; - } - return true; -} - -SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const -{ - for(ui32 i=0; i CCreatureSet::getFreeSlots(ui32 slotsAmount) const -{ - std::vector freeSlots; - - for(ui32 i = 0; i < slotsAmount; i++) - { - auto slot = SlotID(i); - - if(!vstd::contains(stacks, slot)) - freeSlots.push_back(slot); - } - return freeSlots; -} - -std::queue CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const -{ - std::queue freeSlots; - - for (ui32 i = 0; i < slotsAmount; i++) - { - auto slot = SlotID(i); - - if(!vstd::contains(stacks, slot)) - freeSlots.push(slot); - } - return freeSlots; -} - -TMapCreatureSlot CCreatureSet::getCreatureMap() const -{ - TMapCreatureSlot creatureMap; - TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp(); - - // https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find - // https://www.cplusplus.com/reference/map/map/key_comp/ - for(const auto & pair : stacks) - { - const auto * creature = pair.second->type; - auto slot = pair.first; - auto lb = creatureMap.lower_bound(creature); - - if(lb != creatureMap.end() && !(keyComp(creature, lb->first))) - continue; - - creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot)); - } - return creatureMap; -} - -TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const -{ - TCreatureQueue creatureQueue; - - for(const auto & pair : stacks) - { - if(pair.first == exclude) - continue; - creatureQueue.push(std::make_pair(pair.second->type, pair.first)); - } - return creatureQueue; -} - -TQuantity CCreatureSet::getStackCount(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->count; - else - return 0; //TODO? consider issuing a warning -} - -TExpType CCreatureSet::getStackExperience(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->experience; - else - 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 */ -{ - //try to match creature to our preferred stack - if(preferable.validSlot() && vstd::contains(stacks, preferable)) - { - const CCreature *cr = stacks.find(preferable)->second->type; - for(const auto & elem : stacks) - { - if(cr == elem.second->type && elem.first != preferable) - { - out.first = preferable; - out.second = elem.first; - return true; - } - } - } - - for(const auto & stack : stacks) - { - for(const auto & elem : stacks) - { - if(stack.second->type == elem.second->type && stack.first != elem.first) - { - out.first = stack.first; - out.second = elem.first; - return true; - } - } - } - return false; -} - -void CCreatureSet::sweep() -{ - for(auto i=stacks.begin(); i!=stacks.end(); ++i) - { - if(!i->second->count) - { - stacks.erase(i); - sweep(); - break; - } - } -} - -void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) -{ - const CCreature *c = VLC->creh->objects[cre]; - - if(!hasStackAtSlot(slot)) - { - setCreature(slot, cre, count); - } - else if(getCreature(slot) == c && allowMerging) //that slot was empty or contained same type creature - { - setStackCount(slot, getStackCount(slot) + count); - } - else - { - logGlobal->error("Failed adding to slot!"); - } -} - -void CCreatureSet::addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging) -{ - assert(stack->valid(true)); - - if(!hasStackAtSlot(slot)) - { - putStack(slot, stack); - } - else if(allowMerging && stack->type == getCreature(slot)) - { - joinStack(slot, stack); - } - else - { - logGlobal->error("Cannot add to slot %d stack %s", slot.getNum(), stack->nodeName()); - } -} - -bool CCreatureSet::validTypes(bool allowUnrandomized) const -{ - for(const auto & elem : stacks) - { - if(!elem.second->valid(allowUnrandomized)) - return false; - } - return true; -} - -bool CCreatureSet::slotEmpty(const SlotID & slot) const -{ - return !hasStackAtSlot(slot); -} - -bool CCreatureSet::needsLastStack() const -{ - return false; -} - -ui64 CCreatureSet::getArmyStrength() const -{ - ui64 ret = 0; - for(const auto & elem : stacks) - ret += elem.second->getPower(); - return ret; -} - -ui64 CCreatureSet::getPower(const SlotID & slot) const -{ - return getStack(slot).getPower(); -} - -std::string CCreatureSet::getRoughAmount(const SlotID & slot, int mode) const -{ - /// Mode represent return string format - /// "Pack" - 0, "A pack of" - 1, "a pack of" - 2 - CCreature::CreatureQuantityId quantity = CCreature::getQuantityID(getStackCount(slot)); - - if((int)quantity) - { - if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) - return CCreature::getQuantityRangeStringForId(quantity); - - return VLC->generaltexth->arraytxt[(174 + mode) + 3*(int)quantity]; - } - return ""; -} - -std::string CCreatureSet::getArmyDescription() const -{ - std::string text; - std::vector guards; - for(const auto & elem : stacks) - { - auto str = boost::str(boost::format("%s %s") % getRoughAmount(elem.first, 2) % getCreature(elem.first)->getNamePluralTranslated()); - guards.push_back(str); - } - if(!guards.empty()) - { - for(int i = 0; i < guards.size(); i++) - { - text += guards[i]; - if(i + 2 < guards.size()) - text += ", "; - else if(i + 2 == guards.size()) - text += VLC->generaltexth->allTexts[237]; - } - } - return text; -} - -int CCreatureSet::stacksCount() const -{ - return static_cast(stacks.size()); -} - -void CCreatureSet::setFormation(bool tight) -{ - if (tight) - formation = EArmyFormation::TIGHT; - else - formation = EArmyFormation::LOOSE; -} - -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) - stacks[slot]->experience = static_cast(stacks[slot]->experience * (count / static_cast(stacks[slot]->count))); - stacks[slot]->count = count; - armyChanged(); -} - -void CCreatureSet::giveStackExp(TExpType exp) -{ - for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++) - i->second->giveStackExp(exp); -} -void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp) -{ - assert(hasStackAtSlot(slot)); - stacks[slot]->experience = exp; -} - -void CCreatureSet::clearSlots() -{ - while(!stacks.empty()) - { - eraseStack(stacks.begin()->first); - } -} - -const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const -{ - assert(hasStackAtSlot(slot)); - return *getStackPtr(slot); -} - -const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const -{ - if(hasStackAtSlot(slot)) - return stacks.find(slot)->second; - else return nullptr; -} - -void CCreatureSet::eraseStack(const SlotID & slot) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *toErase = detachStack(slot); - vstd::clear_pointer(toErase); -} - -bool CCreatureSet::contains(const CStackInstance *stack) const -{ - if(!stack) - return false; - - for(const auto & elem : stacks) - if(elem.second == stack) - return true; - - return false; -} - -SlotID CCreatureSet::findStack(const CStackInstance *stack) const -{ - const auto * h = dynamic_cast(this); - if (h && h->commander == stack) - return SlotID::COMMANDER_SLOT_PLACEHOLDER; - - if(!stack) - return SlotID(); - - for(const auto & elem : stacks) - if(elem.second == stack) - return elem.first; - - return SlotID(); -} - -CArmedInstance * CCreatureSet::castToArmyObj() -{ - return dynamic_cast(this); -} - -void CCreatureSet::putStack(const SlotID & slot, CStackInstance * stack) -{ - assert(slot.getNum() < GameConstants::ARMY_SIZE); - assert(!hasStackAtSlot(slot)); - stacks[slot] = stack; - stack->setArmyObj(castToArmyObj()); - armyChanged(); -} - -void CCreatureSet::joinStack(const SlotID & slot, CStackInstance * stack) -{ - [[maybe_unused]] const CCreature *c = getCreature(slot); - assert(c == stack->type); - assert(c); - - //TODO move stuff - changeStackCount(slot, stack->count); - vstd::clear_pointer(stack); -} - -void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd) -{ - setStackCount(slot, getStackCount(slot) + toAdd); -} - -CCreatureSet::~CCreatureSet() -{ - clearSlots(); -} - -void CCreatureSet::setToArmy(CSimpleArmy &src) -{ - clearSlots(); - while(src) - { - auto i = src.army.begin(); - - putStack(i->first, new CStackInstance(i->second.first, i->second.second)); - src.army.erase(i); - } -} - -CStackInstance * CCreatureSet::detachStack(const SlotID & slot) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *ret = stacks[slot]; - - //if(CArmedInstance *armedObj = castToArmyObj()) - if(ret) - { - ret->setArmyObj(nullptr); //detaches from current armyobj - assert(!ret->armyObj); //we failed detaching? - } - - stacks.erase(slot); - armyChanged(); - return ret; -} - -void CCreatureSet::setStackType(const SlotID & slot, const CreatureID & type) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *s = stacks[slot]; - s->setType(type); - armyChanged(); -} - -bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks) const -{ - if(!allowMergingStacks) - { - int freeSlots = stacksCount() - GameConstants::ARMY_SIZE; - std::set cresToAdd; - for(const auto & elem : cs.stacks) - { - SlotID dest = getSlotFor(elem.second->type); - if(!dest.validSlot() || hasStackAtSlot(dest)) - cresToAdd.insert(elem.second->type); - } - return cresToAdd.size() <= freeSlots; - } - else - { - CCreatureSet cres; - SlotID j; - - //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 - //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 - else - return false; //no place found - } - return true; //all stacks found their slots - } -} - -bool CCreatureSet::hasStackAtSlot(const SlotID & slot) const -{ - return vstd::contains(stacks, slot); -} - -CCreatureSet & CCreatureSet::operator=(const CCreatureSet&cs) -{ - assert(0); - return *this; -} - -void CCreatureSet::armyChanged() -{ - -} - -void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName, const std::optional fixedSize) -{ - if(handler.saving && stacks.empty()) - return; - - auto a = handler.enterArray(fieldName); - - - if(handler.saving) - { - size_t sz = 0; - - for(const auto & p : stacks) - vstd::amax(sz, p.first.getNum()+1); - - if(fixedSize) - vstd::amax(sz, fixedSize.value()); - - a.resize(sz, JsonNode::JsonType::DATA_STRUCT); - - for(const auto & p : stacks) - { - auto s = a.enterStruct(p.first.getNum()); - p.second->serializeJson(handler); - } - } - else - { - for(size_t idx = 0; idx < a.size(); idx++) - { - auto s = a.enterStruct(idx); - - TQuantity amount = 0; - - handler.serializeInt("amount", amount); - - if(amount > 0) - { - auto * new_stack = new CStackInstance(); - new_stack->serializeJson(handler); - putStack(SlotID(static_cast(idx)), new_stack); - } - } - } -} - -CStackInstance::CStackInstance() - : armyObj(_armyObj) -{ - init(); -} - -CStackInstance::CStackInstance(const CreatureID & id, TQuantity Count, bool isHypothetic): - CBonusSystemNode(isHypothetic), armyObj(_armyObj) -{ - init(); - setType(id); - count = Count; -} - -CStackInstance::CStackInstance(const CCreature *cre, TQuantity Count, bool isHypothetic) - : CBonusSystemNode(isHypothetic), armyObj(_armyObj) -{ - init(); - setType(cre); - count = Count; -} - -void CStackInstance::init() -{ - experience = 0; - count = 0; - type = nullptr; - _armyObj = nullptr; - setNodeType(STACK_INSTANCE); -} - -CCreature::CreatureQuantityId CStackInstance::getQuantityID() const -{ - return CCreature::getQuantityID(count); -} - -int CStackInstance::getExpRank() const -{ - if (!VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - return 0; - int tier = type->getLevel(); - if (vstd::iswithin(tier, 1, 7)) - { - for(int i = static_cast(VLC->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic! - { //exp values vary from 1st level to max exp at 11th level - if (experience >= VLC->creh->expRanks[tier][i]) - return ++i; //faster, but confusing - 0 index mean 1st level of experience - } - return 0; - } - else //higher tier - { - for(int i = static_cast(VLC->creh->expRanks[0].size()) - 2; i > -1; --i) - { - if (experience >= VLC->creh->expRanks[0][i]) - return ++i; - } - return 0; - } -} - -int CStackInstance::getLevel() const -{ - return std::max(1, static_cast(type->getLevel())); -} - -void CStackInstance::giveStackExp(TExpType exp) -{ - int level = type->getLevel(); - if (!vstd::iswithin(level, 1, 7)) - level = 0; - - CCreatureHandler * creh = VLC->creh; - ui32 maxExp = creh->expRanks[level].back(); - - vstd::amin(exp, static_cast(maxExp)); //prevent exp overflow due to different types - vstd::amin(exp, (maxExp * creh->maxExpPerBattle[level])/100); - vstd::amin(experience += exp, maxExp); //can't get more exp than this limit -} - -void CStackInstance::setType(const CreatureID & creID) -{ - if(creID >= 0 && creID < VLC->creh->objects.size()) - setType(VLC->creh->objects[creID]); - else - setType((const CCreature*)nullptr); -} - -void CStackInstance::setType(const CCreature *c) -{ - if(type) - { - detachFrom(const_cast(*type)); - if (type->isMyUpgrade(c) && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); - } - - CStackBasicDescriptor::setType(c); - - if(type) - attachTo(const_cast(*type)); -} -std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const -{ - return VLC->getBth()->bonusToString(bonus, this, description); -} - -std::string CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const -{ - return VLC->getBth()->bonusToGraphics(bonus); -} - -void CStackInstance::setArmyObj(const CArmedInstance * ArmyObj) -{ - if(_armyObj) - detachFrom(const_cast(*_armyObj)); - - _armyObj = ArmyObj; - - if(ArmyObj) - attachTo(const_cast(*_armyObj)); -} - -std::string CStackInstance::getQuantityTXT(bool capitalized) const -{ - CCreature::CreatureQuantityId quantity = getQuantityID(); - - if ((int)quantity) - { - if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) - return CCreature::getQuantityRangeStringForId(quantity); - - return VLC->generaltexth->arraytxt[174 + (int)quantity*3 - 1 - capitalized]; - } - else - return ""; -} - -bool CStackInstance::valid(bool allowUnrandomized) const -{ - if(!randomStack) - { - return (type && type == VLC->creh->objects[type->getId()]); - } - else - return allowUnrandomized; -} - -std::string CStackInstance::nodeName() const -{ - std::ostringstream oss; - oss << "Stack of " << count << " of "; - if(type) - oss << type->getNamePluralTextID(); - else - oss << "[UNDEFINED TYPE]"; - - return oss.str(); -} - -PlayerColor CStackInstance::getOwner() const -{ - return _armyObj ? _armyObj->getOwner() : PlayerColor::NEUTRAL; -} - -void CStackInstance::deserializationFix() -{ - const CArmedInstance *armyBackup = _armyObj; - _armyObj = nullptr; - setArmyObj(armyBackup); - artDeserializationFix(this); -} - -CreatureID CStackInstance::getCreatureID() const -{ - if(type) - return type->getId(); - else - return CreatureID::NONE; -} - -std::string CStackInstance::getName() const -{ - return (count > 1) ? type->getNamePluralTranslated() : type->getNameSingularTranslated(); -} - -ui64 CStackInstance::getPower() const -{ - assert(type); - return type->getAIValue() * count; -} - -ArtBearer::ArtBearer CStackInstance::bearerType() const -{ - return ArtBearer::CREATURE; -} - -void CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) -{ - assert(!getArt(pos)); - assert(art->artType->canBePutAt(this, pos)); - - CArtifactSet::putArtifact(pos, art); - if(ArtifactUtils::isSlotEquipment(pos)) - attachTo(*art); -} - -void CStackInstance::removeArtifact(ArtifactPosition pos) -{ - assert(getArt(pos)); - - detachFrom(*getArt(pos)); - CArtifactSet::removeArtifact(pos); -} - -void CStackInstance::serializeJson(JsonSerializeFormat & handler) -{ - //todo: artifacts - CStackBasicDescriptor::serializeJson(handler);//must be first - - if(handler.saving) - { - if(randomStack) - { - int level = randomStack->level; - int upgrade = randomStack->upgrade; - - handler.serializeInt("level", level, 0); - handler.serializeInt("upgraded", upgrade, 0); - } - } - else - { - //type set by CStackBasicDescriptor::serializeJson - if(type == nullptr) - { - uint8_t level = 0; - uint8_t upgrade = 0; - - handler.serializeInt("level", level, 0); - handler.serializeInt("upgrade", upgrade, 0); - - randomStack = RandomStackInfo{ level, upgrade }; - } - } -} - -FactionID CStackInstance::getFaction() const -{ - if(type) - return type->getFaction(); - - return FactionID::NEUTRAL; -} - -const IBonusBearer* CStackInstance::getBonusBearer() const -{ - return this; -} - -CCommanderInstance::CCommanderInstance() -{ - init(); -} - -CCommanderInstance::CCommanderInstance(const CreatureID & id): name("Commando") -{ - init(); - setType(id); - //TODO - parse them -} - -void CCommanderInstance::init() -{ - alive = true; - experience = 0; - level = 1; - count = 1; - type = nullptr; - _armyObj = nullptr; - setNodeType (CBonusSystemNode::COMMANDER); - secondarySkills.resize (ECommander::SPELL_POWER + 1); -} - -void CCommanderInstance::setAlive (bool Alive) -{ - //TODO: helm of immortality - alive = Alive; - if (!alive) - { - removeBonusesRecursive(Bonus::UntilCommanderKilled); - } -} - -void CCommanderInstance::giveStackExp (TExpType exp) -{ - if (alive) - experience += exp; -} - -int CCommanderInstance::getExpRank() const -{ - return VLC->heroh->level (experience); -} - -int CCommanderInstance::getLevel() const -{ - return std::max (1, getExpRank()); -} - -void CCommanderInstance::levelUp () -{ - level++; - for(const auto & bonus : VLC->creh->commanderLevelPremy) - { //grant all regular level-up bonuses - accumulateBonus(bonus); - } -} - -ArtBearer::ArtBearer CCommanderInstance::bearerType() const -{ - return ArtBearer::COMMANDER; -} - -bool CCommanderInstance::gainsLevel() const -{ - return experience >= static_cast(VLC->heroh->reqExp(level + 1)); -} - -//This constructor should be placed here to avoid side effects -CStackBasicDescriptor::CStackBasicDescriptor() = default; - -CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): - type(VLC->creh->objects[id]), - count(Count) -{ -} - -CStackBasicDescriptor::CStackBasicDescriptor(const CCreature *c, TQuantity Count) - : type(c), count(Count) -{ -} - -const Creature * CStackBasicDescriptor::getType() const -{ - return type; -} - -TQuantity CStackBasicDescriptor::getCount() const -{ - return count; -} - - -void CStackBasicDescriptor::setType(const CCreature * c) -{ - type = c; -} - -void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("amount", count); - - if(handler.saving) - { - if(type) - { - std::string typeName = type->getJsonKey(); - handler.serializeString("type", typeName); - } - } - else - { - std::string typeName; - handler.serializeString("type", typeName); - if(!typeName.empty()) - setType(VLC->creh->getCreature(CModHandler::scopeMap(), typeName)); - } -} - -void CSimpleArmy::clearSlots() -{ - army.clear(); -} - -CSimpleArmy::operator bool() const -{ - return !army.empty(); -} - -bool CSimpleArmy::setCreature(SlotID slot, CreatureID cre, TQuantity count) -{ - assert(!vstd::contains(army, slot)); - army[slot] = std::make_pair(cre, count); - return true; -} - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureSet.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 "CCreatureSet.h" + +#include "ArtifactUtils.h" +#include "CConfigHandler.h" +#include "CCreatureHandler.h" +#include "VCMI_Lib.h" +#include "GameSettings.h" +#include "mapObjects/CGHeroInstance.h" +#include "modding/ModScope.h" +#include "IGameCallback.h" +#include "CGeneralTextHandler.h" +#include "spells/CSpellHandler.h" +#include "CHeroHandler.h" +#include "IBonusTypeHandler.h" +#include "serializer/JsonSerializeFormat.h" + +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + + +bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs) +{ + return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting +} + +const CStackInstance & CCreatureSet::operator[](const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return *i->second; + else + throw std::runtime_error("That slot is empty!"); +} + +const CCreature * CCreatureSet::getCreature(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->type; + else + return nullptr; +} + +bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) /*slots 0 to 6 */ +{ + if(!slot.validSlot()) + { + logGlobal->error("Cannot set slot %d", slot.getNum()); + return false; + } + if(!quantity) + { + logGlobal->warn("Using set creature to delete stack?"); + eraseStack(slot); + return true; + } + + if(hasStackAtSlot(slot)) //remove old creature + eraseStack(slot); + + auto * armyObj = castToArmyObj(); + bool isHypotheticArmy = armyObj ? armyObj->isHypothetic() : false; + + putStack(slot, new CStackInstance(type, quantity, isHypotheticArmy)); + return true; +} + +SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ +{ + return getSlotFor(creature.toCreature(), slotsAmount); +} + +SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const +{ + assert(c && c->valid()); + for(const auto & elem : stacks) + { + assert(elem.second->type->valid()); + if(elem.second->type == c) + { + return elem.first; //if there is already such creature we return its slot id + } + } + return getFreeSlot(slotsAmount); +} + +bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) const +{ + assert(c && c->valid()); + for(const auto & elem : stacks) // elem is const + { + if(elem.first == exclude) // Check slot + continue; + + if(!elem.second || !elem.second->type) // Check creature + continue; + + assert(elem.second->type->valid()); + + if(elem.second->type == c) + return true; + } + return false; +} + +std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + std::vector result; + + for(const auto & elem : stacks) + { + if(elem.first == exclude) + continue; + + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + if(elem.second->count == ignoreAmount || elem.second->count < 1) + continue; + + assert(elem.second->type->valid()); + result.push_back(elem.first); + } + return result; +} + +bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + TQuantity max = 0; + TQuantity min = std::numeric_limits::max(); + + for(const auto & elem : stacks) + { + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + const auto count = elem.second->count; + + if(count == ignoreAmount || count < 1) + continue; + + assert(elem.second->type->valid()); + + if(count > max) + max = count; + if(count < min) + min = count; + if(max - min > 1) + return false; + } + return true; +} + +SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const +{ + for(ui32 i=0; i CCreatureSet::getFreeSlots(ui32 slotsAmount) const +{ + std::vector freeSlots; + + for(ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push_back(slot); + } + return freeSlots; +} + +std::queue CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const +{ + std::queue freeSlots; + + for (ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push(slot); + } + return freeSlots; +} + +TMapCreatureSlot CCreatureSet::getCreatureMap() const +{ + TMapCreatureSlot creatureMap; + TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp(); + + // https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find + // https://www.cplusplus.com/reference/map/map/key_comp/ + for(const auto & pair : stacks) + { + const auto * creature = pair.second->type; + auto slot = pair.first; + auto lb = creatureMap.lower_bound(creature); + + if(lb != creatureMap.end() && !(keyComp(creature, lb->first))) + continue; + + creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot)); + } + return creatureMap; +} + +TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const +{ + TCreatureQueue creatureQueue; + + for(const auto & pair : stacks) + { + if(pair.first == exclude) + continue; + creatureQueue.push(std::make_pair(pair.second->type, pair.first)); + } + return creatureQueue; +} + +TQuantity CCreatureSet::getStackCount(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->count; + else + return 0; //TODO? consider issuing a warning +} + +TExpType CCreatureSet::getStackExperience(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->experience; + else + 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 */ +{ + //try to match creature to our preferred stack + if(preferable.validSlot() && vstd::contains(stacks, preferable)) + { + const CCreature *cr = stacks.find(preferable)->second->type; + for(const auto & elem : stacks) + { + if(cr == elem.second->type && elem.first != preferable) + { + out.first = preferable; + out.second = elem.first; + return true; + } + } + } + + for(const auto & stack : stacks) + { + for(const auto & elem : stacks) + { + if(stack.second->type == elem.second->type && stack.first != elem.first) + { + out.first = stack.first; + out.second = elem.first; + return true; + } + } + } + return false; +} + +void CCreatureSet::sweep() +{ + for(auto i=stacks.begin(); i!=stacks.end(); ++i) + { + if(!i->second->count) + { + stacks.erase(i); + sweep(); + break; + } + } +} + +void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) +{ + const CCreature *c = cre.toCreature(); + + if(!hasStackAtSlot(slot)) + { + setCreature(slot, cre, count); + } + else if(getCreature(slot) == c && allowMerging) //that slot was empty or contained same type creature + { + setStackCount(slot, getStackCount(slot) + count); + } + else + { + logGlobal->error("Failed adding to slot!"); + } +} + +void CCreatureSet::addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging) +{ + assert(stack->valid(true)); + + if(!hasStackAtSlot(slot)) + { + putStack(slot, stack); + } + else if(allowMerging && stack->type == getCreature(slot)) + { + joinStack(slot, stack); + } + else + { + logGlobal->error("Cannot add to slot %d stack %s", slot.getNum(), stack->nodeName()); + } +} + +bool CCreatureSet::validTypes(bool allowUnrandomized) const +{ + for(const auto & elem : stacks) + { + if(!elem.second->valid(allowUnrandomized)) + return false; + } + return true; +} + +bool CCreatureSet::slotEmpty(const SlotID & slot) const +{ + return !hasStackAtSlot(slot); +} + +bool CCreatureSet::needsLastStack() const +{ + return false; +} + +ui64 CCreatureSet::getArmyStrength() const +{ + ui64 ret = 0; + for(const auto & elem : stacks) + ret += elem.second->getPower(); + return ret; +} + +ui64 CCreatureSet::getPower(const SlotID & slot) const +{ + return getStack(slot).getPower(); +} + +std::string CCreatureSet::getRoughAmount(const SlotID & slot, int mode) const +{ + /// Mode represent return string format + /// "Pack" - 0, "A pack of" - 1, "a pack of" - 2 + CCreature::CreatureQuantityId quantity = CCreature::getQuantityID(getStackCount(slot)); + + if((int)quantity) + { + if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) + return CCreature::getQuantityRangeStringForId(quantity); + + return VLC->generaltexth->arraytxt[(174 + mode) + 3*(int)quantity]; + } + return ""; +} + +std::string CCreatureSet::getArmyDescription() const +{ + std::string text; + std::vector guards; + for(const auto & elem : stacks) + { + auto str = boost::str(boost::format("%s %s") % getRoughAmount(elem.first, 2) % getCreature(elem.first)->getNamePluralTranslated()); + guards.push_back(str); + } + if(!guards.empty()) + { + for(int i = 0; i < guards.size(); i++) + { + text += guards[i]; + if(i + 2 < guards.size()) + text += ", "; + else if(i + 2 == guards.size()) + text += VLC->generaltexth->allTexts[237]; + } + } + return text; +} + +int CCreatureSet::stacksCount() const +{ + return static_cast(stacks.size()); +} + +void CCreatureSet::setFormation(EArmyFormation mode) +{ + formation = mode; +} + +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) + stacks[slot]->experience = static_cast(stacks[slot]->experience * (count / static_cast(stacks[slot]->count))); + stacks[slot]->count = count; + armyChanged(); +} + +void CCreatureSet::giveStackExp(TExpType exp) +{ + for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++) + i->second->giveStackExp(exp); +} +void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp) +{ + assert(hasStackAtSlot(slot)); + stacks[slot]->experience = exp; +} + +void CCreatureSet::clearSlots() +{ + while(!stacks.empty()) + { + eraseStack(stacks.begin()->first); + } +} + +const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const +{ + assert(hasStackAtSlot(slot)); + return *getStackPtr(slot); +} + +CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const +{ + if(hasStackAtSlot(slot)) + return stacks.find(slot)->second; + else return nullptr; +} + +void CCreatureSet::eraseStack(const SlotID & slot) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *toErase = detachStack(slot); + vstd::clear_pointer(toErase); +} + +bool CCreatureSet::contains(const CStackInstance *stack) const +{ + if(!stack) + return false; + + for(const auto & elem : stacks) + if(elem.second == stack) + return true; + + return false; +} + +SlotID CCreatureSet::findStack(const CStackInstance *stack) const +{ + const auto * h = dynamic_cast(this); + if (h && h->commander == stack) + return SlotID::COMMANDER_SLOT_PLACEHOLDER; + + if(!stack) + return SlotID(); + + for(const auto & elem : stacks) + if(elem.second == stack) + return elem.first; + + return SlotID(); +} + +CArmedInstance * CCreatureSet::castToArmyObj() +{ + return dynamic_cast(this); +} + +void CCreatureSet::putStack(const SlotID & slot, CStackInstance * stack) +{ + assert(slot.getNum() < GameConstants::ARMY_SIZE); + assert(!hasStackAtSlot(slot)); + stacks[slot] = stack; + stack->setArmyObj(castToArmyObj()); + armyChanged(); +} + +void CCreatureSet::joinStack(const SlotID & slot, CStackInstance * stack) +{ + [[maybe_unused]] const CCreature *c = getCreature(slot); + assert(c == stack->type); + assert(c); + + //TODO move stuff + changeStackCount(slot, stack->count); + vstd::clear_pointer(stack); +} + +void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd) +{ + setStackCount(slot, getStackCount(slot) + toAdd); +} + +CCreatureSet::~CCreatureSet() +{ + clearSlots(); +} + +void CCreatureSet::setToArmy(CSimpleArmy &src) +{ + clearSlots(); + while(src) + { + auto i = src.army.begin(); + + putStack(i->first, new CStackInstance(i->second.first, i->second.second)); + src.army.erase(i); + } +} + +CStackInstance * CCreatureSet::detachStack(const SlotID & slot) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *ret = stacks[slot]; + + //if(CArmedInstance *armedObj = castToArmyObj()) + if(ret) + { + ret->setArmyObj(nullptr); //detaches from current armyobj + assert(!ret->armyObj); //we failed detaching? + } + + stacks.erase(slot); + armyChanged(); + return ret; +} + +void CCreatureSet::setStackType(const SlotID & slot, const CreatureID & type) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *s = stacks[slot]; + s->setType(type); + armyChanged(); +} + +bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks) const +{ + if(!allowMergingStacks) + { + int freeSlots = stacksCount() - GameConstants::ARMY_SIZE; + std::set cresToAdd; + for(const auto & elem : cs.stacks) + { + SlotID dest = getSlotFor(elem.second->type); + if(!dest.validSlot() || hasStackAtSlot(dest)) + cresToAdd.insert(elem.second->type); + } + return cresToAdd.size() <= freeSlots; + } + else + { + CCreatureSet cres; + SlotID j; + + //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 + //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 + else + return false; //no place found + } + return true; //all stacks found their slots + } +} + +bool CCreatureSet::hasStackAtSlot(const SlotID & slot) const +{ + return vstd::contains(stacks, slot); +} + +CCreatureSet & CCreatureSet::operator=(const CCreatureSet&cs) +{ + assert(0); + return *this; +} + +void CCreatureSet::armyChanged() +{ + +} + +void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize) +{ + if(handler.saving && stacks.empty()) + return; + + handler.serializeEnum("formation", formation, NArmyFormation::names); + auto a = handler.enterArray(armyFieldName); + + + if(handler.saving) + { + size_t sz = 0; + + for(const auto & p : stacks) + vstd::amax(sz, p.first.getNum()+1); + + if(fixedSize) + vstd::amax(sz, fixedSize.value()); + + a.resize(sz, JsonNode::JsonType::DATA_STRUCT); + + for(const auto & p : stacks) + { + auto s = a.enterStruct(p.first.getNum()); + p.second->serializeJson(handler); + } + } + else + { + for(size_t idx = 0; idx < a.size(); idx++) + { + auto s = a.enterStruct(idx); + + TQuantity amount = 0; + + handler.serializeInt("amount", amount); + + if(amount > 0) + { + auto * new_stack = new CStackInstance(); + new_stack->serializeJson(handler); + putStack(SlotID(static_cast(idx)), new_stack); + } + } + } +} + +CStackInstance::CStackInstance() + : armyObj(_armyObj) +{ + init(); +} + +CStackInstance::CStackInstance(const CreatureID & id, TQuantity Count, bool isHypothetic): + CBonusSystemNode(isHypothetic), armyObj(_armyObj) +{ + init(); + setType(id); + count = Count; +} + +CStackInstance::CStackInstance(const CCreature *cre, TQuantity Count, bool isHypothetic) + : CBonusSystemNode(isHypothetic), armyObj(_armyObj) +{ + init(); + setType(cre); + count = Count; +} + +void CStackInstance::init() +{ + experience = 0; + count = 0; + type = nullptr; + _armyObj = nullptr; + setNodeType(STACK_INSTANCE); +} + +CCreature::CreatureQuantityId CStackInstance::getQuantityID() const +{ + return CCreature::getQuantityID(count); +} + +int CStackInstance::getExpRank() const +{ + if (!VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + return 0; + int tier = type->getLevel(); + if (vstd::iswithin(tier, 1, 7)) + { + for(int i = static_cast(VLC->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic! + { //exp values vary from 1st level to max exp at 11th level + if (experience >= VLC->creh->expRanks[tier][i]) + return ++i; //faster, but confusing - 0 index mean 1st level of experience + } + return 0; + } + else //higher tier + { + for(int i = static_cast(VLC->creh->expRanks[0].size()) - 2; i > -1; --i) + { + if (experience >= VLC->creh->expRanks[0][i]) + return ++i; + } + return 0; + } +} + +int CStackInstance::getLevel() const +{ + return std::max(1, static_cast(type->getLevel())); +} + +void CStackInstance::giveStackExp(TExpType exp) +{ + int level = type->getLevel(); + if (!vstd::iswithin(level, 1, 7)) + level = 0; + + CCreatureHandler * creh = VLC->creh; + ui32 maxExp = creh->expRanks[level].back(); + + vstd::amin(exp, static_cast(maxExp)); //prevent exp overflow due to different types + vstd::amin(exp, (maxExp * creh->maxExpPerBattle[level])/100); + vstd::amin(experience += exp, maxExp); //can't get more exp than this limit +} + +void CStackInstance::setType(const CreatureID & creID) +{ + if (creID == CreatureID::NONE) + setType(nullptr);//FIXME: unused branch? + else + setType(creID.toCreature()); +} + +void CStackInstance::setType(const CCreature *c) +{ + if(type) + { + detachFrom(const_cast(*type)); + if (type->isMyUpgrade(c) && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); + } + + CStackBasicDescriptor::setType(c); + + if(type) + attachTo(const_cast(*type)); +} +std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const +{ + return VLC->getBth()->bonusToString(bonus, this, description); +} + +ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const +{ + return VLC->getBth()->bonusToGraphics(bonus); +} + +void CStackInstance::setArmyObj(const CArmedInstance * ArmyObj) +{ + if(_armyObj) + detachFrom(const_cast(*_armyObj)); + + _armyObj = ArmyObj; + + if(ArmyObj) + attachTo(const_cast(*_armyObj)); +} + +std::string CStackInstance::getQuantityTXT(bool capitalized) const +{ + CCreature::CreatureQuantityId quantity = getQuantityID(); + + if ((int)quantity) + { + if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) + return CCreature::getQuantityRangeStringForId(quantity); + + return VLC->generaltexth->arraytxt[174 + (int)quantity*3 - 1 - capitalized]; + } + else + return ""; +} + +bool CStackInstance::valid(bool allowUnrandomized) const +{ + if(!randomStack) + { + return (type && type == type->getId().toEntity(VLC)); + } + else + return allowUnrandomized; +} + +std::string CStackInstance::nodeName() const +{ + std::ostringstream oss; + oss << "Stack of " << count << " of "; + if(type) + oss << type->getNamePluralTextID(); + else + oss << "[UNDEFINED TYPE]"; + + return oss.str(); +} + +PlayerColor CStackInstance::getOwner() const +{ + return _armyObj ? _armyObj->getOwner() : PlayerColor::NEUTRAL; +} + +void CStackInstance::deserializationFix() +{ + const CArmedInstance *armyBackup = _armyObj; + _armyObj = nullptr; + setArmyObj(armyBackup); + artDeserializationFix(this); +} + +CreatureID CStackInstance::getCreatureID() const +{ + if(type) + return type->getId(); + else + return CreatureID::NONE; +} + +std::string CStackInstance::getName() const +{ + return (count > 1) ? type->getNamePluralTranslated() : type->getNameSingularTranslated(); +} + +ui64 CStackInstance::getPower() const +{ + assert(type); + return type->getAIValue() * count; +} + +ArtBearer::ArtBearer CStackInstance::bearerType() const +{ + return ArtBearer::CREATURE; +} + +CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) +{ + assert(!getArt(pos)); + assert(art->canBePutAt(this, pos)); + + attachTo(*art); + return CArtifactSet::putArtifact(pos, art); +} + +void CStackInstance::removeArtifact(ArtifactPosition pos) +{ + assert(getArt(pos)); + + detachFrom(*getArt(pos)); + CArtifactSet::removeArtifact(pos); +} + +void CStackInstance::serializeJson(JsonSerializeFormat & handler) +{ + //todo: artifacts + CStackBasicDescriptor::serializeJson(handler);//must be first + + if(handler.saving) + { + if(randomStack) + { + int level = randomStack->level; + int upgrade = randomStack->upgrade; + + handler.serializeInt("level", level, 0); + handler.serializeInt("upgraded", upgrade, 0); + } + } + else + { + //type set by CStackBasicDescriptor::serializeJson + if(type == nullptr) + { + uint8_t level = 0; + uint8_t upgrade = 0; + + handler.serializeInt("level", level, 0); + handler.serializeInt("upgrade", upgrade, 0); + + randomStack = RandomStackInfo{ level, upgrade }; + } + } +} + +FactionID CStackInstance::getFaction() const +{ + if(type) + return type->getFaction(); + + return FactionID::NEUTRAL; +} + +const IBonusBearer* CStackInstance::getBonusBearer() const +{ + return this; +} + +CCommanderInstance::CCommanderInstance() +{ + init(); +} + +CCommanderInstance::CCommanderInstance(const CreatureID & id): name("Commando") +{ + init(); + setType(id); + //TODO - parse them +} + +void CCommanderInstance::init() +{ + alive = true; + experience = 0; + level = 1; + count = 1; + type = nullptr; + _armyObj = nullptr; + setNodeType (CBonusSystemNode::COMMANDER); + secondarySkills.resize (ECommander::SPELL_POWER + 1); +} + +void CCommanderInstance::setAlive (bool Alive) +{ + //TODO: helm of immortality + alive = Alive; + if (!alive) + { + removeBonusesRecursive(Bonus::UntilCommanderKilled); + } +} + +void CCommanderInstance::giveStackExp (TExpType exp) +{ + if (alive) + experience += exp; +} + +int CCommanderInstance::getExpRank() const +{ + return VLC->heroh->level (experience); +} + +int CCommanderInstance::getLevel() const +{ + return std::max (1, getExpRank()); +} + +void CCommanderInstance::levelUp () +{ + level++; + for(const auto & bonus : VLC->creh->commanderLevelPremy) + { //grant all regular level-up bonuses + accumulateBonus(bonus); + } +} + +ArtBearer::ArtBearer CCommanderInstance::bearerType() const +{ + return ArtBearer::COMMANDER; +} + +bool CCommanderInstance::gainsLevel() const +{ + return experience >= static_cast(VLC->heroh->reqExp(level + 1)); +} + +//This constructor should be placed here to avoid side effects +CStackBasicDescriptor::CStackBasicDescriptor() = default; + +CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): + type(id.toCreature()), + count(Count) +{ +} + +CStackBasicDescriptor::CStackBasicDescriptor(const CCreature *c, TQuantity Count) + : type(c), count(Count) +{ +} + +const Creature * CStackBasicDescriptor::getType() const +{ + return type; +} + +CreatureID CStackBasicDescriptor::getId() const +{ + return type->getId(); +} + +TQuantity CStackBasicDescriptor::getCount() const +{ + return count; +} + + +void CStackBasicDescriptor::setType(const CCreature * c) +{ + type = c; +} + +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); +} + +void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("amount", count); + + if(handler.saving) + { + if(type) + { + std::string typeName = type->getJsonKey(); + handler.serializeString("type", typeName); + } + } + else + { + std::string typeName; + handler.serializeString("type", typeName); + if(!typeName.empty()) + setType(VLC->creh->getCreature(ModScope::scopeMap(), typeName)); + } +} + +void CSimpleArmy::clearSlots() +{ + army.clear(); +} + +CSimpleArmy::operator bool() const +{ + return !army.empty(); +} + +bool CSimpleArmy::setCreature(SlotID slot, CreatureID cre, TQuantity count) +{ + assert(!vstd::contains(army, slot)); + army[slot] = std::make_pair(cre, count); + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index fefd14869..305149b5b 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -1,296 +1,298 @@ -/* - * CCreatureSet.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 "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "GameConstants.h" -#include "CArtHandler.h" -#include "CArtifactInstance.h" -#include "CCreatureHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; -class CCreature; -class CGHeroInstance; -class CArmedInstance; -class CCreatureArtifactSet; -class JsonSerializeFormat; - -class DLL_LINKAGE CStackBasicDescriptor -{ -public: - const CCreature *type = nullptr; - TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army - - CStackBasicDescriptor(); - CStackBasicDescriptor(const CreatureID & id, TQuantity Count); - CStackBasicDescriptor(const CCreature *c, TQuantity Count); - virtual ~CStackBasicDescriptor() = default; - - const Creature * getType() const; - TQuantity getCount() const; - - virtual void setType(const CCreature * c); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - auto idNumber = type ? type->getId() : CreatureID(CreatureID::NONE); - h & idNumber; - } - else - { - CreatureID idNumber; - h & idNumber; - if(idNumber != CreatureID::NONE) - setType(dynamic_cast(VLC->creatures()->getByIndex(idNumber))); - else - type = nullptr; - } - h & count; - } - - void serializeJson(JsonSerializeFormat & handler); -}; - -class DLL_LINKAGE CStackInstance : public CBonusSystemNode, public CStackBasicDescriptor, public CArtifactSet, public ACreature -{ -protected: - const CArmedInstance *_armyObj; //stack must be part of some army, army must be part of some object - -public: - struct RandomStackInfo - { - uint8_t level; - uint8_t upgrade; - }; - // helper variable used during loading map, when object (hero or town) have creatures that must have same alignment. - std::optional randomStack; - - const CArmedInstance * const & armyObj; //stack must be part of some army, army must be part of some object - TExpType experience;//commander needs same amount of exp as hero - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & _armyObj; - h & experience; - BONUS_TREE_DESERIALIZATION_FIX - } - - void serializeJson(JsonSerializeFormat & handler); - - //overrides CBonusSystemNode - std::string bonusToString(const std::shared_ptr& bonus, bool description) const override; // how would bonus description look for this particular type of node - std::string bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others - - //IConstBonusProvider - const IBonusBearer* getBonusBearer() const override; - //INativeTerrainProvider - FactionID getFaction() const override; - - virtual ui64 getPower() const; - CCreature::CreatureQuantityId getQuantityID() const; - std::string getQuantityTXT(bool capitalized = true) const; - virtual int getExpRank() const; - virtual int getLevel() const; //different for regular stack and commander - CreatureID getCreatureID() const; //-1 if not available - std::string getName() const; //plural or singular - virtual void init(); - CStackInstance(); - CStackInstance(const CreatureID & id, TQuantity count, bool isHypothetic = false); - CStackInstance(const CCreature *cre, TQuantity count, bool isHypothetic = false); - virtual ~CStackInstance() = default; - - void setType(const CreatureID & creID); - void setType(const CCreature * c) override; - void setArmyObj(const CArmedInstance *ArmyObj); - virtual void giveStackExp(TExpType exp); - bool valid(bool allowUnrandomized) const; - void putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet - void removeArtifact(ArtifactPosition pos) override; - ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - virtual std::string nodeName() const override; //from CBonusSystemnode - void deserializationFix(); - PlayerColor getOwner() const override; -}; - -class DLL_LINKAGE CCommanderInstance : public CStackInstance -{ -public: - //TODO: what if Commander is not a part of creature set? - - //commander class is determined by its base creature - ui8 alive; //maybe change to bool when breaking save compatibility? - ui8 level; //required only to count callbacks - std::string name; // each Commander has different name - std::vector secondarySkills; //ID -> level - std::set specialSkills; - //std::vector arts; - void init() override; - CCommanderInstance(); - CCommanderInstance(const CreatureID & id); - void setAlive (bool alive); - void giveStackExp (TExpType exp) override; - void levelUp (); - - bool gainsLevel() const; //true if commander has lower level than should upon his experience - ui64 getPower() const override {return 0;}; - int getExpRank() const override; - int getLevel() const override; - ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & alive; - h & level; - h & name; - h & secondarySkills; - h & specialSkills; - } -}; - -using TSlots = std::map; -using TSimpleSlots = std::map>; - -using TPairCreatureSlot = std::pair; -using TMapCreatureSlot = std::map; - -struct DLL_LINKAGE CreatureSlotComparer -{ - bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs); -}; - -using TCreatureQueue = std::priority_queue, CreatureSlotComparer>; - -class IArmyDescriptor -{ -public: - virtual void clearSlots() = 0; - virtual bool setCreature(SlotID slot, CreatureID cre, TQuantity count) = 0; -}; - -//simplified version of CCreatureSet -class DLL_LINKAGE CSimpleArmy : public IArmyDescriptor -{ -public: - TSimpleSlots army; - void clearSlots() override; - bool setCreature(SlotID slot, CreatureID cre, TQuantity count) override; - operator bool() const; - - template void serialize(Handler &h, const int version) - { - h & army; - } -}; - -enum class EArmyFormation : uint8_t -{ - LOOSE, - TIGHT -}; - -class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures -{ - CCreatureSet(const CCreatureSet &) = delete; - CCreatureSet &operator=(const CCreatureSet&); -public: - - - TSlots stacks; //slots[slot_id]->> pair(creature_id,creature_quantity) - EArmyFormation formation = EArmyFormation::LOOSE; //0 - wide, 1 - tight - - CCreatureSet() = default; //Should be here to avoid compile errors - virtual ~CCreatureSet(); - virtual void armyChanged(); - - const CStackInstance & operator[](const SlotID & slot) const; - - const TSlots &Slots() const {return stacks;} - - void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature - void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature - void clearSlots() override; - void setFormation(bool tight); - CArmedInstance *castToArmyObj(); - - //basic operations - void putStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the army, slot must be empty - void setStackCount(const SlotID & slot, TQuantity count); //stack must exist! - CStackInstance * detachStack(const SlotID & slot); //removes stack from army but doesn't destroy it (so it can be moved somewhere else or safely deleted) - void setStackType(const SlotID & slot, const CreatureID & type); - void giveStackExp(TExpType exp); - void setStackExp(const SlotID & slot, TExpType exp); - - //derivative - void eraseStack(const SlotID & slot); //slot must be occupied - void joinStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the existing stack of the same type - void changeStackCount(const SlotID & slot, TQuantity toAdd); //stack must exist! - bool setCreature (SlotID slot, CreatureID type, TQuantity quantity) override; //replaces creature in stack; slots 0 to 6, if quantity=0 erases stack - void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. - - const CStackInstance & getStack(const SlotID & slot) const; //stack must exist - const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr - const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; - int getStackCount(const SlotID & slot) const; - TExpType getStackExperience(const SlotID & slot) const; - SlotID findStack(const CStackInstance *stack) const; //-1 if none - SlotID getSlotFor(const CreatureID & creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available - SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available - bool hasCreatureSlots(const CCreature * c, const SlotID & exclude) const; - std::vector getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount = -1) const; - bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots - - SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot - std::vector getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; - std::queue getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; - - 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 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 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; - bool hasStackAtSlot(const SlotID & slot) const; - - bool contains(const CStackInstance *stack) const; - bool canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks = true) const; - - template void serialize(Handler &h, const int version) - { - h & stacks; - h & formation; - } - - void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName, const std::optional fixedSize = std::nullopt); - - operator bool() const - { - return !stacks.empty(); - } - void sweep(); -}; - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureSet.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 "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "GameConstants.h" +#include "CArtHandler.h" +#include "CArtifactInstance.h" +#include "CCreatureHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class CCreature; +class CGHeroInstance; +class CArmedInstance; +class CCreatureArtifactSet; +class JsonSerializeFormat; + +class DLL_LINKAGE CStackBasicDescriptor +{ +public: + const CCreature *type = nullptr; + TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army + + CStackBasicDescriptor(); + CStackBasicDescriptor(const CreatureID & id, TQuantity Count); + CStackBasicDescriptor(const CCreature *c, TQuantity Count); + virtual ~CStackBasicDescriptor() = default; + + const Creature * getType() 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, const int version) + { + if(h.saving) + { + auto idNumber = type ? type->getId() : CreatureID(CreatureID::NONE); + h & idNumber; + } + else + { + CreatureID idNumber; + h & idNumber; + if(idNumber != CreatureID::NONE) + setType(dynamic_cast(VLC->creatures()->getById(idNumber))); + else + type = nullptr; + } + h & count; + } + + void serializeJson(JsonSerializeFormat & handler); +}; + +class DLL_LINKAGE CStackInstance : public CBonusSystemNode, public CStackBasicDescriptor, public CArtifactSet, public ACreature +{ +protected: + const CArmedInstance *_armyObj; //stack must be part of some army, army must be part of some object + +public: + struct RandomStackInfo + { + uint8_t level; + uint8_t upgrade; + }; + // helper variable used during loading map, when object (hero or town) have creatures that must have same alignment. + std::optional randomStack; + + const CArmedInstance * const & armyObj; //stack must be part of some army, army must be part of some object + TExpType experience;//commander needs same amount of exp as hero + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & static_cast(*this); + h & static_cast(*this); + h & _armyObj; + h & experience; + BONUS_TREE_DESERIALIZATION_FIX + } + + void serializeJson(JsonSerializeFormat & handler); + + //overrides CBonusSystemNode + std::string bonusToString(const std::shared_ptr& bonus, bool description) const override; // how would bonus description look for this particular type of node + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others + + //IConstBonusProvider + const IBonusBearer* getBonusBearer() const override; + //INativeTerrainProvider + FactionID getFaction() const override; + + virtual ui64 getPower() const; + CCreature::CreatureQuantityId getQuantityID() const; + std::string getQuantityTXT(bool capitalized = true) const; + virtual int getExpRank() const; + virtual int getLevel() const; //different for regular stack and commander + CreatureID getCreatureID() const; //-1 if not available + std::string getName() const; //plural or singular + virtual void init(); + CStackInstance(); + CStackInstance(const CreatureID & id, TQuantity count, bool isHypothetic = false); + CStackInstance(const CCreature *cre, TQuantity count, bool isHypothetic = false); + virtual ~CStackInstance() = default; + + void setType(const CreatureID & creID); + void setType(const CCreature * c) override; + 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; + ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet + virtual std::string nodeName() const override; //from CBonusSystemnode + void deserializationFix(); + PlayerColor getOwner() const override; +}; + +class DLL_LINKAGE CCommanderInstance : public CStackInstance +{ +public: + //TODO: what if Commander is not a part of creature set? + + //commander class is determined by its base creature + ui8 alive; //maybe change to bool when breaking save compatibility? + ui8 level; //required only to count callbacks + std::string name; // each Commander has different name + std::vector secondarySkills; //ID -> level + std::set specialSkills; + //std::vector arts; + void init() override; + CCommanderInstance(); + CCommanderInstance(const CreatureID & id); + void setAlive (bool alive); + void giveStackExp (TExpType exp) override; + void levelUp (); + + bool gainsLevel() const; //true if commander has lower level than should upon his experience + ui64 getPower() const override {return 0;}; + int getExpRank() const override; + int getLevel() const override; + ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & alive; + h & level; + h & name; + h & secondarySkills; + h & specialSkills; + } +}; + +using TSlots = std::map; +using TSimpleSlots = std::map>; + +using TPairCreatureSlot = std::pair; +using TMapCreatureSlot = std::map; + +struct DLL_LINKAGE CreatureSlotComparer +{ + bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs); +}; + +using TCreatureQueue = std::priority_queue, CreatureSlotComparer>; + +class IArmyDescriptor +{ +public: + virtual void clearSlots() = 0; + virtual bool setCreature(SlotID slot, CreatureID cre, TQuantity count) = 0; +}; + +//simplified version of CCreatureSet +class DLL_LINKAGE CSimpleArmy : public IArmyDescriptor +{ +public: + TSimpleSlots army; + void clearSlots() override; + bool setCreature(SlotID slot, CreatureID cre, TQuantity count) override; + operator bool() const; + + template void serialize(Handler &h, const int version) + { + h & army; + } +}; + +namespace NArmyFormation +{ + static const std::vector names{ "wide", "tight" }; +} + +class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures +{ + CCreatureSet(const CCreatureSet &) = delete; + CCreatureSet &operator=(const CCreatureSet&); +public: + + + TSlots stacks; //slots[slot_id]->> pair(creature_id,creature_quantity) + EArmyFormation formation = EArmyFormation::LOOSE; //0 - wide, 1 - tight + + CCreatureSet() = default; //Should be here to avoid compile errors + virtual ~CCreatureSet(); + virtual void armyChanged(); + + const CStackInstance & operator[](const SlotID & slot) const; + + const TSlots &Slots() const {return stacks;} + + void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature + void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature + void clearSlots() override; + void setFormation(EArmyFormation tight); + CArmedInstance *castToArmyObj(); + + //basic operations + void putStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the army, slot must be empty + void setStackCount(const SlotID & slot, TQuantity count); //stack must exist! + CStackInstance * detachStack(const SlotID & slot); //removes stack from army but doesn't destroy it (so it can be moved somewhere else or safely deleted) + void setStackType(const SlotID & slot, const CreatureID & type); + void giveStackExp(TExpType exp); + void setStackExp(const SlotID & slot, TExpType exp); + + //derivative + void eraseStack(const SlotID & slot); //slot must be occupied + void joinStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the existing stack of the same type + void changeStackCount(const SlotID & slot, TQuantity toAdd); //stack must exist! + bool setCreature (SlotID slot, CreatureID type, TQuantity quantity) override; //replaces creature in stack; slots 0 to 6, if quantity=0 erases stack + void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. + + const CStackInstance & getStack(const SlotID & slot) const; //stack must exist + CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr + const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; + int getStackCount(const SlotID & slot) const; + TExpType getStackExperience(const SlotID & slot) const; + SlotID findStack(const CStackInstance *stack) const; //-1 if none + SlotID getSlotFor(const CreatureID & creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available + SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available + bool hasCreatureSlots(const CCreature * c, const SlotID & exclude) const; + std::vector getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount = -1) const; + bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots + + SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot + std::vector getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + std::queue getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + + 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 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 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; + bool hasStackAtSlot(const SlotID & slot) const; + + bool contains(const CStackInstance *stack) const; + bool canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks = true) const; + + template void serialize(Handler &h, const int version) + { + h & stacks; + h & formation; + } + + void serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize = std::nullopt); + + operator bool() const + { + return !stacks.empty(); + } + void sweep(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 2eddb7372..8629df5cd 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -14,11 +14,12 @@ #include "gameState/InfoAboutArmy.h" #include "gameState/SThievesGuildInfo.h" #include "gameState/TavernHeroesPool.h" +#include "gameState/QuestInfo.h" +#include "mapObjects/CGHeroInstance.h" +#include "networkPacks/ArtifactLocation.h" #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo -#include "NetPacks.h" // for InfoWindow -#include "CModHandler.h" #include "GameSettings.h" #include "TerrainHandler.h" #include "spells/CSpellHandler.h" @@ -43,7 +44,7 @@ int CGameInfoCallback::getResource(PlayerColor Player, GameResID which) const { const PlayerState *p = getPlayerState(Player); ERROR_RET_VAL_IF(!p, "No player info!", -1); - ERROR_RET_VAL_IF(p->resources.size() <= which || which < 0, "No such resource!", -1); + ERROR_RET_VAL_IF(p->resources.size() <= which.getNum() || which.getNum() < 0, "No such resource!", -1); return p->resources[which]; } @@ -52,19 +53,24 @@ const PlayerSettings * CGameInfoCallback::getPlayerSettings(PlayerColor color) c return &gs->scenarioOps->getIthPlayersSettings(color); } -bool CGameInfoCallback::isAllowed(int32_t type, int32_t id) const +bool CGameInfoCallback::isAllowed(SpellID id) const { - switch(type) - { - case 0: - return gs->map->allowedSpells[id]; - case 1: - return gs->map->allowedArtifact[id]; - case 2: - return gs->map->allowedAbilities[id]; - default: - ERROR_RET_VAL_IF(1, "Wrong type!", false); - } + return gs->map->allowedSpells.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(ArtifactID id) const +{ + return gs->map->allowedArtifact.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(SecondarySkill id) const +{ + return gs->map->allowedAbilities.count(id) != 0; +} + +std::optional CGameInfoCallback::getPlayerID() const +{ + return std::nullopt; } const Player * CGameInfoCallback::getPlayer(PlayerColor color) const @@ -100,17 +106,33 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve } } -const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const +TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const +{ + if(!color.isValidPlayer()) + { + return TurnTimerInfo{}; + } + + auto player = gs->players.find(color); + if(player != gs->players.end()) + { + return player->second.turnTimer; + } + + return TurnTimerInfo{}; +} + +const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(ObjectInstanceID identifier) const { if(gs->map->questIdentifierToId.empty()) { //assume that it is VCMI map and quest identifier equals instance identifier - return getObj(ObjectInstanceID(identifier), true); + return getObj(identifier, true); } else { - ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier), "There is no object with such quest identifier!", nullptr); - return getObj(gs->map->questIdentifierToId[identifier]); + ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier.getNum()), "There is no object with such quest identifier!", nullptr); + return getObj(gs->map->questIdentifierToId[identifier.getNum()]); } } @@ -136,7 +158,7 @@ const CGObjectInstance* CGameInfoCallback::getObj(ObjectInstanceID objid, bool v return nullptr; } - if(!isVisible(ret, player) && ret->tempOwner != player) + if(!isVisible(ret, getPlayerID()) && ret->tempOwner != getPlayerID()) { if(verbose) logGlobal->error("Cannot get object with id %d. Object is not visible.", oid); @@ -186,8 +208,10 @@ int32_t CGameInfoCallback::getSpellCost(const spells::Spell * sp, const CGHeroIn //boost::shared_lock lock(*gs->mx); ERROR_RET_VAL_IF(!canGetFullInfo(caster), "Cannot get info about caster!", -1); //if there is a battle - if(gs->curB) - return gs->curB->battleGetSpellCost(sp, caster); + auto casterBattle = gs->getBattle(caster->getOwner()); + + if(casterBattle) + return casterBattle->battleGetSpellCost(sp, caster); //if there is no battle return caster->getSpellCost(sp); @@ -215,7 +239,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[*player].towns) + for(auto town : gs->players[*getPlayerID()].towns) { if(town->hasBuilt(BuildingID::TAVERN)) taverns++; @@ -237,7 +261,7 @@ int CGameInfoCallback::howManyTowns(PlayerColor Player) const bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject) const { - ERROR_RET_VAL_IF(!isVisible(town, player), "Town is not visible!", false); //it's not a town or it's not visible for layer + ERROR_RET_VAL_IF(!isVisible(town, getPlayerID()), "Town is not visible!", false); //it's not a town or it's not visible for layer bool detailed = hasAccess(town->tempOwner); if(town->ID == Obj::TOWN) @@ -246,7 +270,7 @@ bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - detailed = selectedHero->hasVisions(town, 1); + detailed = selectedHero->hasVisions(town, BonusCustomSubtype::visionsTowns); } dest.initFromTown(dynamic_cast(town), detailed); @@ -288,7 +312,9 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if (infoLevel == InfoAboutHero::EInfoLevel::BASIC) { - if(gs->curB && gs->curB->playerHasAccessToHeroInfo(*player, h)) //if it's battle we can get enemy hero full data + auto ourBattle = gs->getBattle(*getPlayerID()); + + if(ourBattle && ourBattle->playerHasAccessToHeroInfo(*getPlayerID(), h)) //if it's battle we can get enemy hero full data infoLevel = InfoAboutHero::EInfoLevel::INBATTLE; else ERROR_RET_VAL_IF(!isVisible(h->visitablePos()), "That hero is not visible!", false); @@ -298,18 +324,17 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - if(selectedHero->hasVisions(hero, 1)) + if(selectedHero->hasVisions(hero, BonusCustomSubtype::visionsHeroes)) infoLevel = InfoAboutHero::EInfoLevel::DETAILED; } dest.initFromHero(h, infoLevel); //DISGUISED bonus implementation - - if(getPlayerRelations(getLocalPlayer(), hero->tempOwner) == PlayerRelations::ENEMIES) + if(getPlayerRelations(*getPlayerID(), hero->tempOwner) == PlayerRelations::ENEMIES) { //todo: bonus cashing - int disguiseLevel = h->valOfBonuses(Selector::typeSubtype(BonusType::DISGUISED, 0)); + int disguiseLevel = h->valOfBonuses(BonusType::DISGUISED); auto doBasicDisguise = [](InfoAboutHero & info) { @@ -390,7 +415,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero return true; } -int CGameInfoCallback::getDate(Date::EDateType mode) const +int CGameInfoCallback::getDate(Date mode) const { //boost::shared_lock lock(*gs->mx); return gs->getDate(mode); @@ -404,7 +429,7 @@ bool CGameInfoCallback::isVisible(int3 pos, const std::optional & P bool CGameInfoCallback::isVisible(int3 pos) const { - return isVisible(pos, player); + return isVisible(pos, getPlayerID()); } bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optional & Player) const @@ -414,7 +439,7 @@ bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optio bool CGameInfoCallback::isVisible(const CGObjectInstance *obj) const { - return isVisible(obj, player); + return isVisible(obj, getPlayerID()); } // const CCreatureSet* CInfoCallback::getGarrison(const CGObjectInstance *obj) const // { @@ -446,7 +471,7 @@ std::vector CGameInfoCallback::getVisitableObjs(int3 for(const CGObjectInstance * obj : t->visitableObjects) { - if(player || obj->ID != Obj::EVENT) //hide events from players + if(getPlayerID() || obj->ID != Obj::EVENT) //hide events from players ret.push_back(obj); } @@ -482,7 +507,7 @@ std::vector CGameInfoCallback::getAvailableHeroes(const const CGTownInstance * town = getTown(townOrTavern->id); if(townOrTavern->ID == Obj::TAVERN || (town && town->hasBuilt(BuildingID::TAVERN))) - return gs->heroesPool->getHeroesFor(*player); + return gs->heroesPool->getHeroesFor(*getPlayerID()); return ret; } @@ -513,8 +538,8 @@ EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) cons //TODO: typedef? std::shared_ptr> CGameInfoCallback::getAllVisibleTiles() const { - assert(player.has_value()); - const auto * team = getPlayerTeam(player.value()); + assert(getPlayerID().has_value()); + const auto * team = getPlayerTeam(getPlayerID().value()); size_t width = gs->map->width; size_t height = gs->map->height; @@ -536,7 +561,7 @@ std::shared_ptr> CGameInfoCallback::ge return std::shared_ptr>(ptr); } -EBuildingState::EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID ) +EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID ) { ERROR_RET_VAL_IF(!canGetFullInfo(t), "Town is not owned!", EBuildingState::TOWN_NOT_OWNED); @@ -584,7 +609,7 @@ EBuildingState::EBuildingState CGameInfoCallback::canBuildStructure( const CGTow { const TerrainTile *tile = getTile(t->bestLocation(), false); - if(!tile || tile->terType->isLand()) + if(!tile || !tile->terType->isWater()) return EBuildingState::NO_WATER; //lack of water } @@ -613,10 +638,10 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const bool CGameInfoCallback::hasAccess(std::optional playerId) const { - return !player || player->isSpectator() || gs->getPlayerRelations(*playerId, *player) != PlayerRelations::ENEMIES; + return !getPlayerID() || getPlayerID()->isSpectator() || gs->getPlayerRelations(*playerId, *getPlayerID()) != PlayerRelations::ENEMIES; } -EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const +EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const { const PlayerState *ps = gs->getPlayerState(player, verbose); ERROR_VERBOSE_OR_NOT_RET_VAL_IF(!ps, verbose, "No such player!", EPlayerStatus::WRONG); @@ -626,36 +651,37 @@ EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bo std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTavern) const { - std::string text; + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 216); + std::string extraText; if(gs->rumor.type == RumorState::TYPE_NONE) - return text; + return text.toString(); auto rumor = gs->rumor.last[gs->rumor.type]; switch(gs->rumor.type) { case RumorState::TYPE_SPECIAL: + text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first); if(rumor.first == RumorState::RUMOR_GRAIL) - extraText = VLC->generaltexth->arraytxt[158 + rumor.second]; + text.replaceTextID(TextIdentifier("core", "arraytxt", 158 + rumor.second).get()); else - extraText = VLC->generaltexth->capColors[rumor.second]; - - text = boost::str(boost::format(VLC->generaltexth->allTexts[rumor.first]) % extraText); + text.replaceTextID(TextIdentifier("core", "plcolors", rumor.second).get()); break; case RumorState::TYPE_MAP: - text = gs->map->rumors[rumor.first].text; + text.replaceRawString(gs->map->rumors[rumor.first].text.toString()); break; case RumorState::TYPE_RAND: - text = VLC->generaltexth->tavernRumors[rumor.first]; + text.replaceTextID(TextIdentifier("core", "randtvrn", rumor.first).get()); break; } - return text; + return text.toString(); } -PlayerRelations::PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const +PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const { return gs->getPlayerRelations(color1, color2); } @@ -690,28 +716,21 @@ bool CGameInfoCallback::isOwnedOrVisited(const CGObjectInstance *obj) const return visitor->ID == Obj::HERO && canGetFullInfo(visitor); //owned or allied hero is a visitor } -PlayerColor CGameInfoCallback::getCurrentPlayer() const +bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const { - return gs->currentPlayer; + return gs->actingPlayers.count(player); } -CGameInfoCallback::CGameInfoCallback(CGameState * GS, std::optional Player): +CGameInfoCallback::CGameInfoCallback(CGameState * GS): gs(GS) { - player = std::move(Player); -} - -std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const -{ - //boost::shared_lock lock(*gs->mx); - return gs->getPlayerTeam(*player)->fogOfWarMap; } int CPlayerSpecificInfoCallback::howManyTowns() const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return CGameInfoCallback::howManyTowns(*player); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return CGameInfoCallback::howManyTowns(*getPlayerID()); } std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(bool onlyOur) const @@ -722,7 +741,7 @@ std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo( { for(const auto & town : i.second.towns) { - if(i.first == player || (!onlyOur && isVisible(town, player))) + if(i.first == getPlayerID() || (!onlyOur && isVisible(town, getPlayerID()))) { ret.push_back(town); } @@ -737,8 +756,8 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo for(auto hero : gs->map->heroesOnMap) { // !player || // - why would we even get access to hero not owned by any player? - if((hero->tempOwner == *player) || - (isVisible(hero->visitablePos(), player) && !onlyOur) ) + if((hero->tempOwner == *getPlayerID()) || + (isVisible(hero->visitablePos(), getPlayerID()) && !onlyOur) ) { ret.push_back(hero); } @@ -746,18 +765,13 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo return ret; } -std::optional CPlayerSpecificInfoCallback::getMyColor() const -{ - return player; -} - int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned) const { if (hero->inTownGarrison && !includeGarrisoned) return -1; size_t index = 0; - auto & heroes = gs->players[*player].heroes; + auto & heroes = gs->players[*getPlayerID()].heroes; for (auto & heroe : heroes) { @@ -772,13 +786,13 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio ) { - if (!player || CGObelisk::obeliskCount == 0) + if (!getPlayerID() || CGObelisk::obeliskCount == 0) { *outKnownRatio = 0.0; } else { - TeamID t = gs->getPlayerTeam(*player)->id; + TeamID t = gs->getPlayerTeam(*getPlayerID())->id; double visited = 0.0; if(CGObelisk::visited.count(t)) visited = static_cast(CGObelisk::visited[t]); @@ -793,7 +807,7 @@ std::vector < const CGObjectInstance * > CPlayerSpecificInfoCallback::getMyObjec std::vector < const CGObjectInstance * > ret; for(const CGObjectInstance * obj : gs->map->objects) { - if(obj && obj->tempOwner == player) + if(obj && obj->tempOwner == getPlayerID()) ret.push_back(obj); } return ret; @@ -803,7 +817,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() { ASSERT_IF_CALLED_WITH_PLAYER std::vector < const CGDwelling * > ret; - for(CGDwelling * dw : gs->getPlayerState(*player)->dwellings) + for(CGDwelling * dw : gs->getPlayerState(*getPlayerID())->dwellings) { ret.push_back(dw); } @@ -813,7 +827,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() std::vector CPlayerSpecificInfoCallback::getMyQuests() const { std::vector ret; - for(const auto & quest : gs->getPlayerState(*player)->quests) + for(const auto & quest : gs->getPlayerState(*getPlayerID())->quests) { ret.push_back (quest); } @@ -823,14 +837,14 @@ std::vector CPlayerSpecificInfoCallback::getMyQuests() const int CPlayerSpecificInfoCallback::howManyHeroes(bool includeGarrisoned) const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return getHeroCount(*player,includeGarrisoned); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return getHeroCount(*getPlayerID(), includeGarrisoned); } const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, bool includeGarrisoned) const { ASSERT_IF_CALLED_WITH_PLAYER - const PlayerState *p = getPlayerState(*player); + const PlayerState *p = getPlayerState(*getPlayerID()); ERROR_RET_VAL_IF(!p, "No player info", nullptr); if (!includeGarrisoned) @@ -846,7 +860,7 @@ const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) const { ASSERT_IF_CALLED_WITH_PLAYER - const PlayerState *p = getPlayerState(*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]; @@ -855,15 +869,15 @@ const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) int CPlayerSpecificInfoCallback::getResourceAmount(GameResID type) const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return getResource(*player, type); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return getResource(*getPlayerID(), type); } TResources CPlayerSpecificInfoCallback::getResourceAmount() const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", TResources()); - return gs->players[*player].resources; + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", TResources()); + return gs->players[*getPlayerID()].resources; } const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const @@ -874,11 +888,11 @@ const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const if (team != gs->teams.end()) { const TeamState *ret = &team->second; - if(!player.has_value()) //neutral (or invalid) player + if(!getPlayerID().has_value()) //neutral (or invalid) player return ret; else { - if (vstd::contains(ret->players, *player)) //specific player + if (vstd::contains(ret->players, *getPlayerID())) //specific player return ret; else { @@ -917,11 +931,6 @@ const CGHeroInstance * CGameInfoCallback::getHeroWithSubid( int subid ) const return gs->map->allHeroes.at(subid).get(); } -PlayerColor CGameInfoCallback::getLocalPlayer() const -{ - return getCurrentPlayer(); -} - bool CGameInfoCallback::isInTheMap(const int3 &pos) const { return gs->map->isInTheMap(pos); @@ -929,7 +938,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const { - gs->getTilesInRange(tiles, pos, radious, getLocalPlayer(), -1, distanceFormula); + gs->getTilesInRange(tiles, pos, radious, ETileVisibility::REVEALED, *getPlayerID(), distanceFormula); } void CGameInfoCallback::calculatePaths(const std::shared_ptr & config) @@ -942,7 +951,6 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo & gs->calculatePaths(hero, out); } - const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const { return gs->map->artInstances[aid.num]; @@ -953,6 +961,22 @@ const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid return gs->map->objects[oid.num]; } +CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const +{ + auto hero = const_cast(getHero(loc.artHolder)); + if(loc.creature.has_value()) + { + if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + return hero->commander; + else + return hero->getStackPtr(loc.creature.value()); + } + else + { + return hero; + } +} + std::vector CGameInfoCallback::getVisibleTeleportObjects(std::vector ids, PlayerColor player) const { vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 8bca8a99d..15d38cf44 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -11,7 +11,8 @@ #include "int3.h" #include "ResourceSet.h" // for Res -#include "battle/CCallbackBase.h" + +#define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} VCMI_LIB_NAMESPACE_BEGIN @@ -37,7 +38,10 @@ struct TeamState; struct QuestInfo; class CGameState; class PathfinderConfig; +struct TurnTimerInfo; +struct ArtifactLocation; +class CArtifactSet; class CArmedInstance; class CGObjectInstance; class CGHeroInstance; @@ -45,25 +49,26 @@ class CGDwelling; class CGTeleport; class CGTownInstance; -class DLL_LINKAGE IGameInfoCallback +class DLL_LINKAGE IGameInfoCallback : boost::noncopyable { public: //TODO: all other public methods of CGameInfoCallback // //various - virtual int getDate(Date::EDateType mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + 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 bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill + virtual bool isAllowed(SpellID id) const = 0; + virtual bool isAllowed(ArtifactID id) const = 0; + virtual bool isAllowed(SecondarySkill id) const = 0; //player + virtual std::optional getPlayerID() const = 0; virtual const Player * getPlayer(PlayerColor color) const = 0; // virtual int getResource(PlayerColor Player, EGameResID which) const = 0; // bool isVisible(int3 pos) const; -// PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; +// PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; // void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object -// EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player -// PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns - virtual PlayerColor getLocalPlayer() const = 0; //player that is currently owning given client (if not a client, then returns current player) +// EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player // const PlayerSettings * getPlayerSettings(PlayerColor color) const; @@ -88,7 +93,7 @@ public: // std::vector getFlaggableObjects(int3 pos) const; // const CGObjectInstance * getTopObj (int3 pos) const; // PlayerColor getOwner(ObjectInstanceID heroID) const; -// const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) +// const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed) //map // int3 guardingCreaturePosition (int3 pos) const; @@ -98,7 +103,6 @@ public: // const TerrainTile * getTile(int3 tile, bool verbose = true) const; // std::shared_ptr> getAllVisibleTiles() const; // bool isInTheMap(const int3 &pos) const; -// void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; //town // const CGTownInstance* getTown(ObjectInstanceID objid) const; @@ -106,13 +110,13 @@ public: // const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) // std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited // std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; -// EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements +// EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements // virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; //from gs // const TeamState *getTeam(TeamID teamID) const; // const TeamState *getPlayerTeam(PlayerColor color) const; -// EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements +// EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements //teleport // std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; @@ -125,13 +129,13 @@ public: // bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const; }; -class DLL_LINKAGE CGameInfoCallback : public virtual CCallbackBase, public IGameInfoCallback +class DLL_LINKAGE CGameInfoCallback : public IGameInfoCallback { protected: CGameState * gs;//todo: replace with protected const getter, only actual Server and Client objects should hold game state CGameInfoCallback() = default; - CGameInfoCallback(CGameState * GS, std::optional Player); + CGameInfoCallback(CGameState * GS); bool hasAccess(std::optional playerId) const; bool canGetFullInfo(const CGObjectInstance *obj) const; //true we player owns obj or ally owns obj or privileged mode @@ -139,20 +143,23 @@ protected: public: //various - int getDate(Date::EDateType mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + 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; - bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill + bool isAllowed(SpellID id) const override; + bool isAllowed(ArtifactID id) const override; + bool isAllowed(SecondarySkill id) const override; //player + std::optional getPlayerID() const override; const Player * getPlayer(PlayerColor color) const override; virtual const PlayerState * getPlayerState(PlayerColor color, bool verbose = true) const; virtual int getResource(PlayerColor Player, GameResID which) const; - virtual PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; + virtual PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; virtual void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object - virtual EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player - virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns - PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player) + virtual EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player + virtual bool isPlayerMakingTurn(PlayerColor player) const; //player that currently makes move // TODO synchronous turns virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; + virtual TurnTimerInfo getPlayerTurnTime(PlayerColor color) const; //map virtual bool isVisible(int3 pos, const std::optional & Player) const; @@ -173,6 +180,7 @@ public: virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const; virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const; + virtual CArtifactSet * getArtSet(const ArtifactLocation & loc) const; //virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; //objects @@ -182,7 +190,7 @@ public: virtual std::vector getFlaggableObjects(int3 pos) const; virtual const CGObjectInstance * getTopObj (int3 pos) const; virtual PlayerColor getOwner(ObjectInstanceID heroID) const; - virtual const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) + virtual const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed) //map virtual int3 guardingCreaturePosition (int3 pos) const; @@ -203,13 +211,13 @@ public: //virtual const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) virtual std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited virtual std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; - virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + virtual EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; //from gs virtual const TeamState *getTeam(TeamID teamID) const; virtual const TeamState *getPlayerTeam(PlayerColor color) const; - //virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + //virtual EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements //teleport virtual std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; @@ -231,7 +239,6 @@ public: virtual int howManyTowns() const; virtual int howManyHeroes(bool includeGarrisoned = true) const; virtual int3 getGrailPos(double *outKnownRatio); - virtual std::optional getMyColor() const; virtual std::vector getTownsInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible virtual int getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned=true) const; @@ -244,8 +251,6 @@ public: virtual int getResourceAmount(GameResID type) const; virtual TResources getResourceAmount() const; - virtual std::shared_ptr> getVisibilityMap() const; //returns visibility map - //virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 5b052d3c8..619acd62b 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -1,272 +1,272 @@ -/* - * CGameInterface.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 "CGameInterface.h" - -#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" -# include "AI/BattleAI/BattleAI.h" -# include "AI/StupidAI/StupidAI.h" -# include "AI/EmptyAI/CEmptyAI.h" -#else -# ifdef VCMI_WINDOWS -# include //for .dll libs -# else -# include -# endif // VCMI_WINDOWS -#endif // STATIC_AI - -VCMI_LIB_NAMESPACE_BEGIN - -template -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ -#ifdef STATIC_AI - // android currently doesn't support loading libs dynamically, so the access to the known libraries - // is possible only via specializations of this template - throw std::runtime_error("Could not resolve ai library " + libpath.generic_string()); -#else - using TGetAIFun = void (*)(std::shared_ptr &); - using TGetNameFun = void (*)(char *); - - char temp[150]; - - TGetAIFun getAI = nullptr; - TGetNameFun getName = nullptr; - -#ifdef VCMI_WINDOWS -#ifdef __MINGW32__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-function-type" -#endif - HMODULE dll = LoadLibraryW(libpath.c_str()); - if (dll) - { - getName = reinterpret_cast(GetProcAddress(dll, "GetAiName")); - getAI = reinterpret_cast(GetProcAddress(dll, methodName.c_str())); - } -#ifdef __MINGW32__ -#pragma GCC diagnostic pop -#endif -#else // !VCMI_WINDOWS - void *dll = dlopen(libpath.string().c_str(), RTLD_LOCAL | RTLD_LAZY); - if (dll) - { - getName = reinterpret_cast(dlsym(dll, "GetAiName")); - getAI = reinterpret_cast(dlsym(dll, methodName.c_str())); - } -#endif // VCMI_WINDOWS - - if (!dll) - { - logGlobal->error("Cannot open dynamic library (%s). Throwing...", libpath.string()); - throw std::runtime_error("Cannot open dynamic library"); - } - else if(!getName || !getAI) - { - logGlobal->error("%s does not export method %s", libpath.string(), methodName); -#ifdef VCMI_WINDOWS - FreeLibrary(dll); -#else - dlclose(dll); -#endif - throw std::runtime_error("Cannot find method " + methodName); - } - - getName(temp); - logGlobal->info("Loaded %s", temp); - - std::shared_ptr ret; - getAI(ret); - if(!ret) - logGlobal->error("Cannot get AI!"); - - return ret; -#endif // STATIC_AI -} - -#ifdef STATIC_AI - -template<> -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ - if(libpath.stem() == "libNullkiller") { - return std::make_shared(); - } - else{ - return std::make_shared(); - } -} - -template<> -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ - if(libpath.stem() == "libBattleAI") - return std::make_shared(); - else if(libpath.stem() == "libStupidAI") - return std::make_shared(); - return std::make_shared(); -} - -#endif // STATIC_AI - -template -std::shared_ptr createAnyAI(const std::string & dllname, const std::string & methodName) -{ - logGlobal->info("Opening %s", dllname); - - const boost::filesystem::path filePath = VCMIDirs::get().fullLibraryPath("AI", dllname); - auto ret = createAny(filePath, methodName); - ret->dllName = dllname; - return ret; -} - -std::shared_ptr CDynLibHandler::getNewAI(const std::string & dllname) -{ - return createAnyAI(dllname, "GetNewAI"); -} - -std::shared_ptr CDynLibHandler::getNewBattleAI(const std::string & dllname) -{ - return createAnyAI(dllname, "GetNewBattleAI"); -} - -#if SCRIPTING_ENABLED -std::shared_ptr CDynLibHandler::getNewScriptingModule(const boost::filesystem::path & dllname) -{ - return createAny(dllname, "GetNewModule"); -} -#endif - -CGlobalAI::CGlobalAI() -{ - human = false; -} - -void CAdventureAI::battleNewRound(int round) -{ - battleAI->battleNewRound(round); -} - -void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca) -{ - battleAI->battleCatapultAttacked(ca); -} - -void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, - const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) -{ - assert(!battleAI); - assert(cbc); - battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); - battleAI->initBattleInterface(env, cbc); - battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); -} - -void CAdventureAI::battleStacksAttacked(const std::vector & bsa, bool ranged) -{ - battleAI->battleStacksAttacked(bsa, ranged); -} - -void CAdventureAI::actionStarted(const BattleAction & action) -{ - battleAI->actionStarted(action); -} - -void CAdventureAI::battleNewRoundFirst(int round) -{ - battleAI->battleNewRoundFirst(round); -} - -void CAdventureAI::actionFinished(const BattleAction & action) -{ - battleAI->actionFinished(action); -} - -void CAdventureAI::battleStacksEffectsSet(const SetStackEffect & sse) -{ - battleAI->battleStacksEffectsSet(sse); -} - -void CAdventureAI::battleObstaclesChanged(const std::vector & obstacles) -{ - battleAI->battleObstaclesChanged(obstacles); -} - -void CAdventureAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) -{ - battleAI->battleStackMoved(stack, dest, distance, teleport); -} - -void CAdventureAI::battleAttack(const BattleAttack * ba) -{ - battleAI->battleAttack(ba); -} - -void CAdventureAI::battleSpellCast(const BattleSpellCast * sc) -{ - battleAI->battleSpellCast(sc); -} - -void CAdventureAI::battleEnd(const BattleResult * br, QueryID queryID) -{ - battleAI->battleEnd(br, queryID); - battleAI.reset(); -} - -void CAdventureAI::battleUnitsChanged(const std::vector & units) -{ - battleAI->battleUnitsChanged(units); -} - -void CAdventureAI::activeStack(const CStack * stack) -{ - battleAI->activeStack(stack); -} - -void CAdventureAI::yourTacticPhase(int distance) -{ - battleAI->yourTacticPhase(distance); -} - -void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - bool hasBattleAI = static_cast(battleAI); - h & hasBattleAI; - if(hasBattleAI) - { - h & battleAI->dllName; - } -} - -void CAdventureAI::loadGame(BinaryDeserializer & h, const int version) /*loading */ -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - 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 +/* + * CGameInterface.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 "CGameInterface.h" + +#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" +# include "AI/BattleAI/BattleAI.h" +# include "AI/StupidAI/StupidAI.h" +# include "AI/EmptyAI/CEmptyAI.h" +#else +# ifdef VCMI_WINDOWS +# include //for .dll libs +# else +# include +# endif // VCMI_WINDOWS +#endif // STATIC_AI + +VCMI_LIB_NAMESPACE_BEGIN + +template +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ +#ifdef STATIC_AI + // android currently doesn't support loading libs dynamically, so the access to the known libraries + // is possible only via specializations of this template + throw std::runtime_error("Could not resolve ai library " + libpath.generic_string()); +#else + using TGetAIFun = void (*)(std::shared_ptr &); + using TGetNameFun = void (*)(char *); + + char temp[150]; + + TGetAIFun getAI = nullptr; + TGetNameFun getName = nullptr; + +#ifdef VCMI_WINDOWS +#ifdef __MINGW32__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + HMODULE dll = LoadLibraryW(libpath.c_str()); + if (dll) + { + getName = reinterpret_cast(GetProcAddress(dll, "GetAiName")); + getAI = reinterpret_cast(GetProcAddress(dll, methodName.c_str())); + } +#ifdef __MINGW32__ +#pragma GCC diagnostic pop +#endif +#else // !VCMI_WINDOWS + void *dll = dlopen(libpath.string().c_str(), RTLD_LOCAL | RTLD_LAZY); + if (dll) + { + getName = reinterpret_cast(dlsym(dll, "GetAiName")); + getAI = reinterpret_cast(dlsym(dll, methodName.c_str())); + } +#endif // VCMI_WINDOWS + + if (!dll) + { + logGlobal->error("Cannot open dynamic library (%s). Throwing...", libpath.string()); + throw std::runtime_error("Cannot open dynamic library"); + } + else if(!getName || !getAI) + { + logGlobal->error("%s does not export method %s", libpath.string(), methodName); +#ifdef VCMI_WINDOWS + FreeLibrary(dll); +#else + dlclose(dll); +#endif + throw std::runtime_error("Cannot find method " + methodName); + } + + getName(temp); + logGlobal->info("Loaded %s", temp); + + std::shared_ptr ret; + getAI(ret); + if(!ret) + logGlobal->error("Cannot get AI!"); + + return ret; +#endif // STATIC_AI +} + +#ifdef STATIC_AI + +template<> +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ + if(libpath.stem() == "libNullkiller") { + return std::make_shared(); + } + else{ + return std::make_shared(); + } +} + +template<> +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ + if(libpath.stem() == "libBattleAI") + return std::make_shared(); + else if(libpath.stem() == "libStupidAI") + return std::make_shared(); + return std::make_shared(); +} + +#endif // STATIC_AI + +template +std::shared_ptr createAnyAI(const std::string & dllname, const std::string & methodName) +{ + logGlobal->info("Opening %s", dllname); + + const boost::filesystem::path filePath = VCMIDirs::get().fullLibraryPath("AI", dllname); + auto ret = createAny(filePath, methodName); + ret->dllName = dllname; + return ret; +} + +std::shared_ptr CDynLibHandler::getNewAI(const std::string & dllname) +{ + return createAnyAI(dllname, "GetNewAI"); +} + +std::shared_ptr CDynLibHandler::getNewBattleAI(const std::string & dllname) +{ + return createAnyAI(dllname, "GetNewBattleAI"); +} + +#if SCRIPTING_ENABLED +std::shared_ptr CDynLibHandler::getNewScriptingModule(const boost::filesystem::path & dllname) +{ + return createAny(dllname, "GetNewModule"); +} +#endif + +CGlobalAI::CGlobalAI() +{ + human = false; +} + +void CAdventureAI::battleNewRound(const BattleID & battleID) +{ + battleAI->battleNewRound(battleID); +} + +void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + battleAI->battleCatapultAttacked(battleID, ca); +} + +void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, + const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +{ + assert(!battleAI); + assert(cbc); + battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); + battleAI->initBattleInterface(env, cbc); + battleAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); +} + +void CAdventureAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + battleAI->battleStacksAttacked(battleID, bsa, ranged); +} + +void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & action) +{ + battleAI->actionStarted(battleID, action); +} + +void CAdventureAI::battleNewRoundFirst(const BattleID & battleID) +{ + battleAI->battleNewRoundFirst(battleID); +} + +void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action) +{ + battleAI->actionFinished(battleID, action); +} + +void CAdventureAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + battleAI->battleStacksEffectsSet(battleID, sse); +} + +void CAdventureAI::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) +{ + battleAI->battleObstaclesChanged(battleID, obstacles); +} + +void CAdventureAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + battleAI->battleStackMoved(battleID, stack, dest, distance, teleport); +} + +void CAdventureAI::battleAttack(const BattleID & battleID, const BattleAttack * ba) +{ + battleAI->battleAttack(battleID, ba); +} + +void CAdventureAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) +{ + battleAI->battleSpellCast(battleID, sc); +} + +void CAdventureAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) +{ + battleAI->battleEnd(battleID, br, queryID); + battleAI.reset(); +} + +void CAdventureAI::battleUnitsChanged(const BattleID & battleID, const std::vector & units) +{ + battleAI->battleUnitsChanged(battleID, units); +} + +void CAdventureAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + battleAI->activeStack(battleID, stack); +} + +void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + battleAI->yourTacticPhase(battleID, distance); +} + +void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + bool hasBattleAI = static_cast(battleAI); + h & hasBattleAI; + if(hasBattleAI) + { + h & battleAI->dllName; + } +} + +void CAdventureAI::loadGame(BinaryDeserializer & h, const int version) /*loading */ +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + 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 9ffcc80ad..6aabc1dcc 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -1,171 +1,169 @@ -/* - * CGameInterface.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 "battle/AutocombatPreferences.h" -#include "battle/BattleAction.h" -#include "IGameEventsReceiver.h" - -#include "spells/ViewSpellInt.h" - -class CBattleCallback; -class CCallback; - -VCMI_LIB_NAMESPACE_BEGIN - -using boost::logic::tribool; - -class Environment; - -class ICallback; -class CGlobalAI; -struct Component; -struct TryMoveHero; -class CGHeroInstance; -class CGTownInstance; -class CGObjectInstance; -class CGBlackMarket; -class CGDwelling; -class CCreatureSet; -class CArmedInstance; -class IShipyard; -class IMarket; -struct BattleResult; -struct BattleAttack; -struct BattleStackAttacked; -struct BattleSpellCast; -struct SetStackEffect; -struct Bonus; -struct PackageApplied; -struct SetObjectProperty; -struct CatapultAttack; -struct StackLocation; -class CStackInstance; -class CCommanderInstance; -class CStack; -class CCreature; -class CLoadFile; -class CSaveFile; -class BinaryDeserializer; -class BinarySerializer; -class BattleStateInfo; -struct ArtifactLocation; -class BattleStateInfoForRetreat; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class Module; -} -#endif - -using TTeleportExitsList = std::vector>; - -class DLL_LINKAGE CBattleGameInterface : public IBattleEventsReceiver -{ -public: - bool human; - PlayerColor playerID; - std::string dllName; - - virtual ~CBattleGameInterface() {}; - virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB){}; - virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences){}; - - //battle call-ins - virtual void activeStack(const CStack * stack)=0; //called when it's turn of that stack - virtual void yourTacticPhase(int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function -}; - -/// Central class for managing human player / AI interface logic -class DLL_LINKAGE CGameInterface : public CBattleGameInterface, public IGameEventsReceiver -{ -public: - virtual ~CGameInterface() = default; - virtual void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB){}; - virtual void yourTurn(){}; //called AFTER playerStartsTurn(player) - - //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; - virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID)=0; - - // Show a dialog, player must take decision. If selection then he has to choose between one of given components, - // if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called - // with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - virtual void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) = 0; - - // all stacks operations between these objects become allowed, interface has to call onEnd when done - virtual void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) = 0; - virtual void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; - virtual void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) = 0; - virtual void finish(){}; //if for some reason we want to end - - virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; - - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) - { - return std::nullopt; - } - - virtual void saveGame(BinarySerializer & h, const int version) = 0; - virtual void loadGame(BinaryDeserializer & h, const int version) = 0; -}; - -class DLL_LINKAGE CDynLibHandler -{ -public: - static std::shared_ptr getNewAI(const std::string & dllname); - static std::shared_ptr getNewBattleAI(const std::string & dllname); -#if SCRIPTING_ENABLED - static std::shared_ptr getNewScriptingModule(const boost::filesystem::path & dllname); -#endif -}; - -class DLL_LINKAGE CGlobalAI : public CGameInterface // AI class (to derivate) -{ -public: - std::shared_ptr env; - CGlobalAI(); -}; - -//class to be inherited by adventure-only AIs, it cedes battle actions to given battle-AI -class DLL_LINKAGE CAdventureAI : public CGlobalAI -{ -public: - CAdventureAI() = default; - - std::shared_ptr battleAI; - std::shared_ptr cbc; - - virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used - - //battle interface - virtual void activeStack(const CStack * stack) override; - virtual void yourTacticPhase(int distance) override; - virtual void battleNewRound(int round) override; - virtual void battleCatapultAttacked(const CatapultAttack & ca) override; - virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; - virtual void battleStacksAttacked(const std::vector & bsa, bool ranged) override; - virtual void actionStarted(const BattleAction &action) override; - virtual void battleNewRoundFirst(int round) override; - virtual void actionFinished(const BattleAction &action) override; - virtual void battleStacksEffectsSet(const SetStackEffect & sse) override; - virtual void battleObstaclesChanged(const std::vector & obstacles) override; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - virtual void battleAttack(const BattleAttack *ba) override; - virtual void battleSpellCast(const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleResult *br, QueryID queryID) override; - virtual void battleUnitsChanged(const std::vector & units) override; - - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CGameInterface.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 "battle/AutocombatPreferences.h" +#include "IGameEventsReceiver.h" + +#include "spells/ViewSpellInt.h" + +class CBattleCallback; +class CCallback; + +VCMI_LIB_NAMESPACE_BEGIN + +using boost::logic::tribool; + +class Environment; + +class ICallback; +class CGlobalAI; +struct Component; +struct TryMoveHero; +class CGHeroInstance; +class CGTownInstance; +class CGObjectInstance; +class CGBlackMarket; +class CGDwelling; +class CCreatureSet; +class CArmedInstance; +class IShipyard; +class IMarket; +class BattleAction; +struct BattleResult; +struct BattleAttack; +struct BattleStackAttacked; +struct BattleSpellCast; +struct SetStackEffect; +struct Bonus; +struct PackageApplied; +struct SetObjectProperty; +struct CatapultAttack; +struct StackLocation; +class CStackInstance; +class CCommanderInstance; +class CStack; +class CCreature; +class CLoadFile; +class CSaveFile; +class BinaryDeserializer; +class BinarySerializer; +class BattleStateInfo; +struct ArtifactLocation; +class BattleStateInfoForRetreat; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class Module; +} +#endif + +using TTeleportExitsList = std::vector>; + +class DLL_LINKAGE CBattleGameInterface : public IBattleEventsReceiver +{ +public: + bool human; + PlayerColor playerID; + std::string dllName; + + virtual ~CBattleGameInterface() {}; + virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB){}; + virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences){}; + + //battle call-ins + virtual void activeStack(const BattleID & battleID, const CStack * stack)=0; //called when it's turn of that stack + virtual void yourTacticPhase(const BattleID & battleID, int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function +}; + +/// Central class for managing human player / AI interface logic +class DLL_LINKAGE CGameInterface : public CBattleGameInterface, public IGameEventsReceiver +{ +public: + virtual ~CGameInterface() = default; + virtual void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB){}; + virtual void yourTurn(QueryID askID){}; //called AFTER playerStartsTurn(player) + + //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; + virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID)=0; + + // Show a dialog, player must take decision. If selection then he has to choose between one of given components, + // if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called + // with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + virtual void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) = 0; + + // all stacks operations between these objects become allowed, interface has to call onEnd when done + virtual void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) = 0; + virtual void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; + virtual void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) = 0; + virtual void finish(){}; //if for some reason we want to end + + 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, const int version) = 0; + virtual void loadGame(BinaryDeserializer & h, const int version) = 0; +}; + +class DLL_LINKAGE CDynLibHandler +{ +public: + static std::shared_ptr getNewAI(const std::string & dllname); + static std::shared_ptr getNewBattleAI(const std::string & dllname); +#if SCRIPTING_ENABLED + static std::shared_ptr getNewScriptingModule(const boost::filesystem::path & dllname); +#endif +}; + +class DLL_LINKAGE CGlobalAI : public CGameInterface // AI class (to derivate) +{ +public: + std::shared_ptr env; + CGlobalAI(); +}; + +//class to be inherited by adventure-only AIs, it cedes battle actions to given battle-AI +class DLL_LINKAGE CAdventureAI : public CGlobalAI +{ +public: + CAdventureAI() = default; + + std::shared_ptr battleAI; + std::shared_ptr cbc; + + virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used + + //battle interface + virtual void activeStack(const BattleID & battleID, const CStack * stack) override; + virtual void yourTacticPhase(const BattleID & battleID, int distance) override; + + virtual void battleNewRound(const BattleID & battleID) override; + virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; + virtual void battleNewRoundFirst(const BattleID & battleID) override; + virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; + virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; + virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; + + virtual void saveGame(BinarySerializer & h, const int version) override; + virtual void loadGame(BinaryDeserializer & h, const int version) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 5739db44f..4c6ba5c49 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -1,679 +1,706 @@ -/* - * CGeneralTextHandler.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 "CGeneralTextHandler.h" - -#include "filesystem/Filesystem.h" -#include "CConfigHandler.h" -#include "CModHandler.h" -#include "GameSettings.h" -#include "mapObjects/CQuest.h" -#include "VCMI_Lib.h" -#include "Languages.h" -#include "TextOperations.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Detects language and encoding of H3 text files based on matching against pregenerated footprints of H3 file -void CGeneralTextHandler::detectInstallParameters() -{ - using LanguageFootprint = std::array; - - static const std::array knownFootprints = - { { - { { 0.1602, 0.0000, 0.0357, 0.0054, 0.0038, 0.0017, 0.0077, 0.0214, 0.0000, 0.0000, 0.1264, 0.1947, 0.2012, 0.1406, 0.0480, 0.0532 } }, - { { 0.0559, 0.0000, 0.1983, 0.0051, 0.0222, 0.0183, 0.4596, 0.2405, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000 } }, - { { 0.0493, 0.0000, 0.1926, 0.0047, 0.0230, 0.0121, 0.4133, 0.2780, 0.0002, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0259, 0.0008 } }, - { { 0.0534, 0.0000, 0.1705, 0.0047, 0.0418, 0.0208, 0.4775, 0.2191, 0.0001, 0.0000, 0.0000, 0.0000, 0.0000, 0.0005, 0.0036, 0.0080 } }, - { { 0.0534, 0.0000, 0.1701, 0.0067, 0.0157, 0.0133, 0.4328, 0.2540, 0.0001, 0.0043, 0.0000, 0.0244, 0.0000, 0.0000, 0.0181, 0.0071 } }, - { { 0.0548, 0.0000, 0.1744, 0.0061, 0.0031, 0.0009, 0.0046, 0.0136, 0.0000, 0.0004, 0.0000, 0.0000, 0.0227, 0.0061, 0.4882, 0.2252 } }, - { { 0.0559, 0.0000, 0.1807, 0.0059, 0.0036, 0.0013, 0.0046, 0.0134, 0.0000, 0.0004, 0.0000, 0.0487, 0.0209, 0.0060, 0.4615, 0.1972 } }, - } }; - - static const std::array knownLanguages = - { { - "chinese", - "english", - "french", - "german", - "polish", - "russian", - "ukrainian" - } }; - - if(!CResourceHandler::get("core")->existsResource(ResourceID("DATA/GENRLTXT.TXT", EResType::TEXT))) - { - Settings language = settings.write["session"]["language"]; - language->String() = "english"; - - Settings confidence = settings.write["session"]["languageDeviation"]; - confidence->Float() = 1.0; - - Settings encoding = settings.write["session"]["encoding"]; - encoding->String() = Languages::getLanguageOptions("english").encoding; - - return; - } - - // load file that will be used for footprint generation - // this is one of the most text-heavy files in game and consists solely from translated texts - auto resource = CResourceHandler::get("core")->load(ResourceID("DATA/GENRLTXT.TXT", EResType::TEXT)); - - std::array charCount{}; - std::array footprint{}; - std::array deviations{}; - - auto data = resource->readAll(); - - // compute how often each character occurs in input file - for (si64 i = 0; i < data.second; ++i) - charCount[data.first[i]] += 1; - - // and convert computed data into weights - // to reduce amount of data, group footprint data into 16-char blocks. - // While this will reduce precision, it should not affect output - // since we expect only tiny differences compared to reference footprints - for (size_t i = 0; i < 256; ++i) - footprint[i/16] += static_cast(charCount[i]) / data.second; - - logGlobal->debug("Language footprint: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f", - footprint[0], footprint[1], footprint[2], footprint[3], footprint[4], footprint[5], footprint[6], footprint[7], - footprint[8], footprint[9], footprint[10], footprint[11], footprint[12], footprint[13], footprint[14], footprint[15] - ); - - for (size_t i = 0; i < deviations.size(); ++i) - { - for (size_t j = 0; j < footprint.size(); ++j) - deviations[i] += std::abs((footprint[j] - knownFootprints[i][j])); - } - - size_t bestIndex = boost::range::min_element(deviations) - deviations.begin(); - - for (size_t i = 0; i < deviations.size(); ++i) - logGlobal->debug("Comparing to %s: %f", knownLanguages[i], deviations[i]); - - Settings language = settings.write["session"]["language"]; - language->String() = knownLanguages[bestIndex]; - - Settings confidence = settings.write["session"]["languageDeviation"]; - confidence->Float() = deviations[bestIndex]; - - Settings encoding = settings.write["session"]["encoding"]; - 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(std::string URI) -{ - ResourceID resource(URI, EResType::TEXT); - 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 CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) -{ - CLegacyConfigParser parser(sourceName); - size_t index = 0; - do - { - registerString( "core", {sourceID, index}, parser.readString()); - index += 1; - } - while (parser.endLine()); -} - -const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & identifier) const -{ - if(stringsLocalizations.count(identifier.get()) == 0) - { - 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 CGeneralTextHandler::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) -{ - assert(!modContext.empty()); - assert(!getModLanguage(modContext).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()]; - - if(value.baseLanguage.empty()) - { - value.baseLanguage = getModLanguage(modContext); - value.baseValue = localized; - } - else - { - if(value.baseValue != localized) - logMod->warn("Duplicate registered string '%s' found! Old value: '%s', new value: '%s'", UID.get(), value.baseValue, localized); - } - } - else - { - StringState result; - result.baseLanguage = getModLanguage(modContext); - result.baseValue = localized; - result.modContext = modContext; - - stringsLocalizations[UID.get()] = result; - } -} - -void CGeneralTextHandler::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) -{ - 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; -} - -bool CGeneralTextHandler::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const -{ - 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 CGeneralTextHandler::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()); -} - -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" ), - levels (*this, "core.skilllev" ), - zelp (*this, "core.help" ), - allTexts (*this, "core.genrltxt" ), - // 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" ) -{ - readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); - readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); - readToVector("core.tcommand", "DATA/TCOMMAND.TXT" ); - readToVector("core.hallinfo", "DATA/HALLINFO.TXT" ); - readToVector("core.castinfo", "DATA/CASTINFO.TXT" ); - readToVector("core.advevent", "DATA/ADVEVENT.TXT" ); - readToVector("core.restypes", "DATA/RESTYPES.TXT" ); - readToVector("core.randsign", "DATA/RANDSIGN.TXT" ); - readToVector("core.overview", "DATA/OVERVIEW.TXT" ); - readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" ); - readToVector("core.priskill", "DATA/PRISKILL.TXT" ); - readToVector("core.jktext", "DATA/JKTEXT.TXT" ); - readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" ); - readToVector("core.turndur", "DATA/TURNDUR.TXT" ); - readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" ); - readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" ); - readToVector("core.skilllev", "DATA/SKILLLEV.TXT" ); - readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); - readToVector("core.minename", "DATA/MINENAME.TXT" ); - readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); - - static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; - if (CResourceHandler::get()->existsResource(ResourceID(QE_MOD_COMMANDS, EResType::TEXT))) - readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); - - { - CLegacyConfigParser parser("DATA/RANDTVRN.TXT"); - parser.endLine(); - size_t index = 0; - do - { - std::string line = parser.readString(); - if(!line.empty()) - { - registerString("core", {"core.randtvrn", index}, line); - index += 1; - } - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser("DATA/GENRLTXT.TXT"); - parser.endLine(); - size_t index = 0; - do - { - registerString("core", {"core.genrltxt", index}, parser.readString()); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser("DATA/HELP.TXT"); - size_t index = 0; - do - { - std::string first = parser.readString(); - std::string second = parser.readString(); - registerString("core", "core.help." + std::to_string(index) + ".hover", first); - registerString("core", "core.help." + std::to_string(index) + ".help", second); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser("DATA/PLCOLORS.TXT"); - size_t index = 0; - do - { - std::string color = parser.readString(); - - registerString("core", {"core.plcolors", index}, color); - color[0] = toupper(color[0]); - registerString("core", {"vcmi.capitalColors", index}, color); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser("DATA/SEERHUT.TXT"); - - //skip header - parser.endLine(); - - for (size_t i = 0; i < 6; ++i) - { - registerString("core", {"core.seerhut.empty", i}, parser.readString()); - } - parser.endLine(); - - for (size_t i = 0; i < 9; ++i) //9 types of quests - { - std::string questName = CQuest::missionName(static_cast(1+i)); - - for (size_t j = 0; j < 5; ++j) - { - std::string questState = CQuest::missionState(j); - - parser.readString(); //front description - for (size_t k = 0; k < 6; ++k) - { - registerString("core", {"core.seerhut.quest", questName, questState, k}, parser.readString()); - } - parser.endLine(); - } - } - - for (size_t k = 0; k < 6; ++k) //Time limit - { - registerString("core", {"core.seerhut.time", k}, parser.readString()); - } - parser.endLine(); - - parser.endLine(); // empty line - parser.endLine(); // header - - for (size_t i = 0; i < 48; ++i) - { - registerString("core", {"core.seerhut.names", i}, parser.readString()); - parser.endLine(); - } - } - { - CLegacyConfigParser parser("DATA/CAMPTEXT.TXT"); - - //skip header - parser.endLine(); - - std::string text; - size_t campaignsCount = 0; - do - { - text = parser.readString(); - if (!text.empty()) - { - registerString("core", {"core.camptext.names", campaignsCount}, text); - campaignsCount += 1; - } - } - while (parser.endLine() && !text.empty()); - - for (size_t campaign=0; campaignsettings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) - { - if(CResourceHandler::get()->existsResource(ResourceID("DATA/ZNPC00.TXT", EResType::TEXT))) - readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); - } -} - -int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const -{ - if(textIndex == 0) - return 0; - if(textIndex < 0) - return -textIndex; - if(count == 1) - return textIndex; - - return textIndex + 1; -} - -void CGeneralTextHandler::dumpAllTexts() -{ - logGlobal->info("BEGIN TEXT EXPORT"); - for(const auto & entry : stringsLocalizations) - { - if (!entry.second.overrideValue.empty()) - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); - else - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); - } - - logGlobal->info("END TEXT EXPORT"); -} - -size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const -{ - assert(campaignID < scenariosCountPerCampaign.size()); - - if(campaignID < scenariosCountPerCampaign.size()) - return scenariosCountPerCampaign[campaignID]; - return 0; -} - -std::string CGeneralTextHandler::getModLanguage(const std::string & modContext) -{ - if (modContext == "core") - return getInstalledLanguage(); - return VLC->modh->getModLanguage(modContext); -} - -std::string CGeneralTextHandler::getPreferredLanguage() -{ - assert(!settings["general"]["language"].String().empty()); - return settings["general"]["language"].String(); -} - -std::string CGeneralTextHandler::getInstalledLanguage() -{ - assert(!settings["session"]["language"].String().empty()); - return settings["session"]["language"].String(); -} - -std::string CGeneralTextHandler::getInstalledEncoding() -{ - assert(!settings["session"]["encoding"].String().empty()); - return settings["session"]["encoding"].String(); -} - -std::vector CGeneralTextHandler::findStringsWithPrefix(const std::string & prefix) -{ - std::vector result; - - for(const auto & entry : stringsLocalizations) - { - if(boost::algorithm::starts_with(entry.first, prefix)) - result.push_back(entry.first); - } - - return result; -} - -LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath): - owner(owner), - basePath(std::move(basePath)) -{} - -std::string LegacyTextContainer::operator[](size_t index) const -{ - return owner.translate(basePath, index); -} - -LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath): - owner(owner), - basePath(std::move(basePath)) -{} - -std::pair LegacyHelpContainer::operator[](size_t index) const -{ - return { - owner.translate(basePath + "." + std::to_string(index) + ".hover"), - owner.translate(basePath + "." + std::to_string(index) + ".help") - }; -} - -VCMI_LIB_NAMESPACE_END +/* + * CGeneralTextHandler.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 "CGeneralTextHandler.h" + +#include "filesystem/Filesystem.h" +#include "serializer/JsonSerializeFormat.h" +#include "CConfigHandler.h" +#include "GameSettings.h" +#include "mapObjects/CQuest.h" +#include "modding/CModHandler.h" +#include "VCMI_Lib.h" +#include "Languages.h" +#include "TextOperations.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Detects language and encoding of H3 text files based on matching against pregenerated footprints of H3 file +void CGeneralTextHandler::detectInstallParameters() +{ + using LanguageFootprint = std::array; + + static const std::array knownFootprints = + { { + { { 0.1602, 0.0000, 0.0357, 0.0054, 0.0038, 0.0017, 0.0077, 0.0214, 0.0000, 0.0000, 0.1264, 0.1947, 0.2012, 0.1406, 0.0480, 0.0532 } }, + { { 0.0559, 0.0000, 0.1983, 0.0051, 0.0222, 0.0183, 0.4596, 0.2405, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000 } }, + { { 0.0493, 0.0000, 0.1926, 0.0047, 0.0230, 0.0121, 0.4133, 0.2780, 0.0002, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0259, 0.0008 } }, + { { 0.0534, 0.0000, 0.1705, 0.0047, 0.0418, 0.0208, 0.4775, 0.2191, 0.0001, 0.0000, 0.0000, 0.0000, 0.0000, 0.0005, 0.0036, 0.0080 } }, + { { 0.0534, 0.0000, 0.1701, 0.0067, 0.0157, 0.0133, 0.4328, 0.2540, 0.0001, 0.0043, 0.0000, 0.0244, 0.0000, 0.0000, 0.0181, 0.0071 } }, + { { 0.0548, 0.0000, 0.1744, 0.0061, 0.0031, 0.0009, 0.0046, 0.0136, 0.0000, 0.0004, 0.0000, 0.0000, 0.0227, 0.0061, 0.4882, 0.2252 } }, + { { 0.0559, 0.0000, 0.1807, 0.0059, 0.0036, 0.0013, 0.0046, 0.0134, 0.0000, 0.0004, 0.0000, 0.0487, 0.0209, 0.0060, 0.4615, 0.1972 } }, + } }; + + static const std::array knownLanguages = + { { + "chinese", + "english", + "french", + "german", + "polish", + "russian", + "ukrainian" + } }; + + if(!CResourceHandler::get("core")->existsResource(TextPath::builtin("DATA/GENRLTXT.TXT"))) + { + Settings language = settings.write["session"]["language"]; + language->String() = "english"; + + Settings confidence = settings.write["session"]["languageDeviation"]; + confidence->Float() = 1.0; + + Settings encoding = settings.write["session"]["encoding"]; + encoding->String() = Languages::getLanguageOptions("english").encoding; + + return; + } + + // load file that will be used for footprint generation + // this is one of the most text-heavy files in game and consists solely from translated texts + auto resource = CResourceHandler::get("core")->load(TextPath::builtin("DATA/GENRLTXT.TXT")); + + std::array charCount{}; + std::array footprint{}; + std::array deviations{}; + + auto data = resource->readAll(); + + // compute how often each character occurs in input file + for (si64 i = 0; i < data.second; ++i) + charCount[data.first[i]] += 1; + + // and convert computed data into weights + // to reduce amount of data, group footprint data into 16-char blocks. + // While this will reduce precision, it should not affect output + // since we expect only tiny differences compared to reference footprints + for (size_t i = 0; i < 256; ++i) + footprint[i/16] += static_cast(charCount[i]) / data.second; + + logGlobal->debug("Language footprint: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f", + footprint[0], footprint[1], footprint[2], footprint[3], footprint[4], footprint[5], footprint[6], footprint[7], + footprint[8], footprint[9], footprint[10], footprint[11], footprint[12], footprint[13], footprint[14], footprint[15] + ); + + for (size_t i = 0; i < deviations.size(); ++i) + { + for (size_t j = 0; j < footprint.size(); ++j) + deviations[i] += std::abs((footprint[j] - knownFootprints[i][j])); + } + + size_t bestIndex = boost::range::min_element(deviations) - deviations.begin(); + + for (size_t i = 0; i < deviations.size(); ++i) + logGlobal->debug("Comparing to %s: %f", knownLanguages[i], deviations[i]); + + Settings language = settings.write["session"]["language"]; + language->String() = knownLanguages[bestIndex]; + + Settings confidence = settings.write["session"]["languageDeviation"]; + confidence->Float() = deviations[bestIndex]; + + Settings encoding = settings.write["session"]["encoding"]; + 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) +{ + 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) +{ + subContainers.push_back(&container); +} + +void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) +{ + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); +} + +const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const +{ + 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) +{ + 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 +{ + 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 +{ + return stringsLocalizations.count(UID.get()); +} + +void TextLocalizationContainer::dumpAllTexts() +{ + logGlobal->info("BEGIN TEXT EXPORT"); + for(const auto & entry : stringsLocalizations) + { + if (!entry.second.overrideValue.empty()) + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); + else + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); + } + + logGlobal->info("END TEXT EXPORT"); +} + +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 +{ + 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; + } +} + +void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) +{ + CLegacyConfigParser parser(TextPath::builtin(sourceName)); + size_t index = 0; + do + { + registerString( "core", {sourceID, index}, parser.readString()); + index += 1; + } + while (parser.endLine()); +} + +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" ), + levels (*this, "core.skilllev" ), + zelp (*this, "core.help" ), + allTexts (*this, "core.genrltxt" ), + // 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" ) +{ + readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); + readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); + readToVector("core.tcommand", "DATA/TCOMMAND.TXT" ); + readToVector("core.hallinfo", "DATA/HALLINFO.TXT" ); + readToVector("core.castinfo", "DATA/CASTINFO.TXT" ); + readToVector("core.advevent", "DATA/ADVEVENT.TXT" ); + readToVector("core.restypes", "DATA/RESTYPES.TXT" ); + readToVector("core.randsign", "DATA/RANDSIGN.TXT" ); + readToVector("core.overview", "DATA/OVERVIEW.TXT" ); + readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" ); + readToVector("core.priskill", "DATA/PRISKILL.TXT" ); + readToVector("core.jktext", "DATA/JKTEXT.TXT" ); + readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" ); + readToVector("core.turndur", "DATA/TURNDUR.TXT" ); + readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" ); + readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" ); + readToVector("core.skilllev", "DATA/SKILLLEV.TXT" ); + readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); + readToVector("core.minename", "DATA/MINENAME.TXT" ); + readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); + readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); + + static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; + if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) + readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); + + { + CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT")); + parser.endLine(); + size_t index = 0; + do + { + std::string line = parser.readString(); + if(!line.empty()) + { + registerString("core", {"core.randtvrn", index}, line); + index += 1; + } + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/GENRLTXT.TXT")); + parser.endLine(); + size_t index = 0; + do + { + registerString("core", {"core.genrltxt", index}, parser.readString()); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/HELP.TXT")); + size_t index = 0; + do + { + std::string first = parser.readString(); + std::string second = parser.readString(); + registerString("core", "core.help." + std::to_string(index) + ".hover", first); + registerString("core", "core.help." + std::to_string(index) + ".help", second); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/PLCOLORS.TXT")); + size_t index = 0; + do + { + std::string color = parser.readString(); + + registerString("core", {"core.plcolors", index}, color); + color[0] = toupper(color[0]); + registerString("core", {"vcmi.capitalColors", index}, color); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/SEERHUT.TXT")); + + //skip header + parser.endLine(); + + for (size_t i = 0; i < 6; ++i) + { + registerString("core", {"core.seerhut.empty", i}, parser.readString()); + } + parser.endLine(); + + for (size_t i = 0; i < 9; ++i) //9 types of quests + { + std::string questName = CQuest::missionName(1+i); + + for (size_t j = 0; j < 5; ++j) + { + std::string questState = CQuest::missionState(j); + + parser.readString(); //front description + for (size_t k = 0; k < 6; ++k) + { + registerString("core", {"core.seerhut.quest", questName, questState, k}, parser.readString()); + } + parser.endLine(); + } + } + + for (size_t k = 0; k < 6; ++k) //Time limit + { + registerString("core", {"core.seerhut.time", k}, parser.readString()); + } + parser.endLine(); + + parser.endLine(); // empty line + parser.endLine(); // header + + for (size_t i = 0; i < 48; ++i) + { + registerString("core", {"core.seerhut.names", i}, parser.readString()); + parser.endLine(); + } + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/CAMPTEXT.TXT")); + + //skip header + parser.endLine(); + + std::string text; + size_t campaignsCount = 0; + do + { + text = parser.readString(); + if (!text.empty()) + { + registerString("core", {"core.camptext.names", campaignsCount}, text); + campaignsCount += 1; + } + } + while (parser.endLine() && !text.empty()); + + for (size_t campaign=0; campaignsettings()->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 +{ + if(textIndex == 0) + return 0; + if(textIndex < 0) + return -textIndex; + if(count == 1) + return textIndex; + + return textIndex + 1; +} + +size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const +{ + assert(campaignID < scenariosCountPerCampaign.size()); + + if(campaignID < scenariosCountPerCampaign.size()) + return scenariosCountPerCampaign[campaignID]; + return 0; +} + +std::string CGeneralTextHandler::getPreferredLanguage() +{ + assert(!settings["general"]["language"].String().empty()); + return settings["general"]["language"].String(); +} + +std::string CGeneralTextHandler::getInstalledLanguage() +{ + assert(!settings["session"]["language"].String().empty()); + return settings["session"]["language"].String(); +} + +std::string CGeneralTextHandler::getInstalledEncoding() +{ + assert(!settings["session"]["encoding"].String().empty()); + return settings["session"]["encoding"].String(); +} + +std::vector CGeneralTextHandler::findStringsWithPrefix(const std::string & prefix) +{ + std::vector result; + + for(const auto & entry : stringsLocalizations) + { + if(boost::algorithm::starts_with(entry.first, prefix)) + result.push_back(entry.first); + } + + return result; +} + +LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath): + owner(owner), + basePath(std::move(basePath)) +{} + +std::string LegacyTextContainer::operator[](size_t index) const +{ + return owner.translate(basePath, index); +} + +LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath): + owner(owner), + basePath(std::move(basePath)) +{} + +std::pair LegacyHelpContainer::operator[](size_t index) const +{ + return { + owner.translate(basePath + "." + std::to_string(index) + ".hover"), + owner.translate(basePath + "." + std::to_string(index) + ".help") + }; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 83a7ee7b4..980a3c03e 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -1,237 +1,288 @@ -/* - * 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 - -VCMI_LIB_NAMESPACE_BEGIN - -class CInputStream; -class JsonNode; - -/// 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(std::string 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...) - {} -}; - -/// Handles all text-related data in game -class DLL_LINKAGE CGeneralTextHandler -{ - 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; - }; - - /// map identifier -> localization - std::unordered_map stringsLocalizations; - - 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; - - std::string getModLanguage(const std::string & modContext); - - /// 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); - -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); - - // returns true if identifier with such name was registered, even if not translated to current language - // not required right now, can be added if necessary - // bool identifierExists( const std::string identifier) const; - - /// 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, dumps all currently known texts into console using Json-like format - void dumpAllTexts(); - - 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 +/* + * 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: + 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, const int Version) + { + 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); + +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); + + // returns true if identifier with such name was registered, even if not translated to current language + bool identifierExists(const TextIdentifier & UID) const; + + /// 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, dumps all currently known texts into console using Json-like format + void dumpAllTexts(); + + /// 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, const int Version) + { + std::string key; + auto sz = stringsLocalizations.size(); + 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]; + } + } + } +}; + +/// 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 index 173678c17..3ae6931a1 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -1,779 +1,776 @@ -/* - * 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 "JsonNode.h" -#include "StringConstants.h" -#include "battle/BattleHex.h" -#include "CCreatureHandler.h" -#include "GameSettings.h" -#include "CModHandler.h" -#include "CTownHandler.h" -#include "CSkillHandler.h" -#include "BattleFieldHandler.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.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 -{ - int totalProb = 0; - for(const auto & possible : possibles) - { - totalProb += secSkillProbability[possible]; - } - if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) - { - auto ran = rand.nextInt(totalProb - 1); - for(const auto & possible : possibles) - { - ran -= secSkillProbability[possible]; - if(ran < 0) - { - return possible; - } - } - } - // FIXME: select randomly? How H3 handles such rare situation? - return *possibles.begin(); -} - -bool CHeroClass::isMagicHero() const -{ - return affinity == MAGIC; -} - -EAlignment CHeroClass::getAlignment() const -{ - return VLC->factions()->getByIndex(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), - commander(nullptr) -{ -} - -void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill::PrimarySkill pSkill) const -{ - const auto & skillName = PrimarySkill::names[pSkill]; - 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 = node["animation"]["battle"]["female"].String(); - heroClass->imageBattleMale = node["animation"]["battle"]["male"].String(); - //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()); - - heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); - - 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 != 100) - logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); - - percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); - if(percentSumm != 100) - logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); - - for(auto skillPair : node["secondarySkills"].Struct()) - { - int probability = static_cast(skillPair.second.Integer()); - VLC->modh->identifiers.requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) - { - if(heroClass->secSkillProbability.size() <= skillID) - heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later - heroClass->secSkillProbability[skillID] = probability; - }); - } - - VLC->modh->identifiers.requestIdentifier ("creature", node["commander"], - [=](si32 commanderID) - { - heroClass->commander = VLC->creh->objects[commanderID]; - }); - - heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); - for(const auto & tavern : node["tavern"].Struct()) - { - int value = static_cast(tavern.second.Float()); - - VLC->modh->identifiers.requestIdentifier(tavern.second.meta, "faction", tavern.first, - [=](si32 factionID) - { - heroClass->selectionProbability[FactionID(factionID)] = value; - }); - } - - VLC->modh->identifiers.requestIdentifier("faction", node["faction"], - [=](si32 factionID) - { - heroClass->faction = factionID; - }); - - VLC->modh->identifiers.requestIdentifier(scope, "object", "hero", [=](si32 index) - { - JsonNode classConf = node["mapObject"]; - classConf["heroClass"].String() = identifier; - classConf.setMeta(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("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(CHeroClass * heroClass : objects) - { - for(CFaction * 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 - heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); - for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) - { - if(heroClass->secSkillProbability[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(CHeroClass * hc : objects) - { - if (!hc->imageMapMale.empty()) - { - JsonNode templ; - templ["animation"].String() = hc->imageMapMale; - VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); - } - } -} - -std::vector CHeroClassHandler::getDefaultAllowed() const -{ - return std::vector(size(), true); -} - -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 = node["battleImage"].String(); - - loadHeroArmy(hero, node); - loadHeroSkills(hero, node); - loadHeroSpecialty(hero, node); - - VLC->modh->identifiers.requestIdentifier("heroClass", node["class"], - [=](si32 classID) - { - hero->heroClass = classes[HeroClassID(classID)]; - }); - - 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()); - - assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); - - VLC->modh->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 < SecSkillLevel::LEVELS_SIZE) - { - size_t currentIndex = hero->secSkillsInit.size(); - hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); - - VLC->modh->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->modh->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 (auto const & upgradeSourceID : oldTargets) - { - const CCreature * upgradeSource = VLC->creh->objects[upgradeSourceID]; - targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); - } - - if (oldTargets.size() == targets.size()) - break; - } - - for(CreatureID cid : targets) - { - const CCreature &specCreature = *VLC->creh->objects[cid]; - int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; - - { - std::shared_ptr bonus = std::make_shared(); - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->type = BonusType::STACKS_SPEED; - bonus->val = 1; - result.push_back(bonus); - } - - { - std::shared_ptr bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = 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); - } - - { - std::shared_ptr bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = 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 (auto const & 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 = hero->getIndex(); - 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->modh->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); - while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) - { - auto i = expPerLevel.size() - 1; - auto diff = expPerLevel[i] - expPerLevel[i-1]; - diff += diff / 5; - expPerLevel.push_back (expPerLevel[i] + diff); - } - expPerLevel.pop_back();//last value is broken -} - -/// 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("DATA/HEROSPEC.TXT"); - CLegacyConfigParser bioParser("DATA/HEROBIOS.TXT"); - CLegacyConfigParser parser("DATA/HOTRAITS.TXT"); - - parser.endLine(); //ignore header - parser.endLine(); - - specParser.endLine(); //ignore header - specParser.endLine(); - - for (int i=0; iimageIndex = static_cast(index) + GameConstants::HERO_PORTRAIT_SHIFT; // 2 special frames + some extra portraits - - objects.emplace_back(object); - - registerObject(scope, "hero", name, 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()); -} - -ui32 CHeroHandler::level (ui64 experience) const -{ - return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); -} - -ui64 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]; - } -} - -std::vector CHeroHandler::getDefaultAllowed() const -{ - // Look Data/HOTRAITS.txt for reference - std::vector allowedHeroes; - allowedHeroes.reserve(size()); - - for(const CHero * hero : objects) - { - allowedHeroes.push_back(hero && !hero->special); - } - - return allowedHeroes; -} - -VCMI_LIB_NAMESPACE_END +/* + * 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 "JsonNode.h" +#include "constants/StringConstants.h" +#include "battle/BattleHex.h" +#include "CCreatureHandler.h" +#include "GameSettings.h" +#include "CTownHandler.h" +#include "CSkillHandler.h" +#include "BattleFieldHandler.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.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; +} + +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), + commander(nullptr) +{ +} + +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()); + + heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + + 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 != 100) + logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + + percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); + if(percentSumm != 100) + logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + + for(auto skillPair : node["secondarySkills"].Struct()) + { + int probability = static_cast(skillPair.second.Integer()); + VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) + { + heroClass->secSkillProbability[skillID] = probability; + }); + } + + VLC->identifiers()->requestIdentifier ("creature", node["commander"], + [=](si32 commanderID) + { + heroClass->commander = VLC->creh->objects[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.meta, "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; + classConf.setMeta(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(CHeroClass * heroClass : objects) + { + for(CFaction * 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(CHeroClass * 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 = classes[HeroClassID(classID)]; + }); + + 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()); + + assert(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 (auto const & 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) + { + auto const & specCreature = *cid.toCreature(); + int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; + + { + std::shared_ptr bonus = std::make_shared(); + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->type = BonusType::STACKS_SPEED; + bonus->val = 1; + result.push_back(bonus); + } + + { + std::shared_ptr 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); + } + + { + std::shared_ptr 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 (auto const & 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); + while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) + { + auto i = expPerLevel.size() - 1; + auto diff = expPerLevel[i] - expPerLevel[i-1]; + diff += diff / 5; + expPerLevel.push_back (expPerLevel[i] + diff); + } + expPerLevel.pop_back();//last value is broken +} + +/// 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()); +} + +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()); +} + +ui32 CHeroHandler::level (ui64 experience) const +{ + return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); +} + +ui64 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]; + } +} + +std::set CHeroHandler::getDefaultAllowed() const +{ + std::set result; + + for(const CHero * 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 index 3f11968fb..3103594bc 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -1,282 +1,214 @@ -/* - * 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 "../lib/ConstTransitivePtr.h" -#include "GameConstants.h" -#include "bonuses/Bonus.h" -#include "bonuses/BonusList.h" -#include "IHandlerBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CHeroClass; -class CGHeroInstance; -struct BattleHex; -class JsonNode; -class CRandomGenerator; -class JsonSerializeFormat; -class BattleField; - -enum class EHeroGender : uint8_t -{ - MALE = 0, - FEMALE = 1, - DEFAULT = 0xff // from h3m, instance has same gender as hero type -}; - -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; - - template void serialize(Handler &h, const int version) - { - h & minAmount; - h & maxAmount; - h & creature; - } - }; - si32 imageIndex = 0; - - std::vector initialArmy; - - CHeroClass * heroClass{}; - 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; - std::string 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); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & imageIndex; - h & initialArmy; - h & heroClass; - h & secSkillsInit; - h & specialty; - h & spells; - h & haveSpellBook; - h & gender; - h & special; - h & onlyOnWaterMap; - h & onlyOnMapWithoutWater; - h & iconSpecSmall; - h & iconSpecLarge; - h & portraitSmall; - h & portraitLarge; - h & identifier; - h & modScope; - h & battleImage; - } -}; - -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; - - CCreature * 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::vector secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order - - std::map selectionProbability; //probability of selection in towns - - std::string imageBattleMale; - std::string 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); - - template void serialize(Handler & h, const int version) - { - h & modScope; - h & identifier; - h & faction; - h & id; - h & defaultTavernChance; - h & primarySkillInitial; - h & primarySkillLowLevel; - h & primarySkillHighLevel; - h & secSkillProbability; - h & selectionProbability; - h & affinity; - h & commander; - h & imageBattleMale; - h & imageBattleFemale; - h & imageMapMale; - h & imageMapFemale; - - if(!h.saving) - { - for(int & i : secSkillProbability) - vstd::amax(i, 0); - } - } - EAlignment getAlignment() const; -}; - -class DLL_LINKAGE CHeroClassHandler : public CHandlerBase -{ - void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill::PrimarySkill pSkill) const; - -public: - std::vector loadLegacyData() override; - - void afterLoadFinalization() override; - - std::vector getDefaultAllowed() const override; - - ~CHeroClassHandler(); - - template void serialize(Handler &h, const int version) - { - h & objects; - } - -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 201 values. Any higher levels require experience larger that ui64 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: - CHeroClassHandler classes; - - ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount - ui64 reqExp(ui32 level) const; //calculates experience required for given level - - 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::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & classes; - h & objects; - h & expPerLevel; - } - -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 +/* + * 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 : uint8_t +{ + MALE = 0, + FEMALE = 1, + DEFAULT = 0xff // from h3m, instance has same gender as hero type +}; + +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; + + CHeroClass * heroClass{}; + 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; + + CCreature * 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; +}; + +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 201 values. Any higher levels require experience larger that ui64 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: + CHeroClassHandler classes; + + ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount + ui64 reqExp(ui32 level) const; //calculates experience required for given level + + 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/CModHandler.cpp b/lib/CModHandler.cpp deleted file mode 100644 index 9b8247295..000000000 --- a/lib/CModHandler.cpp +++ /dev/null @@ -1,1269 +0,0 @@ -/* - * CModHandler.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 "CModHandler.h" -#include "rmg/CRmgTemplateStorage.h" -#include "filesystem/FileStream.h" -#include "filesystem/AdapterLoaders.h" -#include "filesystem/CFilesystemLoader.h" -#include "filesystem/Filesystem.h" - -#include "CCreatureHandler.h" -#include "CArtHandler.h" -#include "CTownHandler.h" -#include "CHeroHandler.h" -#include "StringConstants.h" -#include "CStopWatch.h" -#include "IHandlerBase.h" -#include "spells/CSpellHandler.h" -#include "CSkillHandler.h" -#include "CGeneralTextHandler.h" -#include "Languages.h" -#include "ScriptHandler.h" -#include "RoadHandler.h" -#include "GameSettings.h" -#include "RiverHandler.h" -#include "TerrainHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -CIdentifierStorage::CIdentifierStorage(): - state(LOADING) -{ -} - -void CIdentifierStorage::checkIdentifier(std::string & ID) -{ - if (boost::algorithm::ends_with(ID, ".")) - logMod->warn("BIG WARNING: identifier %s seems to be broken!", ID); - else - { - size_t pos = 0; - do - { - if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase - { - logMod->warn("Warning: identifier %s is not in camelCase!", ID); - ID[pos] = std::tolower(ID[pos]);// Try to fix the ID - } - pos = ID.find('.', pos); - } - while(pos++ != std::string::npos); - } -} - -void CIdentifierStorage::requestIdentifier(ObjectCallback callback) -{ - checkIdentifier(callback.type); - checkIdentifier(callback.name); - - assert(!callback.localScope.empty()); - - if (state != FINISHED) // enqueue request if loading is still in progress - scheduledRequests.push_back(callback); - else // execute immediately for "late" requests - resolveIdentifier(callback); -} - -CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional) -{ - assert(!scope.empty()); - - auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); - auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); - - if (scope == scopeAndFullName.first) - logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); - - ObjectCallback result; - result.localScope = scope; - result.remoteScope = scopeAndFullName.first; - result.type = typeAndName.first; - result.name = typeAndName.second; - result.callback = callback; - result.optional = optional; - return result; -} - -CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional) -{ - assert(!scope.empty()); - - auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); - auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); - - if(!typeAndName.first.empty()) - { - if (typeAndName.first != type) - logMod->error("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); - else - logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope); - } - - if (scope == scopeAndFullName.first) - logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); - - ObjectCallback result; - result.localScope = scope; - result.remoteScope = scopeAndFullName.first; - result.type = type; - result.name = typeAndName.second; - result.callback = callback; - result.optional = optional; - return result; -} - -void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameWithType(scope, fullName, callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameWithType(name.meta, name.String(), callback, false)); -} - -void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true)); -} - -void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, true)); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name , type ,scope); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name.String(), type, name.meta); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", name.String(), name.meta); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); - - return std::optional(); -} - -void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) -{ - ObjectData data; - data.scope = scope; - data.id = identifier; - - std::string fullID = type + '.' + name; - checkIdentifier(fullID); - - std::pair mapping = std::make_pair(fullID, data); - if(!vstd::containsMapping(registeredObjects, mapping)) - { - logMod->trace("registered %s as %s:%s", fullID, scope, identifier); - registeredObjects.insert(mapping); - } -} - -std::vector CIdentifierStorage::getPossibleIdentifiers(const ObjectCallback & request) -{ - std::set allowedScopes; - bool isValidScope = true; - - // called have not specified destination mod explicitly - if (request.remoteScope.empty()) - { - // special scope that should have access to all in-game objects - if (request.localScope == CModHandler::scopeGame()) - { - for(const auto & modName : VLC->modh->getActiveMods()) - allowedScopes.insert(modName); - } - - // normally ID's from all required mods, own mod and virtual built-in mod are allowed - else if(request.localScope != CModHandler::scopeBuiltin() && !request.localScope.empty()) - { - allowedScopes = VLC->modh->getModDependencies(request.localScope, isValidScope); - - if(!isValidScope) - return std::vector(); - - allowedScopes.insert(request.localScope); - } - - // all mods can access built-in mod - allowedScopes.insert(CModHandler::scopeBuiltin()); - } - else - { - //if destination mod was specified explicitly, restrict lookup to this mod - if(request.remoteScope == CModHandler::scopeBuiltin() ) - { - //built-in mod is an implicit dependency for all mods, allow access into it - allowedScopes.insert(request.remoteScope); - } - else if ( request.localScope == CModHandler::scopeGame() ) - { - // allow access, this is special scope that should have access to all in-game objects - allowedScopes.insert(request.remoteScope); - } - else if(request.remoteScope == request.localScope ) - { - // allow self-access - allowedScopes.insert(request.remoteScope); - } - else - { - // allow access only if mod is in our dependencies - auto myDeps = VLC->modh->getModDependencies(request.localScope, isValidScope); - - if(!isValidScope) - return std::vector(); - - if(myDeps.count(request.remoteScope)) - allowedScopes.insert(request.remoteScope); - } - } - - std::string fullID = request.type + '.' + request.name; - - auto entries = registeredObjects.equal_range(fullID); - if (entries.first != entries.second) - { - std::vector locatedIDs; - - for (auto it = entries.first; it != entries.second; it++) - { - if (vstd::contains(allowedScopes, it->second.scope)) - { - locatedIDs.push_back(it->second); - } - } - return locatedIDs; - } - return std::vector(); -} - -bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) -{ - auto identifiers = getPossibleIdentifiers(request); - if (identifiers.size() == 1) // normally resolved ID - { - request.callback(identifiers.front().id); - return true; - } - - if (request.optional && identifiers.empty()) // failed to resolve optinal ID - { - return true; - } - - // error found. Try to generate some debug info - if(identifiers.empty()) - logMod->error("Unknown identifier!"); - else - logMod->error("Ambiguous identifier request!"); - - logMod->error("Request for %s.%s from mod %s", request.type, request.name, request.localScope); - - for(const auto & id : identifiers) - { - logMod->error("\tID is available in mod %s", id.scope); - } - return false; -} - -void CIdentifierStorage::finalize() -{ - state = FINALIZING; - bool errorsFound = false; - - while ( !scheduledRequests.empty() ) - { - // Use local copy since new requests may appear during resolving, invalidating any iterators - auto request = scheduledRequests.back(); - scheduledRequests.pop_back(); - - if (!resolveIdentifier(request)) - errorsFound = true; - } - - if (errorsFound) - { - for(const auto & object : registeredObjects) - { - logMod->trace("%s : %s -> %d", object.second.scope, object.first, object.second.id); - } - logMod->error("All known identifiers were dumped into log file"); - } - assert(errorsFound == false); - state = FINISHED; -} - -ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): - handler(handler), - objectName(objectName), - originalData(handler->loadLegacyData()) -{ - for(auto & node : originalData) - { - node.setMeta(CModHandler::scopeBuiltin()); - } -} - -bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector & fileList, bool validate) -{ - bool result = false; - JsonNode data = JsonUtils::assembleFromFiles(fileList, result); - data.setMeta(modName); - - ModInfo & modInfo = modData[modName]; - - for(auto entry : data.Struct()) - { - size_t colon = entry.first.find(':'); - - if (colon == std::string::npos) - { - // normal object, local to this mod - modInfo.modData[entry.first].swap(entry.second); - } - else - { - std::string remoteName = entry.first.substr(0, colon); - std::string objectName = entry.first.substr(colon + 1); - - // patching this mod? Send warning and continue - this situation can be handled normally - if (remoteName == modName) - logMod->warn("Redundant namespace definition for %s", objectName); - - logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); - JsonNode & remoteConf = modData[remoteName].patches[objectName]; - - JsonUtils::merge(remoteConf, entry.second); - } - } - return result; -} - -bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) -{ - ModInfo & modInfo = modData[modName]; - bool result = true; - - auto performValidate = [&,this](JsonNode & data, const std::string & name){ - handler->beforeValidate(data); - if (validate) - result &= JsonUtils::validate(data, "vcmi:" + objectName, name); - }; - - // apply patches - if (!modInfo.patches.isNull()) - JsonUtils::merge(modInfo.modData, modInfo.patches); - - for(auto & entry : modInfo.modData.Struct()) - { - const std::string & name = entry.first; - JsonNode & data = entry.second; - - if (data.meta != modName) - logMod->warn("Mod %s is attempting to inject object %s into mod %s! This may not be supported in future versions!", data.meta, name, modName); - - if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) - { - if (modName != "core") - logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName); - - // try to add H3 object data - size_t index = static_cast(data["index"].Float()); - - if(originalData.size() > index) - { - logMod->trace("found original data in loadMod(%s) at index %d", name, index); - JsonUtils::merge(originalData[index], data); - std::swap(originalData[index], data); - originalData[index].clear(); // do not use same data twice (same ID) - } - else - { - logMod->trace("no original data in loadMod(%s) at index %d", name, index); - } - performValidate(data, name); - handler->loadObject(modName, name, data, index); - } - else - { - // normal new object - logMod->trace("no index in loadMod(%s)", name); - performValidate(data,name); - handler->loadObject(modName, name, data); - } - } - return result; -} - -void ContentTypeHandler::loadCustom() -{ - handler->loadCustom(); -} - -void ContentTypeHandler::afterLoadFinalization() -{ - handler->afterLoadFinalization(); -} - -void CContentHandler::init() -{ - handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(&VLC->heroh->classes, "heroClass"))); - handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth, "artifact"))); - handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh, "creature"))); - handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh, "faction"))); - handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh, "object"))); - handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh, "hero"))); - handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell"))); - handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill"))); - handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh, "template"))); -#if SCRIPTING_ENABLED - handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script"))); -#endif - handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield"))); - handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler, "terrain"))); - handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler, "river"))); - handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler, "road"))); - handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle"))); - //TODO: any other types of moddables? -} - -bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) -{ - bool result = true; - for(auto & handler : handlers) - { - result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo >(), validate); - } - return result; -} - -bool CContentHandler::loadMod(const std::string & modName, bool validate) -{ - bool result = true; - for(auto & handler : handlers) - { - result &= handler.second.loadMod(modName, validate); - } - return result; -} - -void CContentHandler::loadCustom() -{ - for(auto & handler : handlers) - { - handler.second.loadCustom(); - } -} - -void CContentHandler::afterLoadFinalization() -{ - for(auto & handler : handlers) - { - handler.second.afterLoadFinalization(); - } -} - -void CContentHandler::preloadData(CModInfo & mod) -{ - bool validate = (mod.validation != CModInfo::PASSED); - - // print message in format [<8-symbols checksum>] - logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); - - if (validate && mod.identifier != CModHandler::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.name); - else - logMod->error("\t\t[FAIL] %s", mod.name); - } - else - logMod->info("\t\t[SKIP] %s", mod.name); -} - -const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const -{ - return handlers.at(name); -} - -static JsonNode loadModSettings(const std::string & path) -{ - if (CResourceHandler::get("local")->existsResource(ResourceID(path))) - { - return JsonNode(ResourceID(path, EResType::TEXT)); - } - // Probably new install. Create initial configuration - CResourceHandler::get("local")->createResource(path); - return JsonNode(); -} - -JsonNode addMeta(JsonNode config, const std::string & meta) -{ - config.setMeta(meta); - return config; -} - -CModInfo::CModInfo(): - checksum(0), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING) -{ - -} - -CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): - identifier(identifier), - name(config["name"].String()), - description(config["description"].String()), - dependencies(config["depends"].convertTo>()), - conflicts(config["conflicts"].convertTo>()), - checksum(0), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING), - config(addMeta(config, identifier)) -{ - version = CModVersion::fromString(config["version"].String()); - 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') << 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/"); -} - -std::string CModInfo::getModFile(const std::string & name) -{ - return getModDir(name) + "/mod.json"; -} - -void CModInfo::updateChecksum(ui32 newChecksum) -{ - // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != checksum) - { - checksum = newChecksum; - validation = PENDING; - } -} - -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" - }; - - ResourceID modFileResource(CModInfo::getModFile(identifier)); - - if(CResourceHandler::get("initial")->existsResource(modFileResource)) - { - const JsonNode modConfig(modFileResource); - - for (auto const & key : keysToTest) - { - if (!modConfig[key].isNull()) - { - modGameplayAffecting = true; - return *modGameplayAffecting; - } - } - } - modGameplayAffecting = false; - return *modGameplayAffecting; -} - -void CModInfo::loadLocalData(const JsonNode & data) -{ - bool validated = false; - implicitlyEnabled = true; - explicitlyEnabled = !config["keepDisabled"].Bool(); - checksum = 0; - if (data.getType() == JsonNode::JsonType::DATA_BOOL) - { - explicitlyEnabled = data.Bool(); - } - if (data.getType() == JsonNode::JsonType::DATA_STRUCT) - { - explicitlyEnabled = data["active"].Bool(); - validated = data["validated"].Bool(); - checksum = strtol(data["checksum"].String().c_str(), nullptr, 16); - } - - //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion())); - - if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); - - if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment - { - if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) - { - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", name); - implicitlyEnabled = false; - } - } - - if (isEnabled()) - validation = validated ? PASSED : PENDING; - else - validation = validated ? PASSED : FAILED; -} - -bool CModInfo::isEnabled() const -{ - return implicitlyEnabled && explicitlyEnabled; -} - -void CModInfo::setEnabled(bool on) -{ - explicitlyEnabled = on; -} - -CModHandler::CModHandler() : content(std::make_shared()) -{ - //TODO: moddable spell schools - for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) - identifiers.registerObject(CModHandler::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); - - identifiers.registerObject(CModHandler::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); - - for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) - { - identifiers.registerObject(CModHandler::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); - } - - for(int i=0; i 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.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.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) -> CModInfo::EValidationStatus - { - if(mod.dependencies.size() > resolvedModIDs.size()) - return CModInfo::PENDING; - - for(const TModID & dependency : mod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency)) - return CModInfo::PENDING; - } - return CModInfo::PASSED; - }; - - 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)) == CModInfo::PASSED) - { - 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; - } - - // 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)) - logMod->error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.name, dependency); - } - } - 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 ResourceID & 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; -} - -bool CModHandler::isScopeReserved(const TModID & scope) -{ - //following scopes are reserved - either in use by mod system or by filesystem - static const std::array reservedScopes = { - "core", "map", "game", "root", "saves", "config", "local", "initial", "mapEditor" - }; - - return std::find(reservedScopes.begin(), reservedScopes.end(), scope) != reservedScopes.end(); -} - -const TModID & CModHandler::scopeBuiltin() -{ - static const TModID scope = "core"; - return scope; -} - -const TModID & CModHandler::scopeGame() -{ - static const TModID scope = "game"; - return scope; -} - -const TModID & CModHandler::scopeMap() -{ - //TODO: implement accessing map dependencies for both H3 and VCMI maps - // for now, allow access to any identifiers - static const TModID scope = "game"; - return scope; -} - -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 ( isScopeReserved(modFullName)) - { - logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); - return; - } - - if(CResourceHandler::get("initial")->existsResource(ResourceID(CModInfo::getModFile(modFullName)))) - { - CModInfo mod(modFullName, modSettings[modName], JsonNode(ResourceID(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(bool onlyEssential) -{ - JsonNode modConfig; - - if(onlyEssential) - { - loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods - } - else - { - modConfig = loadModSettings("config/modSettings.json"); - loadMods("", "", modConfig["activeMods"], true); - } - - coreMod = CModInfo(CModHandler::scopeBuiltin(), modConfig[CModHandler::scopeBuiltin()], JsonNode(ResourceID("config/gameConfig.json"))); - coreMod.name = "Original game files"; -} - -std::vector CModHandler::getAllMods() -{ - std::vector modlist; - modlist.reserve(allMods.size()); - for (auto & entry : allMods) - modlist.push_back(entry.first); - return modlist; -} - -std::vector CModHandler::getActiveMods() -{ - return activeMods; -} - -const CModInfo & CModHandler::getModInfo(const TModID & modId) const -{ - return allMods.at(modId); -} - -static JsonNode genDefaultFS() -{ - // default FS config for mods: directory "Content" that acts as H3 root directory - JsonNode defaultFS; - defaultFS[""].Vector().resize(2); - defaultFS[""].Vector()[0]["type"].String() = "zip"; - defaultFS[""].Vector()[0]["path"].String() = "/Content.zip"; - defaultFS[""].Vector()[1]["type"].String() = "dir"; - defaultFS[""].Vector()[1]["path"].String() = "/Content"; - return defaultFS; -} - -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"]); - 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 != CModHandler::scopeBuiltin()) - { - ResourceID modConfFile(CModInfo::getModFile(modName), EResType::TEXT); - 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 ResourceID & resID) - { - return resID.getType() == EResType::TEXT && - ( boost::starts_with(resID.getName(), "DATA") || - boost::starts_with(resID.getName(), "CONFIG")); - }); - - for (const ResourceID & file : files) - { - ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); - } - return modChecksum.checksum(); -} - -void CModHandler::loadModFilesystems() -{ - CGeneralTextHandler::detectInstallParameters(); - - activeMods = validateAndSortDependencies(activeMods); - - coreMod.updateChecksum(calculateModChecksum(CModHandler::scopeBuiltin(), CResourceHandler::get(CModHandler::scopeBuiltin()))); - - for(std::string & modName : activeMods) - { - CModInfo & mod = allMods[modName]; - CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); - } -} - -TModID CModHandler::findResourceOrigin(const ResourceID & name) -{ - for(const auto & modID : boost::adaptors::reverse(activeMods)) - { - if(CResourceHandler::get(modID)->existsResource(name)) - return modID; - } - - if(CResourceHandler::get("core")->existsResource(name)) - return "core"; - - if(CResourceHandler::get("mapEditor")->existsResource(name)) - return "core"; // Workaround for loading maps via map editor - - assert(0); - return ""; -} - -std::string CModHandler::getModLanguage(const TModID& modId) const -{ - if ( modId == "core") - return VLC->generaltexth->getInstalledLanguage(); - return allMods.at(modId).baseLanguage; -} - -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; - - logMod->error("Mod not found: '%s'", modId); - return {}; -} - -void CModHandler::initializeConfig() -{ - VLC->settingsHandler->load(coreMod.config["settings"]); - - for(const TModID & modName : activeMods) - { - const auto & mod = allMods[modName]; - if (!mod.config["settings"].isNull()) - VLC->settingsHandler->load(mod.config["settings"]); - } -} - -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 (!language.hasTranslation) - continue; - - 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]; - - std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); - std::string modBaseLanguage = allMods[modName].baseLanguage; - - auto baseTranslationList = mod.config["translations"].convertTo >(); - auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo >(); - - JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList); - JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList); - - VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation); - VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation); -} - -void CModHandler::load() -{ - CStopWatch totalTime; - CStopWatch timer; - - logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); - - content->init(); - - for(const TModID & modName : activeMods) - { - logMod->trace("Generating checksum for %s", modName); - allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(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()); - - content->load(coreMod); - for(const TModID & modName : activeMods) - content->load(allMods[modName]); - -#if SCRIPTING_ENABLED - VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load -#endif - - content->loadCustom(); - - for(const TModID & modName : activeMods) - loadTranslation(modName); - - for(const TModID & modName : activeMods) - if (!validateTranslations(modName)) - allMods[modName].validation = CModInfo::FAILED; - - logMod->info("\tLoading mod data: %d ms", timer.getDiff()); - VLC->creh->loadCrExpMod(); - identifiers.finalize(); - logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); - - content->afterLoadFinalization(); - logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); - logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); -} - -void CModHandler::afterLoad(bool onlyEssential) -{ - JsonNode modSettings; - for (auto & modEntry : allMods) - { - std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); - - modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); - } - modSettings[CModHandler::scopeBuiltin()] = coreMod.saveLocalData(); - - if(!onlyEssential) - { - FileStream file(*CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json")), std::ofstream::out | std::ofstream::trunc); - file << modSettings.toJson(); - } - -} - -std::string CModHandler::normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier) -{ - auto p = vstd::splitStringToPair(identifier, ':'); - - if(p.first.empty()) - p.first = scope; - - if(p.first == remoteScope) - p.first.clear(); - - return p.first.empty() ? p.second : p.first + ":" + p.second; -} - -void CModHandler::parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier) -{ - auto p = vstd::splitStringToPair(fullIdentifier, ':'); - - scope = p.first; - - auto p2 = vstd::splitStringToPair(p.second, '.'); - - if(!p2.first.empty()) - { - type = p2.first; - identifier = p2.second; - } - else - { - type = p.second; - identifier.clear(); - } -} - -std::string CModHandler::makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier) -{ - if(type.empty()) - logGlobal->error("Full identifier (%s %s) requires type name", scope, identifier); - - std::string actualScope = scope; - std::string actualName = identifier; - - //ignore scope if identifier is scoped - auto scopeAndName = vstd::splitStringToPair(identifier, ':'); - - if(!scopeAndName.first.empty()) - { - actualScope = scopeAndName.first; - actualName = scopeAndName.second; - } - - if(actualScope.empty()) - { - return actualName.empty() ? type : type + "." + actualName; - } - else - { - return actualName.empty() ? actualScope+ ":" + type : actualScope + ":" + type + "." + actualName; - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/CModHandler.h b/lib/CModHandler.h deleted file mode 100644 index 887422dbc..000000000 --- a/lib/CModHandler.h +++ /dev/null @@ -1,414 +0,0 @@ -/* - * CModHandler.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 "JsonNode.h" -#include "CModVersion.h" - -#ifdef __UCLIBC__ -#undef major -#undef minor -#undef patch -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class CModHandler; -class CModIndentifier; -class CModInfo; -class JsonNode; -class IHandlerBase; - -/// class that stores all object identifiers strings and maps them to numeric ID's -/// if possible, objects ID's should be in format ., camelCase e.g. "creature.grandElf" -class DLL_LINKAGE CIdentifierStorage -{ - enum ELoadingState - { - LOADING, - FINALIZING, - FINISHED - }; - - struct ObjectCallback // entry created on ID request - { - std::string localScope; /// scope from which this ID was requested - std::string remoteScope; /// scope in which this object must be found - std::string type; /// type, e.g. creature, faction, hero, etc - std::string name; /// string ID - std::function callback; - bool optional; - - /// Builds callback from identifier in form "targetMod:type.name" - static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional); - - /// Builds callback from identifier in form "targetMod:name" - static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional); - - private: - ObjectCallback() = default; - }; - - struct ObjectData // entry created on ID registration - { - si32 id; - std::string scope; /// scope in which this ID located - - bool operator==(const ObjectData & other) const - { - return id == other.id && scope == other.scope; - } - - template void serialize(Handler &h, const int version) - { - h & id; - h & scope; - } - }; - - std::multimap registeredObjects; - std::vector scheduledRequests; - - ELoadingState state; - - /// Check if identifier can be valid (camelCase, point as separator) - static void checkIdentifier(std::string & ID); - - void requestIdentifier(ObjectCallback callback); - bool resolveIdentifier(const ObjectCallback & callback); - std::vector getPossibleIdentifiers(const ObjectCallback & callback); -public: - CIdentifierStorage(); - virtual ~CIdentifierStorage() = default; - /// request identifier for specific object name. - /// Function callback will be called during ID resolution phase of loading - void requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback); - ///fullName = [remoteScope:]type.name - void requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback); - void requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback); - void requestIdentifier(const JsonNode & name, const std::function & callback); - - /// 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); - void tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback); - - /// get identifier immediately. If identifier is not know and not silent call will result in error message - std::optional getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent = false); - std::optional getIdentifier(const std::string & type, const JsonNode & name, bool silent = false); - std::optional getIdentifier(const JsonNode & name, bool silent = false); - std::optional getIdentifier(const std::string & scope, const std::string & fullName, bool silent = false); - - /// registers new object - void registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier); - - /// called at the very end of loading to check for any missing ID's - void finalize(); - - template void serialize(Handler &h, const int version) - { - h & registeredObjects; - h & state; - } -}; - -/// internal type to handle loading of one data type (e.g. artifacts, creatures) -class DLL_LINKAGE ContentTypeHandler -{ -public: - struct ModInfo - { - /// mod data from this mod and for this mod - JsonNode modData; - /// mod data for this mod from other mods (patches) - JsonNode patches; - }; - /// handler to which all data will be loaded - IHandlerBase * handler; - std::string objectName; - - /// contains all loaded H3 data - std::vector originalData; - std::map modData; - - ContentTypeHandler(IHandlerBase * handler, const std::string & objectName); - - /// 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 loadMod(const std::string & modName, bool validate); - void loadCustom(); - void afterLoadFinalization(); -}; - -/// 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); - - /// actually loads data in mod - void load(CModInfo & mod); - - void loadCustom(); - - /// all data was loaded, time for final validation / integration - void afterLoadFinalization(); - - const ContentTypeHandler & operator[] (const std::string & name) const; -}; - -using TModID = std::string; - -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; - -public: - enum EValidationStatus - { - PENDING, - FAILED, - PASSED - }; - - /// identifier, identical to name of folder with mod - std::string identifier; - - /// human-readable strings - std::string name; - std::string description; - - /// version of the mod - CModVersion version; - - /// 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; - - /// CRC-32 checksum of the mod - ui32 checksum; - - EValidationStatus validation; - - JsonNode config; - - CModInfo(); - CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config); - - JsonNode saveLocalData() const; - void updateChecksum(ui32 newChecksum); - - /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects - bool checkModGameplayAffecting() const; - - bool isEnabled() const; - void setEnabled(bool on); - - static std::string getModDir(const std::string & name); - static std::string getModFile(const std::string & name); - -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; - - void loadLocalData(const JsonNode & data); -}; - -class DLL_LINKAGE CModHandler -{ - std::map allMods; - std::vector activeMods;//active mods, in order in which they were loaded - CModInfo coreMod; - - 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); - - bool validateTranslations(TModID modName) const; -public: - - /// returns true if scope is reserved for internal use and can not be used by mods - static bool isScopeReserved(const TModID & scope); - - /// reserved scope name for referencing built-in (e.g. H3) objects - static const TModID & scopeBuiltin(); - - /// reserved scope name for accessing objects from any loaded mod - static const TModID & scopeGame(); - - /// reserved scope name for accessing object for map loading - static const TModID & scopeMap(); - - class DLL_LINKAGE Incompatibility: public std::exception - { - public: - using StringPair = std::pair; - using ModList = std::list; - - Incompatibility(ModList && _missingMods): - missingMods(std::move(_missingMods)) - { - std::ostringstream _ss; - for(const auto & m : missingMods) - _ss << m.first << ' ' << m.second << std::endl; - message = _ss.str(); - } - - const char * what() const noexcept override - { - return message.c_str(); - } - - private: - //list of mods required to load the game - // first: mod name - // second: mod version - const ModList missingMods; - std::string message; - }; - - CIdentifierStorage identifiers; - - std::shared_ptr content; //(!)Do not serialize - - /// receives list of available mods and trying to load mod.json from all of them - void initializeConfig(); - void loadMods(bool onlyEssential = false); - void loadModFilesystems(); - - /// returns ID of mod that provides selected file resource - TModID findResourceOrigin(const ResourceID & name); - - std::string getModLanguage(const TModID & modId) const; - - std::set getModDependencies(const TModID & modId, bool & isModFound) const; - - /// returns list of all (active) mods - std::vector getAllMods(); - std::vector getActiveMods(); - - const CModInfo & getModInfo(const TModID & modId) const; - - /// load content from all available mods - void load(); - void afterLoad(bool onlyEssential); - - CModHandler(); - virtual ~CModHandler() = default; - - static std::string normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier); - - static void parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier); - - static std::string makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - h & activeMods; - for(const auto & m : activeMods) - h & allMods[m].version; - } - else - { - loadMods(); - std::vector saveActiveMods; - std::vector newActiveMods; - h & saveActiveMods; - - Incompatibility::ModList missingMods; - - for(const auto & m : activeMods) - { - if (vstd::contains(saveActiveMods, m)) - continue; - - auto & modInfo = allMods.at(m); - if(modInfo.checkModGameplayAffecting()) - missingMods.emplace_back(m, modInfo.version.toString()); - } - - for(const auto & m : saveActiveMods) - { - CModVersion mver; - h & mver; - - if (allMods.count(m) == 0) - { - missingMods.emplace_back(m, mver.toString()); - continue; - } - - auto & modInfo = allMods.at(m); - - bool modAffectsGameplay = modInfo.checkModGameplayAffecting(); - bool modVersionCompatible = modInfo.version.isNull() || mver.isNull() || modInfo.version.compatible(mver); - bool modEnabledLocally = vstd::contains(activeMods, m); - bool modCanBeEnabled = modEnabledLocally && modVersionCompatible; - - allMods[m].setEnabled(modCanBeEnabled); - - if (modCanBeEnabled) - newActiveMods.push_back(m); - - if (!modCanBeEnabled && modAffectsGameplay) - missingMods.emplace_back(m, mver.toString()); - } - - if(!missingMods.empty()) - throw Incompatibility(std::move(missingMods)); - - std::swap(activeMods, newActiveMods); - } - - h & identifiers; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 41d2676d9..9cb13671a 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -11,39 +11,25 @@ #include "CPlayerState.h" #include "gameState/QuestInfo.h" +#include "CGeneralTextHandler.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN PlayerState::PlayerState() - : color(-1), human(false), enteredWinningCheatCode(false), + : color(-1), human(false), cheated(false), enteredWinningCheatCode(false), enteredLosingCheatCode(false), status(EPlayerStatus::INGAME) { setNodeType(PLAYER); } -PlayerState::PlayerState(PlayerState && other) noexcept: - CBonusSystemNode(std::move(other)), - color(other.color), - human(other.human), - team(other.team), - resources(other.resources), - enteredWinningCheatCode(other.enteredWinningCheatCode), - enteredLosingCheatCode(other.enteredLosingCheatCode), - status(other.status), - daysWithoutCastle(other.daysWithoutCastle) -{ - std::swap(visitedObjects, other.visitedObjects); - std::swap(heroes, other.heroes); - std::swap(towns, other.towns); - std::swap(dwellings, other.dwellings); - std::swap(quests, other.quests); -} +PlayerState::PlayerState(PlayerState && other) noexcept = default; PlayerState::~PlayerState() = default; std::string PlayerState::nodeName() const { - return "Player " + color.getStrCap(false); + return "Player " + color.toString(); } PlayerColor PlayerState::getId() const @@ -60,18 +46,22 @@ int32_t PlayerState::getIconIndex() const { return color.getNum(); } + std::string PlayerState::getJsonKey() const { - return color.getStr(false); + return color.toString(); } + std::string PlayerState::getNameTranslated() const { - return color.getStr(true); + return VLC->generaltexth->translate(getNameTextID()); } + std::string PlayerState::getNameTextID() const { - return color.getStr(false); + return TextIdentifier("core.plcolors", color.getNum()).get(); } + void PlayerState::registerIcons(const IconRegistar & cb) const { //We cannot register new icons for players diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index cfb99617c..175d6ff21 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -15,6 +15,8 @@ #include "bonuses/Bonus.h" #include "bonuses/CBonusSystemNode.h" #include "ResourceSet.h" +#include "TurnTimerInfo.h" +#include "ConstTransitivePtr.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,20 +27,44 @@ struct QuestInfo; struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player { + struct VisitedObjectGlobal + { + MapObjectID id; + MapObjectSubID subID; + + bool operator < (const VisitedObjectGlobal & other) const + { + if (id != other.id) + return id < other.id; + else + return subID < other.subID; + } + + template void serialize(Handler &h, const int version) + { + h & id; + subID.serializeIdentifier(h, id, version); + } + }; + public: PlayerColor color; bool human; //true if human controlled player, false for AI TeamID team; TResources resources; 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 + bool cheated; bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory - EPlayerStatus::EStatus status; + EPlayerStatus status; std::optional daysWithoutCastle; + TurnTimerInfo turnTimer; PlayerState(); PlayerState(PlayerState && other) noexcept; @@ -71,13 +97,17 @@ public: h & team; h & resources; h & status; + h & turnTimer; h & heroes; h & towns; h & dwellings; h & quests; h & visitedObjects; + h & visitedObjectsGlobal; h & status; h & daysWithoutCastle; + h & cheated; + h & battleBonuses; h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); @@ -90,7 +120,7 @@ public: TeamID id; //position in gameState::teams std::set players; // members of this team //TODO: boost::array, bool if possible - std::shared_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden + std::unique_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden TeamState(); TeamState(TeamState && other) noexcept; diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 4f896444b..657356411 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -13,8 +13,6 @@ VCMI_LIB_NAMESPACE_BEGIN -boost::thread_specific_ptr CRandomGenerator::defaultRand; - CRandomGenerator::CRandomGenerator() { resetSeed(); @@ -84,11 +82,8 @@ double CRandomGenerator::nextDouble() CRandomGenerator & CRandomGenerator::getDefault() { - if(!defaultRand.get()) - { - defaultRand.reset(new CRandomGenerator()); - } - return *defaultRand; + static thread_local CRandomGenerator defaultRand; + return defaultRand; } TGenerator & CRandomGenerator::getStdGenerator() diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 8e27b46fa..cb0360e60 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -14,7 +14,11 @@ VCMI_LIB_NAMESPACE_BEGIN -using TGenerator = std::mt19937; +/// 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 +/// 2. It has very low state size, leading to low overhead in size of saved games (due to large number of random generator instances in game) +using TGenerator = std::minstd_rand; using TIntDist = std::uniform_int_distribution; using TInt64Dist = std::uniform_int_distribution; using TRealDist = std::uniform_real_distribution; @@ -80,7 +84,6 @@ public: private: TGenerator rand; - static boost::thread_specific_ptr defaultRand; public: template diff --git a/lib/CScriptingModule.h b/lib/CScriptingModule.h index f615021a4..b76d60ed5 100644 --- a/lib/CScriptingModule.h +++ b/lib/CScriptingModule.h @@ -1,54 +1,54 @@ -/* - * CScriptingModule.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 - -#if SCRIPTING_ENABLED -#include - -VCMI_LIB_NAMESPACE_BEGIN - -namespace spells -{ - namespace effects - { - class Registry; - } -} - -namespace scripting -{ - -class DLL_LINKAGE ContextBase : public Context -{ -public: - ContextBase(vstd::CLoggerBase * logger_); - virtual ~ContextBase() = default; - -protected: - vstd::CLoggerBase * logger; -}; - -class DLL_LINKAGE Module -{ -public: - Module() = default; - virtual ~Module() = default; - - virtual std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const = 0; - - virtual std::shared_ptr createContextFor(const Script * source, const Environment * env) const = 0; - - virtual void registerSpellEffect(spells::effects::Registry * registry, const Script * source) const = 0; -}; - -} - -VCMI_LIB_NAMESPACE_END -#endif +/* + * CScriptingModule.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 + +#if SCRIPTING_ENABLED +#include + +VCMI_LIB_NAMESPACE_BEGIN + +namespace spells +{ + namespace effects + { + class Registry; + } +} + +namespace scripting +{ + +class DLL_LINKAGE ContextBase : public Context +{ +public: + ContextBase(vstd::CLoggerBase * logger_); + virtual ~ContextBase() = default; + +protected: + vstd::CLoggerBase * logger; +}; + +class DLL_LINKAGE Module +{ +public: + Module() = default; + virtual ~Module() = default; + + virtual std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const = 0; + + virtual std::shared_ptr createContextFor(const Script * source, const Environment * env) const = 0; + + virtual void registerSpellEffect(spells::effects::Registry * registry, const Script * source) const = 0; +}; + +} + +VCMI_LIB_NAMESPACE_END +#endif diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 95b69a2ff..85452226c 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -16,11 +16,13 @@ #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModUtility.h" +#include "modding/ModScope.h" #include "JsonNode.h" -#include "CModHandler.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -75,7 +77,7 @@ void CSkill::registerIcons(const IconRegistar & cb) const { for(int level = 1; level <= 3; level++) { - int frame = 2 + level + 3 * id; + int frame = 2 + level + 3 * id.getNum(); const LevelInfo & skillAtLevel = at(level); cb(frame, 0, "SECSK32", skillAtLevel.iconSmall); cb(frame, 0, "SECSKILL", skillAtLevel.iconMedium); @@ -91,7 +93,7 @@ SecondarySkill CSkill::getId() const void CSkill::addNewBonus(const std::shared_ptr & b, int level) { b->source = BonusSource::SECONDARY_SKILL; - b->sid = id; + b->sid = BonusSourceID(id); b->duration = BonusDuration::PERMANENT; b->description = getNameTranslated(); levels[level-1].effects.push_back(b); @@ -118,7 +120,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInf DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill) { - out << "Skill(" << (int)skill.id << "," << skill.identifier << "): ["; + out << "Skill(" << skill.id.getNum() << "," << skill.identifier << "): ["; for(int i=0; i < skill.levels.size(); i++) out << (i ? "," : "") << skill.levels[i]; return out << "]"; @@ -144,7 +146,7 @@ void CSkill::serializeJson(JsonSerializeFormat & handler) ///CSkillHandler std::vector CSkillHandler::loadLegacyData() { - CLegacyConfigParser parser("DATA/SSTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/SSTRAITS.TXT")); //skip header parser.endLine(); @@ -231,7 +233,7 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & skillAtLevel.iconMedium = levelNode["images"]["medium"].String(); skillAtLevel.iconLarge = levelNode["images"]["large"].String(); } - logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id); + logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum()); return skill; } @@ -254,29 +256,14 @@ void CSkillHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSkillHandler::getDefaultAllowed() const +std::set CSkillHandler::getDefaultAllowed() const { - std::vector allowedSkills(objects.size(), true); - return allowedSkills; -} + std::set result; -si32 CSkillHandler::decodeSkill(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "skill", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} + for (auto const & skill : objects) + result.insert(skill->getId()); -std::string CSkillHandler::encodeSkill(const si32 index) -{ - return (*VLC->skillh)[SecondarySkill(index)]->identifier; -} - -std::string CSkillHandler::encodeSkillWithType(const si32 index) -{ - return CModHandler::makeFullIdentifier("", "skill", encodeSkill(index)); + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index da5c8777b..fe699edcb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -1,129 +1,100 @@ -/* - * CSkillHandler.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 "../lib/bonuses/Bonus.h" -#include "GameConstants.h" -#include "IHandlerBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonSerializeFormat; - -class DLL_LINKAGE CSkill : public Skill -{ -public: - struct LevelInfo - { - std::string iconSmall; - std::string iconMedium; - std::string iconLarge; - std::vector> effects; - - template void serialize(Handler & h, const int version) - { - h & iconSmall; - h & iconMedium; - h & iconLarge; - h & effects; - } - }; - -private: - std::vector levels; // bonuses provided by basic, advanced and expert level - void addNewBonus(const std::shared_ptr & b, int level); - - SecondarySkill id; - std::string modScope; - std::string identifier; - -public: - CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); - ~CSkill() = default; - - enum class Obligatory : ui8 - { - MAJOR = 0, - MINOR = 1, - }; - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - SecondarySkill getId() const override; - - std::string getNameTextID() const override; - std::string getNameTranslated() const override; - - std::string getDescriptionTextID(int level) const override; - std::string getDescriptionTranslated(int level) const override; - - const LevelInfo & at(int level) const; - LevelInfo & at(int level); - - std::string toString() const; - bool obligatory(Obligatory val) const { return val == Obligatory::MAJOR ? obligatoryMajor : obligatoryMinor; }; - - std::array gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - bool onlyOnWaterMap; - - template void serialize(Handler & h, const int version) - { - h & id; - h & identifier; - h & gainChance; - h & levels; - h & obligatoryMajor; - h & obligatoryMinor; - h & onlyOnWaterMap; - } - - friend class CSkillHandler; - friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); - friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); -private: - bool obligatoryMajor; - bool obligatoryMinor; -}; - -class DLL_LINKAGE CSkillHandler: public CHandlerBase -{ -public: - ///IHandler base - std::vector loadLegacyData() override; - void afterLoadFinalization() override; - void beforeValidate(JsonNode & object) override; - - std::vector getDefaultAllowed() const override; - - ///json serialization helpers - static si32 decodeSkill(const std::string & identifier); - static std::string encodeSkill(const si32 index); - static std::string encodeSkillWithType(const si32 index); - - template void serialize(Handler & h, const int version) - { - h & objects; - } - -protected: - const std::vector & getTypeNames() const override; - CSkill * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CSkillHandler.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 "../lib/bonuses/Bonus.h" +#include "GameConstants.h" +#include "IHandlerBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonSerializeFormat; + +class DLL_LINKAGE CSkill : public Skill +{ +public: + struct LevelInfo + { + std::string iconSmall; + std::string iconMedium; + std::string iconLarge; + std::vector> effects; + }; + +private: + std::vector levels; // bonuses provided by basic, advanced and expert level + void addNewBonus(const std::shared_ptr & b, int level); + + SecondarySkill id; + std::string modScope; + std::string identifier; + +public: + CSkill(const SecondarySkill & id = SecondarySkill::NONE, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); + ~CSkill() = default; + + enum class Obligatory : ui8 + { + MAJOR = 0, + MINOR = 1, + }; + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + SecondarySkill getId() const override; + + std::string getNameTextID() const override; + std::string getNameTranslated() const override; + + std::string getDescriptionTextID(int level) const override; + std::string getDescriptionTranslated(int level) const override; + + const LevelInfo & at(int level) const; + LevelInfo & at(int level); + + std::string toString() const; + bool obligatory(Obligatory val) const { return val == Obligatory::MAJOR ? obligatoryMajor : obligatoryMinor; }; + + std::array gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + bool onlyOnWaterMap; + + friend class CSkillHandler; + friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); + friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); +private: + bool obligatoryMajor; + bool obligatoryMinor; +}; + +class DLL_LINKAGE CSkillHandler: public CHandlerBase +{ +public: + ///IHandler base + std::vector loadLegacyData() override; + void afterLoadFinalization() override; + void beforeValidate(JsonNode & object) override; + + std::set getDefaultAllowed() const; + +protected: + const std::vector & getTypeNames() const override; + CSkill * 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 99edd485b..881296a10 100644 --- a/lib/CSoundBase.h +++ b/lib/CSoundBase.h @@ -1,1051 +1,1051 @@ -/* - * CSoundBase.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 - -// Use some magic to keep the list of files and their code name in sync. - -#define VCMI_SOUND_LIST \ -/* Sounds for map actions */ \ -VCMI_SOUND_NAME(KillFade) VCMI_SOUND_FILE(KILLFADE.wav) /* hero or monster disappears */ \ -/* Other sounds (TODO: separate out the sounds for units, spells and the rest */ \ -VCMI_SOUND_NAME(AAGLAttack) VCMI_SOUND_FILE(AAGLATTK.wav) \ -VCMI_SOUND_NAME(AAGLDefend) VCMI_SOUND_FILE(AAGLDFND.wav) \ -VCMI_SOUND_NAME(AAGLKill) VCMI_SOUND_FILE(AAGLKILL.wav) \ -VCMI_SOUND_NAME(AAGLMove) VCMI_SOUND_FILE(AAGLMOVE.wav) \ -VCMI_SOUND_NAME(AAGLWNCE) VCMI_SOUND_FILE(AAGLWNCE.wav) \ -VCMI_SOUND_NAME(acid) VCMI_SOUND_FILE(ACID.wav) \ -VCMI_SOUND_NAME(ADVLAttack) VCMI_SOUND_FILE(ADVLATTK.wav) \ -VCMI_SOUND_NAME(ADVLDefend) VCMI_SOUND_FILE(ADVLDFND.wav) \ -VCMI_SOUND_NAME(ADVLEXT1) VCMI_SOUND_FILE(ADVLEXT1.wav) \ -VCMI_SOUND_NAME(ADVLEXT2) VCMI_SOUND_FILE(ADVLEXT2.wav) \ -VCMI_SOUND_NAME(ADVLKill) VCMI_SOUND_FILE(ADVLKILL.wav) \ -VCMI_SOUND_NAME(ADVLMove) VCMI_SOUND_FILE(ADVLMOVE.wav) \ -VCMI_SOUND_NAME(ADVLWNCE) VCMI_SOUND_FILE(ADVLWNCE.wav) \ -VCMI_SOUND_NAME(AELMAttack) VCMI_SOUND_FILE(AELMATTK.wav) \ -VCMI_SOUND_NAME(AELMDefend) VCMI_SOUND_FILE(AELMDFND.wav) \ -VCMI_SOUND_NAME(AELMKill) VCMI_SOUND_FILE(AELMKILL.wav) \ -VCMI_SOUND_NAME(AELMMove) VCMI_SOUND_FILE(AELMMOVE.wav) \ -VCMI_SOUND_NAME(AELMWNCE) VCMI_SOUND_FILE(AELMWNCE.wav) \ -VCMI_SOUND_NAME(AGE) VCMI_SOUND_FILE(AGE.wav) \ -VCMI_SOUND_NAME(AGRMAttack) VCMI_SOUND_FILE(AGRMATTK.wav) \ -VCMI_SOUND_NAME(AGRMDefend) VCMI_SOUND_FILE(AGRMDFND.wav) \ -VCMI_SOUND_NAME(AGRMKill) VCMI_SOUND_FILE(AGRMKILL.wav) \ -VCMI_SOUND_NAME(AGRMMove) VCMI_SOUND_FILE(AGRMMOVE.wav) \ -VCMI_SOUND_NAME(AGRMShot) VCMI_SOUND_FILE(AGRMSHOT.wav) \ -VCMI_SOUND_NAME(AGRMWNCE) VCMI_SOUND_FILE(AGRMWNCE.wav) \ -VCMI_SOUND_NAME(AIRSHELD) VCMI_SOUND_FILE(AIRSHELD.wav) \ -VCMI_SOUND_NAME(ALIZAttack) VCMI_SOUND_FILE(ALIZATTK.wav) \ -VCMI_SOUND_NAME(ALIZDefend) VCMI_SOUND_FILE(ALIZDFND.wav) \ -VCMI_SOUND_NAME(ALIZKill) VCMI_SOUND_FILE(ALIZKILL.wav) \ -VCMI_SOUND_NAME(ALIZMove) VCMI_SOUND_FILE(ALIZMOVE.wav) \ -VCMI_SOUND_NAME(ALIZShot) VCMI_SOUND_FILE(ALIZSHOT.wav) \ -VCMI_SOUND_NAME(ALIZWNCE) VCMI_SOUND_FILE(ALIZWNCE.wav) \ -VCMI_SOUND_NAME(AMAGAttack) VCMI_SOUND_FILE(AMAGATTK.wav) \ -VCMI_SOUND_NAME(AMAGDefend) VCMI_SOUND_FILE(AMAGDFND.wav) \ -VCMI_SOUND_NAME(AMAGKill) VCMI_SOUND_FILE(AMAGKILL.wav) \ -VCMI_SOUND_NAME(AMAGMove) VCMI_SOUND_FILE(AMAGMOVE.wav) \ -VCMI_SOUND_NAME(AMAGShot) VCMI_SOUND_FILE(AMAGSHOT.wav) \ -VCMI_SOUND_NAME(AMAGWNCE) VCMI_SOUND_FILE(AMAGWNCE.wav) \ -VCMI_SOUND_NAME(ANGLAttack) VCMI_SOUND_FILE(ANGLATTK.wav) \ -VCMI_SOUND_NAME(ANGLDefend) VCMI_SOUND_FILE(ANGLDFND.wav) \ -VCMI_SOUND_NAME(ANGLKill) VCMI_SOUND_FILE(ANGLKILL.wav) \ -VCMI_SOUND_NAME(ANGLMove) VCMI_SOUND_FILE(ANGLMOVE.wav) \ -VCMI_SOUND_NAME(ANGLWNCE) VCMI_SOUND_FILE(ANGLWNCE.wav) \ -VCMI_SOUND_NAME(ANIMDEAD) VCMI_SOUND_FILE(ANIMDEAD.wav) \ -VCMI_SOUND_NAME(ANTIMAGK) VCMI_SOUND_FILE(ANTIMAGK.wav) \ -VCMI_SOUND_NAME(APEGAttack) VCMI_SOUND_FILE(APEGATTK.wav) \ -VCMI_SOUND_NAME(APEGDefend) VCMI_SOUND_FILE(APEGDFND.wav) \ -VCMI_SOUND_NAME(APEGKill) VCMI_SOUND_FILE(APEGKILL.wav) \ -VCMI_SOUND_NAME(APEGMove) VCMI_SOUND_FILE(APEGMOVE.wav) \ -VCMI_SOUND_NAME(APEGWNCE) VCMI_SOUND_FILE(APEGWNCE.wav) \ -VCMI_SOUND_NAME(ARMGEDN) VCMI_SOUND_FILE(ARMGEDN.wav) \ -VCMI_SOUND_NAME(AZURAttack) VCMI_SOUND_FILE(AZURATTK.wav) \ -VCMI_SOUND_NAME(AZURDefend) VCMI_SOUND_FILE(AZURDFND.wav) \ -VCMI_SOUND_NAME(AZURKill) VCMI_SOUND_FILE(AZURKILL.wav) \ -VCMI_SOUND_NAME(AZURMove) VCMI_SOUND_FILE(AZURMOVE.wav) \ -VCMI_SOUND_NAME(AZURWNCE) VCMI_SOUND_FILE(AZURWNCE.wav) \ -VCMI_SOUND_NAME(BACKLASH) VCMI_SOUND_FILE(BACKLASH.wav) \ -VCMI_SOUND_NAME(BADLUCK) VCMI_SOUND_FILE(BADLUCK.wav) \ -VCMI_SOUND_NAME(BADMRLE) VCMI_SOUND_FILE(BADMRLE.wav) \ -VCMI_SOUND_NAME(BALLKill) VCMI_SOUND_FILE(BALLKILL.wav) \ -VCMI_SOUND_NAME(BALLShot) VCMI_SOUND_FILE(BALLSHOT.wav) \ -VCMI_SOUND_NAME(BALLWNCE) VCMI_SOUND_FILE(BALLWNCE.wav) \ -VCMI_SOUND_NAME(BASLAttack) VCMI_SOUND_FILE(BASLATTK.wav) \ -VCMI_SOUND_NAME(BASLDefend) VCMI_SOUND_FILE(BASLDFND.wav) \ -VCMI_SOUND_NAME(BASLKill) VCMI_SOUND_FILE(BASLKILL.wav) \ -VCMI_SOUND_NAME(BASLMove) VCMI_SOUND_FILE(BASLMOVE.wav) \ -VCMI_SOUND_NAME(BASLWNCE) VCMI_SOUND_FILE(BASLWNCE.wav) \ -VCMI_SOUND_NAME(battle00) VCMI_SOUND_FILE(BATTLE00.wav) \ -VCMI_SOUND_NAME(battle01) VCMI_SOUND_FILE(BATTLE01.wav) \ -VCMI_SOUND_NAME(battle02) VCMI_SOUND_FILE(BATTLE02.wav) \ -VCMI_SOUND_NAME(battle03) VCMI_SOUND_FILE(BATTLE03.wav) \ -VCMI_SOUND_NAME(battle04) VCMI_SOUND_FILE(BATTLE04.wav) \ -VCMI_SOUND_NAME(battle05) VCMI_SOUND_FILE(BATTLE05.wav) \ -VCMI_SOUND_NAME(battle06) VCMI_SOUND_FILE(BATTLE06.wav) \ -VCMI_SOUND_NAME(battle07) VCMI_SOUND_FILE(BATTLE07.wav) \ -VCMI_SOUND_NAME(BDRFAttack) VCMI_SOUND_FILE(BDRFATTK.wav) \ -VCMI_SOUND_NAME(BDRFDefend) VCMI_SOUND_FILE(BDRFDFND.wav) \ -VCMI_SOUND_NAME(BDRFKill) VCMI_SOUND_FILE(BDRFKILL.wav) \ -VCMI_SOUND_NAME(BDRFMove) VCMI_SOUND_FILE(BDRFMOVE.wav) \ -VCMI_SOUND_NAME(BDRFWNCE) VCMI_SOUND_FILE(BDRFWNCE.wav) \ -VCMI_SOUND_NAME(BERSERK) VCMI_SOUND_FILE(BERSERK.wav) \ -VCMI_SOUND_NAME(BGORAttack) VCMI_SOUND_FILE(BGORATTK.wav) \ -VCMI_SOUND_NAME(BGORDefend) VCMI_SOUND_FILE(BGORDFND.wav) \ -VCMI_SOUND_NAME(BGORKill) VCMI_SOUND_FILE(BGORKILL.wav) \ -VCMI_SOUND_NAME(BGORMove) VCMI_SOUND_FILE(BGORMOVE.wav) \ -VCMI_SOUND_NAME(BGORWNCE) VCMI_SOUND_FILE(BGORWNCE.wav) \ -VCMI_SOUND_NAME(BHDRAttack) VCMI_SOUND_FILE(BHDRATTK.wav) \ -VCMI_SOUND_NAME(BHDRDETH) VCMI_SOUND_FILE(BHDRDETH.wav) \ -VCMI_SOUND_NAME(BHDRDefend) VCMI_SOUND_FILE(BHDRDFND.wav) \ -VCMI_SOUND_NAME(BHDRKill) VCMI_SOUND_FILE(BHDRKILL.wav) \ -VCMI_SOUND_NAME(BHDRMove) VCMI_SOUND_FILE(BHDRMOVE.wav) \ -VCMI_SOUND_NAME(BHDRShot) VCMI_SOUND_FILE(BHDRSHOT.wav) \ -VCMI_SOUND_NAME(BHDRWNCE) VCMI_SOUND_FILE(BHDRWNCE.wav) \ -VCMI_SOUND_NAME(BIND) VCMI_SOUND_FILE(BIND.wav) \ -VCMI_SOUND_NAME(BKDRAttack) VCMI_SOUND_FILE(BKDRATTK.wav) \ -VCMI_SOUND_NAME(BKDRDefend) VCMI_SOUND_FILE(BKDRDFND.wav) \ -VCMI_SOUND_NAME(BKDRKill) VCMI_SOUND_FILE(BKDRKILL.wav) \ -VCMI_SOUND_NAME(BKDRMove) VCMI_SOUND_FILE(BKDRMOVE.wav) \ -VCMI_SOUND_NAME(BKDRWNCE) VCMI_SOUND_FILE(BKDRWNCE.wav) \ -VCMI_SOUND_NAME(BKNTAttack) VCMI_SOUND_FILE(BKNTATTK.wav) \ -VCMI_SOUND_NAME(BKNTDefend) VCMI_SOUND_FILE(BKNTDFND.wav) \ -VCMI_SOUND_NAME(BKNTKill) VCMI_SOUND_FILE(BKNTKILL.wav) \ -VCMI_SOUND_NAME(BKNTMove) VCMI_SOUND_FILE(BKNTMOVE.wav) \ -VCMI_SOUND_NAME(BKNTWNCE) VCMI_SOUND_FILE(BKNTWNCE.wav) \ -VCMI_SOUND_NAME(bless) VCMI_SOUND_FILE(BLESS.wav) \ -VCMI_SOUND_NAME(blind) VCMI_SOUND_FILE(BLIND.wav) \ -VCMI_SOUND_NAME(bloodlus) VCMI_SOUND_FILE(BLOODLUS.wav) \ -VCMI_SOUND_NAME(BLRDAttack) VCMI_SOUND_FILE(BLRDATTK.wav) \ -VCMI_SOUND_NAME(BLRDDefend) VCMI_SOUND_FILE(BLRDDFND.wav) \ -VCMI_SOUND_NAME(BLRDKill) VCMI_SOUND_FILE(BLRDKILL.wav) \ -VCMI_SOUND_NAME(BLRDMove) VCMI_SOUND_FILE(BLRDMOVE.wav) \ -VCMI_SOUND_NAME(BLRDWNCE) VCMI_SOUND_FILE(BLRDWNCE.wav) \ -VCMI_SOUND_NAME(BMTHAttack) VCMI_SOUND_FILE(BMTHATTK.wav) \ -VCMI_SOUND_NAME(BMTHDefend) VCMI_SOUND_FILE(BMTHDFND.wav) \ -VCMI_SOUND_NAME(BMTHKill) VCMI_SOUND_FILE(BMTHKILL.wav) \ -VCMI_SOUND_NAME(BMTHMove) VCMI_SOUND_FILE(BMTHMOVE.wav) \ -VCMI_SOUND_NAME(BMTHWNCE) VCMI_SOUND_FILE(BMTHWNCE.wav) \ -VCMI_SOUND_NAME(BOARAttack) VCMI_SOUND_FILE(BOARATTK.wav) \ -VCMI_SOUND_NAME(BOARDefend) VCMI_SOUND_FILE(BOARDFND.wav) \ -VCMI_SOUND_NAME(BOARKill) VCMI_SOUND_FILE(BOARKILL.wav) \ -VCMI_SOUND_NAME(BOARMove) VCMI_SOUND_FILE(BOARMOVE.wav) \ -VCMI_SOUND_NAME(BOARWNCE) VCMI_SOUND_FILE(BOARWNCE.wav) \ -VCMI_SOUND_NAME(BODRAttack) VCMI_SOUND_FILE(BODRATTK.wav) \ -VCMI_SOUND_NAME(BODRDefend) VCMI_SOUND_FILE(BODRDFND.wav) \ -VCMI_SOUND_NAME(BODRKill) VCMI_SOUND_FILE(BODRKILL.wav) \ -VCMI_SOUND_NAME(BODRMove) VCMI_SOUND_FILE(BODRMOVE.wav) \ -VCMI_SOUND_NAME(BODRWNCE) VCMI_SOUND_FILE(BODRWNCE.wav) \ -VCMI_SOUND_NAME(BTREAttack) VCMI_SOUND_FILE(BTREATTK.wav) \ -VCMI_SOUND_NAME(BTREDefend) VCMI_SOUND_FILE(BTREDFND.wav) \ -VCMI_SOUND_NAME(BTREKill) VCMI_SOUND_FILE(BTREKILL.wav) \ -VCMI_SOUND_NAME(BTREMove) VCMI_SOUND_FILE(BTREMOVE.wav) \ -VCMI_SOUND_NAME(BTREWNCE) VCMI_SOUND_FILE(BTREWNCE.wav) \ -VCMI_SOUND_NAME(newBuilding) VCMI_SOUND_FILE(BUILDTWN.wav) \ -VCMI_SOUND_NAME(button) VCMI_SOUND_FILE(BUTTON.wav) \ -VCMI_SOUND_NAME(CALFAttack) VCMI_SOUND_FILE(CALFATTK.wav) \ -VCMI_SOUND_NAME(CALFDefend) VCMI_SOUND_FILE(CALFDFND.wav) \ -VCMI_SOUND_NAME(CALFKill) VCMI_SOUND_FILE(CALFKILL.wav) \ -VCMI_SOUND_NAME(CALFMove) VCMI_SOUND_FILE(CALFMOVE.wav) \ -VCMI_SOUND_NAME(CALFShot) VCMI_SOUND_FILE(CALFSHOT.wav) \ -VCMI_SOUND_NAME(CALFWNCE) VCMI_SOUND_FILE(CALFWNCE.wav) \ -VCMI_SOUND_NAME(CARTKill) VCMI_SOUND_FILE(CARTKILL.wav) \ -VCMI_SOUND_NAME(CARTWNCE) VCMI_SOUND_FILE(CARTWNCE.wav) \ -VCMI_SOUND_NAME(CATAKill) VCMI_SOUND_FILE(CATAKILL.wav) \ -VCMI_SOUND_NAME(CATAShot) VCMI_SOUND_FILE(CATASHOT.wav) \ -VCMI_SOUND_NAME(CATAWNCE) VCMI_SOUND_FILE(CATAWNCE.wav) \ -VCMI_SOUND_NAME(CAVAAttack) VCMI_SOUND_FILE(CAVAATTK.wav) \ -VCMI_SOUND_NAME(CAVADefend) VCMI_SOUND_FILE(CAVADFND.wav) \ -VCMI_SOUND_NAME(CAVAKill) VCMI_SOUND_FILE(CAVAKILL.wav) \ -VCMI_SOUND_NAME(CAVAMove) VCMI_SOUND_FILE(CAVAMOVE.wav) \ -VCMI_SOUND_NAME(CAVAWNCE) VCMI_SOUND_FILE(CAVAWNCE.wav) \ -VCMI_SOUND_NAME(CAVEHEAD) VCMI_SOUND_FILE(CAVEHEAD.wav) \ -VCMI_SOUND_NAME(CCYCAttack) VCMI_SOUND_FILE(CCYCATTK.wav) \ -VCMI_SOUND_NAME(CCYCDefend) VCMI_SOUND_FILE(CCYCDFND.wav) \ -VCMI_SOUND_NAME(CCYCKill) VCMI_SOUND_FILE(CCYCKILL.wav) \ -VCMI_SOUND_NAME(CCYCMove) VCMI_SOUND_FILE(CCYCMOVE.wav) \ -VCMI_SOUND_NAME(CCYCShot) VCMI_SOUND_FILE(CCYCSHOT.wav) \ -VCMI_SOUND_NAME(CCYCWNCE) VCMI_SOUND_FILE(CCYCWNCE.wav) \ -VCMI_SOUND_NAME(CERBAttack) VCMI_SOUND_FILE(CERBATTK.wav) \ -VCMI_SOUND_NAME(CERBDefend) VCMI_SOUND_FILE(CERBDFND.wav) \ -VCMI_SOUND_NAME(CERBKill) VCMI_SOUND_FILE(CERBKILL.wav) \ -VCMI_SOUND_NAME(CERBMove) VCMI_SOUND_FILE(CERBMOVE.wav) \ -VCMI_SOUND_NAME(CERBWNCE) VCMI_SOUND_FILE(CERBWNCE.wav) \ -VCMI_SOUND_NAME(CGORAttack) VCMI_SOUND_FILE(CGORATTK.wav) \ -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(chat) VCMI_SOUND_FILE(CHAT.wav) \ -VCMI_SOUND_NAME(chest) VCMI_SOUND_FILE(CHEST.wav) \ -VCMI_SOUND_NAME(CHMPAttack) VCMI_SOUND_FILE(CHMPATTK.wav) \ -VCMI_SOUND_NAME(CHMPDefend) VCMI_SOUND_FILE(CHMPDFND.wav) \ -VCMI_SOUND_NAME(CHMPKill) VCMI_SOUND_FILE(CHMPKILL.wav) \ -VCMI_SOUND_NAME(CHMPMove) VCMI_SOUND_FILE(CHMPMOVE.wav) \ -VCMI_SOUND_NAME(CHMPWNCE) VCMI_SOUND_FILE(CHMPWNCE.wav) \ -VCMI_SOUND_NAME(CHYDAttack) VCMI_SOUND_FILE(CHYDATTK.wav) \ -VCMI_SOUND_NAME(CHYDDefend) VCMI_SOUND_FILE(CHYDDFND.wav) \ -VCMI_SOUND_NAME(CHYDKill) VCMI_SOUND_FILE(CHYDKILL.wav) \ -VCMI_SOUND_NAME(CHYDMove) VCMI_SOUND_FILE(CHYDMOVE.wav) \ -VCMI_SOUND_NAME(CHYDWNCE) VCMI_SOUND_FILE(CHYDWNCE.wav) \ -VCMI_SOUND_NAME(CLIMAX) VCMI_SOUND_FILE(CLIMAX.wav) \ -VCMI_SOUND_NAME(CLONE) VCMI_SOUND_FILE(CLONE.wav) \ -VCMI_SOUND_NAME(CNTRAttack) VCMI_SOUND_FILE(CNTRATTK.wav) \ -VCMI_SOUND_NAME(CNTRDefend) VCMI_SOUND_FILE(CNTRDFND.wav) \ -VCMI_SOUND_NAME(CNTRKill) VCMI_SOUND_FILE(CNTRKILL.wav) \ -VCMI_SOUND_NAME(CNTRMove) VCMI_SOUND_FILE(CNTRMOVE.wav) \ -VCMI_SOUND_NAME(CNTRShot) VCMI_SOUND_FILE(CNTRSHOT.wav) \ -VCMI_SOUND_NAME(Counterstrike) VCMI_SOUND_FILE(CNTRSTRK.wav) \ -VCMI_SOUND_NAME(CNTRWNCE) VCMI_SOUND_FILE(CNTRWNCE.wav) \ -VCMI_SOUND_NAME(COLDRAY) VCMI_SOUND_FILE(COLDRAY.wav) \ -VCMI_SOUND_NAME(COLDRING) VCMI_SOUND_FILE(COLDRING.wav) \ -VCMI_SOUND_NAME(CRUSAttack) VCMI_SOUND_FILE(CRUSATTK.wav) \ -VCMI_SOUND_NAME(CRUSDefend) VCMI_SOUND_FILE(CRUSDFND.wav) \ -VCMI_SOUND_NAME(CRUSKill) VCMI_SOUND_FILE(CRUSKILL.wav) \ -VCMI_SOUND_NAME(CRUSMove) VCMI_SOUND_FILE(CRUSMOVE.wav) \ -VCMI_SOUND_NAME(CRUSWNCE) VCMI_SOUND_FILE(CRUSWNCE.wav) \ -VCMI_SOUND_NAME(CRYSAttack) VCMI_SOUND_FILE(CRYSATTK.wav) \ -VCMI_SOUND_NAME(CRYSDefend) VCMI_SOUND_FILE(CRYSDFND.wav) \ -VCMI_SOUND_NAME(CRYSKill) VCMI_SOUND_FILE(CRYSKILL.wav) \ -VCMI_SOUND_NAME(CRYSMove) VCMI_SOUND_FILE(CRYSMOVE.wav) \ -VCMI_SOUND_NAME(CRYSWNCE) VCMI_SOUND_FILE(CRYSWNCE.wav) \ -VCMI_SOUND_NAME(CURE) VCMI_SOUND_FILE(CURE.wav) \ -VCMI_SOUND_NAME(CURSE) VCMI_SOUND_FILE(CURSE.wav) \ -VCMI_SOUND_NAME(cyclopAttack) VCMI_SOUND_FILE(CYCLATTK.wav) \ -VCMI_SOUND_NAME(cyclopDefend) VCMI_SOUND_FILE(CYCLDFND.wav) \ -VCMI_SOUND_NAME(cyclopKill) VCMI_SOUND_FILE(CYCLKILL.wav) \ -VCMI_SOUND_NAME(cyclopMove) VCMI_SOUND_FILE(CYCLMOVE.wav) \ -VCMI_SOUND_NAME(cyclopShot) VCMI_SOUND_FILE(CYCLSHOT.wav) \ -VCMI_SOUND_NAME(cyclopWNCE) VCMI_SOUND_FILE(CYCLWNCE.wav) \ -VCMI_SOUND_NAME(DANGER) VCMI_SOUND_FILE(DANGER.wav) \ -VCMI_SOUND_NAME(deathBlow) VCMI_SOUND_FILE(DEATHBLO.wav) \ -VCMI_SOUND_NAME(deathCloud) VCMI_SOUND_FILE(DEATHCLD.wav) \ -VCMI_SOUND_NAME(deathRIP) VCMI_SOUND_FILE(DEATHRIP.wav) \ -VCMI_SOUND_NAME(deathSTR) VCMI_SOUND_FILE(DEATHSTR.wav) \ -VCMI_SOUND_NAME(DECAY) VCMI_SOUND_FILE(DECAY.wav) \ -VCMI_SOUND_NAME(DEFAULT) VCMI_SOUND_FILE(DEFAULT.wav) \ -VCMI_SOUND_NAME(DEVLAttack) VCMI_SOUND_FILE(DEVLATTK.wav) \ -VCMI_SOUND_NAME(DEVLDefend) VCMI_SOUND_FILE(DEVLDFND.wav) \ -VCMI_SOUND_NAME(DEVLEXT1) VCMI_SOUND_FILE(DEVLEXT1.wav) \ -VCMI_SOUND_NAME(DEVLEXT2) VCMI_SOUND_FILE(DEVLEXT2.wav) \ -VCMI_SOUND_NAME(DEVLKill) VCMI_SOUND_FILE(DEVLKILL.wav) \ -VCMI_SOUND_NAME(DEVLMove) VCMI_SOUND_FILE(DEVLMOVE.wav) \ -VCMI_SOUND_NAME(DEVLWNCE) VCMI_SOUND_FILE(DEVLWNCE.wav) \ -VCMI_SOUND_NAME(DFLYAttack) VCMI_SOUND_FILE(DFLYATTK.wav) \ -VCMI_SOUND_NAME(DFLYDefend) VCMI_SOUND_FILE(DFLYDFND.wav) \ -VCMI_SOUND_NAME(DFLYKill) VCMI_SOUND_FILE(DFLYKILL.wav) \ -VCMI_SOUND_NAME(DFLYMove) VCMI_SOUND_FILE(DFLYMOVE.wav) \ -VCMI_SOUND_NAME(DFLYWNCE) VCMI_SOUND_FILE(DFLYWNCE.wav) \ -VCMI_SOUND_NAME(DGLMAttack) VCMI_SOUND_FILE(DGLMATTK.wav) \ -VCMI_SOUND_NAME(DGLMDefend) VCMI_SOUND_FILE(DGLMDFND.wav) \ -VCMI_SOUND_NAME(DGLMKill) VCMI_SOUND_FILE(DGLMKILL.wav) \ -VCMI_SOUND_NAME(DGLMMove) VCMI_SOUND_FILE(DGLMMOVE.wav) \ -VCMI_SOUND_NAME(DGLMWNCE) VCMI_SOUND_FILE(DGLMWNCE.wav) \ -VCMI_SOUND_NAME(DHDMAttack) VCMI_SOUND_FILE(DHDMATTK.wav) \ -VCMI_SOUND_NAME(DHDMDefend) VCMI_SOUND_FILE(DHDMDFND.wav) \ -VCMI_SOUND_NAME(DHDMKill) VCMI_SOUND_FILE(DHDMKILL.wav) \ -VCMI_SOUND_NAME(DHDMMove) VCMI_SOUND_FILE(DHDMMOVE.wav) \ -VCMI_SOUND_NAME(DHDMWNCE) VCMI_SOUND_FILE(DHDMWNCE.wav) \ -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(DISRUPTR) VCMI_SOUND_FILE(DISRUPTR.wav) \ -VCMI_SOUND_NAME(dragonHall) VCMI_SOUND_FILE(DRAGON.wav) \ -VCMI_SOUND_NAME(DRAINLIF) VCMI_SOUND_FILE(DRAINLIF.wav) \ -VCMI_SOUND_NAME(DRAWBRG) VCMI_SOUND_FILE(DRAWBRG.wav) \ -VCMI_SOUND_NAME(DRGNSLAY) VCMI_SOUND_FILE(DRGNSLAY.wav) \ -VCMI_SOUND_NAME(DWRFAttack) VCMI_SOUND_FILE(DWRFATTK.wav) \ -VCMI_SOUND_NAME(DWRFDefend) VCMI_SOUND_FILE(DWRFDFND.wav) \ -VCMI_SOUND_NAME(DWRFKill) VCMI_SOUND_FILE(DWRFKILL.wav) \ -VCMI_SOUND_NAME(DWRFMove) VCMI_SOUND_FILE(DWRFMOVE.wav) \ -VCMI_SOUND_NAME(DWRFWNCE) VCMI_SOUND_FILE(DWRFWNCE.wav) \ -VCMI_SOUND_NAME(ECNTAttack) VCMI_SOUND_FILE(ECNTATTK.wav) \ -VCMI_SOUND_NAME(ECNTDefend) VCMI_SOUND_FILE(ECNTDFND.wav) \ -VCMI_SOUND_NAME(ECNTKill) VCMI_SOUND_FILE(ECNTKILL.wav) \ -VCMI_SOUND_NAME(ECNTMove) VCMI_SOUND_FILE(ECNTMOVE.wav) \ -VCMI_SOUND_NAME(ECNTWNCE) VCMI_SOUND_FILE(ECNTWNCE.wav) \ -VCMI_SOUND_NAME(EELMAttack) VCMI_SOUND_FILE(EELMATTK.wav) \ -VCMI_SOUND_NAME(EELMDefend) VCMI_SOUND_FILE(EELMDFND.wav) \ -VCMI_SOUND_NAME(EELMKill) VCMI_SOUND_FILE(EELMKILL.wav) \ -VCMI_SOUND_NAME(EELMMove) VCMI_SOUND_FILE(EELMMOVE.wav) \ -VCMI_SOUND_NAME(EELMWNCE) VCMI_SOUND_FILE(EELMWNCE.wav) \ -VCMI_SOUND_NAME(EFRTAttack) VCMI_SOUND_FILE(EFRTATTK.wav) \ -VCMI_SOUND_NAME(EFRTDefend) VCMI_SOUND_FILE(EFRTDFND.wav) \ -VCMI_SOUND_NAME(EFRTKill) VCMI_SOUND_FILE(EFRTKILL.wav) \ -VCMI_SOUND_NAME(EFRTMove) VCMI_SOUND_FILE(EFRTMOVE.wav) \ -VCMI_SOUND_NAME(EFRTWNCE) VCMI_SOUND_FILE(EFRTWNCE.wav) \ -VCMI_SOUND_NAME(ENCHAttack) VCMI_SOUND_FILE(ENCHATTK.wav) \ -VCMI_SOUND_NAME(ENCHDefend) VCMI_SOUND_FILE(ENCHDFND.wav) \ -VCMI_SOUND_NAME(ENCHKill) VCMI_SOUND_FILE(ENCHKILL.wav) \ -VCMI_SOUND_NAME(ENCHMove) VCMI_SOUND_FILE(ENCHMOVE.wav) \ -VCMI_SOUND_NAME(ENCHShot) VCMI_SOUND_FILE(ENCHSHOT.wav) \ -VCMI_SOUND_NAME(ENCHWNCE) VCMI_SOUND_FILE(ENCHWNCE.wav) \ -VCMI_SOUND_NAME(ENERAttack) VCMI_SOUND_FILE(ENERATTK.wav) \ -VCMI_SOUND_NAME(ENERDefend) VCMI_SOUND_FILE(ENERDFND.wav) \ -VCMI_SOUND_NAME(ENERKill) VCMI_SOUND_FILE(ENERKILL.wav) \ -VCMI_SOUND_NAME(ENERMove) VCMI_SOUND_FILE(ENERMOVE.wav) \ -VCMI_SOUND_NAME(ENERWNCE) VCMI_SOUND_FILE(ENERWNCE.wav) \ -VCMI_SOUND_NAME(ERTHQUAK) VCMI_SOUND_FILE(ERTHQUAK.wav) \ -VCMI_SOUND_NAME(ESULAttack) VCMI_SOUND_FILE(ESULATTK.wav) \ -VCMI_SOUND_NAME(ESULDefend) VCMI_SOUND_FILE(ESULDFND.wav) \ -VCMI_SOUND_NAME(ESULKill) VCMI_SOUND_FILE(ESULKILL.wav) \ -VCMI_SOUND_NAME(ESULMove) VCMI_SOUND_FILE(ESULMOVE.wav) \ -VCMI_SOUND_NAME(ESULShot) VCMI_SOUND_FILE(ESULSHOT.wav) \ -VCMI_SOUND_NAME(ESULWNCE) VCMI_SOUND_FILE(ESULWNCE.wav) \ -VCMI_SOUND_NAME(EVLIAttack) VCMI_SOUND_FILE(EVLIATTK.wav) \ -VCMI_SOUND_NAME(EVLIDETH) VCMI_SOUND_FILE(EVLIDETH.wav) \ -VCMI_SOUND_NAME(EVLIDefend) VCMI_SOUND_FILE(EVLIDFND.wav) \ -VCMI_SOUND_NAME(EVLIKill) VCMI_SOUND_FILE(EVLIKILL.wav) \ -VCMI_SOUND_NAME(EVLIMove) VCMI_SOUND_FILE(EVLIMOVE.wav) \ -VCMI_SOUND_NAME(EVLIShot) VCMI_SOUND_FILE(EVLISHOT.wav) \ -VCMI_SOUND_NAME(EVLIWNCE) VCMI_SOUND_FILE(EVLIWNCE.wav) \ -VCMI_SOUND_NAME(experience) VCMI_SOUND_FILE(EXPERNCE.wav) \ -VCMI_SOUND_NAME(FAERAttack) VCMI_SOUND_FILE(FAERATTK.wav) \ -VCMI_SOUND_NAME(FAERDefend) VCMI_SOUND_FILE(FAERDFND.wav) \ -VCMI_SOUND_NAME(faerie) VCMI_SOUND_FILE(FAERIE.wav) \ -VCMI_SOUND_NAME(FAERKill) VCMI_SOUND_FILE(FAERKILL.wav) \ -VCMI_SOUND_NAME(FAERMove) VCMI_SOUND_FILE(FAERMOVE.wav) \ -VCMI_SOUND_NAME(FAERShot) VCMI_SOUND_FILE(FAERSHOT.wav) \ -VCMI_SOUND_NAME(FAERWNCE) VCMI_SOUND_FILE(FAERWNCE.wav) \ -VCMI_SOUND_NAME(FAIDKill) VCMI_SOUND_FILE(FAIDKILL.wav) \ -VCMI_SOUND_NAME(FAIDWNCE) VCMI_SOUND_FILE(FAIDWNCE.wav) \ -VCMI_SOUND_NAME(FDFLAttack) VCMI_SOUND_FILE(FDFLATTK.wav) \ -VCMI_SOUND_NAME(FDFLDefend) VCMI_SOUND_FILE(FDFLDFND.wav) \ -VCMI_SOUND_NAME(FDFLKill) VCMI_SOUND_FILE(FDFLKILL.wav) \ -VCMI_SOUND_NAME(FDFLMove) VCMI_SOUND_FILE(FDFLMOVE.wav) \ -VCMI_SOUND_NAME(FDFLShot) VCMI_SOUND_FILE(FDFLSHOT.wav) \ -VCMI_SOUND_NAME(FDFLWNCE) VCMI_SOUND_FILE(FDFLWNCE.wav) \ -VCMI_SOUND_NAME(FEAR) VCMI_SOUND_FILE(FEAR.wav) \ -VCMI_SOUND_NAME(FELMAttack) VCMI_SOUND_FILE(FELMATTK.wav) \ -VCMI_SOUND_NAME(FELMDefend) VCMI_SOUND_FILE(FELMDFND.wav) \ -VCMI_SOUND_NAME(FELMKill) VCMI_SOUND_FILE(FELMKILL.wav) \ -VCMI_SOUND_NAME(FELMMove) VCMI_SOUND_FILE(FELMMOVE.wav) \ -VCMI_SOUND_NAME(FELMWNCE) VCMI_SOUND_FILE(FELMWNCE.wav) \ -VCMI_SOUND_NAME(FIRBAttack) VCMI_SOUND_FILE(FIRBATTK.wav) \ -VCMI_SOUND_NAME(FIRBDefend) VCMI_SOUND_FILE(FIRBDFND.wav) \ -VCMI_SOUND_NAME(FIRBKill) VCMI_SOUND_FILE(FIRBKILL.wav) \ -VCMI_SOUND_NAME(FIRBMove) VCMI_SOUND_FILE(FIRBMOVE.wav) \ -VCMI_SOUND_NAME(FIRBWNCE) VCMI_SOUND_FILE(FIRBWNCE.wav) \ -VCMI_SOUND_NAME(fireball) VCMI_SOUND_FILE(FIREBALL.wav) \ -VCMI_SOUND_NAME(fireblast) VCMI_SOUND_FILE(FIREBLST.wav) \ -VCMI_SOUND_NAME(FIRESHIE) VCMI_SOUND_FILE(FIRESHIE.wav) \ -VCMI_SOUND_NAME(FIRESHLD) VCMI_SOUND_FILE(FIRESHLD.wav) \ -VCMI_SOUND_NAME(fireStorm) VCMI_SOUND_FILE(FIRESTRM.wav) \ -VCMI_SOUND_NAME(fireWall) VCMI_SOUND_FILE(FIREWALL.wav) \ -VCMI_SOUND_NAME(FLAGMINE) VCMI_SOUND_FILE(FLAGMINE.wav) \ -VCMI_SOUND_NAME(FLYSPELL) VCMI_SOUND_FILE(FLYSPELL.wav) \ -VCMI_SOUND_NAME(FMLRAttack) VCMI_SOUND_FILE(FMLRATTK.wav) \ -VCMI_SOUND_NAME(FMLRDefend) VCMI_SOUND_FILE(FMLRDFND.wav) \ -VCMI_SOUND_NAME(FMLRKill) VCMI_SOUND_FILE(FMLRKILL.wav) \ -VCMI_SOUND_NAME(FMLRMove) VCMI_SOUND_FILE(FMLRMOVE.wav) \ -VCMI_SOUND_NAME(FMLRWNCE) VCMI_SOUND_FILE(FMLRWNCE.wav) \ -VCMI_SOUND_NAME(FORCEFLD) VCMI_SOUND_FILE(FORCEFLD.wav) \ -VCMI_SOUND_NAME(FORGET) VCMI_SOUND_FILE(FORGET.wav) \ -VCMI_SOUND_NAME(FORTUNE) VCMI_SOUND_FILE(FORTUNE.wav) \ -VCMI_SOUND_NAME(FRENZY) VCMI_SOUND_FILE(FRENZY.wav) \ -VCMI_SOUND_NAME(FROSTING) VCMI_SOUND_FILE(FROSTING.wav) \ -VCMI_SOUND_NAME(gazebo) VCMI_SOUND_FILE(GAZEBO.wav) \ -VCMI_SOUND_NAME(GBASAttack) VCMI_SOUND_FILE(GBASATTK.wav) \ -VCMI_SOUND_NAME(GBASDefend) VCMI_SOUND_FILE(GBASDFND.wav) \ -VCMI_SOUND_NAME(GBASKill) VCMI_SOUND_FILE(GBASKILL.wav) \ -VCMI_SOUND_NAME(GBASMove) VCMI_SOUND_FILE(GBASMOVE.wav) \ -VCMI_SOUND_NAME(GBASWNCE) VCMI_SOUND_FILE(GBASWNCE.wav) \ -VCMI_SOUND_NAME(GBLNAttack) VCMI_SOUND_FILE(GBLNATTK.wav) \ -VCMI_SOUND_NAME(GBLNDefend) VCMI_SOUND_FILE(GBLNDFND.wav) \ -VCMI_SOUND_NAME(GBLNKill) VCMI_SOUND_FILE(GBLNKILL.wav) \ -VCMI_SOUND_NAME(GBLNMove) VCMI_SOUND_FILE(GBLNMOVE.wav) \ -VCMI_SOUND_NAME(GBLNWNCE) VCMI_SOUND_FILE(GBLNWNCE.wav) \ -VCMI_SOUND_NAME(GELFAttack) VCMI_SOUND_FILE(GELFATTK.wav) \ -VCMI_SOUND_NAME(GELFDefend) VCMI_SOUND_FILE(GELFDFND.wav) \ -VCMI_SOUND_NAME(GELFKill) VCMI_SOUND_FILE(GELFKILL.wav) \ -VCMI_SOUND_NAME(GELFMove) VCMI_SOUND_FILE(GELFMOVE.wav) \ -VCMI_SOUND_NAME(GELFShot) VCMI_SOUND_FILE(GELFSHOT.wav) \ -VCMI_SOUND_NAME(GELFWNCE) VCMI_SOUND_FILE(GELFWNCE.wav) \ -VCMI_SOUND_NAME(GENIAttack) VCMI_SOUND_FILE(GENIATTK.wav) \ -VCMI_SOUND_NAME(GENIDefend) VCMI_SOUND_FILE(GENIDFND.wav) \ -VCMI_SOUND_NAME(GENIE) VCMI_SOUND_FILE(GENIE.wav) \ -VCMI_SOUND_NAME(GENIKill) VCMI_SOUND_FILE(GENIKILL.wav) \ -VCMI_SOUND_NAME(GENIMove) VCMI_SOUND_FILE(GENIMOVE.wav) \ -VCMI_SOUND_NAME(GENIWNCE) VCMI_SOUND_FILE(GENIWNCE.wav) \ -VCMI_SOUND_NAME(GETPROTECTION) VCMI_SOUND_FILE(GETPROTECTION.wav) \ -VCMI_SOUND_NAME(GGLMAttack) VCMI_SOUND_FILE(GGLMATTK.wav) \ -VCMI_SOUND_NAME(GGLMDefend) VCMI_SOUND_FILE(GGLMDFND.wav) \ -VCMI_SOUND_NAME(GGLMKill) VCMI_SOUND_FILE(GGLMKILL.wav) \ -VCMI_SOUND_NAME(GGLMMove) VCMI_SOUND_FILE(GGLMMOVE.wav) \ -VCMI_SOUND_NAME(GGLMWNCE) VCMI_SOUND_FILE(GGLMWNCE.wav) \ -VCMI_SOUND_NAME(GHDRAttack) VCMI_SOUND_FILE(GHDRATTK.wav) \ -VCMI_SOUND_NAME(GHDRDefend) VCMI_SOUND_FILE(GHDRDFND.wav) \ -VCMI_SOUND_NAME(GHDRKill) VCMI_SOUND_FILE(GHDRKILL.wav) \ -VCMI_SOUND_NAME(GHDRMove) VCMI_SOUND_FILE(GHDRMOVE.wav) \ -VCMI_SOUND_NAME(GHDRWNCE) VCMI_SOUND_FILE(GHDRWNCE.wav) \ -VCMI_SOUND_NAME(GNLMAttack) VCMI_SOUND_FILE(GNLMATTK.wav) \ -VCMI_SOUND_NAME(GNLMDefend) VCMI_SOUND_FILE(GNLMDFND.wav) \ -VCMI_SOUND_NAME(GNLMKill) VCMI_SOUND_FILE(GNLMKILL.wav) \ -VCMI_SOUND_NAME(GNLMMove) VCMI_SOUND_FILE(GNLMMOVE.wav) \ -VCMI_SOUND_NAME(GNLMWNCE) VCMI_SOUND_FILE(GNLMWNCE.wav) \ -VCMI_SOUND_NAME(GNOLAttack) VCMI_SOUND_FILE(GNOLATTK.wav) \ -VCMI_SOUND_NAME(GNOLDefend) VCMI_SOUND_FILE(GNOLDFND.wav) \ -VCMI_SOUND_NAME(GNOLKill) VCMI_SOUND_FILE(GNOLKILL.wav) \ -VCMI_SOUND_NAME(GNOLMove) VCMI_SOUND_FILE(GNOLMOVE.wav) \ -VCMI_SOUND_NAME(GNOLWNCE) VCMI_SOUND_FILE(GNOLWNCE.wav) \ -VCMI_SOUND_NAME(GODRAttack) VCMI_SOUND_FILE(GODRATTK.wav) \ -VCMI_SOUND_NAME(GODRDefend) VCMI_SOUND_FILE(GODRDFND.wav) \ -VCMI_SOUND_NAME(GODRKill) VCMI_SOUND_FILE(GODRKILL.wav) \ -VCMI_SOUND_NAME(GODRMove) VCMI_SOUND_FILE(GODRMOVE.wav) \ -VCMI_SOUND_NAME(GODRWNCE) VCMI_SOUND_FILE(GODRWNCE.wav) \ -VCMI_SOUND_NAME(GOGFLAME) VCMI_SOUND_FILE(GOGFLAME.wav) \ -VCMI_SOUND_NAME(GOGGAttack) VCMI_SOUND_FILE(GOGGATTK.wav) \ -VCMI_SOUND_NAME(GOGGDefend) VCMI_SOUND_FILE(GOGGDFND.wav) \ -VCMI_SOUND_NAME(GOGGKill) VCMI_SOUND_FILE(GOGGKILL.wav) \ -VCMI_SOUND_NAME(GOGGMove) VCMI_SOUND_FILE(GOGGMOVE.wav) \ -VCMI_SOUND_NAME(GOGGShot) VCMI_SOUND_FILE(GOGGSHOT.wav) \ -VCMI_SOUND_NAME(GOGGWNCE) VCMI_SOUND_FILE(GOGGWNCE.wav) \ -VCMI_SOUND_NAME(GOODLUCK) VCMI_SOUND_FILE(GOODLUCK.wav) \ -VCMI_SOUND_NAME(GOODMRLE) VCMI_SOUND_FILE(GOODMRLE.wav) \ -VCMI_SOUND_NAME(GRAVEYARD) VCMI_SOUND_FILE(GRAVEYARD.wav) \ -VCMI_SOUND_NAME(GRDRAttack) VCMI_SOUND_FILE(GRDRATTK.wav) \ -VCMI_SOUND_NAME(GRDRDefend) VCMI_SOUND_FILE(GRDRDFND.wav) \ -VCMI_SOUND_NAME(GRDRKill) VCMI_SOUND_FILE(GRDRKILL.wav) \ -VCMI_SOUND_NAME(GRDRMove) VCMI_SOUND_FILE(GRDRMOVE.wav) \ -VCMI_SOUND_NAME(GRDRWNCE) VCMI_SOUND_FILE(GRDRWNCE.wav) \ -VCMI_SOUND_NAME(GRIFAttack) VCMI_SOUND_FILE(GRIFATTK.wav) \ -VCMI_SOUND_NAME(GRIFDefend) VCMI_SOUND_FILE(GRIFDFND.wav) \ -VCMI_SOUND_NAME(GRIFKill) VCMI_SOUND_FILE(GRIFKILL.wav) \ -VCMI_SOUND_NAME(GRIFMove) VCMI_SOUND_FILE(GRIFMOVE.wav) \ -VCMI_SOUND_NAME(GRIFWNCE) VCMI_SOUND_FILE(GRIFWNCE.wav) \ -VCMI_SOUND_NAME(GTITAttack) VCMI_SOUND_FILE(GTITATTK.wav) \ -VCMI_SOUND_NAME(GTITDefend) VCMI_SOUND_FILE(GTITDFND.wav) \ -VCMI_SOUND_NAME(GTITKill) VCMI_SOUND_FILE(GTITKILL.wav) \ -VCMI_SOUND_NAME(GTITMove) VCMI_SOUND_FILE(GTITMOVE.wav) \ -VCMI_SOUND_NAME(GTITShot) VCMI_SOUND_FILE(GTITSHOT.wav) \ -VCMI_SOUND_NAME(GTITWNCE) VCMI_SOUND_FILE(GTITWNCE.wav) \ -VCMI_SOUND_NAME(GWRDAttack) VCMI_SOUND_FILE(GWRDATTK.wav) \ -VCMI_SOUND_NAME(GWRDDefend) VCMI_SOUND_FILE(GWRDDFND.wav) \ -VCMI_SOUND_NAME(GWRDKill) VCMI_SOUND_FILE(GWRDKILL.wav) \ -VCMI_SOUND_NAME(GWRDMove) VCMI_SOUND_FILE(GWRDMOVE.wav) \ -VCMI_SOUND_NAME(GWRDWNCE) VCMI_SOUND_FILE(GWRDWNCE.wav) \ -VCMI_SOUND_NAME(HALBAttack) VCMI_SOUND_FILE(HALBATTK.wav) \ -VCMI_SOUND_NAME(HALBDefend) VCMI_SOUND_FILE(HALBDFND.wav) \ -VCMI_SOUND_NAME(HALBKill) VCMI_SOUND_FILE(HALBKILL.wav) \ -VCMI_SOUND_NAME(HALBMove) VCMI_SOUND_FILE(HALBMOVE.wav) \ -VCMI_SOUND_NAME(HALBWNCE) VCMI_SOUND_FILE(HALBWNCE.wav) \ -VCMI_SOUND_NAME(HALFAttack) VCMI_SOUND_FILE(HALFATTK.wav) \ -VCMI_SOUND_NAME(HALFDefend) VCMI_SOUND_FILE(HALFDFND.wav) \ -VCMI_SOUND_NAME(HALFKill) VCMI_SOUND_FILE(HALFKILL.wav) \ -VCMI_SOUND_NAME(HALFMove) VCMI_SOUND_FILE(HALFMOVE.wav) \ -VCMI_SOUND_NAME(HALFShot) VCMI_SOUND_FILE(HALFSHOT.wav) \ -VCMI_SOUND_NAME(HALFWNCE) VCMI_SOUND_FILE(HALFWNCE.wav) \ -VCMI_SOUND_NAME(HARPAttack) VCMI_SOUND_FILE(HARPATTK.wav) \ -VCMI_SOUND_NAME(HARPDefend) VCMI_SOUND_FILE(HARPDFND.wav) \ -VCMI_SOUND_NAME(HARPKill) VCMI_SOUND_FILE(HARPKILL.wav) \ -VCMI_SOUND_NAME(HARPMove) VCMI_SOUND_FILE(HARPMOVE.wav) \ -VCMI_SOUND_NAME(HARPWNCE) VCMI_SOUND_FILE(HARPWNCE.wav) \ -VCMI_SOUND_NAME(HASTE) VCMI_SOUND_FILE(HASTE.wav) \ -VCMI_SOUND_NAME(HCRSAttack) VCMI_SOUND_FILE(HCRSATTK.wav) \ -VCMI_SOUND_NAME(HCRSDefend) VCMI_SOUND_FILE(HCRSDFND.wav) \ -VCMI_SOUND_NAME(HCRSKill) VCMI_SOUND_FILE(HCRSKILL.wav) \ -VCMI_SOUND_NAME(HCRSMove) VCMI_SOUND_FILE(HCRSMOVE.wav) \ -VCMI_SOUND_NAME(HCRSShot) VCMI_SOUND_FILE(HCRSSHOT.wav) \ -VCMI_SOUND_NAME(HCRSWNCE) VCMI_SOUND_FILE(HCRSWNCE.wav) \ -VCMI_SOUND_NAME(HGOBAttack) VCMI_SOUND_FILE(HGOBATTK.wav) \ -VCMI_SOUND_NAME(HGOBDefend) VCMI_SOUND_FILE(HGOBDFND.wav) \ -VCMI_SOUND_NAME(HGOBKill) VCMI_SOUND_FILE(HGOBKILL.wav) \ -VCMI_SOUND_NAME(HGOBMove) VCMI_SOUND_FILE(HGOBMOVE.wav) \ -VCMI_SOUND_NAME(HGOBWNCE) VCMI_SOUND_FILE(HGOBWNCE.wav) \ -VCMI_SOUND_NAME(HGWRAttack) VCMI_SOUND_FILE(HGWRATTK.wav) \ -VCMI_SOUND_NAME(HGWRDefend) VCMI_SOUND_FILE(HGWRDFND.wav) \ -VCMI_SOUND_NAME(HGWRKill) VCMI_SOUND_FILE(HGWRKILL.wav) \ -VCMI_SOUND_NAME(HGWRMove) VCMI_SOUND_FILE(HGWRMOVE.wav) \ -VCMI_SOUND_NAME(HGWRWNCE) VCMI_SOUND_FILE(HGWRWNCE.wav) \ -VCMI_SOUND_NAME(HHAGAttack) VCMI_SOUND_FILE(HHAGATTK.wav) \ -VCMI_SOUND_NAME(HHAGDefend) VCMI_SOUND_FILE(HHAGDFND.wav) \ -VCMI_SOUND_NAME(HHAGKill) VCMI_SOUND_FILE(HHAGKILL.wav) \ -VCMI_SOUND_NAME(HHAGMove) VCMI_SOUND_FILE(HHAGMOVE.wav) \ -VCMI_SOUND_NAME(HHAGShot) VCMI_SOUND_FILE(HHAGSHOT.wav) \ -VCMI_SOUND_NAME(HHAGWNCE) VCMI_SOUND_FILE(HHAGWNCE.wav) \ -VCMI_SOUND_NAME(HHNDAttack) VCMI_SOUND_FILE(HHNDATTK.wav) \ -VCMI_SOUND_NAME(HHNDDefend) VCMI_SOUND_FILE(HHNDDFND.wav) \ -VCMI_SOUND_NAME(HHNDKill) VCMI_SOUND_FILE(HHNDKILL.wav) \ -VCMI_SOUND_NAME(HHNDMove) VCMI_SOUND_FILE(HHNDMOVE.wav) \ -VCMI_SOUND_NAME(HHNDWNCE) VCMI_SOUND_FILE(HHNDWNCE.wav) \ -VCMI_SOUND_NAME(horseDirt) VCMI_SOUND_FILE(HORSE00.wav) \ -VCMI_SOUND_NAME(horseSand) VCMI_SOUND_FILE(HORSE01.wav) \ -VCMI_SOUND_NAME(horseGrass) VCMI_SOUND_FILE(HORSE02.wav) \ -VCMI_SOUND_NAME(horseSnow) VCMI_SOUND_FILE(HORSE03.wav) \ -VCMI_SOUND_NAME(horseSwamp) VCMI_SOUND_FILE(HORSE04.wav) \ -VCMI_SOUND_NAME(horseRough) VCMI_SOUND_FILE(HORSE05.wav) \ -VCMI_SOUND_NAME(horseSubterranean) VCMI_SOUND_FILE(HORSE06.wav) \ -VCMI_SOUND_NAME(horseLava) VCMI_SOUND_FILE(HORSE07.wav) \ -VCMI_SOUND_NAME(horseWater) VCMI_SOUND_FILE(HORSE08.wav) \ -VCMI_SOUND_NAME(horseRock) VCMI_SOUND_FILE(HORSE09.wav) \ -VCMI_SOUND_NAME(horseFly) VCMI_SOUND_FILE(HORSE10.wav) \ -VCMI_SOUND_NAME(horsePenaltyDirt) VCMI_SOUND_FILE(HORSE20.wav) \ -VCMI_SOUND_NAME(horsePenaltySand) VCMI_SOUND_FILE(HORSE21.wav) \ -VCMI_SOUND_NAME(horsePenaltyGrass) VCMI_SOUND_FILE(HORSE22.wav) \ -VCMI_SOUND_NAME(horsePenaltySnow) VCMI_SOUND_FILE(HORSE23.wav) \ -VCMI_SOUND_NAME(horsePenaltySwamp) VCMI_SOUND_FILE(HORSE24.wav) \ -VCMI_SOUND_NAME(horsePenaltyRough) VCMI_SOUND_FILE(HORSE25.wav) \ -VCMI_SOUND_NAME(horsePenaltySubterranean) VCMI_SOUND_FILE(HORSE26.wav) \ -VCMI_SOUND_NAME(horsePenaltyLava) VCMI_SOUND_FILE(HORSE27.wav) \ -VCMI_SOUND_NAME(horsePenaltyRock) VCMI_SOUND_FILE(HORSE29.wav) \ -VCMI_SOUND_NAME(hydraAttack) VCMI_SOUND_FILE(HYDRATTK.wav) \ -VCMI_SOUND_NAME(hydraDefend) VCMI_SOUND_FILE(HYDRDFND.wav) \ -VCMI_SOUND_NAME(hydraKill) VCMI_SOUND_FILE(HYDRKILL.wav) \ -VCMI_SOUND_NAME(hydraMove) VCMI_SOUND_FILE(HYDRMOVE.wav) \ -VCMI_SOUND_NAME(hydraWNCE) VCMI_SOUND_FILE(HYDRWNCE.wav) \ -VCMI_SOUND_NAME(HYPNOTIZ) VCMI_SOUND_FILE(HYPNOTIZ.wav) \ -VCMI_SOUND_NAME(ICELAttack) VCMI_SOUND_FILE(ICELATTK.wav) \ -VCMI_SOUND_NAME(ICELDefend) VCMI_SOUND_FILE(ICELDFND.wav) \ -VCMI_SOUND_NAME(ICELKill) VCMI_SOUND_FILE(ICELKILL.wav) \ -VCMI_SOUND_NAME(ICELMove) VCMI_SOUND_FILE(ICELMOVE.wav) \ -VCMI_SOUND_NAME(ICELShot) VCMI_SOUND_FILE(ICELSHOT.wav) \ -VCMI_SOUND_NAME(ICELWNCE) VCMI_SOUND_FILE(ICELWNCE.wav) \ -VCMI_SOUND_NAME(ICERAYEX) VCMI_SOUND_FILE(ICERAYEX.wav) \ -VCMI_SOUND_NAME(ICERAY) VCMI_SOUND_FILE(ICERAY.wav) \ -VCMI_SOUND_NAME(IGLMAttack) VCMI_SOUND_FILE(IGLMATTK.wav) \ -VCMI_SOUND_NAME(IGLMDefend) VCMI_SOUND_FILE(IGLMDFND.wav) \ -VCMI_SOUND_NAME(IGLMKill) VCMI_SOUND_FILE(IGLMKILL.wav) \ -VCMI_SOUND_NAME(IGLMMove) VCMI_SOUND_FILE(IGLMMOVE.wav) \ -VCMI_SOUND_NAME(IGLMWNCE) VCMI_SOUND_FILE(IGLMWNCE.wav) \ -VCMI_SOUND_NAME(IMPPAttack) VCMI_SOUND_FILE(IMPPATTK.wav) \ -VCMI_SOUND_NAME(IMPPDefend) VCMI_SOUND_FILE(IMPPDFND.wav) \ -VCMI_SOUND_NAME(IMPPKill) VCMI_SOUND_FILE(IMPPKILL.wav) \ -VCMI_SOUND_NAME(IMPPMove) VCMI_SOUND_FILE(IMPPMOVE.wav) \ -VCMI_SOUND_NAME(IMPPWNCE) VCMI_SOUND_FILE(IMPPWNCE.wav) \ -VCMI_SOUND_NAME(ITRGAttack) VCMI_SOUND_FILE(ITRGATTK.wav) \ -VCMI_SOUND_NAME(ITRGDefend) VCMI_SOUND_FILE(ITRGDFND.wav) \ -VCMI_SOUND_NAME(ITRGKill) VCMI_SOUND_FILE(ITRGKILL.wav) \ -VCMI_SOUND_NAME(ITRGMove) VCMI_SOUND_FILE(ITRGMOVE.wav) \ -VCMI_SOUND_NAME(ITRGWNCE) VCMI_SOUND_FILE(ITRGWNCE.wav) \ -VCMI_SOUND_NAME(KEEPShot) VCMI_SOUND_FILE(KEEPSHOT.wav) \ -VCMI_SOUND_NAME(LANDKill) VCMI_SOUND_FILE(LANDKILL.wav) \ -VCMI_SOUND_NAME(LANDMINE) VCMI_SOUND_FILE(LANDMINE.wav) \ -VCMI_SOUND_NAME(LCRSAttack) VCMI_SOUND_FILE(LCRSATTK.wav) \ -VCMI_SOUND_NAME(LCRSDefend) VCMI_SOUND_FILE(LCRSDFND.wav) \ -VCMI_SOUND_NAME(LCRSKill) VCMI_SOUND_FILE(LCRSKILL.wav) \ -VCMI_SOUND_NAME(LCRSMove) VCMI_SOUND_FILE(LCRSMOVE.wav) \ -VCMI_SOUND_NAME(LCRSShot) VCMI_SOUND_FILE(LCRSSHOT.wav) \ -VCMI_SOUND_NAME(LCRSWNCE) VCMI_SOUND_FILE(LCRSWNCE.wav) \ -VCMI_SOUND_NAME(LICHATK2) VCMI_SOUND_FILE(LICHATK2.wav) \ -VCMI_SOUND_NAME(LICHAttack) VCMI_SOUND_FILE(LICHATTK.wav) \ -VCMI_SOUND_NAME(LICHDefend) VCMI_SOUND_FILE(LICHDFND.wav) \ -VCMI_SOUND_NAME(LICHKill) VCMI_SOUND_FILE(LICHKILL.wav) \ -VCMI_SOUND_NAME(LICHMove) VCMI_SOUND_FILE(LICHMOVE.wav) \ -VCMI_SOUND_NAME(LICHShot) VCMI_SOUND_FILE(LICHSHOT.wav) \ -VCMI_SOUND_NAME(LICHWNCE) VCMI_SOUND_FILE(LICHWNCE.wav) \ -VCMI_SOUND_NAME(LIGHTBLT) VCMI_SOUND_FILE(LIGHTBLT.wav) \ -VCMI_SOUND_NAME(LIGHTHOUSE) VCMI_SOUND_FILE(LIGHTHOUSE.wav) \ -VCMI_SOUND_NAME(LOOPAIR) VCMI_SOUND_FILE(LOOPAIR.wav) \ -VCMI_SOUND_NAME(LOOPANIM) VCMI_SOUND_FILE(LOOPANIM.wav) \ -VCMI_SOUND_NAME(LOOPARCH) VCMI_SOUND_FILE(LOOPARCH.wav) \ -VCMI_SOUND_NAME(LOOPAREN) VCMI_SOUND_FILE(LOOPAREN.wav) \ -VCMI_SOUND_NAME(LOOPBEHE) VCMI_SOUND_FILE(LOOPBEHE.wav) \ -VCMI_SOUND_NAME(LOOPBIRD) VCMI_SOUND_FILE(LOOPBIRD.wav) \ -VCMI_SOUND_NAME(LOOPBUOY) VCMI_SOUND_FILE(LOOPBUOY.wav) \ -VCMI_SOUND_NAME(LOOPCAMP) VCMI_SOUND_FILE(LOOPCAMP.wav) \ -VCMI_SOUND_NAME(LOOPCAVE) VCMI_SOUND_FILE(LOOPCAVE.wav) \ -VCMI_SOUND_NAME(LOOPCRYS) VCMI_SOUND_FILE(LOOPCRYS.wav) \ -VCMI_SOUND_NAME(LOOPCURS) VCMI_SOUND_FILE(LOOPCURS.wav) \ -VCMI_SOUND_NAME(LOOPDEAD) VCMI_SOUND_FILE(LOOPDEAD.wav) \ -VCMI_SOUND_NAME(LOOPDEN) VCMI_SOUND_FILE(LOOPDEN.wav) \ -VCMI_SOUND_NAME(LOOPDEVL) VCMI_SOUND_FILE(LOOPDEVL.wav) \ -VCMI_SOUND_NAME(LOOPDOG) VCMI_SOUND_FILE(LOOPDOG.wav) \ -VCMI_SOUND_NAME(LOOPDRAG) VCMI_SOUND_FILE(LOOPDRAG.wav) \ -VCMI_SOUND_NAME(LOOPDWAR) VCMI_SOUND_FILE(LOOPDWAR.wav) \ -VCMI_SOUND_NAME(LOOPEART) VCMI_SOUND_FILE(LOOPEART.wav) \ -VCMI_SOUND_NAME(LOOPELF) VCMI_SOUND_FILE(LOOPELF.wav) \ -VCMI_SOUND_NAME(LOOPFACT) VCMI_SOUND_FILE(LOOPFACT.wav) \ -VCMI_SOUND_NAME(LOOPFAER) VCMI_SOUND_FILE(LOOPFAER.wav) \ -VCMI_SOUND_NAME(LOOPFALL) VCMI_SOUND_FILE(LOOPFALL.wav) \ -VCMI_SOUND_NAME(LOOPFIRE) VCMI_SOUND_FILE(LOOPFIRE.wav) \ -VCMI_SOUND_NAME(LOOPFLAG) VCMI_SOUND_FILE(LOOPFLAG.wav) \ -VCMI_SOUND_NAME(LOOPFOUN) VCMI_SOUND_FILE(LOOPFOUN.wav) \ -VCMI_SOUND_NAME(LOOPGARD) VCMI_SOUND_FILE(LOOPGARD.wav) \ -VCMI_SOUND_NAME(LOOPGATE) VCMI_SOUND_FILE(LOOPGATE.wav) \ -VCMI_SOUND_NAME(LOOPGEMP) VCMI_SOUND_FILE(LOOPGEMP.wav) \ -VCMI_SOUND_NAME(LOOPGOBL) VCMI_SOUND_FILE(LOOPGOBL.wav) \ -VCMI_SOUND_NAME(LOOPGREM) VCMI_SOUND_FILE(LOOPGREM.wav) \ -VCMI_SOUND_NAME(LOOPGRIF) VCMI_SOUND_FILE(LOOPGRIF.wav) \ -VCMI_SOUND_NAME(LOOPHARP) VCMI_SOUND_FILE(LOOPHARP.wav) \ -VCMI_SOUND_NAME(LOOPHORS) VCMI_SOUND_FILE(LOOPHORS.wav) \ -VCMI_SOUND_NAME(LOOPHYDR) VCMI_SOUND_FILE(LOOPHYDR.wav) \ -VCMI_SOUND_NAME(LOOPLEAR) VCMI_SOUND_FILE(LOOPLEAR.wav) \ -VCMI_SOUND_NAME(LOOPLEPR) VCMI_SOUND_FILE(LOOPLEPR.wav) \ -VCMI_SOUND_NAME(LOOPLUMB) VCMI_SOUND_FILE(LOOPLUMB.wav) \ -VCMI_SOUND_NAME(LOOPMAGI) VCMI_SOUND_FILE(LOOPMAGI.wav) \ -VCMI_SOUND_NAME(LOOPMANT) VCMI_SOUND_FILE(LOOPMANT.wav) \ -VCMI_SOUND_NAME(LOOPMARK) VCMI_SOUND_FILE(LOOPMARK.wav) \ -VCMI_SOUND_NAME(LOOPMEDU) VCMI_SOUND_FILE(LOOPMEDU.wav) \ -VCMI_SOUND_NAME(LOOPMERC) VCMI_SOUND_FILE(LOOPMERC.wav) \ -VCMI_SOUND_NAME(LOOPMILL) VCMI_SOUND_FILE(LOOPMILL.wav) \ -VCMI_SOUND_NAME(LOOPMINE) VCMI_SOUND_FILE(LOOPMINE.wav) \ -VCMI_SOUND_NAME(LOOPMON1) VCMI_SOUND_FILE(LOOPMON1.wav) \ -VCMI_SOUND_NAME(LOOPMON2) VCMI_SOUND_FILE(LOOPMON2.wav) \ -VCMI_SOUND_NAME(LOOPMONK) VCMI_SOUND_FILE(LOOPMONK.wav) \ -VCMI_SOUND_NAME(LOOPMONS) VCMI_SOUND_FILE(LOOPMONS.wav) \ -VCMI_SOUND_NAME(LOOPNAGA) VCMI_SOUND_FILE(LOOPNAGA.wav) \ -VCMI_SOUND_NAME(LOOPOCEA) VCMI_SOUND_FILE(LOOPOCEA.wav) \ -VCMI_SOUND_NAME(LOOPOGRE) VCMI_SOUND_FILE(LOOPOGRE.wav) \ -VCMI_SOUND_NAME(LOOPORC) VCMI_SOUND_FILE(LOOPORC.wav) \ -VCMI_SOUND_NAME(LOOPPEGA) VCMI_SOUND_FILE(LOOPPEGA.wav) \ -VCMI_SOUND_NAME(LOOPPIKE) VCMI_SOUND_FILE(LOOPPIKE.wav) \ -VCMI_SOUND_NAME(LOOPSANC) VCMI_SOUND_FILE(LOOPSANC.wav) \ -VCMI_SOUND_NAME(LOOPSHRIN) VCMI_SOUND_FILE(LOOPSHRIN.wav) \ -VCMI_SOUND_NAME(LOOPSIRE) VCMI_SOUND_FILE(LOOPSIRE.wav) \ -VCMI_SOUND_NAME(LOOPSKEL) VCMI_SOUND_FILE(LOOPSKEL.wav) \ -VCMI_SOUND_NAME(LOOPSTAR) VCMI_SOUND_FILE(LOOPSTAR.wav) \ -VCMI_SOUND_NAME(LOOPSULF) VCMI_SOUND_FILE(LOOPSULF.wav) \ -VCMI_SOUND_NAME(LOOPSWAR) VCMI_SOUND_FILE(LOOPSWAR.wav) \ -VCMI_SOUND_NAME(LOOPSWOR) VCMI_SOUND_FILE(LOOPSWOR.wav) \ -VCMI_SOUND_NAME(LOOPTAV) VCMI_SOUND_FILE(LOOPTAV.wav) \ -VCMI_SOUND_NAME(LOOPTITA) VCMI_SOUND_FILE(LOOPTITA.wav) \ -VCMI_SOUND_NAME(LOOPUNIC) VCMI_SOUND_FILE(LOOPUNIC.wav) \ -VCMI_SOUND_NAME(LOOPVENT) VCMI_SOUND_FILE(LOOPVENT.wav) \ -VCMI_SOUND_NAME(LOOPVOLC) VCMI_SOUND_FILE(LOOPVOLC.wav) \ -VCMI_SOUND_NAME(LOOPWHIR) VCMI_SOUND_FILE(LOOPWHIR.wav) \ -VCMI_SOUND_NAME(LOOPWIND) VCMI_SOUND_FILE(LOOPWIND.wav) \ -VCMI_SOUND_NAME(LOOPWOLF) VCMI_SOUND_FILE(LOOPWOLF.wav) \ -VCMI_SOUND_NAME(LTITAttack) VCMI_SOUND_FILE(LTITATTK.wav) \ -VCMI_SOUND_NAME(LTITDefend) VCMI_SOUND_FILE(LTITDFND.wav) \ -VCMI_SOUND_NAME(LTITKill) VCMI_SOUND_FILE(LTITKILL.wav) \ -VCMI_SOUND_NAME(LTITMove) VCMI_SOUND_FILE(LTITMOVE.wav) \ -VCMI_SOUND_NAME(LTITWNCE) VCMI_SOUND_FILE(LTITWNCE.wav) \ -VCMI_SOUND_NAME(LUCK) VCMI_SOUND_FILE(LUCK.wav) \ -VCMI_SOUND_NAME(MAGCAROW) VCMI_SOUND_FILE(MAGCAROW.wav) \ -VCMI_SOUND_NAME(MAGCHDRN) VCMI_SOUND_FILE(MAGCHDRN.wav) \ -VCMI_SOUND_NAME(MAGCHFIL) VCMI_SOUND_FILE(MAGCHFIL.wav) \ -VCMI_SOUND_NAME(MAGEAttack) VCMI_SOUND_FILE(MAGEATTK.wav) \ -VCMI_SOUND_NAME(MAGEDefend) VCMI_SOUND_FILE(MAGEDFND.wav) \ -VCMI_SOUND_NAME(MAGEKill) VCMI_SOUND_FILE(MAGEKILL.wav) \ -VCMI_SOUND_NAME(MAGEMove) VCMI_SOUND_FILE(MAGEMOVE.wav) \ -VCMI_SOUND_NAME(MAGEShot) VCMI_SOUND_FILE(MAGESHOT.wav) \ -VCMI_SOUND_NAME(MAGEWNCE) VCMI_SOUND_FILE(MAGEWNCE.wav) \ -VCMI_SOUND_NAME(MAGICBLT) VCMI_SOUND_FILE(MAGICBLT.wav) \ -VCMI_SOUND_NAME(MAGICRES) VCMI_SOUND_FILE(MAGICRES.wav) \ -VCMI_SOUND_NAME(MAGMAttack) VCMI_SOUND_FILE(MAGMATTK.wav) \ -VCMI_SOUND_NAME(MAGMDefend) VCMI_SOUND_FILE(MAGMDFND.wav) \ -VCMI_SOUND_NAME(MAGMKill) VCMI_SOUND_FILE(MAGMKILL.wav) \ -VCMI_SOUND_NAME(MAGMMove) VCMI_SOUND_FILE(MAGMMOVE.wav) \ -VCMI_SOUND_NAME(MAGMWNCE) VCMI_SOUND_FILE(MAGMWNCE.wav) \ -VCMI_SOUND_NAME(MANADRAI) VCMI_SOUND_FILE(MANADRAI.wav) \ -VCMI_SOUND_NAME(MANTAttack) VCMI_SOUND_FILE(MANTATTK.wav) \ -VCMI_SOUND_NAME(MANTDefend) VCMI_SOUND_FILE(MANTDFND.wav) \ -VCMI_SOUND_NAME(MANTKill) VCMI_SOUND_FILE(MANTKILL.wav) \ -VCMI_SOUND_NAME(MANTMove) VCMI_SOUND_FILE(MANTMOVE.wav) \ -VCMI_SOUND_NAME(MANTShot) VCMI_SOUND_FILE(MANTSHOT.wav) \ -VCMI_SOUND_NAME(MANTWNCE) VCMI_SOUND_FILE(MANTWNCE.wav) \ -VCMI_SOUND_NAME(MEDQAttack) VCMI_SOUND_FILE(MEDQATTK.wav) \ -VCMI_SOUND_NAME(MEDQDefend) VCMI_SOUND_FILE(MEDQDFND.wav) \ -VCMI_SOUND_NAME(MEDQKill) VCMI_SOUND_FILE(MEDQKILL.wav) \ -VCMI_SOUND_NAME(MEDQMove) VCMI_SOUND_FILE(MEDQMOVE.wav) \ -VCMI_SOUND_NAME(MEDQShot) VCMI_SOUND_FILE(MEDQSHOT.wav) \ -VCMI_SOUND_NAME(MEDQWNCE) VCMI_SOUND_FILE(MEDQWNCE.wav) \ -VCMI_SOUND_NAME(MEDUAttack) VCMI_SOUND_FILE(MEDUATTK.wav) \ -VCMI_SOUND_NAME(MEDUDefend) VCMI_SOUND_FILE(MEDUDFND.wav) \ -VCMI_SOUND_NAME(MEDUKill) VCMI_SOUND_FILE(MEDUKILL.wav) \ -VCMI_SOUND_NAME(MEDUMove) VCMI_SOUND_FILE(MEDUMOVE.wav) \ -VCMI_SOUND_NAME(MEDUShot) VCMI_SOUND_FILE(MEDUSHOT.wav) \ -VCMI_SOUND_NAME(MEDUWNCE) VCMI_SOUND_FILE(MEDUWNCE.wav) \ -VCMI_SOUND_NAME(METEOR) VCMI_SOUND_FILE(METEOR.wav) \ -VCMI_SOUND_NAME(MGELAttack) VCMI_SOUND_FILE(MGELATTK.wav) \ -VCMI_SOUND_NAME(MGELDefend) VCMI_SOUND_FILE(MGELDFND.wav) \ -VCMI_SOUND_NAME(MGELKill) VCMI_SOUND_FILE(MGELKILL.wav) \ -VCMI_SOUND_NAME(MGELMove) VCMI_SOUND_FILE(MGELMOVE.wav) \ -VCMI_SOUND_NAME(MGELWNCE) VCMI_SOUND_FILE(MGELWNCE.wav) \ -VCMI_SOUND_NAME(MGOGAttack) VCMI_SOUND_FILE(MGOGATTK.wav) \ -VCMI_SOUND_NAME(MGOGDefend) VCMI_SOUND_FILE(MGOGDFND.wav) \ -VCMI_SOUND_NAME(MGOGKill) VCMI_SOUND_FILE(MGOGKILL.wav) \ -VCMI_SOUND_NAME(MGOGMove) VCMI_SOUND_FILE(MGOGMOVE.wav) \ -VCMI_SOUND_NAME(MGOGShot) VCMI_SOUND_FILE(MGOGSHOT.wav) \ -VCMI_SOUND_NAME(MGOGWNCE) VCMI_SOUND_FILE(MGOGWNCE.wav) \ -VCMI_SOUND_NAME(MGRMAttack) VCMI_SOUND_FILE(MGRMATTK.wav) \ -VCMI_SOUND_NAME(MGRMDefend) VCMI_SOUND_FILE(MGRMDFND.wav) \ -VCMI_SOUND_NAME(MGRMKill) VCMI_SOUND_FILE(MGRMKILL.wav) \ -VCMI_SOUND_NAME(MGRMMove) VCMI_SOUND_FILE(MGRMMOVE.wav) \ -VCMI_SOUND_NAME(MGRMShot) VCMI_SOUND_FILE(MGRMSHOT.wav) \ -VCMI_SOUND_NAME(MGRMWNCE) VCMI_SOUND_FILE(MGRMWNCE.wav) \ -VCMI_SOUND_NAME(MILITARY) VCMI_SOUND_FILE(MILITARY.wav) \ -VCMI_SOUND_NAME(MINKAttack) VCMI_SOUND_FILE(MINKATTK.wav) \ -VCMI_SOUND_NAME(MINKDefend) VCMI_SOUND_FILE(MINKDFND.wav) \ -VCMI_SOUND_NAME(MINKKill) VCMI_SOUND_FILE(MINKKILL.wav) \ -VCMI_SOUND_NAME(MINKMove) VCMI_SOUND_FILE(MINKMOVE.wav) \ -VCMI_SOUND_NAME(MINKShot) VCMI_SOUND_FILE(MINKSHOT.wav) \ -VCMI_SOUND_NAME(MINKWNCE) VCMI_SOUND_FILE(MINKWNCE.wav) \ -VCMI_SOUND_NAME(MINOAttack) VCMI_SOUND_FILE(MINOATTK.wav) \ -VCMI_SOUND_NAME(MINODefend) VCMI_SOUND_FILE(MINODFND.wav) \ -VCMI_SOUND_NAME(MINOKill) VCMI_SOUND_FILE(MINOKILL.wav) \ -VCMI_SOUND_NAME(MINOMove) VCMI_SOUND_FILE(MINOMOVE.wav) \ -VCMI_SOUND_NAME(MINOWNCE) VCMI_SOUND_FILE(MINOWNCE.wav) \ -VCMI_SOUND_NAME(MIRRORIM) VCMI_SOUND_FILE(MIRRORIM.wav) \ -VCMI_SOUND_NAME(MIRTH) VCMI_SOUND_FILE(MIRTH.wav) \ -VCMI_SOUND_NAME(MISFORT) VCMI_SOUND_FILE(MISFORT.wav) \ -VCMI_SOUND_NAME(MNRDEATH) VCMI_SOUND_FILE(MNRDEATH.wav) \ -VCMI_SOUND_NAME(monkAttack) VCMI_SOUND_FILE(MONKATTK.wav) \ -VCMI_SOUND_NAME(monkDefend) VCMI_SOUND_FILE(MONKDFND.wav) \ -VCMI_SOUND_NAME(monkKill) VCMI_SOUND_FILE(MONKKILL.wav) \ -VCMI_SOUND_NAME(monkMove) VCMI_SOUND_FILE(MONKMOVE.wav) \ -VCMI_SOUND_NAME(monkShot) VCMI_SOUND_FILE(MONKSHOT.wav) \ -VCMI_SOUND_NAME(monkWNCE) VCMI_SOUND_FILE(MONKWNCE.wav) \ -VCMI_SOUND_NAME(MORALE) VCMI_SOUND_FILE(MORALE.wav) \ -VCMI_SOUND_NAME(MUCKMIRE) VCMI_SOUND_FILE(MUCKMIRE.wav) \ -VCMI_SOUND_NAME(MUMYAttack) VCMI_SOUND_FILE(MUMYATTK.wav) \ -VCMI_SOUND_NAME(MUMYDefend) VCMI_SOUND_FILE(MUMYDFND.wav) \ -VCMI_SOUND_NAME(MUMYKill) VCMI_SOUND_FILE(MUMYKILL.wav) \ -VCMI_SOUND_NAME(MUMYMove) VCMI_SOUND_FILE(MUMYMOVE.wav) \ -VCMI_SOUND_NAME(MUMYWNCE) VCMI_SOUND_FILE(MUMYWNCE.wav) \ -VCMI_SOUND_NAME(MYSTERY) VCMI_SOUND_FILE(MYSTERY.wav) \ -VCMI_SOUND_NAME(newDay) VCMI_SOUND_FILE(NEWDAY.wav) \ -VCMI_SOUND_NAME(newMonth) VCMI_SOUND_FILE(NEWMONTH.wav) \ -VCMI_SOUND_NAME(newWeek) VCMI_SOUND_FILE(NEWWEEK.wav) \ -VCMI_SOUND_NAME(NGRDAttack) VCMI_SOUND_FILE(NGRDATTK.wav) \ -VCMI_SOUND_NAME(NGRDDefend) VCMI_SOUND_FILE(NGRDDFND.wav) \ -VCMI_SOUND_NAME(NGRDKill) VCMI_SOUND_FILE(NGRDKILL.wav) \ -VCMI_SOUND_NAME(NGRDMove) VCMI_SOUND_FILE(NGRDMOVE.wav) \ -VCMI_SOUND_NAME(NGRDWNCE) VCMI_SOUND_FILE(NGRDWNCE.wav) \ -VCMI_SOUND_NAME(NMADAttack) VCMI_SOUND_FILE(NMADATTK.wav) \ -VCMI_SOUND_NAME(NMADDefend) VCMI_SOUND_FILE(NMADDFND.wav) \ -VCMI_SOUND_NAME(NMADKill) VCMI_SOUND_FILE(NMADKILL.wav) \ -VCMI_SOUND_NAME(NMADMove) VCMI_SOUND_FILE(NMADMOVE.wav) \ -VCMI_SOUND_NAME(NMADWNCE) VCMI_SOUND_FILE(NMADWNCE.wav) \ -VCMI_SOUND_NAME(NOMAD) VCMI_SOUND_FILE(NOMAD.wav) \ -VCMI_SOUND_NAME(NOSFAttack) VCMI_SOUND_FILE(NOSFATTK.wav) \ -VCMI_SOUND_NAME(NOSFDefend) VCMI_SOUND_FILE(NOSFDFND.wav) \ -VCMI_SOUND_NAME(NOSFEXT1) VCMI_SOUND_FILE(NOSFEXT1.wav) \ -VCMI_SOUND_NAME(NOSFEXT2) VCMI_SOUND_FILE(NOSFEXT2.wav) \ -VCMI_SOUND_NAME(NOSFKill) VCMI_SOUND_FILE(NOSFKILL.wav) \ -VCMI_SOUND_NAME(NOSFMove) VCMI_SOUND_FILE(NOSFMOVE.wav) \ -VCMI_SOUND_NAME(NOSFShot) VCMI_SOUND_FILE(NOSFSHOT.wav) \ -VCMI_SOUND_NAME(NOSFWNCE) VCMI_SOUND_FILE(NOSFWNCE.wav) \ -VCMI_SOUND_NAME(NSENAttack) VCMI_SOUND_FILE(NSENATTK.wav) \ -VCMI_SOUND_NAME(NSENDefend) VCMI_SOUND_FILE(NSENDFND.wav) \ -VCMI_SOUND_NAME(NSENKill) VCMI_SOUND_FILE(NSENKILL.wav) \ -VCMI_SOUND_NAME(NSENMove) VCMI_SOUND_FILE(NSENMOVE.wav) \ -VCMI_SOUND_NAME(NSENWNCE) VCMI_SOUND_FILE(NSENWNCE.wav) \ -VCMI_SOUND_NAME(heroNewLevel) VCMI_SOUND_FILE(NWHEROLV.wav) \ -VCMI_SOUND_NAME(OBELISK) VCMI_SOUND_FILE(OBELISK.wav) \ -VCMI_SOUND_NAME(OGREAttack) VCMI_SOUND_FILE(OGREATTK.wav) \ -VCMI_SOUND_NAME(OGREDefend) VCMI_SOUND_FILE(OGREDFND.wav) \ -VCMI_SOUND_NAME(OGREKill) VCMI_SOUND_FILE(OGREKILL.wav) \ -VCMI_SOUND_NAME(OGREMove) VCMI_SOUND_FILE(OGREMOVE.wav) \ -VCMI_SOUND_NAME(OGREWNCE) VCMI_SOUND_FILE(OGREWNCE.wav) \ -VCMI_SOUND_NAME(OGRGAttack) VCMI_SOUND_FILE(OGRGATTK.wav) \ -VCMI_SOUND_NAME(OGRGDefend) VCMI_SOUND_FILE(OGRGDFND.wav) \ -VCMI_SOUND_NAME(OGRGKill) VCMI_SOUND_FILE(OGRGKILL.wav) \ -VCMI_SOUND_NAME(OGRGMove) VCMI_SOUND_FILE(OGRGMOVE.wav) \ -VCMI_SOUND_NAME(OGRGWNCE) VCMI_SOUND_FILE(OGRGWNCE.wav) \ -VCMI_SOUND_NAME(OGRMAttack) VCMI_SOUND_FILE(OGRMATTK.wav) \ -VCMI_SOUND_NAME(OGRMDefend) VCMI_SOUND_FILE(OGRMDFND.wav) \ -VCMI_SOUND_NAME(OGRMKill) VCMI_SOUND_FILE(OGRMKILL.wav) \ -VCMI_SOUND_NAME(OGRMMove) VCMI_SOUND_FILE(OGRMMOVE.wav) \ -VCMI_SOUND_NAME(OGRMShot) VCMI_SOUND_FILE(OGRMSHOT.wav) \ -VCMI_SOUND_NAME(OGRMWNCE) VCMI_SOUND_FILE(OGRMWNCE.wav) \ -VCMI_SOUND_NAME(OORCAttack) VCMI_SOUND_FILE(OORCATTK.wav) \ -VCMI_SOUND_NAME(OORCDefend) VCMI_SOUND_FILE(OORCDFND.wav) \ -VCMI_SOUND_NAME(OORCKill) VCMI_SOUND_FILE(OORCKILL.wav) \ -VCMI_SOUND_NAME(OORCMove) VCMI_SOUND_FILE(OORCMOVE.wav) \ -VCMI_SOUND_NAME(OORCShot) VCMI_SOUND_FILE(OORCSHOT.wav) \ -VCMI_SOUND_NAME(OORCWNCE) VCMI_SOUND_FILE(OORCWNCE.wav) \ -VCMI_SOUND_NAME(ORCCAttack) VCMI_SOUND_FILE(ORCCATTK.wav) \ -VCMI_SOUND_NAME(ORCCDefend) VCMI_SOUND_FILE(ORCCDFND.wav) \ -VCMI_SOUND_NAME(ORCCKill) VCMI_SOUND_FILE(ORCCKILL.wav) \ -VCMI_SOUND_NAME(ORCCMove) VCMI_SOUND_FILE(ORCCMOVE.wav) \ -VCMI_SOUND_NAME(ORCCShot) VCMI_SOUND_FILE(ORCCSHOT.wav) \ -VCMI_SOUND_NAME(ORCCWNCE) VCMI_SOUND_FILE(ORCCWNCE.wav) \ -VCMI_SOUND_NAME(PARALYZE) VCMI_SOUND_FILE(PARALYZE.wav) \ -VCMI_SOUND_NAME(PEGAAttack) VCMI_SOUND_FILE(PEGAATTK.wav) \ -VCMI_SOUND_NAME(PEGADefend) VCMI_SOUND_FILE(PEGADFND.wav) \ -VCMI_SOUND_NAME(PEGAKill) VCMI_SOUND_FILE(PEGAKILL.wav) \ -VCMI_SOUND_NAME(PEGAMove) VCMI_SOUND_FILE(PEGAMOVE.wav) \ -VCMI_SOUND_NAME(PEGAWNCE) VCMI_SOUND_FILE(PEGAWNCE.wav) \ -VCMI_SOUND_NAME(PFNDAttack) VCMI_SOUND_FILE(PFNDATTK.wav) \ -VCMI_SOUND_NAME(PFNDDefend) VCMI_SOUND_FILE(PFNDDFND.wav) \ -VCMI_SOUND_NAME(PFNDKill) VCMI_SOUND_FILE(PFNDKILL.wav) \ -VCMI_SOUND_NAME(PFNDMove) VCMI_SOUND_FILE(PFNDMOVE.wav) \ -VCMI_SOUND_NAME(PFNDWNCE) VCMI_SOUND_FILE(PFNDWNCE.wav) \ -VCMI_SOUND_NAME(PFOEAttack) VCMI_SOUND_FILE(PFOEATTK.wav) \ -VCMI_SOUND_NAME(PFOEDefend) VCMI_SOUND_FILE(PFOEDFND.wav) \ -VCMI_SOUND_NAME(PFOEKill) VCMI_SOUND_FILE(PFOEKILL.wav) \ -VCMI_SOUND_NAME(PFOEMove) VCMI_SOUND_FILE(PFOEMOVE.wav) \ -VCMI_SOUND_NAME(PFOEWNCE) VCMI_SOUND_FILE(PFOEWNCE.wav) \ -VCMI_SOUND_NAME(PHOEAttack) VCMI_SOUND_FILE(PHOEATTK.wav) \ -VCMI_SOUND_NAME(PHOEDefend) VCMI_SOUND_FILE(PHOEDFND.wav) \ -VCMI_SOUND_NAME(PHOEKill) VCMI_SOUND_FILE(PHOEKILL.wav) \ -VCMI_SOUND_NAME(PHOEMove) VCMI_SOUND_FILE(PHOEMOVE.wav) \ -VCMI_SOUND_NAME(PHOEWNCE) VCMI_SOUND_FILE(PHOEWNCE.wav) \ -VCMI_SOUND_NAME(pickup01) VCMI_SOUND_FILE(PICKUP01.wav) \ -VCMI_SOUND_NAME(pickup02) VCMI_SOUND_FILE(PICKUP02.wav) \ -VCMI_SOUND_NAME(pickup03) VCMI_SOUND_FILE(PICKUP03.wav) \ -VCMI_SOUND_NAME(pickup04) VCMI_SOUND_FILE(PICKUP04.wav) \ -VCMI_SOUND_NAME(pickup05) VCMI_SOUND_FILE(PICKUP05.wav) \ -VCMI_SOUND_NAME(pickup06) VCMI_SOUND_FILE(PICKUP06.wav) \ -VCMI_SOUND_NAME(pickup07) VCMI_SOUND_FILE(PICKUP07.wav) \ -VCMI_SOUND_NAME(pikemanAttack) VCMI_SOUND_FILE(PIKEATTK.wav) \ -VCMI_SOUND_NAME(pikemanDefend) VCMI_SOUND_FILE(PIKEDFND.wav) \ -VCMI_SOUND_NAME(pikemanKill) VCMI_SOUND_FILE(PIKEKILL.wav) \ -VCMI_SOUND_NAME(pikemanMove) VCMI_SOUND_FILE(PIKEMOVE.wav) \ -VCMI_SOUND_NAME(pikemanWNCE) VCMI_SOUND_FILE(PIKEWNCE.wav) \ -VCMI_SOUND_NAME(pixieAttack) VCMI_SOUND_FILE(PIXIATTK.wav) \ -VCMI_SOUND_NAME(pixieDefend) VCMI_SOUND_FILE(PIXIDFND.wav) \ -VCMI_SOUND_NAME(pixieKill) VCMI_SOUND_FILE(PIXIKILL.wav) \ -VCMI_SOUND_NAME(pixieMove) VCMI_SOUND_FILE(PIXIMOVE.wav) \ -VCMI_SOUND_NAME(pixieWNCE) VCMI_SOUND_FILE(PIXIWNCE.wav) \ -VCMI_SOUND_NAME(PLAYCOME) VCMI_SOUND_FILE(PLAYCOME.wav) \ -VCMI_SOUND_NAME(PLAYEXIT) VCMI_SOUND_FILE(PLAYEXIT.wav) \ -VCMI_SOUND_NAME(PLAYTURN) VCMI_SOUND_FILE(PLAYTURN.wav) \ -VCMI_SOUND_NAME(PLCHAttack) VCMI_SOUND_FILE(PLCHATTK.wav) \ -VCMI_SOUND_NAME(PLCHDefend) VCMI_SOUND_FILE(PLCHDFND.wav) \ -VCMI_SOUND_NAME(PLCHKill) VCMI_SOUND_FILE(PLCHKILL.wav) \ -VCMI_SOUND_NAME(PLCHMove) VCMI_SOUND_FILE(PLCHMOVE.wav) \ -VCMI_SOUND_NAME(PLCHShot) VCMI_SOUND_FILE(PLCHSHOT.wav) \ -VCMI_SOUND_NAME(PLCHWNCE) VCMI_SOUND_FILE(PLCHWNCE.wav) \ -VCMI_SOUND_NAME(PLIZAttack) VCMI_SOUND_FILE(PLIZATTK.wav) \ -VCMI_SOUND_NAME(PLIZDefend) VCMI_SOUND_FILE(PLIZDFND.wav) \ -VCMI_SOUND_NAME(PLIZKill) VCMI_SOUND_FILE(PLIZKILL.wav) \ -VCMI_SOUND_NAME(PLIZMove) VCMI_SOUND_FILE(PLIZMOVE.wav) \ -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(PROTECTA) VCMI_SOUND_FILE(PROTECTA.wav) \ -VCMI_SOUND_NAME(PROTECTE) VCMI_SOUND_FILE(PROTECTE.wav) \ -VCMI_SOUND_NAME(PROTECTF) VCMI_SOUND_FILE(PROTECTF.wav) \ -VCMI_SOUND_NAME(PROTECT) VCMI_SOUND_FILE(PROTECT.wav) \ -VCMI_SOUND_NAME(PROTECTW) VCMI_SOUND_FILE(PROTECTW.wav) \ -VCMI_SOUND_NAME(PSNTAttack) VCMI_SOUND_FILE(PSNTATTK.wav) \ -VCMI_SOUND_NAME(PSNTDefend) VCMI_SOUND_FILE(PSNTDFND.wav) \ -VCMI_SOUND_NAME(PSNTKill) VCMI_SOUND_FILE(PSNTKILL.wav) \ -VCMI_SOUND_NAME(PSNTMove) VCMI_SOUND_FILE(PSNTMOVE.wav) \ -VCMI_SOUND_NAME(PSNTWNCE) VCMI_SOUND_FILE(PSNTWNCE.wav) \ -VCMI_SOUND_NAME(PSYCAttack) VCMI_SOUND_FILE(PSYCATTK.wav) \ -VCMI_SOUND_NAME(PSYCDefend) VCMI_SOUND_FILE(PSYCDFND.wav) \ -VCMI_SOUND_NAME(PSYCKill) VCMI_SOUND_FILE(PSYCKILL.wav) \ -VCMI_SOUND_NAME(PSYCMove) VCMI_SOUND_FILE(PSYCMOVE.wav) \ -VCMI_SOUND_NAME(PSYCWNCE) VCMI_SOUND_FILE(PSYCWNCE.wav) \ -VCMI_SOUND_NAME(QUEST) VCMI_SOUND_FILE(QUEST.wav) \ -VCMI_SOUND_NAME(QUIKSAND) VCMI_SOUND_FILE(QUIKSAND.wav) \ -VCMI_SOUND_NAME(RDDRAttack) VCMI_SOUND_FILE(RDDRATTK.wav) \ -VCMI_SOUND_NAME(RDDRDefend) VCMI_SOUND_FILE(RDDRDFND.wav) \ -VCMI_SOUND_NAME(RDDRKill) VCMI_SOUND_FILE(RDDRKILL.wav) \ -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(RGRFAttack) VCMI_SOUND_FILE(RGRFATTK.wav) \ -VCMI_SOUND_NAME(RGRFDefend) VCMI_SOUND_FILE(RGRFDFND.wav) \ -VCMI_SOUND_NAME(RGRFKill) VCMI_SOUND_FILE(RGRFKILL.wav) \ -VCMI_SOUND_NAME(RGRFMove) VCMI_SOUND_FILE(RGRFMOVE.wav) \ -VCMI_SOUND_NAME(RGRFWNCE) VCMI_SOUND_FILE(RGRFWNCE.wav) \ -VCMI_SOUND_NAME(ROCCAttack) VCMI_SOUND_FILE(ROCCATTK.wav) \ -VCMI_SOUND_NAME(ROCCDefend) VCMI_SOUND_FILE(ROCCDFND.wav) \ -VCMI_SOUND_NAME(ROCCKill) VCMI_SOUND_FILE(ROCCKILL.wav) \ -VCMI_SOUND_NAME(ROCCMove) VCMI_SOUND_FILE(ROCCMOVE.wav) \ -VCMI_SOUND_NAME(ROCCWNCE) VCMI_SOUND_FILE(ROCCWNCE.wav) \ -VCMI_SOUND_NAME(ROGUAttack) VCMI_SOUND_FILE(ROGUATTK.wav) \ -VCMI_SOUND_NAME(ROGUDefend) VCMI_SOUND_FILE(ROGUDFND.wav) \ -VCMI_SOUND_NAME(ROGUE) VCMI_SOUND_FILE(ROGUE.wav) \ -VCMI_SOUND_NAME(ROGUKill) VCMI_SOUND_FILE(ROGUKILL.wav) \ -VCMI_SOUND_NAME(ROGUMove) VCMI_SOUND_FILE(ROGUMOVE.wav) \ -VCMI_SOUND_NAME(ROGUWNCE) VCMI_SOUND_FILE(ROGUWNCE.wav) \ -VCMI_SOUND_NAME(RSBRYFZL) VCMI_SOUND_FILE(RSBRYFZL.wav) \ -VCMI_SOUND_NAME(RUSTAttack) VCMI_SOUND_FILE(RUSTATTK.wav) \ -VCMI_SOUND_NAME(RUSTDefend) VCMI_SOUND_FILE(RUSTDFND.wav) \ -VCMI_SOUND_NAME(RUSTKill) VCMI_SOUND_FILE(RUSTKILL.wav) \ -VCMI_SOUND_NAME(RUSTMove) VCMI_SOUND_FILE(RUSTMOVE.wav) \ -VCMI_SOUND_NAME(RUSTWNCE) VCMI_SOUND_FILE(RUSTWNCE.wav) \ -VCMI_SOUND_NAME(SACBRETH) VCMI_SOUND_FILE(SACBRETH.wav) \ -VCMI_SOUND_NAME(SACRIF1) VCMI_SOUND_FILE(SACRIF1.wav) \ -VCMI_SOUND_NAME(SACRIF2) VCMI_SOUND_FILE(SACRIF2.wav) \ -VCMI_SOUND_NAME(SCRPAttack) VCMI_SOUND_FILE(SCRPATTK.wav) \ -VCMI_SOUND_NAME(SCRPDefend) VCMI_SOUND_FILE(SCRPDFND.wav) \ -VCMI_SOUND_NAME(SCRPKill) VCMI_SOUND_FILE(SCRPKILL.wav) \ -VCMI_SOUND_NAME(SCRPMove) VCMI_SOUND_FILE(SCRPMOVE.wav) \ -VCMI_SOUND_NAME(SCRPShot) VCMI_SOUND_FILE(SCRPSHOT.wav) \ -VCMI_SOUND_NAME(SCRPWNCE) VCMI_SOUND_FILE(SCRPWNCE.wav) \ -VCMI_SOUND_NAME(SCUTBOAT) VCMI_SOUND_FILE(SCUTBOAT.wav) \ -VCMI_SOUND_NAME(SGLMAttack) VCMI_SOUND_FILE(SGLMATTK.wav) \ -VCMI_SOUND_NAME(SGLMDefend) VCMI_SOUND_FILE(SGLMDFND.wav) \ -VCMI_SOUND_NAME(SGLMKill) VCMI_SOUND_FILE(SGLMKILL.wav) \ -VCMI_SOUND_NAME(SGLMMove) VCMI_SOUND_FILE(SGLMMOVE.wav) \ -VCMI_SOUND_NAME(SGLMWNCE) VCMI_SOUND_FILE(SGLMWNCE.wav) \ -VCMI_SOUND_NAME(SGRGAttack) VCMI_SOUND_FILE(SGRGATTK.wav) \ -VCMI_SOUND_NAME(SGRGDefend) VCMI_SOUND_FILE(SGRGDFND.wav) \ -VCMI_SOUND_NAME(SGRGKill) VCMI_SOUND_FILE(SGRGKILL.wav) \ -VCMI_SOUND_NAME(SGRGMove) VCMI_SOUND_FILE(SGRGMOVE.wav) \ -VCMI_SOUND_NAME(SGRGWNCE) VCMI_SOUND_FILE(SGRGWNCE.wav) \ -VCMI_SOUND_NAME(SHDMAttack) VCMI_SOUND_FILE(SHDMATTK.wav) \ -VCMI_SOUND_NAME(SHDMDefend) VCMI_SOUND_FILE(SHDMDFND.wav) \ -VCMI_SOUND_NAME(SHDMKill) VCMI_SOUND_FILE(SHDMKILL.wav) \ -VCMI_SOUND_NAME(SHDMMove) VCMI_SOUND_FILE(SHDMMOVE.wav) \ -VCMI_SOUND_NAME(SHDMWNCE) VCMI_SOUND_FILE(SHDMWNCE.wav) \ -VCMI_SOUND_NAME(SHIELD) VCMI_SOUND_FILE(SHIELD.wav) \ -VCMI_SOUND_NAME(SKELAttack) VCMI_SOUND_FILE(SKELATTK.wav) \ -VCMI_SOUND_NAME(SKELDefend) VCMI_SOUND_FILE(SKELDFND.wav) \ -VCMI_SOUND_NAME(SKELKill) VCMI_SOUND_FILE(SKELKILL.wav) \ -VCMI_SOUND_NAME(SKELMove) VCMI_SOUND_FILE(SKELMOVE.wav) \ -VCMI_SOUND_NAME(SKELWNCE) VCMI_SOUND_FILE(SKELWNCE.wav) \ -VCMI_SOUND_NAME(SKLWAttack) VCMI_SOUND_FILE(SKLWATTK.wav) \ -VCMI_SOUND_NAME(SKLWDefend) VCMI_SOUND_FILE(SKLWDFND.wav) \ -VCMI_SOUND_NAME(SKLWKill) VCMI_SOUND_FILE(SKLWKILL.wav) \ -VCMI_SOUND_NAME(SKLWMove) VCMI_SOUND_FILE(SKLWMOVE.wav) \ -VCMI_SOUND_NAME(SKLWWNCE) VCMI_SOUND_FILE(SKLWWNCE.wav) \ -VCMI_SOUND_NAME(SLAYER) VCMI_SOUND_FILE(SLAYER.wav) \ -VCMI_SOUND_NAME(SORROW) VCMI_SOUND_FILE(SORROW.wav) \ -VCMI_SOUND_NAME(SPONTCOMB) VCMI_SOUND_FILE(SPONTCOMB.wav) \ -VCMI_SOUND_NAME(SPRTAttack) VCMI_SOUND_FILE(SPRTATTK.wav) \ -VCMI_SOUND_NAME(SPRTDefend) VCMI_SOUND_FILE(SPRTDFND.wav) \ -VCMI_SOUND_NAME(SPRTKill) VCMI_SOUND_FILE(SPRTKILL.wav) \ -VCMI_SOUND_NAME(SPRTMove) VCMI_SOUND_FILE(SPRTMOVE.wav) \ -VCMI_SOUND_NAME(SPRTWNCE) VCMI_SOUND_FILE(SPRTWNCE.wav) \ -VCMI_SOUND_NAME(STORAttack) VCMI_SOUND_FILE(STORATTK.wav) \ -VCMI_SOUND_NAME(STORDefend) VCMI_SOUND_FILE(STORDFND.wav) \ -VCMI_SOUND_NAME(STORE) VCMI_SOUND_FILE(STORE.wav) \ -VCMI_SOUND_NAME(STORKill) VCMI_SOUND_FILE(STORKILL.wav) \ -VCMI_SOUND_NAME(STORMove) VCMI_SOUND_FILE(STORMOVE.wav) \ -VCMI_SOUND_NAME(STORM) VCMI_SOUND_FILE(STORM.wav) \ -VCMI_SOUND_NAME(STORShot) VCMI_SOUND_FILE(STORSHOT.wav) \ -VCMI_SOUND_NAME(STORWNCE) VCMI_SOUND_FILE(STORWNCE.wav) \ -VCMI_SOUND_NAME(SUMMBOAT) VCMI_SOUND_FILE(SUMMBOAT.wav) \ -VCMI_SOUND_NAME(SUMNELM) VCMI_SOUND_FILE(SUMNELM.wav) \ -VCMI_SOUND_NAME(SWRDAttack) VCMI_SOUND_FILE(SWRDATTK.wav) \ -VCMI_SOUND_NAME(SWRDDefend) VCMI_SOUND_FILE(SWRDDFND.wav) \ -VCMI_SOUND_NAME(SWRDKill) VCMI_SOUND_FILE(SWRDKILL.wav) \ -VCMI_SOUND_NAME(SWRDMove) VCMI_SOUND_FILE(SWRDMOVE.wav) \ -VCMI_SOUND_NAME(SWRDWNCE) VCMI_SOUND_FILE(SWRDWNCE.wav) \ -VCMI_SOUND_NAME(SYSMSG) VCMI_SOUND_FILE(SYSMSG.wav) \ -VCMI_SOUND_NAME(TAILWIND) VCMI_SOUND_FILE(TAILWIND.wav) \ -VCMI_SOUND_NAME(TBRDAttack) VCMI_SOUND_FILE(TBRDATTK.wav) \ -VCMI_SOUND_NAME(TBRDDefend) VCMI_SOUND_FILE(TBRDDFND.wav) \ -VCMI_SOUND_NAME(TBRDKill) VCMI_SOUND_FILE(TBRDKILL.wav) \ -VCMI_SOUND_NAME(TBRDMove) VCMI_SOUND_FILE(TBRDMOVE.wav) \ -VCMI_SOUND_NAME(TBRDWNCE) VCMI_SOUND_FILE(TBRDWNCE.wav) \ -VCMI_SOUND_NAME(TELEIN) VCMI_SOUND_FILE(TELEIN.wav) \ -VCMI_SOUND_NAME(TELPTIN) VCMI_SOUND_FILE(TELPTIN.wav) \ -VCMI_SOUND_NAME(TELPTOUT) VCMI_SOUND_FILE(TELPTOUT.wav) \ -VCMI_SOUND_NAME(temple) VCMI_SOUND_FILE(TEMPLE.wav) \ -VCMI_SOUND_NAME(timeOver) VCMI_SOUND_FILE(TIMEOVER.wav) \ -VCMI_SOUND_NAME(treasure) VCMI_SOUND_FILE(TREASURE.wav) \ -VCMI_SOUND_NAME(TREEAttack) VCMI_SOUND_FILE(TREEATTK.wav) \ -VCMI_SOUND_NAME(TREEDefend) VCMI_SOUND_FILE(TREEDFND.wav) \ -VCMI_SOUND_NAME(TREEKill) VCMI_SOUND_FILE(TREEKILL.wav) \ -VCMI_SOUND_NAME(TREEMove) VCMI_SOUND_FILE(TREEMOVE.wav) \ -VCMI_SOUND_NAME(TREEWNCE) VCMI_SOUND_FILE(TREEWNCE.wav) \ -VCMI_SOUND_NAME(TRLLAttack) VCMI_SOUND_FILE(TRLLATTK.wav) \ -VCMI_SOUND_NAME(TRLLDefend) VCMI_SOUND_FILE(TRLLDFND.wav) \ -VCMI_SOUND_NAME(TRLLKill) VCMI_SOUND_FILE(TRLLKILL.wav) \ -VCMI_SOUND_NAME(TRLLMove) VCMI_SOUND_FILE(TRLLMOVE.wav) \ -VCMI_SOUND_NAME(TRLLWNCE) VCMI_SOUND_FILE(TRLLWNCE.wav) \ -VCMI_SOUND_NAME(TROGAttack) VCMI_SOUND_FILE(TROGATTK.wav) \ -VCMI_SOUND_NAME(TROGDefend) VCMI_SOUND_FILE(TROGDFND.wav) \ -VCMI_SOUND_NAME(TROGKill) VCMI_SOUND_FILE(TROGKILL.wav) \ -VCMI_SOUND_NAME(TROGMove) VCMI_SOUND_FILE(TROGMOVE.wav) \ -VCMI_SOUND_NAME(TROGWNCE) VCMI_SOUND_FILE(TROGWNCE.wav) \ -VCMI_SOUND_NAME(TUFFSKIN) VCMI_SOUND_FILE(TUFFSKIN.wav) \ -VCMI_SOUND_NAME(ULTIMATEARTIFACT) VCMI_SOUND_FILE(ULTIMATEARTIFACT.wav) \ -VCMI_SOUND_NAME(UNICAttack) VCMI_SOUND_FILE(UNICATTK.wav) \ -VCMI_SOUND_NAME(UNICDefend) VCMI_SOUND_FILE(UNICDFND.wav) \ -VCMI_SOUND_NAME(UNICKill) VCMI_SOUND_FILE(UNICKILL.wav) \ -VCMI_SOUND_NAME(UNICMove) VCMI_SOUND_FILE(UNICMOVE.wav) \ -VCMI_SOUND_NAME(UNICWNCE) VCMI_SOUND_FILE(UNICWNCE.wav) \ -VCMI_SOUND_NAME(VAMPAttack) VCMI_SOUND_FILE(VAMPATTK.wav) \ -VCMI_SOUND_NAME(VAMPDefend) VCMI_SOUND_FILE(VAMPDFND.wav) \ -VCMI_SOUND_NAME(VAMPEXT1) VCMI_SOUND_FILE(VAMPEXT1.wav) \ -VCMI_SOUND_NAME(VAMPEXT2) VCMI_SOUND_FILE(VAMPEXT2.wav) \ -VCMI_SOUND_NAME(VAMPKill) VCMI_SOUND_FILE(VAMPKILL.wav) \ -VCMI_SOUND_NAME(VAMPMove) VCMI_SOUND_FILE(VAMPMOVE.wav) \ -VCMI_SOUND_NAME(VAMPWNCE) VCMI_SOUND_FILE(VAMPWNCE.wav) \ -VCMI_SOUND_NAME(VIEW) VCMI_SOUND_FILE(VIEW.wav) \ -VCMI_SOUND_NAME(VISIONS) VCMI_SOUND_FILE(VISIONS.wav) \ -VCMI_SOUND_NAME(WALLHIT) VCMI_SOUND_FILE(WALLHIT.wav) \ -VCMI_SOUND_NAME(WALLMISS) VCMI_SOUND_FILE(WALLMISS.wav) \ -VCMI_SOUND_NAME(WATRWALK) VCMI_SOUND_FILE(WATRWALK.wav) \ -VCMI_SOUND_NAME(WEAKNESS) VCMI_SOUND_FILE(WEAKNESS.wav) \ -VCMI_SOUND_NAME(WELFAttack) VCMI_SOUND_FILE(WELFATTK.wav) \ -VCMI_SOUND_NAME(WELFDefend) VCMI_SOUND_FILE(WELFDFND.wav) \ -VCMI_SOUND_NAME(WELFKill) VCMI_SOUND_FILE(WELFKILL.wav) \ -VCMI_SOUND_NAME(WELFMove) VCMI_SOUND_FILE(WELFMOVE.wav) \ -VCMI_SOUND_NAME(WELFShot) VCMI_SOUND_FILE(WELFSHOT.wav) \ -VCMI_SOUND_NAME(WELFWNCE) VCMI_SOUND_FILE(WELFWNCE.wav) \ -VCMI_SOUND_NAME(WELMAttack) VCMI_SOUND_FILE(WELMATTK.wav) \ -VCMI_SOUND_NAME(WELMDefend) VCMI_SOUND_FILE(WELMDFND.wav) \ -VCMI_SOUND_NAME(WELMKill) VCMI_SOUND_FILE(WELMKILL.wav) \ -VCMI_SOUND_NAME(WELMMove) VCMI_SOUND_FILE(WELMMOVE.wav) \ -VCMI_SOUND_NAME(WELMWNCE) VCMI_SOUND_FILE(WELMWNCE.wav) \ -VCMI_SOUND_NAME(WGHTAttack) VCMI_SOUND_FILE(WGHTATTK.wav) \ -VCMI_SOUND_NAME(WGHTDefend) VCMI_SOUND_FILE(WGHTDFND.wav) \ -VCMI_SOUND_NAME(WGHTKill) VCMI_SOUND_FILE(WGHTKILL.wav) \ -VCMI_SOUND_NAME(WGHTMove) VCMI_SOUND_FILE(WGHTMOVE.wav) \ -VCMI_SOUND_NAME(WGHTWNCE) VCMI_SOUND_FILE(WGHTWNCE.wav) \ -VCMI_SOUND_NAME(WRTHAttack) VCMI_SOUND_FILE(WRTHATTK.wav) \ -VCMI_SOUND_NAME(WRTHDefend) VCMI_SOUND_FILE(WRTHDFND.wav) \ -VCMI_SOUND_NAME(WRTHKill) VCMI_SOUND_FILE(WRTHKILL.wav) \ -VCMI_SOUND_NAME(WRTHMove) VCMI_SOUND_FILE(WRTHMOVE.wav) \ -VCMI_SOUND_NAME(WRTHWNCE) VCMI_SOUND_FILE(WRTHWNCE.wav) \ -VCMI_SOUND_NAME(WUNCAttack) VCMI_SOUND_FILE(WUNCATTK.wav) \ -VCMI_SOUND_NAME(WUNCDefend) VCMI_SOUND_FILE(WUNCDFND.wav) \ -VCMI_SOUND_NAME(WUNCKill) VCMI_SOUND_FILE(WUNCKILL.wav) \ -VCMI_SOUND_NAME(WUNCMove) VCMI_SOUND_FILE(WUNCMOVE.wav) \ -VCMI_SOUND_NAME(WUNCShot) VCMI_SOUND_FILE(WUNCSHOT.wav) \ -VCMI_SOUND_NAME(WUNCWNCE) VCMI_SOUND_FILE(WUNCWNCE.wav) \ -VCMI_SOUND_NAME(WYVMAttack) VCMI_SOUND_FILE(WYVMATTK.wav) \ -VCMI_SOUND_NAME(WYVMDefend) VCMI_SOUND_FILE(WYVMDFND.wav) \ -VCMI_SOUND_NAME(WYVMKill) VCMI_SOUND_FILE(WYVMKILL.wav) \ -VCMI_SOUND_NAME(WYVMMove) VCMI_SOUND_FILE(WYVMMOVE.wav) \ -VCMI_SOUND_NAME(WYVMWNCE) VCMI_SOUND_FILE(WYVMWNCE.wav) \ -VCMI_SOUND_NAME(WYVNAttack) VCMI_SOUND_FILE(WYVNATTK.wav) \ -VCMI_SOUND_NAME(WYVNDefend) VCMI_SOUND_FILE(WYVNDFND.wav) \ -VCMI_SOUND_NAME(WYVNKill) VCMI_SOUND_FILE(WYVNKILL.wav) \ -VCMI_SOUND_NAME(WYVNMove) VCMI_SOUND_FILE(WYVNMOVE.wav) \ -VCMI_SOUND_NAME(WYVNWNCE) VCMI_SOUND_FILE(WYVNWNCE.wav) \ -VCMI_SOUND_NAME(YBMHAttack) VCMI_SOUND_FILE(YBMHATTK.wav) \ -VCMI_SOUND_NAME(YBMHDefend) VCMI_SOUND_FILE(YBMHDFND.wav) \ -VCMI_SOUND_NAME(YBMHKill) VCMI_SOUND_FILE(YBMHKILL.wav) \ -VCMI_SOUND_NAME(YBMHMove) VCMI_SOUND_FILE(YBMHMOVE.wav) \ -VCMI_SOUND_NAME(YBMHWNCE) VCMI_SOUND_FILE(YBMHWNCE.wav) \ -VCMI_SOUND_NAME(zelotAttack) VCMI_SOUND_FILE(ZELTATTK.wav) \ -VCMI_SOUND_NAME(zelotDefend) VCMI_SOUND_FILE(ZELTDFND.wav) \ -VCMI_SOUND_NAME(zelotKill) VCMI_SOUND_FILE(ZELTKILL.wav) \ -VCMI_SOUND_NAME(zelotMove) VCMI_SOUND_FILE(ZELTMOVE.wav) \ -VCMI_SOUND_NAME(zelotShot) VCMI_SOUND_FILE(ZELTSHOT.wav) \ -VCMI_SOUND_NAME(zelotWNCE) VCMI_SOUND_FILE(ZELTWNCE.wav) \ -VCMI_SOUND_NAME(ZMBLAttack) VCMI_SOUND_FILE(ZMBLATTK.wav) \ -VCMI_SOUND_NAME(ZMBLDefend) VCMI_SOUND_FILE(ZMBLDFND.wav) \ -VCMI_SOUND_NAME(ZMBLKill) VCMI_SOUND_FILE(ZMBLKILL.wav) \ -VCMI_SOUND_NAME(ZMBLMove) VCMI_SOUND_FILE(ZMBLMOVE.wav) \ -VCMI_SOUND_NAME(ZMBLWNCE) VCMI_SOUND_FILE(ZMBLWNCE.wav) \ -VCMI_SOUND_NAME(ZOMBAttack) VCMI_SOUND_FILE(ZOMBATTK.wav) \ -VCMI_SOUND_NAME(ZOMBDefend) VCMI_SOUND_FILE(ZOMBDFND.wav) \ -VCMI_SOUND_NAME(ZOMBKill) VCMI_SOUND_FILE(ZOMBKILL.wav) \ -VCMI_SOUND_NAME(ZOMBMove) VCMI_SOUND_FILE(ZOMBMOVE.wav) \ -VCMI_SOUND_NAME(ZOMBWNCE) VCMI_SOUND_FILE(ZOMBWNCE.wav) - -class soundBase -{ -public: - // Make a list of enums - // We must keep an entry 0 for an invalid or no sound. -#define VCMI_SOUND_NAME(x) x, -#define VCMI_SOUND_FILE(y) - enum soundID { - invalid=0, - sound_todo=1, // temp entry until code is fixed - VCMI_SOUND_LIST - sound_after_last - }; -#undef VCMI_SOUND_FILE -#undef VCMI_SOUND_NAME -}; - -VCMI_LIB_NAMESPACE_END +/* + * CSoundBase.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 + +// Use some magic to keep the list of files and their code name in sync. + +#define VCMI_SOUND_LIST \ +/* Sounds for map actions */ \ +VCMI_SOUND_NAME(KillFade) VCMI_SOUND_FILE(KILLFADE.wav) /* hero or monster disappears */ \ +/* Other sounds (TODO: separate out the sounds for units, spells and the rest */ \ +VCMI_SOUND_NAME(AAGLAttack) VCMI_SOUND_FILE(AAGLATTK.wav) \ +VCMI_SOUND_NAME(AAGLDefend) VCMI_SOUND_FILE(AAGLDFND.wav) \ +VCMI_SOUND_NAME(AAGLKill) VCMI_SOUND_FILE(AAGLKILL.wav) \ +VCMI_SOUND_NAME(AAGLMove) VCMI_SOUND_FILE(AAGLMOVE.wav) \ +VCMI_SOUND_NAME(AAGLWNCE) VCMI_SOUND_FILE(AAGLWNCE.wav) \ +VCMI_SOUND_NAME(acid) VCMI_SOUND_FILE(ACID.wav) \ +VCMI_SOUND_NAME(ADVLAttack) VCMI_SOUND_FILE(ADVLATTK.wav) \ +VCMI_SOUND_NAME(ADVLDefend) VCMI_SOUND_FILE(ADVLDFND.wav) \ +VCMI_SOUND_NAME(ADVLEXT1) VCMI_SOUND_FILE(ADVLEXT1.wav) \ +VCMI_SOUND_NAME(ADVLEXT2) VCMI_SOUND_FILE(ADVLEXT2.wav) \ +VCMI_SOUND_NAME(ADVLKill) VCMI_SOUND_FILE(ADVLKILL.wav) \ +VCMI_SOUND_NAME(ADVLMove) VCMI_SOUND_FILE(ADVLMOVE.wav) \ +VCMI_SOUND_NAME(ADVLWNCE) VCMI_SOUND_FILE(ADVLWNCE.wav) \ +VCMI_SOUND_NAME(AELMAttack) VCMI_SOUND_FILE(AELMATTK.wav) \ +VCMI_SOUND_NAME(AELMDefend) VCMI_SOUND_FILE(AELMDFND.wav) \ +VCMI_SOUND_NAME(AELMKill) VCMI_SOUND_FILE(AELMKILL.wav) \ +VCMI_SOUND_NAME(AELMMove) VCMI_SOUND_FILE(AELMMOVE.wav) \ +VCMI_SOUND_NAME(AELMWNCE) VCMI_SOUND_FILE(AELMWNCE.wav) \ +VCMI_SOUND_NAME(AGE) VCMI_SOUND_FILE(AGE.wav) \ +VCMI_SOUND_NAME(AGRMAttack) VCMI_SOUND_FILE(AGRMATTK.wav) \ +VCMI_SOUND_NAME(AGRMDefend) VCMI_SOUND_FILE(AGRMDFND.wav) \ +VCMI_SOUND_NAME(AGRMKill) VCMI_SOUND_FILE(AGRMKILL.wav) \ +VCMI_SOUND_NAME(AGRMMove) VCMI_SOUND_FILE(AGRMMOVE.wav) \ +VCMI_SOUND_NAME(AGRMShot) VCMI_SOUND_FILE(AGRMSHOT.wav) \ +VCMI_SOUND_NAME(AGRMWNCE) VCMI_SOUND_FILE(AGRMWNCE.wav) \ +VCMI_SOUND_NAME(AIRSHELD) VCMI_SOUND_FILE(AIRSHELD.wav) \ +VCMI_SOUND_NAME(ALIZAttack) VCMI_SOUND_FILE(ALIZATTK.wav) \ +VCMI_SOUND_NAME(ALIZDefend) VCMI_SOUND_FILE(ALIZDFND.wav) \ +VCMI_SOUND_NAME(ALIZKill) VCMI_SOUND_FILE(ALIZKILL.wav) \ +VCMI_SOUND_NAME(ALIZMove) VCMI_SOUND_FILE(ALIZMOVE.wav) \ +VCMI_SOUND_NAME(ALIZShot) VCMI_SOUND_FILE(ALIZSHOT.wav) \ +VCMI_SOUND_NAME(ALIZWNCE) VCMI_SOUND_FILE(ALIZWNCE.wav) \ +VCMI_SOUND_NAME(AMAGAttack) VCMI_SOUND_FILE(AMAGATTK.wav) \ +VCMI_SOUND_NAME(AMAGDefend) VCMI_SOUND_FILE(AMAGDFND.wav) \ +VCMI_SOUND_NAME(AMAGKill) VCMI_SOUND_FILE(AMAGKILL.wav) \ +VCMI_SOUND_NAME(AMAGMove) VCMI_SOUND_FILE(AMAGMOVE.wav) \ +VCMI_SOUND_NAME(AMAGShot) VCMI_SOUND_FILE(AMAGSHOT.wav) \ +VCMI_SOUND_NAME(AMAGWNCE) VCMI_SOUND_FILE(AMAGWNCE.wav) \ +VCMI_SOUND_NAME(ANGLAttack) VCMI_SOUND_FILE(ANGLATTK.wav) \ +VCMI_SOUND_NAME(ANGLDefend) VCMI_SOUND_FILE(ANGLDFND.wav) \ +VCMI_SOUND_NAME(ANGLKill) VCMI_SOUND_FILE(ANGLKILL.wav) \ +VCMI_SOUND_NAME(ANGLMove) VCMI_SOUND_FILE(ANGLMOVE.wav) \ +VCMI_SOUND_NAME(ANGLWNCE) VCMI_SOUND_FILE(ANGLWNCE.wav) \ +VCMI_SOUND_NAME(ANIMDEAD) VCMI_SOUND_FILE(ANIMDEAD.wav) \ +VCMI_SOUND_NAME(ANTIMAGK) VCMI_SOUND_FILE(ANTIMAGK.wav) \ +VCMI_SOUND_NAME(APEGAttack) VCMI_SOUND_FILE(APEGATTK.wav) \ +VCMI_SOUND_NAME(APEGDefend) VCMI_SOUND_FILE(APEGDFND.wav) \ +VCMI_SOUND_NAME(APEGKill) VCMI_SOUND_FILE(APEGKILL.wav) \ +VCMI_SOUND_NAME(APEGMove) VCMI_SOUND_FILE(APEGMOVE.wav) \ +VCMI_SOUND_NAME(APEGWNCE) VCMI_SOUND_FILE(APEGWNCE.wav) \ +VCMI_SOUND_NAME(ARMGEDN) VCMI_SOUND_FILE(ARMGEDN.wav) \ +VCMI_SOUND_NAME(AZURAttack) VCMI_SOUND_FILE(AZURATTK.wav) \ +VCMI_SOUND_NAME(AZURDefend) VCMI_SOUND_FILE(AZURDFND.wav) \ +VCMI_SOUND_NAME(AZURKill) VCMI_SOUND_FILE(AZURKILL.wav) \ +VCMI_SOUND_NAME(AZURMove) VCMI_SOUND_FILE(AZURMOVE.wav) \ +VCMI_SOUND_NAME(AZURWNCE) VCMI_SOUND_FILE(AZURWNCE.wav) \ +VCMI_SOUND_NAME(BACKLASH) VCMI_SOUND_FILE(BACKLASH.wav) \ +VCMI_SOUND_NAME(BADLUCK) VCMI_SOUND_FILE(BADLUCK.wav) \ +VCMI_SOUND_NAME(BADMRLE) VCMI_SOUND_FILE(BADMRLE.wav) \ +VCMI_SOUND_NAME(BALLKill) VCMI_SOUND_FILE(BALLKILL.wav) \ +VCMI_SOUND_NAME(BALLShot) VCMI_SOUND_FILE(BALLSHOT.wav) \ +VCMI_SOUND_NAME(BALLWNCE) VCMI_SOUND_FILE(BALLWNCE.wav) \ +VCMI_SOUND_NAME(BASLAttack) VCMI_SOUND_FILE(BASLATTK.wav) \ +VCMI_SOUND_NAME(BASLDefend) VCMI_SOUND_FILE(BASLDFND.wav) \ +VCMI_SOUND_NAME(BASLKill) VCMI_SOUND_FILE(BASLKILL.wav) \ +VCMI_SOUND_NAME(BASLMove) VCMI_SOUND_FILE(BASLMOVE.wav) \ +VCMI_SOUND_NAME(BASLWNCE) VCMI_SOUND_FILE(BASLWNCE.wav) \ +VCMI_SOUND_NAME(battle00) VCMI_SOUND_FILE(BATTLE00.wav) \ +VCMI_SOUND_NAME(battle01) VCMI_SOUND_FILE(BATTLE01.wav) \ +VCMI_SOUND_NAME(battle02) VCMI_SOUND_FILE(BATTLE02.wav) \ +VCMI_SOUND_NAME(battle03) VCMI_SOUND_FILE(BATTLE03.wav) \ +VCMI_SOUND_NAME(battle04) VCMI_SOUND_FILE(BATTLE04.wav) \ +VCMI_SOUND_NAME(battle05) VCMI_SOUND_FILE(BATTLE05.wav) \ +VCMI_SOUND_NAME(battle06) VCMI_SOUND_FILE(BATTLE06.wav) \ +VCMI_SOUND_NAME(battle07) VCMI_SOUND_FILE(BATTLE07.wav) \ +VCMI_SOUND_NAME(BDRFAttack) VCMI_SOUND_FILE(BDRFATTK.wav) \ +VCMI_SOUND_NAME(BDRFDefend) VCMI_SOUND_FILE(BDRFDFND.wav) \ +VCMI_SOUND_NAME(BDRFKill) VCMI_SOUND_FILE(BDRFKILL.wav) \ +VCMI_SOUND_NAME(BDRFMove) VCMI_SOUND_FILE(BDRFMOVE.wav) \ +VCMI_SOUND_NAME(BDRFWNCE) VCMI_SOUND_FILE(BDRFWNCE.wav) \ +VCMI_SOUND_NAME(BERSERK) VCMI_SOUND_FILE(BERSERK.wav) \ +VCMI_SOUND_NAME(BGORAttack) VCMI_SOUND_FILE(BGORATTK.wav) \ +VCMI_SOUND_NAME(BGORDefend) VCMI_SOUND_FILE(BGORDFND.wav) \ +VCMI_SOUND_NAME(BGORKill) VCMI_SOUND_FILE(BGORKILL.wav) \ +VCMI_SOUND_NAME(BGORMove) VCMI_SOUND_FILE(BGORMOVE.wav) \ +VCMI_SOUND_NAME(BGORWNCE) VCMI_SOUND_FILE(BGORWNCE.wav) \ +VCMI_SOUND_NAME(BHDRAttack) VCMI_SOUND_FILE(BHDRATTK.wav) \ +VCMI_SOUND_NAME(BHDRDETH) VCMI_SOUND_FILE(BHDRDETH.wav) \ +VCMI_SOUND_NAME(BHDRDefend) VCMI_SOUND_FILE(BHDRDFND.wav) \ +VCMI_SOUND_NAME(BHDRKill) VCMI_SOUND_FILE(BHDRKILL.wav) \ +VCMI_SOUND_NAME(BHDRMove) VCMI_SOUND_FILE(BHDRMOVE.wav) \ +VCMI_SOUND_NAME(BHDRShot) VCMI_SOUND_FILE(BHDRSHOT.wav) \ +VCMI_SOUND_NAME(BHDRWNCE) VCMI_SOUND_FILE(BHDRWNCE.wav) \ +VCMI_SOUND_NAME(BIND) VCMI_SOUND_FILE(BIND.wav) \ +VCMI_SOUND_NAME(BKDRAttack) VCMI_SOUND_FILE(BKDRATTK.wav) \ +VCMI_SOUND_NAME(BKDRDefend) VCMI_SOUND_FILE(BKDRDFND.wav) \ +VCMI_SOUND_NAME(BKDRKill) VCMI_SOUND_FILE(BKDRKILL.wav) \ +VCMI_SOUND_NAME(BKDRMove) VCMI_SOUND_FILE(BKDRMOVE.wav) \ +VCMI_SOUND_NAME(BKDRWNCE) VCMI_SOUND_FILE(BKDRWNCE.wav) \ +VCMI_SOUND_NAME(BKNTAttack) VCMI_SOUND_FILE(BKNTATTK.wav) \ +VCMI_SOUND_NAME(BKNTDefend) VCMI_SOUND_FILE(BKNTDFND.wav) \ +VCMI_SOUND_NAME(BKNTKill) VCMI_SOUND_FILE(BKNTKILL.wav) \ +VCMI_SOUND_NAME(BKNTMove) VCMI_SOUND_FILE(BKNTMOVE.wav) \ +VCMI_SOUND_NAME(BKNTWNCE) VCMI_SOUND_FILE(BKNTWNCE.wav) \ +VCMI_SOUND_NAME(bless) VCMI_SOUND_FILE(BLESS.wav) \ +VCMI_SOUND_NAME(blind) VCMI_SOUND_FILE(BLIND.wav) \ +VCMI_SOUND_NAME(bloodlus) VCMI_SOUND_FILE(BLOODLUS.wav) \ +VCMI_SOUND_NAME(BLRDAttack) VCMI_SOUND_FILE(BLRDATTK.wav) \ +VCMI_SOUND_NAME(BLRDDefend) VCMI_SOUND_FILE(BLRDDFND.wav) \ +VCMI_SOUND_NAME(BLRDKill) VCMI_SOUND_FILE(BLRDKILL.wav) \ +VCMI_SOUND_NAME(BLRDMove) VCMI_SOUND_FILE(BLRDMOVE.wav) \ +VCMI_SOUND_NAME(BLRDWNCE) VCMI_SOUND_FILE(BLRDWNCE.wav) \ +VCMI_SOUND_NAME(BMTHAttack) VCMI_SOUND_FILE(BMTHATTK.wav) \ +VCMI_SOUND_NAME(BMTHDefend) VCMI_SOUND_FILE(BMTHDFND.wav) \ +VCMI_SOUND_NAME(BMTHKill) VCMI_SOUND_FILE(BMTHKILL.wav) \ +VCMI_SOUND_NAME(BMTHMove) VCMI_SOUND_FILE(BMTHMOVE.wav) \ +VCMI_SOUND_NAME(BMTHWNCE) VCMI_SOUND_FILE(BMTHWNCE.wav) \ +VCMI_SOUND_NAME(BOARAttack) VCMI_SOUND_FILE(BOARATTK.wav) \ +VCMI_SOUND_NAME(BOARDefend) VCMI_SOUND_FILE(BOARDFND.wav) \ +VCMI_SOUND_NAME(BOARKill) VCMI_SOUND_FILE(BOARKILL.wav) \ +VCMI_SOUND_NAME(BOARMove) VCMI_SOUND_FILE(BOARMOVE.wav) \ +VCMI_SOUND_NAME(BOARWNCE) VCMI_SOUND_FILE(BOARWNCE.wav) \ +VCMI_SOUND_NAME(BODRAttack) VCMI_SOUND_FILE(BODRATTK.wav) \ +VCMI_SOUND_NAME(BODRDefend) VCMI_SOUND_FILE(BODRDFND.wav) \ +VCMI_SOUND_NAME(BODRKill) VCMI_SOUND_FILE(BODRKILL.wav) \ +VCMI_SOUND_NAME(BODRMove) VCMI_SOUND_FILE(BODRMOVE.wav) \ +VCMI_SOUND_NAME(BODRWNCE) VCMI_SOUND_FILE(BODRWNCE.wav) \ +VCMI_SOUND_NAME(BTREAttack) VCMI_SOUND_FILE(BTREATTK.wav) \ +VCMI_SOUND_NAME(BTREDefend) VCMI_SOUND_FILE(BTREDFND.wav) \ +VCMI_SOUND_NAME(BTREKill) VCMI_SOUND_FILE(BTREKILL.wav) \ +VCMI_SOUND_NAME(BTREMove) VCMI_SOUND_FILE(BTREMOVE.wav) \ +VCMI_SOUND_NAME(BTREWNCE) VCMI_SOUND_FILE(BTREWNCE.wav) \ +VCMI_SOUND_NAME(newBuilding) VCMI_SOUND_FILE(BUILDTWN.wav) \ +VCMI_SOUND_NAME(button) VCMI_SOUND_FILE(BUTTON.wav) \ +VCMI_SOUND_NAME(CALFAttack) VCMI_SOUND_FILE(CALFATTK.wav) \ +VCMI_SOUND_NAME(CALFDefend) VCMI_SOUND_FILE(CALFDFND.wav) \ +VCMI_SOUND_NAME(CALFKill) VCMI_SOUND_FILE(CALFKILL.wav) \ +VCMI_SOUND_NAME(CALFMove) VCMI_SOUND_FILE(CALFMOVE.wav) \ +VCMI_SOUND_NAME(CALFShot) VCMI_SOUND_FILE(CALFSHOT.wav) \ +VCMI_SOUND_NAME(CALFWNCE) VCMI_SOUND_FILE(CALFWNCE.wav) \ +VCMI_SOUND_NAME(CARTKill) VCMI_SOUND_FILE(CARTKILL.wav) \ +VCMI_SOUND_NAME(CARTWNCE) VCMI_SOUND_FILE(CARTWNCE.wav) \ +VCMI_SOUND_NAME(CATAKill) VCMI_SOUND_FILE(CATAKILL.wav) \ +VCMI_SOUND_NAME(CATAShot) VCMI_SOUND_FILE(CATASHOT.wav) \ +VCMI_SOUND_NAME(CATAWNCE) VCMI_SOUND_FILE(CATAWNCE.wav) \ +VCMI_SOUND_NAME(CAVAAttack) VCMI_SOUND_FILE(CAVAATTK.wav) \ +VCMI_SOUND_NAME(CAVADefend) VCMI_SOUND_FILE(CAVADFND.wav) \ +VCMI_SOUND_NAME(CAVAKill) VCMI_SOUND_FILE(CAVAKILL.wav) \ +VCMI_SOUND_NAME(CAVAMove) VCMI_SOUND_FILE(CAVAMOVE.wav) \ +VCMI_SOUND_NAME(CAVAWNCE) VCMI_SOUND_FILE(CAVAWNCE.wav) \ +VCMI_SOUND_NAME(CAVEHEAD) VCMI_SOUND_FILE(CAVEHEAD.wav) \ +VCMI_SOUND_NAME(CCYCAttack) VCMI_SOUND_FILE(CCYCATTK.wav) \ +VCMI_SOUND_NAME(CCYCDefend) VCMI_SOUND_FILE(CCYCDFND.wav) \ +VCMI_SOUND_NAME(CCYCKill) VCMI_SOUND_FILE(CCYCKILL.wav) \ +VCMI_SOUND_NAME(CCYCMove) VCMI_SOUND_FILE(CCYCMOVE.wav) \ +VCMI_SOUND_NAME(CCYCShot) VCMI_SOUND_FILE(CCYCSHOT.wav) \ +VCMI_SOUND_NAME(CCYCWNCE) VCMI_SOUND_FILE(CCYCWNCE.wav) \ +VCMI_SOUND_NAME(CERBAttack) VCMI_SOUND_FILE(CERBATTK.wav) \ +VCMI_SOUND_NAME(CERBDefend) VCMI_SOUND_FILE(CERBDFND.wav) \ +VCMI_SOUND_NAME(CERBKill) VCMI_SOUND_FILE(CERBKILL.wav) \ +VCMI_SOUND_NAME(CERBMove) VCMI_SOUND_FILE(CERBMOVE.wav) \ +VCMI_SOUND_NAME(CERBWNCE) VCMI_SOUND_FILE(CERBWNCE.wav) \ +VCMI_SOUND_NAME(CGORAttack) VCMI_SOUND_FILE(CGORATTK.wav) \ +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(chat) VCMI_SOUND_FILE(CHAT.wav) \ +VCMI_SOUND_NAME(chest) VCMI_SOUND_FILE(CHEST.wav) \ +VCMI_SOUND_NAME(CHMPAttack) VCMI_SOUND_FILE(CHMPATTK.wav) \ +VCMI_SOUND_NAME(CHMPDefend) VCMI_SOUND_FILE(CHMPDFND.wav) \ +VCMI_SOUND_NAME(CHMPKill) VCMI_SOUND_FILE(CHMPKILL.wav) \ +VCMI_SOUND_NAME(CHMPMove) VCMI_SOUND_FILE(CHMPMOVE.wav) \ +VCMI_SOUND_NAME(CHMPWNCE) VCMI_SOUND_FILE(CHMPWNCE.wav) \ +VCMI_SOUND_NAME(CHYDAttack) VCMI_SOUND_FILE(CHYDATTK.wav) \ +VCMI_SOUND_NAME(CHYDDefend) VCMI_SOUND_FILE(CHYDDFND.wav) \ +VCMI_SOUND_NAME(CHYDKill) VCMI_SOUND_FILE(CHYDKILL.wav) \ +VCMI_SOUND_NAME(CHYDMove) VCMI_SOUND_FILE(CHYDMOVE.wav) \ +VCMI_SOUND_NAME(CHYDWNCE) VCMI_SOUND_FILE(CHYDWNCE.wav) \ +VCMI_SOUND_NAME(CLIMAX) VCMI_SOUND_FILE(CLIMAX.wav) \ +VCMI_SOUND_NAME(CLONE) VCMI_SOUND_FILE(CLONE.wav) \ +VCMI_SOUND_NAME(CNTRAttack) VCMI_SOUND_FILE(CNTRATTK.wav) \ +VCMI_SOUND_NAME(CNTRDefend) VCMI_SOUND_FILE(CNTRDFND.wav) \ +VCMI_SOUND_NAME(CNTRKill) VCMI_SOUND_FILE(CNTRKILL.wav) \ +VCMI_SOUND_NAME(CNTRMove) VCMI_SOUND_FILE(CNTRMOVE.wav) \ +VCMI_SOUND_NAME(CNTRShot) VCMI_SOUND_FILE(CNTRSHOT.wav) \ +VCMI_SOUND_NAME(Counterstrike) VCMI_SOUND_FILE(CNTRSTRK.wav) \ +VCMI_SOUND_NAME(CNTRWNCE) VCMI_SOUND_FILE(CNTRWNCE.wav) \ +VCMI_SOUND_NAME(COLDRAY) VCMI_SOUND_FILE(COLDRAY.wav) \ +VCMI_SOUND_NAME(COLDRING) VCMI_SOUND_FILE(COLDRING.wav) \ +VCMI_SOUND_NAME(CRUSAttack) VCMI_SOUND_FILE(CRUSATTK.wav) \ +VCMI_SOUND_NAME(CRUSDefend) VCMI_SOUND_FILE(CRUSDFND.wav) \ +VCMI_SOUND_NAME(CRUSKill) VCMI_SOUND_FILE(CRUSKILL.wav) \ +VCMI_SOUND_NAME(CRUSMove) VCMI_SOUND_FILE(CRUSMOVE.wav) \ +VCMI_SOUND_NAME(CRUSWNCE) VCMI_SOUND_FILE(CRUSWNCE.wav) \ +VCMI_SOUND_NAME(CRYSAttack) VCMI_SOUND_FILE(CRYSATTK.wav) \ +VCMI_SOUND_NAME(CRYSDefend) VCMI_SOUND_FILE(CRYSDFND.wav) \ +VCMI_SOUND_NAME(CRYSKill) VCMI_SOUND_FILE(CRYSKILL.wav) \ +VCMI_SOUND_NAME(CRYSMove) VCMI_SOUND_FILE(CRYSMOVE.wav) \ +VCMI_SOUND_NAME(CRYSWNCE) VCMI_SOUND_FILE(CRYSWNCE.wav) \ +VCMI_SOUND_NAME(CURE) VCMI_SOUND_FILE(CURE.wav) \ +VCMI_SOUND_NAME(CURSE) VCMI_SOUND_FILE(CURSE.wav) \ +VCMI_SOUND_NAME(cyclopAttack) VCMI_SOUND_FILE(CYCLATTK.wav) \ +VCMI_SOUND_NAME(cyclopDefend) VCMI_SOUND_FILE(CYCLDFND.wav) \ +VCMI_SOUND_NAME(cyclopKill) VCMI_SOUND_FILE(CYCLKILL.wav) \ +VCMI_SOUND_NAME(cyclopMove) VCMI_SOUND_FILE(CYCLMOVE.wav) \ +VCMI_SOUND_NAME(cyclopShot) VCMI_SOUND_FILE(CYCLSHOT.wav) \ +VCMI_SOUND_NAME(cyclopWNCE) VCMI_SOUND_FILE(CYCLWNCE.wav) \ +VCMI_SOUND_NAME(DANGER) VCMI_SOUND_FILE(DANGER.wav) \ +VCMI_SOUND_NAME(deathBlow) VCMI_SOUND_FILE(DEATHBLO.wav) \ +VCMI_SOUND_NAME(deathCloud) VCMI_SOUND_FILE(DEATHCLD.wav) \ +VCMI_SOUND_NAME(deathRIP) VCMI_SOUND_FILE(DEATHRIP.wav) \ +VCMI_SOUND_NAME(deathSTR) VCMI_SOUND_FILE(DEATHSTR.wav) \ +VCMI_SOUND_NAME(DECAY) VCMI_SOUND_FILE(DECAY.wav) \ +VCMI_SOUND_NAME(DEFAULT) VCMI_SOUND_FILE(DEFAULT.wav) \ +VCMI_SOUND_NAME(DEVLAttack) VCMI_SOUND_FILE(DEVLATTK.wav) \ +VCMI_SOUND_NAME(DEVLDefend) VCMI_SOUND_FILE(DEVLDFND.wav) \ +VCMI_SOUND_NAME(DEVLEXT1) VCMI_SOUND_FILE(DEVLEXT1.wav) \ +VCMI_SOUND_NAME(DEVLEXT2) VCMI_SOUND_FILE(DEVLEXT2.wav) \ +VCMI_SOUND_NAME(DEVLKill) VCMI_SOUND_FILE(DEVLKILL.wav) \ +VCMI_SOUND_NAME(DEVLMove) VCMI_SOUND_FILE(DEVLMOVE.wav) \ +VCMI_SOUND_NAME(DEVLWNCE) VCMI_SOUND_FILE(DEVLWNCE.wav) \ +VCMI_SOUND_NAME(DFLYAttack) VCMI_SOUND_FILE(DFLYATTK.wav) \ +VCMI_SOUND_NAME(DFLYDefend) VCMI_SOUND_FILE(DFLYDFND.wav) \ +VCMI_SOUND_NAME(DFLYKill) VCMI_SOUND_FILE(DFLYKILL.wav) \ +VCMI_SOUND_NAME(DFLYMove) VCMI_SOUND_FILE(DFLYMOVE.wav) \ +VCMI_SOUND_NAME(DFLYWNCE) VCMI_SOUND_FILE(DFLYWNCE.wav) \ +VCMI_SOUND_NAME(DGLMAttack) VCMI_SOUND_FILE(DGLMATTK.wav) \ +VCMI_SOUND_NAME(DGLMDefend) VCMI_SOUND_FILE(DGLMDFND.wav) \ +VCMI_SOUND_NAME(DGLMKill) VCMI_SOUND_FILE(DGLMKILL.wav) \ +VCMI_SOUND_NAME(DGLMMove) VCMI_SOUND_FILE(DGLMMOVE.wav) \ +VCMI_SOUND_NAME(DGLMWNCE) VCMI_SOUND_FILE(DGLMWNCE.wav) \ +VCMI_SOUND_NAME(DHDMAttack) VCMI_SOUND_FILE(DHDMATTK.wav) \ +VCMI_SOUND_NAME(DHDMDefend) VCMI_SOUND_FILE(DHDMDFND.wav) \ +VCMI_SOUND_NAME(DHDMKill) VCMI_SOUND_FILE(DHDMKILL.wav) \ +VCMI_SOUND_NAME(DHDMMove) VCMI_SOUND_FILE(DHDMMOVE.wav) \ +VCMI_SOUND_NAME(DHDMWNCE) VCMI_SOUND_FILE(DHDMWNCE.wav) \ +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(DISRUPTR) VCMI_SOUND_FILE(DISRUPTR.wav) \ +VCMI_SOUND_NAME(dragonHall) VCMI_SOUND_FILE(DRAGON.wav) \ +VCMI_SOUND_NAME(DRAINLIF) VCMI_SOUND_FILE(DRAINLIF.wav) \ +VCMI_SOUND_NAME(DRAWBRG) VCMI_SOUND_FILE(DRAWBRG.wav) \ +VCMI_SOUND_NAME(DRGNSLAY) VCMI_SOUND_FILE(DRGNSLAY.wav) \ +VCMI_SOUND_NAME(DWRFAttack) VCMI_SOUND_FILE(DWRFATTK.wav) \ +VCMI_SOUND_NAME(DWRFDefend) VCMI_SOUND_FILE(DWRFDFND.wav) \ +VCMI_SOUND_NAME(DWRFKill) VCMI_SOUND_FILE(DWRFKILL.wav) \ +VCMI_SOUND_NAME(DWRFMove) VCMI_SOUND_FILE(DWRFMOVE.wav) \ +VCMI_SOUND_NAME(DWRFWNCE) VCMI_SOUND_FILE(DWRFWNCE.wav) \ +VCMI_SOUND_NAME(ECNTAttack) VCMI_SOUND_FILE(ECNTATTK.wav) \ +VCMI_SOUND_NAME(ECNTDefend) VCMI_SOUND_FILE(ECNTDFND.wav) \ +VCMI_SOUND_NAME(ECNTKill) VCMI_SOUND_FILE(ECNTKILL.wav) \ +VCMI_SOUND_NAME(ECNTMove) VCMI_SOUND_FILE(ECNTMOVE.wav) \ +VCMI_SOUND_NAME(ECNTWNCE) VCMI_SOUND_FILE(ECNTWNCE.wav) \ +VCMI_SOUND_NAME(EELMAttack) VCMI_SOUND_FILE(EELMATTK.wav) \ +VCMI_SOUND_NAME(EELMDefend) VCMI_SOUND_FILE(EELMDFND.wav) \ +VCMI_SOUND_NAME(EELMKill) VCMI_SOUND_FILE(EELMKILL.wav) \ +VCMI_SOUND_NAME(EELMMove) VCMI_SOUND_FILE(EELMMOVE.wav) \ +VCMI_SOUND_NAME(EELMWNCE) VCMI_SOUND_FILE(EELMWNCE.wav) \ +VCMI_SOUND_NAME(EFRTAttack) VCMI_SOUND_FILE(EFRTATTK.wav) \ +VCMI_SOUND_NAME(EFRTDefend) VCMI_SOUND_FILE(EFRTDFND.wav) \ +VCMI_SOUND_NAME(EFRTKill) VCMI_SOUND_FILE(EFRTKILL.wav) \ +VCMI_SOUND_NAME(EFRTMove) VCMI_SOUND_FILE(EFRTMOVE.wav) \ +VCMI_SOUND_NAME(EFRTWNCE) VCMI_SOUND_FILE(EFRTWNCE.wav) \ +VCMI_SOUND_NAME(ENCHAttack) VCMI_SOUND_FILE(ENCHATTK.wav) \ +VCMI_SOUND_NAME(ENCHDefend) VCMI_SOUND_FILE(ENCHDFND.wav) \ +VCMI_SOUND_NAME(ENCHKill) VCMI_SOUND_FILE(ENCHKILL.wav) \ +VCMI_SOUND_NAME(ENCHMove) VCMI_SOUND_FILE(ENCHMOVE.wav) \ +VCMI_SOUND_NAME(ENCHShot) VCMI_SOUND_FILE(ENCHSHOT.wav) \ +VCMI_SOUND_NAME(ENCHWNCE) VCMI_SOUND_FILE(ENCHWNCE.wav) \ +VCMI_SOUND_NAME(ENERAttack) VCMI_SOUND_FILE(ENERATTK.wav) \ +VCMI_SOUND_NAME(ENERDefend) VCMI_SOUND_FILE(ENERDFND.wav) \ +VCMI_SOUND_NAME(ENERKill) VCMI_SOUND_FILE(ENERKILL.wav) \ +VCMI_SOUND_NAME(ENERMove) VCMI_SOUND_FILE(ENERMOVE.wav) \ +VCMI_SOUND_NAME(ENERWNCE) VCMI_SOUND_FILE(ENERWNCE.wav) \ +VCMI_SOUND_NAME(ERTHQUAK) VCMI_SOUND_FILE(ERTHQUAK.wav) \ +VCMI_SOUND_NAME(ESULAttack) VCMI_SOUND_FILE(ESULATTK.wav) \ +VCMI_SOUND_NAME(ESULDefend) VCMI_SOUND_FILE(ESULDFND.wav) \ +VCMI_SOUND_NAME(ESULKill) VCMI_SOUND_FILE(ESULKILL.wav) \ +VCMI_SOUND_NAME(ESULMove) VCMI_SOUND_FILE(ESULMOVE.wav) \ +VCMI_SOUND_NAME(ESULShot) VCMI_SOUND_FILE(ESULSHOT.wav) \ +VCMI_SOUND_NAME(ESULWNCE) VCMI_SOUND_FILE(ESULWNCE.wav) \ +VCMI_SOUND_NAME(EVLIAttack) VCMI_SOUND_FILE(EVLIATTK.wav) \ +VCMI_SOUND_NAME(EVLIDETH) VCMI_SOUND_FILE(EVLIDETH.wav) \ +VCMI_SOUND_NAME(EVLIDefend) VCMI_SOUND_FILE(EVLIDFND.wav) \ +VCMI_SOUND_NAME(EVLIKill) VCMI_SOUND_FILE(EVLIKILL.wav) \ +VCMI_SOUND_NAME(EVLIMove) VCMI_SOUND_FILE(EVLIMOVE.wav) \ +VCMI_SOUND_NAME(EVLIShot) VCMI_SOUND_FILE(EVLISHOT.wav) \ +VCMI_SOUND_NAME(EVLIWNCE) VCMI_SOUND_FILE(EVLIWNCE.wav) \ +VCMI_SOUND_NAME(experience) VCMI_SOUND_FILE(EXPERNCE.wav) \ +VCMI_SOUND_NAME(FAERAttack) VCMI_SOUND_FILE(FAERATTK.wav) \ +VCMI_SOUND_NAME(FAERDefend) VCMI_SOUND_FILE(FAERDFND.wav) \ +VCMI_SOUND_NAME(faerie) VCMI_SOUND_FILE(FAERIE.wav) \ +VCMI_SOUND_NAME(FAERKill) VCMI_SOUND_FILE(FAERKILL.wav) \ +VCMI_SOUND_NAME(FAERMove) VCMI_SOUND_FILE(FAERMOVE.wav) \ +VCMI_SOUND_NAME(FAERShot) VCMI_SOUND_FILE(FAERSHOT.wav) \ +VCMI_SOUND_NAME(FAERWNCE) VCMI_SOUND_FILE(FAERWNCE.wav) \ +VCMI_SOUND_NAME(FAIDKill) VCMI_SOUND_FILE(FAIDKILL.wav) \ +VCMI_SOUND_NAME(FAIDWNCE) VCMI_SOUND_FILE(FAIDWNCE.wav) \ +VCMI_SOUND_NAME(FDFLAttack) VCMI_SOUND_FILE(FDFLATTK.wav) \ +VCMI_SOUND_NAME(FDFLDefend) VCMI_SOUND_FILE(FDFLDFND.wav) \ +VCMI_SOUND_NAME(FDFLKill) VCMI_SOUND_FILE(FDFLKILL.wav) \ +VCMI_SOUND_NAME(FDFLMove) VCMI_SOUND_FILE(FDFLMOVE.wav) \ +VCMI_SOUND_NAME(FDFLShot) VCMI_SOUND_FILE(FDFLSHOT.wav) \ +VCMI_SOUND_NAME(FDFLWNCE) VCMI_SOUND_FILE(FDFLWNCE.wav) \ +VCMI_SOUND_NAME(FEAR) VCMI_SOUND_FILE(FEAR.wav) \ +VCMI_SOUND_NAME(FELMAttack) VCMI_SOUND_FILE(FELMATTK.wav) \ +VCMI_SOUND_NAME(FELMDefend) VCMI_SOUND_FILE(FELMDFND.wav) \ +VCMI_SOUND_NAME(FELMKill) VCMI_SOUND_FILE(FELMKILL.wav) \ +VCMI_SOUND_NAME(FELMMove) VCMI_SOUND_FILE(FELMMOVE.wav) \ +VCMI_SOUND_NAME(FELMWNCE) VCMI_SOUND_FILE(FELMWNCE.wav) \ +VCMI_SOUND_NAME(FIRBAttack) VCMI_SOUND_FILE(FIRBATTK.wav) \ +VCMI_SOUND_NAME(FIRBDefend) VCMI_SOUND_FILE(FIRBDFND.wav) \ +VCMI_SOUND_NAME(FIRBKill) VCMI_SOUND_FILE(FIRBKILL.wav) \ +VCMI_SOUND_NAME(FIRBMove) VCMI_SOUND_FILE(FIRBMOVE.wav) \ +VCMI_SOUND_NAME(FIRBWNCE) VCMI_SOUND_FILE(FIRBWNCE.wav) \ +VCMI_SOUND_NAME(fireball) VCMI_SOUND_FILE(FIREBALL.wav) \ +VCMI_SOUND_NAME(fireblast) VCMI_SOUND_FILE(FIREBLST.wav) \ +VCMI_SOUND_NAME(FIRESHIE) VCMI_SOUND_FILE(FIRESHIE.wav) \ +VCMI_SOUND_NAME(FIRESHLD) VCMI_SOUND_FILE(FIRESHLD.wav) \ +VCMI_SOUND_NAME(fireStorm) VCMI_SOUND_FILE(FIRESTRM.wav) \ +VCMI_SOUND_NAME(fireWall) VCMI_SOUND_FILE(FIREWALL.wav) \ +VCMI_SOUND_NAME(FLAGMINE) VCMI_SOUND_FILE(FLAGMINE.wav) \ +VCMI_SOUND_NAME(FLYSPELL) VCMI_SOUND_FILE(FLYSPELL.wav) \ +VCMI_SOUND_NAME(FMLRAttack) VCMI_SOUND_FILE(FMLRATTK.wav) \ +VCMI_SOUND_NAME(FMLRDefend) VCMI_SOUND_FILE(FMLRDFND.wav) \ +VCMI_SOUND_NAME(FMLRKill) VCMI_SOUND_FILE(FMLRKILL.wav) \ +VCMI_SOUND_NAME(FMLRMove) VCMI_SOUND_FILE(FMLRMOVE.wav) \ +VCMI_SOUND_NAME(FMLRWNCE) VCMI_SOUND_FILE(FMLRWNCE.wav) \ +VCMI_SOUND_NAME(FORCEFLD) VCMI_SOUND_FILE(FORCEFLD.wav) \ +VCMI_SOUND_NAME(FORGET) VCMI_SOUND_FILE(FORGET.wav) \ +VCMI_SOUND_NAME(FORTUNE) VCMI_SOUND_FILE(FORTUNE.wav) \ +VCMI_SOUND_NAME(FRENZY) VCMI_SOUND_FILE(FRENZY.wav) \ +VCMI_SOUND_NAME(FROSTING) VCMI_SOUND_FILE(FROSTING.wav) \ +VCMI_SOUND_NAME(gazebo) VCMI_SOUND_FILE(GAZEBO.wav) \ +VCMI_SOUND_NAME(GBASAttack) VCMI_SOUND_FILE(GBASATTK.wav) \ +VCMI_SOUND_NAME(GBASDefend) VCMI_SOUND_FILE(GBASDFND.wav) \ +VCMI_SOUND_NAME(GBASKill) VCMI_SOUND_FILE(GBASKILL.wav) \ +VCMI_SOUND_NAME(GBASMove) VCMI_SOUND_FILE(GBASMOVE.wav) \ +VCMI_SOUND_NAME(GBASWNCE) VCMI_SOUND_FILE(GBASWNCE.wav) \ +VCMI_SOUND_NAME(GBLNAttack) VCMI_SOUND_FILE(GBLNATTK.wav) \ +VCMI_SOUND_NAME(GBLNDefend) VCMI_SOUND_FILE(GBLNDFND.wav) \ +VCMI_SOUND_NAME(GBLNKill) VCMI_SOUND_FILE(GBLNKILL.wav) \ +VCMI_SOUND_NAME(GBLNMove) VCMI_SOUND_FILE(GBLNMOVE.wav) \ +VCMI_SOUND_NAME(GBLNWNCE) VCMI_SOUND_FILE(GBLNWNCE.wav) \ +VCMI_SOUND_NAME(GELFAttack) VCMI_SOUND_FILE(GELFATTK.wav) \ +VCMI_SOUND_NAME(GELFDefend) VCMI_SOUND_FILE(GELFDFND.wav) \ +VCMI_SOUND_NAME(GELFKill) VCMI_SOUND_FILE(GELFKILL.wav) \ +VCMI_SOUND_NAME(GELFMove) VCMI_SOUND_FILE(GELFMOVE.wav) \ +VCMI_SOUND_NAME(GELFShot) VCMI_SOUND_FILE(GELFSHOT.wav) \ +VCMI_SOUND_NAME(GELFWNCE) VCMI_SOUND_FILE(GELFWNCE.wav) \ +VCMI_SOUND_NAME(GENIAttack) VCMI_SOUND_FILE(GENIATTK.wav) \ +VCMI_SOUND_NAME(GENIDefend) VCMI_SOUND_FILE(GENIDFND.wav) \ +VCMI_SOUND_NAME(GENIE) VCMI_SOUND_FILE(GENIE.wav) \ +VCMI_SOUND_NAME(GENIKill) VCMI_SOUND_FILE(GENIKILL.wav) \ +VCMI_SOUND_NAME(GENIMove) VCMI_SOUND_FILE(GENIMOVE.wav) \ +VCMI_SOUND_NAME(GENIWNCE) VCMI_SOUND_FILE(GENIWNCE.wav) \ +VCMI_SOUND_NAME(GETPROTECTION) VCMI_SOUND_FILE(GETPROTECTION.wav) \ +VCMI_SOUND_NAME(GGLMAttack) VCMI_SOUND_FILE(GGLMATTK.wav) \ +VCMI_SOUND_NAME(GGLMDefend) VCMI_SOUND_FILE(GGLMDFND.wav) \ +VCMI_SOUND_NAME(GGLMKill) VCMI_SOUND_FILE(GGLMKILL.wav) \ +VCMI_SOUND_NAME(GGLMMove) VCMI_SOUND_FILE(GGLMMOVE.wav) \ +VCMI_SOUND_NAME(GGLMWNCE) VCMI_SOUND_FILE(GGLMWNCE.wav) \ +VCMI_SOUND_NAME(GHDRAttack) VCMI_SOUND_FILE(GHDRATTK.wav) \ +VCMI_SOUND_NAME(GHDRDefend) VCMI_SOUND_FILE(GHDRDFND.wav) \ +VCMI_SOUND_NAME(GHDRKill) VCMI_SOUND_FILE(GHDRKILL.wav) \ +VCMI_SOUND_NAME(GHDRMove) VCMI_SOUND_FILE(GHDRMOVE.wav) \ +VCMI_SOUND_NAME(GHDRWNCE) VCMI_SOUND_FILE(GHDRWNCE.wav) \ +VCMI_SOUND_NAME(GNLMAttack) VCMI_SOUND_FILE(GNLMATTK.wav) \ +VCMI_SOUND_NAME(GNLMDefend) VCMI_SOUND_FILE(GNLMDFND.wav) \ +VCMI_SOUND_NAME(GNLMKill) VCMI_SOUND_FILE(GNLMKILL.wav) \ +VCMI_SOUND_NAME(GNLMMove) VCMI_SOUND_FILE(GNLMMOVE.wav) \ +VCMI_SOUND_NAME(GNLMWNCE) VCMI_SOUND_FILE(GNLMWNCE.wav) \ +VCMI_SOUND_NAME(GNOLAttack) VCMI_SOUND_FILE(GNOLATTK.wav) \ +VCMI_SOUND_NAME(GNOLDefend) VCMI_SOUND_FILE(GNOLDFND.wav) \ +VCMI_SOUND_NAME(GNOLKill) VCMI_SOUND_FILE(GNOLKILL.wav) \ +VCMI_SOUND_NAME(GNOLMove) VCMI_SOUND_FILE(GNOLMOVE.wav) \ +VCMI_SOUND_NAME(GNOLWNCE) VCMI_SOUND_FILE(GNOLWNCE.wav) \ +VCMI_SOUND_NAME(GODRAttack) VCMI_SOUND_FILE(GODRATTK.wav) \ +VCMI_SOUND_NAME(GODRDefend) VCMI_SOUND_FILE(GODRDFND.wav) \ +VCMI_SOUND_NAME(GODRKill) VCMI_SOUND_FILE(GODRKILL.wav) \ +VCMI_SOUND_NAME(GODRMove) VCMI_SOUND_FILE(GODRMOVE.wav) \ +VCMI_SOUND_NAME(GODRWNCE) VCMI_SOUND_FILE(GODRWNCE.wav) \ +VCMI_SOUND_NAME(GOGFLAME) VCMI_SOUND_FILE(GOGFLAME.wav) \ +VCMI_SOUND_NAME(GOGGAttack) VCMI_SOUND_FILE(GOGGATTK.wav) \ +VCMI_SOUND_NAME(GOGGDefend) VCMI_SOUND_FILE(GOGGDFND.wav) \ +VCMI_SOUND_NAME(GOGGKill) VCMI_SOUND_FILE(GOGGKILL.wav) \ +VCMI_SOUND_NAME(GOGGMove) VCMI_SOUND_FILE(GOGGMOVE.wav) \ +VCMI_SOUND_NAME(GOGGShot) VCMI_SOUND_FILE(GOGGSHOT.wav) \ +VCMI_SOUND_NAME(GOGGWNCE) VCMI_SOUND_FILE(GOGGWNCE.wav) \ +VCMI_SOUND_NAME(GOODLUCK) VCMI_SOUND_FILE(GOODLUCK.wav) \ +VCMI_SOUND_NAME(GOODMRLE) VCMI_SOUND_FILE(GOODMRLE.wav) \ +VCMI_SOUND_NAME(GRAVEYARD) VCMI_SOUND_FILE(GRAVEYARD.wav) \ +VCMI_SOUND_NAME(GRDRAttack) VCMI_SOUND_FILE(GRDRATTK.wav) \ +VCMI_SOUND_NAME(GRDRDefend) VCMI_SOUND_FILE(GRDRDFND.wav) \ +VCMI_SOUND_NAME(GRDRKill) VCMI_SOUND_FILE(GRDRKILL.wav) \ +VCMI_SOUND_NAME(GRDRMove) VCMI_SOUND_FILE(GRDRMOVE.wav) \ +VCMI_SOUND_NAME(GRDRWNCE) VCMI_SOUND_FILE(GRDRWNCE.wav) \ +VCMI_SOUND_NAME(GRIFAttack) VCMI_SOUND_FILE(GRIFATTK.wav) \ +VCMI_SOUND_NAME(GRIFDefend) VCMI_SOUND_FILE(GRIFDFND.wav) \ +VCMI_SOUND_NAME(GRIFKill) VCMI_SOUND_FILE(GRIFKILL.wav) \ +VCMI_SOUND_NAME(GRIFMove) VCMI_SOUND_FILE(GRIFMOVE.wav) \ +VCMI_SOUND_NAME(GRIFWNCE) VCMI_SOUND_FILE(GRIFWNCE.wav) \ +VCMI_SOUND_NAME(GTITAttack) VCMI_SOUND_FILE(GTITATTK.wav) \ +VCMI_SOUND_NAME(GTITDefend) VCMI_SOUND_FILE(GTITDFND.wav) \ +VCMI_SOUND_NAME(GTITKill) VCMI_SOUND_FILE(GTITKILL.wav) \ +VCMI_SOUND_NAME(GTITMove) VCMI_SOUND_FILE(GTITMOVE.wav) \ +VCMI_SOUND_NAME(GTITShot) VCMI_SOUND_FILE(GTITSHOT.wav) \ +VCMI_SOUND_NAME(GTITWNCE) VCMI_SOUND_FILE(GTITWNCE.wav) \ +VCMI_SOUND_NAME(GWRDAttack) VCMI_SOUND_FILE(GWRDATTK.wav) \ +VCMI_SOUND_NAME(GWRDDefend) VCMI_SOUND_FILE(GWRDDFND.wav) \ +VCMI_SOUND_NAME(GWRDKill) VCMI_SOUND_FILE(GWRDKILL.wav) \ +VCMI_SOUND_NAME(GWRDMove) VCMI_SOUND_FILE(GWRDMOVE.wav) \ +VCMI_SOUND_NAME(GWRDWNCE) VCMI_SOUND_FILE(GWRDWNCE.wav) \ +VCMI_SOUND_NAME(HALBAttack) VCMI_SOUND_FILE(HALBATTK.wav) \ +VCMI_SOUND_NAME(HALBDefend) VCMI_SOUND_FILE(HALBDFND.wav) \ +VCMI_SOUND_NAME(HALBKill) VCMI_SOUND_FILE(HALBKILL.wav) \ +VCMI_SOUND_NAME(HALBMove) VCMI_SOUND_FILE(HALBMOVE.wav) \ +VCMI_SOUND_NAME(HALBWNCE) VCMI_SOUND_FILE(HALBWNCE.wav) \ +VCMI_SOUND_NAME(HALFAttack) VCMI_SOUND_FILE(HALFATTK.wav) \ +VCMI_SOUND_NAME(HALFDefend) VCMI_SOUND_FILE(HALFDFND.wav) \ +VCMI_SOUND_NAME(HALFKill) VCMI_SOUND_FILE(HALFKILL.wav) \ +VCMI_SOUND_NAME(HALFMove) VCMI_SOUND_FILE(HALFMOVE.wav) \ +VCMI_SOUND_NAME(HALFShot) VCMI_SOUND_FILE(HALFSHOT.wav) \ +VCMI_SOUND_NAME(HALFWNCE) VCMI_SOUND_FILE(HALFWNCE.wav) \ +VCMI_SOUND_NAME(HARPAttack) VCMI_SOUND_FILE(HARPATTK.wav) \ +VCMI_SOUND_NAME(HARPDefend) VCMI_SOUND_FILE(HARPDFND.wav) \ +VCMI_SOUND_NAME(HARPKill) VCMI_SOUND_FILE(HARPKILL.wav) \ +VCMI_SOUND_NAME(HARPMove) VCMI_SOUND_FILE(HARPMOVE.wav) \ +VCMI_SOUND_NAME(HARPWNCE) VCMI_SOUND_FILE(HARPWNCE.wav) \ +VCMI_SOUND_NAME(HASTE) VCMI_SOUND_FILE(HASTE.wav) \ +VCMI_SOUND_NAME(HCRSAttack) VCMI_SOUND_FILE(HCRSATTK.wav) \ +VCMI_SOUND_NAME(HCRSDefend) VCMI_SOUND_FILE(HCRSDFND.wav) \ +VCMI_SOUND_NAME(HCRSKill) VCMI_SOUND_FILE(HCRSKILL.wav) \ +VCMI_SOUND_NAME(HCRSMove) VCMI_SOUND_FILE(HCRSMOVE.wav) \ +VCMI_SOUND_NAME(HCRSShot) VCMI_SOUND_FILE(HCRSSHOT.wav) \ +VCMI_SOUND_NAME(HCRSWNCE) VCMI_SOUND_FILE(HCRSWNCE.wav) \ +VCMI_SOUND_NAME(HGOBAttack) VCMI_SOUND_FILE(HGOBATTK.wav) \ +VCMI_SOUND_NAME(HGOBDefend) VCMI_SOUND_FILE(HGOBDFND.wav) \ +VCMI_SOUND_NAME(HGOBKill) VCMI_SOUND_FILE(HGOBKILL.wav) \ +VCMI_SOUND_NAME(HGOBMove) VCMI_SOUND_FILE(HGOBMOVE.wav) \ +VCMI_SOUND_NAME(HGOBWNCE) VCMI_SOUND_FILE(HGOBWNCE.wav) \ +VCMI_SOUND_NAME(HGWRAttack) VCMI_SOUND_FILE(HGWRATTK.wav) \ +VCMI_SOUND_NAME(HGWRDefend) VCMI_SOUND_FILE(HGWRDFND.wav) \ +VCMI_SOUND_NAME(HGWRKill) VCMI_SOUND_FILE(HGWRKILL.wav) \ +VCMI_SOUND_NAME(HGWRMove) VCMI_SOUND_FILE(HGWRMOVE.wav) \ +VCMI_SOUND_NAME(HGWRWNCE) VCMI_SOUND_FILE(HGWRWNCE.wav) \ +VCMI_SOUND_NAME(HHAGAttack) VCMI_SOUND_FILE(HHAGATTK.wav) \ +VCMI_SOUND_NAME(HHAGDefend) VCMI_SOUND_FILE(HHAGDFND.wav) \ +VCMI_SOUND_NAME(HHAGKill) VCMI_SOUND_FILE(HHAGKILL.wav) \ +VCMI_SOUND_NAME(HHAGMove) VCMI_SOUND_FILE(HHAGMOVE.wav) \ +VCMI_SOUND_NAME(HHAGShot) VCMI_SOUND_FILE(HHAGSHOT.wav) \ +VCMI_SOUND_NAME(HHAGWNCE) VCMI_SOUND_FILE(HHAGWNCE.wav) \ +VCMI_SOUND_NAME(HHNDAttack) VCMI_SOUND_FILE(HHNDATTK.wav) \ +VCMI_SOUND_NAME(HHNDDefend) VCMI_SOUND_FILE(HHNDDFND.wav) \ +VCMI_SOUND_NAME(HHNDKill) VCMI_SOUND_FILE(HHNDKILL.wav) \ +VCMI_SOUND_NAME(HHNDMove) VCMI_SOUND_FILE(HHNDMOVE.wav) \ +VCMI_SOUND_NAME(HHNDWNCE) VCMI_SOUND_FILE(HHNDWNCE.wav) \ +VCMI_SOUND_NAME(horseDirt) VCMI_SOUND_FILE(HORSE00.wav) \ +VCMI_SOUND_NAME(horseSand) VCMI_SOUND_FILE(HORSE01.wav) \ +VCMI_SOUND_NAME(horseGrass) VCMI_SOUND_FILE(HORSE02.wav) \ +VCMI_SOUND_NAME(horseSnow) VCMI_SOUND_FILE(HORSE03.wav) \ +VCMI_SOUND_NAME(horseSwamp) VCMI_SOUND_FILE(HORSE04.wav) \ +VCMI_SOUND_NAME(horseRough) VCMI_SOUND_FILE(HORSE05.wav) \ +VCMI_SOUND_NAME(horseSubterranean) VCMI_SOUND_FILE(HORSE06.wav) \ +VCMI_SOUND_NAME(horseLava) VCMI_SOUND_FILE(HORSE07.wav) \ +VCMI_SOUND_NAME(horseWater) VCMI_SOUND_FILE(HORSE08.wav) \ +VCMI_SOUND_NAME(horseRock) VCMI_SOUND_FILE(HORSE09.wav) \ +VCMI_SOUND_NAME(horseFly) VCMI_SOUND_FILE(HORSE10.wav) \ +VCMI_SOUND_NAME(horsePenaltyDirt) VCMI_SOUND_FILE(HORSE20.wav) \ +VCMI_SOUND_NAME(horsePenaltySand) VCMI_SOUND_FILE(HORSE21.wav) \ +VCMI_SOUND_NAME(horsePenaltyGrass) VCMI_SOUND_FILE(HORSE22.wav) \ +VCMI_SOUND_NAME(horsePenaltySnow) VCMI_SOUND_FILE(HORSE23.wav) \ +VCMI_SOUND_NAME(horsePenaltySwamp) VCMI_SOUND_FILE(HORSE24.wav) \ +VCMI_SOUND_NAME(horsePenaltyRough) VCMI_SOUND_FILE(HORSE25.wav) \ +VCMI_SOUND_NAME(horsePenaltySubterranean) VCMI_SOUND_FILE(HORSE26.wav) \ +VCMI_SOUND_NAME(horsePenaltyLava) VCMI_SOUND_FILE(HORSE27.wav) \ +VCMI_SOUND_NAME(horsePenaltyRock) VCMI_SOUND_FILE(HORSE29.wav) \ +VCMI_SOUND_NAME(hydraAttack) VCMI_SOUND_FILE(HYDRATTK.wav) \ +VCMI_SOUND_NAME(hydraDefend) VCMI_SOUND_FILE(HYDRDFND.wav) \ +VCMI_SOUND_NAME(hydraKill) VCMI_SOUND_FILE(HYDRKILL.wav) \ +VCMI_SOUND_NAME(hydraMove) VCMI_SOUND_FILE(HYDRMOVE.wav) \ +VCMI_SOUND_NAME(hydraWNCE) VCMI_SOUND_FILE(HYDRWNCE.wav) \ +VCMI_SOUND_NAME(HYPNOTIZ) VCMI_SOUND_FILE(HYPNOTIZ.wav) \ +VCMI_SOUND_NAME(ICELAttack) VCMI_SOUND_FILE(ICELATTK.wav) \ +VCMI_SOUND_NAME(ICELDefend) VCMI_SOUND_FILE(ICELDFND.wav) \ +VCMI_SOUND_NAME(ICELKill) VCMI_SOUND_FILE(ICELKILL.wav) \ +VCMI_SOUND_NAME(ICELMove) VCMI_SOUND_FILE(ICELMOVE.wav) \ +VCMI_SOUND_NAME(ICELShot) VCMI_SOUND_FILE(ICELSHOT.wav) \ +VCMI_SOUND_NAME(ICELWNCE) VCMI_SOUND_FILE(ICELWNCE.wav) \ +VCMI_SOUND_NAME(ICERAYEX) VCMI_SOUND_FILE(ICERAYEX.wav) \ +VCMI_SOUND_NAME(ICERAY) VCMI_SOUND_FILE(ICERAY.wav) \ +VCMI_SOUND_NAME(IGLMAttack) VCMI_SOUND_FILE(IGLMATTK.wav) \ +VCMI_SOUND_NAME(IGLMDefend) VCMI_SOUND_FILE(IGLMDFND.wav) \ +VCMI_SOUND_NAME(IGLMKill) VCMI_SOUND_FILE(IGLMKILL.wav) \ +VCMI_SOUND_NAME(IGLMMove) VCMI_SOUND_FILE(IGLMMOVE.wav) \ +VCMI_SOUND_NAME(IGLMWNCE) VCMI_SOUND_FILE(IGLMWNCE.wav) \ +VCMI_SOUND_NAME(IMPPAttack) VCMI_SOUND_FILE(IMPPATTK.wav) \ +VCMI_SOUND_NAME(IMPPDefend) VCMI_SOUND_FILE(IMPPDFND.wav) \ +VCMI_SOUND_NAME(IMPPKill) VCMI_SOUND_FILE(IMPPKILL.wav) \ +VCMI_SOUND_NAME(IMPPMove) VCMI_SOUND_FILE(IMPPMOVE.wav) \ +VCMI_SOUND_NAME(IMPPWNCE) VCMI_SOUND_FILE(IMPPWNCE.wav) \ +VCMI_SOUND_NAME(ITRGAttack) VCMI_SOUND_FILE(ITRGATTK.wav) \ +VCMI_SOUND_NAME(ITRGDefend) VCMI_SOUND_FILE(ITRGDFND.wav) \ +VCMI_SOUND_NAME(ITRGKill) VCMI_SOUND_FILE(ITRGKILL.wav) \ +VCMI_SOUND_NAME(ITRGMove) VCMI_SOUND_FILE(ITRGMOVE.wav) \ +VCMI_SOUND_NAME(ITRGWNCE) VCMI_SOUND_FILE(ITRGWNCE.wav) \ +VCMI_SOUND_NAME(KEEPShot) VCMI_SOUND_FILE(KEEPSHOT.wav) \ +VCMI_SOUND_NAME(LANDKill) VCMI_SOUND_FILE(LANDKILL.wav) \ +VCMI_SOUND_NAME(LANDMINE) VCMI_SOUND_FILE(LANDMINE.wav) \ +VCMI_SOUND_NAME(LCRSAttack) VCMI_SOUND_FILE(LCRSATTK.wav) \ +VCMI_SOUND_NAME(LCRSDefend) VCMI_SOUND_FILE(LCRSDFND.wav) \ +VCMI_SOUND_NAME(LCRSKill) VCMI_SOUND_FILE(LCRSKILL.wav) \ +VCMI_SOUND_NAME(LCRSMove) VCMI_SOUND_FILE(LCRSMOVE.wav) \ +VCMI_SOUND_NAME(LCRSShot) VCMI_SOUND_FILE(LCRSSHOT.wav) \ +VCMI_SOUND_NAME(LCRSWNCE) VCMI_SOUND_FILE(LCRSWNCE.wav) \ +VCMI_SOUND_NAME(LICHATK2) VCMI_SOUND_FILE(LICHATK2.wav) \ +VCMI_SOUND_NAME(LICHAttack) VCMI_SOUND_FILE(LICHATTK.wav) \ +VCMI_SOUND_NAME(LICHDefend) VCMI_SOUND_FILE(LICHDFND.wav) \ +VCMI_SOUND_NAME(LICHKill) VCMI_SOUND_FILE(LICHKILL.wav) \ +VCMI_SOUND_NAME(LICHMove) VCMI_SOUND_FILE(LICHMOVE.wav) \ +VCMI_SOUND_NAME(LICHShot) VCMI_SOUND_FILE(LICHSHOT.wav) \ +VCMI_SOUND_NAME(LICHWNCE) VCMI_SOUND_FILE(LICHWNCE.wav) \ +VCMI_SOUND_NAME(LIGHTBLT) VCMI_SOUND_FILE(LIGHTBLT.wav) \ +VCMI_SOUND_NAME(LIGHTHOUSE) VCMI_SOUND_FILE(LIGHTHOUSE.wav) \ +VCMI_SOUND_NAME(LOOPAIR) VCMI_SOUND_FILE(LOOPAIR.wav) \ +VCMI_SOUND_NAME(LOOPANIM) VCMI_SOUND_FILE(LOOPANIM.wav) \ +VCMI_SOUND_NAME(LOOPARCH) VCMI_SOUND_FILE(LOOPARCH.wav) \ +VCMI_SOUND_NAME(LOOPAREN) VCMI_SOUND_FILE(LOOPAREN.wav) \ +VCMI_SOUND_NAME(LOOPBEHE) VCMI_SOUND_FILE(LOOPBEHE.wav) \ +VCMI_SOUND_NAME(LOOPBIRD) VCMI_SOUND_FILE(LOOPBIRD.wav) \ +VCMI_SOUND_NAME(LOOPBUOY) VCMI_SOUND_FILE(LOOPBUOY.wav) \ +VCMI_SOUND_NAME(LOOPCAMP) VCMI_SOUND_FILE(LOOPCAMP.wav) \ +VCMI_SOUND_NAME(LOOPCAVE) VCMI_SOUND_FILE(LOOPCAVE.wav) \ +VCMI_SOUND_NAME(LOOPCRYS) VCMI_SOUND_FILE(LOOPCRYS.wav) \ +VCMI_SOUND_NAME(LOOPCURS) VCMI_SOUND_FILE(LOOPCURS.wav) \ +VCMI_SOUND_NAME(LOOPDEAD) VCMI_SOUND_FILE(LOOPDEAD.wav) \ +VCMI_SOUND_NAME(LOOPDEN) VCMI_SOUND_FILE(LOOPDEN.wav) \ +VCMI_SOUND_NAME(LOOPDEVL) VCMI_SOUND_FILE(LOOPDEVL.wav) \ +VCMI_SOUND_NAME(LOOPDOG) VCMI_SOUND_FILE(LOOPDOG.wav) \ +VCMI_SOUND_NAME(LOOPDRAG) VCMI_SOUND_FILE(LOOPDRAG.wav) \ +VCMI_SOUND_NAME(LOOPDWAR) VCMI_SOUND_FILE(LOOPDWAR.wav) \ +VCMI_SOUND_NAME(LOOPEART) VCMI_SOUND_FILE(LOOPEART.wav) \ +VCMI_SOUND_NAME(LOOPELF) VCMI_SOUND_FILE(LOOPELF.wav) \ +VCMI_SOUND_NAME(LOOPFACT) VCMI_SOUND_FILE(LOOPFACT.wav) \ +VCMI_SOUND_NAME(LOOPFAER) VCMI_SOUND_FILE(LOOPFAER.wav) \ +VCMI_SOUND_NAME(LOOPFALL) VCMI_SOUND_FILE(LOOPFALL.wav) \ +VCMI_SOUND_NAME(LOOPFIRE) VCMI_SOUND_FILE(LOOPFIRE.wav) \ +VCMI_SOUND_NAME(LOOPFLAG) VCMI_SOUND_FILE(LOOPFLAG.wav) \ +VCMI_SOUND_NAME(LOOPFOUN) VCMI_SOUND_FILE(LOOPFOUN.wav) \ +VCMI_SOUND_NAME(LOOPGARD) VCMI_SOUND_FILE(LOOPGARD.wav) \ +VCMI_SOUND_NAME(LOOPGATE) VCMI_SOUND_FILE(LOOPGATE.wav) \ +VCMI_SOUND_NAME(LOOPGEMP) VCMI_SOUND_FILE(LOOPGEMP.wav) \ +VCMI_SOUND_NAME(LOOPGOBL) VCMI_SOUND_FILE(LOOPGOBL.wav) \ +VCMI_SOUND_NAME(LOOPGREM) VCMI_SOUND_FILE(LOOPGREM.wav) \ +VCMI_SOUND_NAME(LOOPGRIF) VCMI_SOUND_FILE(LOOPGRIF.wav) \ +VCMI_SOUND_NAME(LOOPHARP) VCMI_SOUND_FILE(LOOPHARP.wav) \ +VCMI_SOUND_NAME(LOOPHORS) VCMI_SOUND_FILE(LOOPHORS.wav) \ +VCMI_SOUND_NAME(LOOPHYDR) VCMI_SOUND_FILE(LOOPHYDR.wav) \ +VCMI_SOUND_NAME(LOOPLEAR) VCMI_SOUND_FILE(LOOPLEAR.wav) \ +VCMI_SOUND_NAME(LOOPLEPR) VCMI_SOUND_FILE(LOOPLEPR.wav) \ +VCMI_SOUND_NAME(LOOPLUMB) VCMI_SOUND_FILE(LOOPLUMB.wav) \ +VCMI_SOUND_NAME(LOOPMAGI) VCMI_SOUND_FILE(LOOPMAGI.wav) \ +VCMI_SOUND_NAME(LOOPMANT) VCMI_SOUND_FILE(LOOPMANT.wav) \ +VCMI_SOUND_NAME(LOOPMARK) VCMI_SOUND_FILE(LOOPMARK.wav) \ +VCMI_SOUND_NAME(LOOPMEDU) VCMI_SOUND_FILE(LOOPMEDU.wav) \ +VCMI_SOUND_NAME(LOOPMERC) VCMI_SOUND_FILE(LOOPMERC.wav) \ +VCMI_SOUND_NAME(LOOPMILL) VCMI_SOUND_FILE(LOOPMILL.wav) \ +VCMI_SOUND_NAME(LOOPMINE) VCMI_SOUND_FILE(LOOPMINE.wav) \ +VCMI_SOUND_NAME(LOOPMON1) VCMI_SOUND_FILE(LOOPMON1.wav) \ +VCMI_SOUND_NAME(LOOPMON2) VCMI_SOUND_FILE(LOOPMON2.wav) \ +VCMI_SOUND_NAME(LOOPMONK) VCMI_SOUND_FILE(LOOPMONK.wav) \ +VCMI_SOUND_NAME(LOOPMONS) VCMI_SOUND_FILE(LOOPMONS.wav) \ +VCMI_SOUND_NAME(LOOPNAGA) VCMI_SOUND_FILE(LOOPNAGA.wav) \ +VCMI_SOUND_NAME(LOOPOCEA) VCMI_SOUND_FILE(LOOPOCEA.wav) \ +VCMI_SOUND_NAME(LOOPOGRE) VCMI_SOUND_FILE(LOOPOGRE.wav) \ +VCMI_SOUND_NAME(LOOPORC) VCMI_SOUND_FILE(LOOPORC.wav) \ +VCMI_SOUND_NAME(LOOPPEGA) VCMI_SOUND_FILE(LOOPPEGA.wav) \ +VCMI_SOUND_NAME(LOOPPIKE) VCMI_SOUND_FILE(LOOPPIKE.wav) \ +VCMI_SOUND_NAME(LOOPSANC) VCMI_SOUND_FILE(LOOPSANC.wav) \ +VCMI_SOUND_NAME(LOOPSHRIN) VCMI_SOUND_FILE(LOOPSHRIN.wav) \ +VCMI_SOUND_NAME(LOOPSIRE) VCMI_SOUND_FILE(LOOPSIRE.wav) \ +VCMI_SOUND_NAME(LOOPSKEL) VCMI_SOUND_FILE(LOOPSKEL.wav) \ +VCMI_SOUND_NAME(LOOPSTAR) VCMI_SOUND_FILE(LOOPSTAR.wav) \ +VCMI_SOUND_NAME(LOOPSULF) VCMI_SOUND_FILE(LOOPSULF.wav) \ +VCMI_SOUND_NAME(LOOPSWAR) VCMI_SOUND_FILE(LOOPSWAR.wav) \ +VCMI_SOUND_NAME(LOOPSWOR) VCMI_SOUND_FILE(LOOPSWOR.wav) \ +VCMI_SOUND_NAME(LOOPTAV) VCMI_SOUND_FILE(LOOPTAV.wav) \ +VCMI_SOUND_NAME(LOOPTITA) VCMI_SOUND_FILE(LOOPTITA.wav) \ +VCMI_SOUND_NAME(LOOPUNIC) VCMI_SOUND_FILE(LOOPUNIC.wav) \ +VCMI_SOUND_NAME(LOOPVENT) VCMI_SOUND_FILE(LOOPVENT.wav) \ +VCMI_SOUND_NAME(LOOPVOLC) VCMI_SOUND_FILE(LOOPVOLC.wav) \ +VCMI_SOUND_NAME(LOOPWHIR) VCMI_SOUND_FILE(LOOPWHIR.wav) \ +VCMI_SOUND_NAME(LOOPWIND) VCMI_SOUND_FILE(LOOPWIND.wav) \ +VCMI_SOUND_NAME(LOOPWOLF) VCMI_SOUND_FILE(LOOPWOLF.wav) \ +VCMI_SOUND_NAME(LTITAttack) VCMI_SOUND_FILE(LTITATTK.wav) \ +VCMI_SOUND_NAME(LTITDefend) VCMI_SOUND_FILE(LTITDFND.wav) \ +VCMI_SOUND_NAME(LTITKill) VCMI_SOUND_FILE(LTITKILL.wav) \ +VCMI_SOUND_NAME(LTITMove) VCMI_SOUND_FILE(LTITMOVE.wav) \ +VCMI_SOUND_NAME(LTITWNCE) VCMI_SOUND_FILE(LTITWNCE.wav) \ +VCMI_SOUND_NAME(LUCK) VCMI_SOUND_FILE(LUCK.wav) \ +VCMI_SOUND_NAME(MAGCAROW) VCMI_SOUND_FILE(MAGCAROW.wav) \ +VCMI_SOUND_NAME(MAGCHDRN) VCMI_SOUND_FILE(MAGCHDRN.wav) \ +VCMI_SOUND_NAME(MAGCHFIL) VCMI_SOUND_FILE(MAGCHFIL.wav) \ +VCMI_SOUND_NAME(MAGEAttack) VCMI_SOUND_FILE(MAGEATTK.wav) \ +VCMI_SOUND_NAME(MAGEDefend) VCMI_SOUND_FILE(MAGEDFND.wav) \ +VCMI_SOUND_NAME(MAGEKill) VCMI_SOUND_FILE(MAGEKILL.wav) \ +VCMI_SOUND_NAME(MAGEMove) VCMI_SOUND_FILE(MAGEMOVE.wav) \ +VCMI_SOUND_NAME(MAGEShot) VCMI_SOUND_FILE(MAGESHOT.wav) \ +VCMI_SOUND_NAME(MAGEWNCE) VCMI_SOUND_FILE(MAGEWNCE.wav) \ +VCMI_SOUND_NAME(MAGICBLT) VCMI_SOUND_FILE(MAGICBLT.wav) \ +VCMI_SOUND_NAME(MAGICRES) VCMI_SOUND_FILE(MAGICRES.wav) \ +VCMI_SOUND_NAME(MAGMAttack) VCMI_SOUND_FILE(MAGMATTK.wav) \ +VCMI_SOUND_NAME(MAGMDefend) VCMI_SOUND_FILE(MAGMDFND.wav) \ +VCMI_SOUND_NAME(MAGMKill) VCMI_SOUND_FILE(MAGMKILL.wav) \ +VCMI_SOUND_NAME(MAGMMove) VCMI_SOUND_FILE(MAGMMOVE.wav) \ +VCMI_SOUND_NAME(MAGMWNCE) VCMI_SOUND_FILE(MAGMWNCE.wav) \ +VCMI_SOUND_NAME(MANADRAI) VCMI_SOUND_FILE(MANADRAI.wav) \ +VCMI_SOUND_NAME(MANTAttack) VCMI_SOUND_FILE(MANTATTK.wav) \ +VCMI_SOUND_NAME(MANTDefend) VCMI_SOUND_FILE(MANTDFND.wav) \ +VCMI_SOUND_NAME(MANTKill) VCMI_SOUND_FILE(MANTKILL.wav) \ +VCMI_SOUND_NAME(MANTMove) VCMI_SOUND_FILE(MANTMOVE.wav) \ +VCMI_SOUND_NAME(MANTShot) VCMI_SOUND_FILE(MANTSHOT.wav) \ +VCMI_SOUND_NAME(MANTWNCE) VCMI_SOUND_FILE(MANTWNCE.wav) \ +VCMI_SOUND_NAME(MEDQAttack) VCMI_SOUND_FILE(MEDQATTK.wav) \ +VCMI_SOUND_NAME(MEDQDefend) VCMI_SOUND_FILE(MEDQDFND.wav) \ +VCMI_SOUND_NAME(MEDQKill) VCMI_SOUND_FILE(MEDQKILL.wav) \ +VCMI_SOUND_NAME(MEDQMove) VCMI_SOUND_FILE(MEDQMOVE.wav) \ +VCMI_SOUND_NAME(MEDQShot) VCMI_SOUND_FILE(MEDQSHOT.wav) \ +VCMI_SOUND_NAME(MEDQWNCE) VCMI_SOUND_FILE(MEDQWNCE.wav) \ +VCMI_SOUND_NAME(MEDUAttack) VCMI_SOUND_FILE(MEDUATTK.wav) \ +VCMI_SOUND_NAME(MEDUDefend) VCMI_SOUND_FILE(MEDUDFND.wav) \ +VCMI_SOUND_NAME(MEDUKill) VCMI_SOUND_FILE(MEDUKILL.wav) \ +VCMI_SOUND_NAME(MEDUMove) VCMI_SOUND_FILE(MEDUMOVE.wav) \ +VCMI_SOUND_NAME(MEDUShot) VCMI_SOUND_FILE(MEDUSHOT.wav) \ +VCMI_SOUND_NAME(MEDUWNCE) VCMI_SOUND_FILE(MEDUWNCE.wav) \ +VCMI_SOUND_NAME(METEOR) VCMI_SOUND_FILE(METEOR.wav) \ +VCMI_SOUND_NAME(MGELAttack) VCMI_SOUND_FILE(MGELATTK.wav) \ +VCMI_SOUND_NAME(MGELDefend) VCMI_SOUND_FILE(MGELDFND.wav) \ +VCMI_SOUND_NAME(MGELKill) VCMI_SOUND_FILE(MGELKILL.wav) \ +VCMI_SOUND_NAME(MGELMove) VCMI_SOUND_FILE(MGELMOVE.wav) \ +VCMI_SOUND_NAME(MGELWNCE) VCMI_SOUND_FILE(MGELWNCE.wav) \ +VCMI_SOUND_NAME(MGOGAttack) VCMI_SOUND_FILE(MGOGATTK.wav) \ +VCMI_SOUND_NAME(MGOGDefend) VCMI_SOUND_FILE(MGOGDFND.wav) \ +VCMI_SOUND_NAME(MGOGKill) VCMI_SOUND_FILE(MGOGKILL.wav) \ +VCMI_SOUND_NAME(MGOGMove) VCMI_SOUND_FILE(MGOGMOVE.wav) \ +VCMI_SOUND_NAME(MGOGShot) VCMI_SOUND_FILE(MGOGSHOT.wav) \ +VCMI_SOUND_NAME(MGOGWNCE) VCMI_SOUND_FILE(MGOGWNCE.wav) \ +VCMI_SOUND_NAME(MGRMAttack) VCMI_SOUND_FILE(MGRMATTK.wav) \ +VCMI_SOUND_NAME(MGRMDefend) VCMI_SOUND_FILE(MGRMDFND.wav) \ +VCMI_SOUND_NAME(MGRMKill) VCMI_SOUND_FILE(MGRMKILL.wav) \ +VCMI_SOUND_NAME(MGRMMove) VCMI_SOUND_FILE(MGRMMOVE.wav) \ +VCMI_SOUND_NAME(MGRMShot) VCMI_SOUND_FILE(MGRMSHOT.wav) \ +VCMI_SOUND_NAME(MGRMWNCE) VCMI_SOUND_FILE(MGRMWNCE.wav) \ +VCMI_SOUND_NAME(MILITARY) VCMI_SOUND_FILE(MILITARY.wav) \ +VCMI_SOUND_NAME(MINKAttack) VCMI_SOUND_FILE(MINKATTK.wav) \ +VCMI_SOUND_NAME(MINKDefend) VCMI_SOUND_FILE(MINKDFND.wav) \ +VCMI_SOUND_NAME(MINKKill) VCMI_SOUND_FILE(MINKKILL.wav) \ +VCMI_SOUND_NAME(MINKMove) VCMI_SOUND_FILE(MINKMOVE.wav) \ +VCMI_SOUND_NAME(MINKShot) VCMI_SOUND_FILE(MINKSHOT.wav) \ +VCMI_SOUND_NAME(MINKWNCE) VCMI_SOUND_FILE(MINKWNCE.wav) \ +VCMI_SOUND_NAME(MINOAttack) VCMI_SOUND_FILE(MINOATTK.wav) \ +VCMI_SOUND_NAME(MINODefend) VCMI_SOUND_FILE(MINODFND.wav) \ +VCMI_SOUND_NAME(MINOKill) VCMI_SOUND_FILE(MINOKILL.wav) \ +VCMI_SOUND_NAME(MINOMove) VCMI_SOUND_FILE(MINOMOVE.wav) \ +VCMI_SOUND_NAME(MINOWNCE) VCMI_SOUND_FILE(MINOWNCE.wav) \ +VCMI_SOUND_NAME(MIRRORIM) VCMI_SOUND_FILE(MIRRORIM.wav) \ +VCMI_SOUND_NAME(MIRTH) VCMI_SOUND_FILE(MIRTH.wav) \ +VCMI_SOUND_NAME(MISFORT) VCMI_SOUND_FILE(MISFORT.wav) \ +VCMI_SOUND_NAME(MNRDEATH) VCMI_SOUND_FILE(MNRDEATH.wav) \ +VCMI_SOUND_NAME(monkAttack) VCMI_SOUND_FILE(MONKATTK.wav) \ +VCMI_SOUND_NAME(monkDefend) VCMI_SOUND_FILE(MONKDFND.wav) \ +VCMI_SOUND_NAME(monkKill) VCMI_SOUND_FILE(MONKKILL.wav) \ +VCMI_SOUND_NAME(monkMove) VCMI_SOUND_FILE(MONKMOVE.wav) \ +VCMI_SOUND_NAME(monkShot) VCMI_SOUND_FILE(MONKSHOT.wav) \ +VCMI_SOUND_NAME(monkWNCE) VCMI_SOUND_FILE(MONKWNCE.wav) \ +VCMI_SOUND_NAME(MORALE) VCMI_SOUND_FILE(MORALE.wav) \ +VCMI_SOUND_NAME(MUCKMIRE) VCMI_SOUND_FILE(MUCKMIRE.wav) \ +VCMI_SOUND_NAME(MUMYAttack) VCMI_SOUND_FILE(MUMYATTK.wav) \ +VCMI_SOUND_NAME(MUMYDefend) VCMI_SOUND_FILE(MUMYDFND.wav) \ +VCMI_SOUND_NAME(MUMYKill) VCMI_SOUND_FILE(MUMYKILL.wav) \ +VCMI_SOUND_NAME(MUMYMove) VCMI_SOUND_FILE(MUMYMOVE.wav) \ +VCMI_SOUND_NAME(MUMYWNCE) VCMI_SOUND_FILE(MUMYWNCE.wav) \ +VCMI_SOUND_NAME(MYSTERY) VCMI_SOUND_FILE(MYSTERY.wav) \ +VCMI_SOUND_NAME(newDay) VCMI_SOUND_FILE(NEWDAY.wav) \ +VCMI_SOUND_NAME(newMonth) VCMI_SOUND_FILE(NEWMONTH.wav) \ +VCMI_SOUND_NAME(newWeek) VCMI_SOUND_FILE(NEWWEEK.wav) \ +VCMI_SOUND_NAME(NGRDAttack) VCMI_SOUND_FILE(NGRDATTK.wav) \ +VCMI_SOUND_NAME(NGRDDefend) VCMI_SOUND_FILE(NGRDDFND.wav) \ +VCMI_SOUND_NAME(NGRDKill) VCMI_SOUND_FILE(NGRDKILL.wav) \ +VCMI_SOUND_NAME(NGRDMove) VCMI_SOUND_FILE(NGRDMOVE.wav) \ +VCMI_SOUND_NAME(NGRDWNCE) VCMI_SOUND_FILE(NGRDWNCE.wav) \ +VCMI_SOUND_NAME(NMADAttack) VCMI_SOUND_FILE(NMADATTK.wav) \ +VCMI_SOUND_NAME(NMADDefend) VCMI_SOUND_FILE(NMADDFND.wav) \ +VCMI_SOUND_NAME(NMADKill) VCMI_SOUND_FILE(NMADKILL.wav) \ +VCMI_SOUND_NAME(NMADMove) VCMI_SOUND_FILE(NMADMOVE.wav) \ +VCMI_SOUND_NAME(NMADWNCE) VCMI_SOUND_FILE(NMADWNCE.wav) \ +VCMI_SOUND_NAME(NOMAD) VCMI_SOUND_FILE(NOMAD.wav) \ +VCMI_SOUND_NAME(NOSFAttack) VCMI_SOUND_FILE(NOSFATTK.wav) \ +VCMI_SOUND_NAME(NOSFDefend) VCMI_SOUND_FILE(NOSFDFND.wav) \ +VCMI_SOUND_NAME(NOSFEXT1) VCMI_SOUND_FILE(NOSFEXT1.wav) \ +VCMI_SOUND_NAME(NOSFEXT2) VCMI_SOUND_FILE(NOSFEXT2.wav) \ +VCMI_SOUND_NAME(NOSFKill) VCMI_SOUND_FILE(NOSFKILL.wav) \ +VCMI_SOUND_NAME(NOSFMove) VCMI_SOUND_FILE(NOSFMOVE.wav) \ +VCMI_SOUND_NAME(NOSFShot) VCMI_SOUND_FILE(NOSFSHOT.wav) \ +VCMI_SOUND_NAME(NOSFWNCE) VCMI_SOUND_FILE(NOSFWNCE.wav) \ +VCMI_SOUND_NAME(NSENAttack) VCMI_SOUND_FILE(NSENATTK.wav) \ +VCMI_SOUND_NAME(NSENDefend) VCMI_SOUND_FILE(NSENDFND.wav) \ +VCMI_SOUND_NAME(NSENKill) VCMI_SOUND_FILE(NSENKILL.wav) \ +VCMI_SOUND_NAME(NSENMove) VCMI_SOUND_FILE(NSENMOVE.wav) \ +VCMI_SOUND_NAME(NSENWNCE) VCMI_SOUND_FILE(NSENWNCE.wav) \ +VCMI_SOUND_NAME(heroNewLevel) VCMI_SOUND_FILE(NWHEROLV.wav) \ +VCMI_SOUND_NAME(OBELISK) VCMI_SOUND_FILE(OBELISK.wav) \ +VCMI_SOUND_NAME(OGREAttack) VCMI_SOUND_FILE(OGREATTK.wav) \ +VCMI_SOUND_NAME(OGREDefend) VCMI_SOUND_FILE(OGREDFND.wav) \ +VCMI_SOUND_NAME(OGREKill) VCMI_SOUND_FILE(OGREKILL.wav) \ +VCMI_SOUND_NAME(OGREMove) VCMI_SOUND_FILE(OGREMOVE.wav) \ +VCMI_SOUND_NAME(OGREWNCE) VCMI_SOUND_FILE(OGREWNCE.wav) \ +VCMI_SOUND_NAME(OGRGAttack) VCMI_SOUND_FILE(OGRGATTK.wav) \ +VCMI_SOUND_NAME(OGRGDefend) VCMI_SOUND_FILE(OGRGDFND.wav) \ +VCMI_SOUND_NAME(OGRGKill) VCMI_SOUND_FILE(OGRGKILL.wav) \ +VCMI_SOUND_NAME(OGRGMove) VCMI_SOUND_FILE(OGRGMOVE.wav) \ +VCMI_SOUND_NAME(OGRGWNCE) VCMI_SOUND_FILE(OGRGWNCE.wav) \ +VCMI_SOUND_NAME(OGRMAttack) VCMI_SOUND_FILE(OGRMATTK.wav) \ +VCMI_SOUND_NAME(OGRMDefend) VCMI_SOUND_FILE(OGRMDFND.wav) \ +VCMI_SOUND_NAME(OGRMKill) VCMI_SOUND_FILE(OGRMKILL.wav) \ +VCMI_SOUND_NAME(OGRMMove) VCMI_SOUND_FILE(OGRMMOVE.wav) \ +VCMI_SOUND_NAME(OGRMShot) VCMI_SOUND_FILE(OGRMSHOT.wav) \ +VCMI_SOUND_NAME(OGRMWNCE) VCMI_SOUND_FILE(OGRMWNCE.wav) \ +VCMI_SOUND_NAME(OORCAttack) VCMI_SOUND_FILE(OORCATTK.wav) \ +VCMI_SOUND_NAME(OORCDefend) VCMI_SOUND_FILE(OORCDFND.wav) \ +VCMI_SOUND_NAME(OORCKill) VCMI_SOUND_FILE(OORCKILL.wav) \ +VCMI_SOUND_NAME(OORCMove) VCMI_SOUND_FILE(OORCMOVE.wav) \ +VCMI_SOUND_NAME(OORCShot) VCMI_SOUND_FILE(OORCSHOT.wav) \ +VCMI_SOUND_NAME(OORCWNCE) VCMI_SOUND_FILE(OORCWNCE.wav) \ +VCMI_SOUND_NAME(ORCCAttack) VCMI_SOUND_FILE(ORCCATTK.wav) \ +VCMI_SOUND_NAME(ORCCDefend) VCMI_SOUND_FILE(ORCCDFND.wav) \ +VCMI_SOUND_NAME(ORCCKill) VCMI_SOUND_FILE(ORCCKILL.wav) \ +VCMI_SOUND_NAME(ORCCMove) VCMI_SOUND_FILE(ORCCMOVE.wav) \ +VCMI_SOUND_NAME(ORCCShot) VCMI_SOUND_FILE(ORCCSHOT.wav) \ +VCMI_SOUND_NAME(ORCCWNCE) VCMI_SOUND_FILE(ORCCWNCE.wav) \ +VCMI_SOUND_NAME(PARALYZE) VCMI_SOUND_FILE(PARALYZE.wav) \ +VCMI_SOUND_NAME(PEGAAttack) VCMI_SOUND_FILE(PEGAATTK.wav) \ +VCMI_SOUND_NAME(PEGADefend) VCMI_SOUND_FILE(PEGADFND.wav) \ +VCMI_SOUND_NAME(PEGAKill) VCMI_SOUND_FILE(PEGAKILL.wav) \ +VCMI_SOUND_NAME(PEGAMove) VCMI_SOUND_FILE(PEGAMOVE.wav) \ +VCMI_SOUND_NAME(PEGAWNCE) VCMI_SOUND_FILE(PEGAWNCE.wav) \ +VCMI_SOUND_NAME(PFNDAttack) VCMI_SOUND_FILE(PFNDATTK.wav) \ +VCMI_SOUND_NAME(PFNDDefend) VCMI_SOUND_FILE(PFNDDFND.wav) \ +VCMI_SOUND_NAME(PFNDKill) VCMI_SOUND_FILE(PFNDKILL.wav) \ +VCMI_SOUND_NAME(PFNDMove) VCMI_SOUND_FILE(PFNDMOVE.wav) \ +VCMI_SOUND_NAME(PFNDWNCE) VCMI_SOUND_FILE(PFNDWNCE.wav) \ +VCMI_SOUND_NAME(PFOEAttack) VCMI_SOUND_FILE(PFOEATTK.wav) \ +VCMI_SOUND_NAME(PFOEDefend) VCMI_SOUND_FILE(PFOEDFND.wav) \ +VCMI_SOUND_NAME(PFOEKill) VCMI_SOUND_FILE(PFOEKILL.wav) \ +VCMI_SOUND_NAME(PFOEMove) VCMI_SOUND_FILE(PFOEMOVE.wav) \ +VCMI_SOUND_NAME(PFOEWNCE) VCMI_SOUND_FILE(PFOEWNCE.wav) \ +VCMI_SOUND_NAME(PHOEAttack) VCMI_SOUND_FILE(PHOEATTK.wav) \ +VCMI_SOUND_NAME(PHOEDefend) VCMI_SOUND_FILE(PHOEDFND.wav) \ +VCMI_SOUND_NAME(PHOEKill) VCMI_SOUND_FILE(PHOEKILL.wav) \ +VCMI_SOUND_NAME(PHOEMove) VCMI_SOUND_FILE(PHOEMOVE.wav) \ +VCMI_SOUND_NAME(PHOEWNCE) VCMI_SOUND_FILE(PHOEWNCE.wav) \ +VCMI_SOUND_NAME(pickup01) VCMI_SOUND_FILE(PICKUP01.wav) \ +VCMI_SOUND_NAME(pickup02) VCMI_SOUND_FILE(PICKUP02.wav) \ +VCMI_SOUND_NAME(pickup03) VCMI_SOUND_FILE(PICKUP03.wav) \ +VCMI_SOUND_NAME(pickup04) VCMI_SOUND_FILE(PICKUP04.wav) \ +VCMI_SOUND_NAME(pickup05) VCMI_SOUND_FILE(PICKUP05.wav) \ +VCMI_SOUND_NAME(pickup06) VCMI_SOUND_FILE(PICKUP06.wav) \ +VCMI_SOUND_NAME(pickup07) VCMI_SOUND_FILE(PICKUP07.wav) \ +VCMI_SOUND_NAME(pikemanAttack) VCMI_SOUND_FILE(PIKEATTK.wav) \ +VCMI_SOUND_NAME(pikemanDefend) VCMI_SOUND_FILE(PIKEDFND.wav) \ +VCMI_SOUND_NAME(pikemanKill) VCMI_SOUND_FILE(PIKEKILL.wav) \ +VCMI_SOUND_NAME(pikemanMove) VCMI_SOUND_FILE(PIKEMOVE.wav) \ +VCMI_SOUND_NAME(pikemanWNCE) VCMI_SOUND_FILE(PIKEWNCE.wav) \ +VCMI_SOUND_NAME(pixieAttack) VCMI_SOUND_FILE(PIXIATTK.wav) \ +VCMI_SOUND_NAME(pixieDefend) VCMI_SOUND_FILE(PIXIDFND.wav) \ +VCMI_SOUND_NAME(pixieKill) VCMI_SOUND_FILE(PIXIKILL.wav) \ +VCMI_SOUND_NAME(pixieMove) VCMI_SOUND_FILE(PIXIMOVE.wav) \ +VCMI_SOUND_NAME(pixieWNCE) VCMI_SOUND_FILE(PIXIWNCE.wav) \ +VCMI_SOUND_NAME(PLAYCOME) VCMI_SOUND_FILE(PLAYCOME.wav) \ +VCMI_SOUND_NAME(PLAYEXIT) VCMI_SOUND_FILE(PLAYEXIT.wav) \ +VCMI_SOUND_NAME(PLAYTURN) VCMI_SOUND_FILE(PLAYTURN.wav) \ +VCMI_SOUND_NAME(PLCHAttack) VCMI_SOUND_FILE(PLCHATTK.wav) \ +VCMI_SOUND_NAME(PLCHDefend) VCMI_SOUND_FILE(PLCHDFND.wav) \ +VCMI_SOUND_NAME(PLCHKill) VCMI_SOUND_FILE(PLCHKILL.wav) \ +VCMI_SOUND_NAME(PLCHMove) VCMI_SOUND_FILE(PLCHMOVE.wav) \ +VCMI_SOUND_NAME(PLCHShot) VCMI_SOUND_FILE(PLCHSHOT.wav) \ +VCMI_SOUND_NAME(PLCHWNCE) VCMI_SOUND_FILE(PLCHWNCE.wav) \ +VCMI_SOUND_NAME(PLIZAttack) VCMI_SOUND_FILE(PLIZATTK.wav) \ +VCMI_SOUND_NAME(PLIZDefend) VCMI_SOUND_FILE(PLIZDFND.wav) \ +VCMI_SOUND_NAME(PLIZKill) VCMI_SOUND_FILE(PLIZKILL.wav) \ +VCMI_SOUND_NAME(PLIZMove) VCMI_SOUND_FILE(PLIZMOVE.wav) \ +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(PROTECTA) VCMI_SOUND_FILE(PROTECTA.wav) \ +VCMI_SOUND_NAME(PROTECTE) VCMI_SOUND_FILE(PROTECTE.wav) \ +VCMI_SOUND_NAME(PROTECTF) VCMI_SOUND_FILE(PROTECTF.wav) \ +VCMI_SOUND_NAME(PROTECT) VCMI_SOUND_FILE(PROTECT.wav) \ +VCMI_SOUND_NAME(PROTECTW) VCMI_SOUND_FILE(PROTECTW.wav) \ +VCMI_SOUND_NAME(PSNTAttack) VCMI_SOUND_FILE(PSNTATTK.wav) \ +VCMI_SOUND_NAME(PSNTDefend) VCMI_SOUND_FILE(PSNTDFND.wav) \ +VCMI_SOUND_NAME(PSNTKill) VCMI_SOUND_FILE(PSNTKILL.wav) \ +VCMI_SOUND_NAME(PSNTMove) VCMI_SOUND_FILE(PSNTMOVE.wav) \ +VCMI_SOUND_NAME(PSNTWNCE) VCMI_SOUND_FILE(PSNTWNCE.wav) \ +VCMI_SOUND_NAME(PSYCAttack) VCMI_SOUND_FILE(PSYCATTK.wav) \ +VCMI_SOUND_NAME(PSYCDefend) VCMI_SOUND_FILE(PSYCDFND.wav) \ +VCMI_SOUND_NAME(PSYCKill) VCMI_SOUND_FILE(PSYCKILL.wav) \ +VCMI_SOUND_NAME(PSYCMove) VCMI_SOUND_FILE(PSYCMOVE.wav) \ +VCMI_SOUND_NAME(PSYCWNCE) VCMI_SOUND_FILE(PSYCWNCE.wav) \ +VCMI_SOUND_NAME(QUEST) VCMI_SOUND_FILE(QUEST.wav) \ +VCMI_SOUND_NAME(QUIKSAND) VCMI_SOUND_FILE(QUIKSAND.wav) \ +VCMI_SOUND_NAME(RDDRAttack) VCMI_SOUND_FILE(RDDRATTK.wav) \ +VCMI_SOUND_NAME(RDDRDefend) VCMI_SOUND_FILE(RDDRDFND.wav) \ +VCMI_SOUND_NAME(RDDRKill) VCMI_SOUND_FILE(RDDRKILL.wav) \ +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(RGRFAttack) VCMI_SOUND_FILE(RGRFATTK.wav) \ +VCMI_SOUND_NAME(RGRFDefend) VCMI_SOUND_FILE(RGRFDFND.wav) \ +VCMI_SOUND_NAME(RGRFKill) VCMI_SOUND_FILE(RGRFKILL.wav) \ +VCMI_SOUND_NAME(RGRFMove) VCMI_SOUND_FILE(RGRFMOVE.wav) \ +VCMI_SOUND_NAME(RGRFWNCE) VCMI_SOUND_FILE(RGRFWNCE.wav) \ +VCMI_SOUND_NAME(ROCCAttack) VCMI_SOUND_FILE(ROCCATTK.wav) \ +VCMI_SOUND_NAME(ROCCDefend) VCMI_SOUND_FILE(ROCCDFND.wav) \ +VCMI_SOUND_NAME(ROCCKill) VCMI_SOUND_FILE(ROCCKILL.wav) \ +VCMI_SOUND_NAME(ROCCMove) VCMI_SOUND_FILE(ROCCMOVE.wav) \ +VCMI_SOUND_NAME(ROCCWNCE) VCMI_SOUND_FILE(ROCCWNCE.wav) \ +VCMI_SOUND_NAME(ROGUAttack) VCMI_SOUND_FILE(ROGUATTK.wav) \ +VCMI_SOUND_NAME(ROGUDefend) VCMI_SOUND_FILE(ROGUDFND.wav) \ +VCMI_SOUND_NAME(ROGUE) VCMI_SOUND_FILE(ROGUE.wav) \ +VCMI_SOUND_NAME(ROGUKill) VCMI_SOUND_FILE(ROGUKILL.wav) \ +VCMI_SOUND_NAME(ROGUMove) VCMI_SOUND_FILE(ROGUMOVE.wav) \ +VCMI_SOUND_NAME(ROGUWNCE) VCMI_SOUND_FILE(ROGUWNCE.wav) \ +VCMI_SOUND_NAME(RSBRYFZL) VCMI_SOUND_FILE(RSBRYFZL.wav) \ +VCMI_SOUND_NAME(RUSTAttack) VCMI_SOUND_FILE(RUSTATTK.wav) \ +VCMI_SOUND_NAME(RUSTDefend) VCMI_SOUND_FILE(RUSTDFND.wav) \ +VCMI_SOUND_NAME(RUSTKill) VCMI_SOUND_FILE(RUSTKILL.wav) \ +VCMI_SOUND_NAME(RUSTMove) VCMI_SOUND_FILE(RUSTMOVE.wav) \ +VCMI_SOUND_NAME(RUSTWNCE) VCMI_SOUND_FILE(RUSTWNCE.wav) \ +VCMI_SOUND_NAME(SACBRETH) VCMI_SOUND_FILE(SACBRETH.wav) \ +VCMI_SOUND_NAME(SACRIF1) VCMI_SOUND_FILE(SACRIF1.wav) \ +VCMI_SOUND_NAME(SACRIF2) VCMI_SOUND_FILE(SACRIF2.wav) \ +VCMI_SOUND_NAME(SCRPAttack) VCMI_SOUND_FILE(SCRPATTK.wav) \ +VCMI_SOUND_NAME(SCRPDefend) VCMI_SOUND_FILE(SCRPDFND.wav) \ +VCMI_SOUND_NAME(SCRPKill) VCMI_SOUND_FILE(SCRPKILL.wav) \ +VCMI_SOUND_NAME(SCRPMove) VCMI_SOUND_FILE(SCRPMOVE.wav) \ +VCMI_SOUND_NAME(SCRPShot) VCMI_SOUND_FILE(SCRPSHOT.wav) \ +VCMI_SOUND_NAME(SCRPWNCE) VCMI_SOUND_FILE(SCRPWNCE.wav) \ +VCMI_SOUND_NAME(SCUTBOAT) VCMI_SOUND_FILE(SCUTBOAT.wav) \ +VCMI_SOUND_NAME(SGLMAttack) VCMI_SOUND_FILE(SGLMATTK.wav) \ +VCMI_SOUND_NAME(SGLMDefend) VCMI_SOUND_FILE(SGLMDFND.wav) \ +VCMI_SOUND_NAME(SGLMKill) VCMI_SOUND_FILE(SGLMKILL.wav) \ +VCMI_SOUND_NAME(SGLMMove) VCMI_SOUND_FILE(SGLMMOVE.wav) \ +VCMI_SOUND_NAME(SGLMWNCE) VCMI_SOUND_FILE(SGLMWNCE.wav) \ +VCMI_SOUND_NAME(SGRGAttack) VCMI_SOUND_FILE(SGRGATTK.wav) \ +VCMI_SOUND_NAME(SGRGDefend) VCMI_SOUND_FILE(SGRGDFND.wav) \ +VCMI_SOUND_NAME(SGRGKill) VCMI_SOUND_FILE(SGRGKILL.wav) \ +VCMI_SOUND_NAME(SGRGMove) VCMI_SOUND_FILE(SGRGMOVE.wav) \ +VCMI_SOUND_NAME(SGRGWNCE) VCMI_SOUND_FILE(SGRGWNCE.wav) \ +VCMI_SOUND_NAME(SHDMAttack) VCMI_SOUND_FILE(SHDMATTK.wav) \ +VCMI_SOUND_NAME(SHDMDefend) VCMI_SOUND_FILE(SHDMDFND.wav) \ +VCMI_SOUND_NAME(SHDMKill) VCMI_SOUND_FILE(SHDMKILL.wav) \ +VCMI_SOUND_NAME(SHDMMove) VCMI_SOUND_FILE(SHDMMOVE.wav) \ +VCMI_SOUND_NAME(SHDMWNCE) VCMI_SOUND_FILE(SHDMWNCE.wav) \ +VCMI_SOUND_NAME(SHIELD) VCMI_SOUND_FILE(SHIELD.wav) \ +VCMI_SOUND_NAME(SKELAttack) VCMI_SOUND_FILE(SKELATTK.wav) \ +VCMI_SOUND_NAME(SKELDefend) VCMI_SOUND_FILE(SKELDFND.wav) \ +VCMI_SOUND_NAME(SKELKill) VCMI_SOUND_FILE(SKELKILL.wav) \ +VCMI_SOUND_NAME(SKELMove) VCMI_SOUND_FILE(SKELMOVE.wav) \ +VCMI_SOUND_NAME(SKELWNCE) VCMI_SOUND_FILE(SKELWNCE.wav) \ +VCMI_SOUND_NAME(SKLWAttack) VCMI_SOUND_FILE(SKLWATTK.wav) \ +VCMI_SOUND_NAME(SKLWDefend) VCMI_SOUND_FILE(SKLWDFND.wav) \ +VCMI_SOUND_NAME(SKLWKill) VCMI_SOUND_FILE(SKLWKILL.wav) \ +VCMI_SOUND_NAME(SKLWMove) VCMI_SOUND_FILE(SKLWMOVE.wav) \ +VCMI_SOUND_NAME(SKLWWNCE) VCMI_SOUND_FILE(SKLWWNCE.wav) \ +VCMI_SOUND_NAME(SLAYER) VCMI_SOUND_FILE(SLAYER.wav) \ +VCMI_SOUND_NAME(SORROW) VCMI_SOUND_FILE(SORROW.wav) \ +VCMI_SOUND_NAME(SPONTCOMB) VCMI_SOUND_FILE(SPONTCOMB.wav) \ +VCMI_SOUND_NAME(SPRTAttack) VCMI_SOUND_FILE(SPRTATTK.wav) \ +VCMI_SOUND_NAME(SPRTDefend) VCMI_SOUND_FILE(SPRTDFND.wav) \ +VCMI_SOUND_NAME(SPRTKill) VCMI_SOUND_FILE(SPRTKILL.wav) \ +VCMI_SOUND_NAME(SPRTMove) VCMI_SOUND_FILE(SPRTMOVE.wav) \ +VCMI_SOUND_NAME(SPRTWNCE) VCMI_SOUND_FILE(SPRTWNCE.wav) \ +VCMI_SOUND_NAME(STORAttack) VCMI_SOUND_FILE(STORATTK.wav) \ +VCMI_SOUND_NAME(STORDefend) VCMI_SOUND_FILE(STORDFND.wav) \ +VCMI_SOUND_NAME(STORE) VCMI_SOUND_FILE(STORE.wav) \ +VCMI_SOUND_NAME(STORKill) VCMI_SOUND_FILE(STORKILL.wav) \ +VCMI_SOUND_NAME(STORMove) VCMI_SOUND_FILE(STORMOVE.wav) \ +VCMI_SOUND_NAME(STORM) VCMI_SOUND_FILE(STORM.wav) \ +VCMI_SOUND_NAME(STORShot) VCMI_SOUND_FILE(STORSHOT.wav) \ +VCMI_SOUND_NAME(STORWNCE) VCMI_SOUND_FILE(STORWNCE.wav) \ +VCMI_SOUND_NAME(SUMMBOAT) VCMI_SOUND_FILE(SUMMBOAT.wav) \ +VCMI_SOUND_NAME(SUMNELM) VCMI_SOUND_FILE(SUMNELM.wav) \ +VCMI_SOUND_NAME(SWRDAttack) VCMI_SOUND_FILE(SWRDATTK.wav) \ +VCMI_SOUND_NAME(SWRDDefend) VCMI_SOUND_FILE(SWRDDFND.wav) \ +VCMI_SOUND_NAME(SWRDKill) VCMI_SOUND_FILE(SWRDKILL.wav) \ +VCMI_SOUND_NAME(SWRDMove) VCMI_SOUND_FILE(SWRDMOVE.wav) \ +VCMI_SOUND_NAME(SWRDWNCE) VCMI_SOUND_FILE(SWRDWNCE.wav) \ +VCMI_SOUND_NAME(SYSMSG) VCMI_SOUND_FILE(SYSMSG.wav) \ +VCMI_SOUND_NAME(TAILWIND) VCMI_SOUND_FILE(TAILWIND.wav) \ +VCMI_SOUND_NAME(TBRDAttack) VCMI_SOUND_FILE(TBRDATTK.wav) \ +VCMI_SOUND_NAME(TBRDDefend) VCMI_SOUND_FILE(TBRDDFND.wav) \ +VCMI_SOUND_NAME(TBRDKill) VCMI_SOUND_FILE(TBRDKILL.wav) \ +VCMI_SOUND_NAME(TBRDMove) VCMI_SOUND_FILE(TBRDMOVE.wav) \ +VCMI_SOUND_NAME(TBRDWNCE) VCMI_SOUND_FILE(TBRDWNCE.wav) \ +VCMI_SOUND_NAME(TELEIN) VCMI_SOUND_FILE(TELEIN.wav) \ +VCMI_SOUND_NAME(TELPTIN) VCMI_SOUND_FILE(TELPTIN.wav) \ +VCMI_SOUND_NAME(TELPTOUT) VCMI_SOUND_FILE(TELPTOUT.wav) \ +VCMI_SOUND_NAME(temple) VCMI_SOUND_FILE(TEMPLE.wav) \ +VCMI_SOUND_NAME(timeOver) VCMI_SOUND_FILE(TIMEOVER.wav) \ +VCMI_SOUND_NAME(treasure) VCMI_SOUND_FILE(TREASURE.wav) \ +VCMI_SOUND_NAME(TREEAttack) VCMI_SOUND_FILE(TREEATTK.wav) \ +VCMI_SOUND_NAME(TREEDefend) VCMI_SOUND_FILE(TREEDFND.wav) \ +VCMI_SOUND_NAME(TREEKill) VCMI_SOUND_FILE(TREEKILL.wav) \ +VCMI_SOUND_NAME(TREEMove) VCMI_SOUND_FILE(TREEMOVE.wav) \ +VCMI_SOUND_NAME(TREEWNCE) VCMI_SOUND_FILE(TREEWNCE.wav) \ +VCMI_SOUND_NAME(TRLLAttack) VCMI_SOUND_FILE(TRLLATTK.wav) \ +VCMI_SOUND_NAME(TRLLDefend) VCMI_SOUND_FILE(TRLLDFND.wav) \ +VCMI_SOUND_NAME(TRLLKill) VCMI_SOUND_FILE(TRLLKILL.wav) \ +VCMI_SOUND_NAME(TRLLMove) VCMI_SOUND_FILE(TRLLMOVE.wav) \ +VCMI_SOUND_NAME(TRLLWNCE) VCMI_SOUND_FILE(TRLLWNCE.wav) \ +VCMI_SOUND_NAME(TROGAttack) VCMI_SOUND_FILE(TROGATTK.wav) \ +VCMI_SOUND_NAME(TROGDefend) VCMI_SOUND_FILE(TROGDFND.wav) \ +VCMI_SOUND_NAME(TROGKill) VCMI_SOUND_FILE(TROGKILL.wav) \ +VCMI_SOUND_NAME(TROGMove) VCMI_SOUND_FILE(TROGMOVE.wav) \ +VCMI_SOUND_NAME(TROGWNCE) VCMI_SOUND_FILE(TROGWNCE.wav) \ +VCMI_SOUND_NAME(TUFFSKIN) VCMI_SOUND_FILE(TUFFSKIN.wav) \ +VCMI_SOUND_NAME(ULTIMATEARTIFACT) VCMI_SOUND_FILE(ULTIMATEARTIFACT.wav) \ +VCMI_SOUND_NAME(UNICAttack) VCMI_SOUND_FILE(UNICATTK.wav) \ +VCMI_SOUND_NAME(UNICDefend) VCMI_SOUND_FILE(UNICDFND.wav) \ +VCMI_SOUND_NAME(UNICKill) VCMI_SOUND_FILE(UNICKILL.wav) \ +VCMI_SOUND_NAME(UNICMove) VCMI_SOUND_FILE(UNICMOVE.wav) \ +VCMI_SOUND_NAME(UNICWNCE) VCMI_SOUND_FILE(UNICWNCE.wav) \ +VCMI_SOUND_NAME(VAMPAttack) VCMI_SOUND_FILE(VAMPATTK.wav) \ +VCMI_SOUND_NAME(VAMPDefend) VCMI_SOUND_FILE(VAMPDFND.wav) \ +VCMI_SOUND_NAME(VAMPEXT1) VCMI_SOUND_FILE(VAMPEXT1.wav) \ +VCMI_SOUND_NAME(VAMPEXT2) VCMI_SOUND_FILE(VAMPEXT2.wav) \ +VCMI_SOUND_NAME(VAMPKill) VCMI_SOUND_FILE(VAMPKILL.wav) \ +VCMI_SOUND_NAME(VAMPMove) VCMI_SOUND_FILE(VAMPMOVE.wav) \ +VCMI_SOUND_NAME(VAMPWNCE) VCMI_SOUND_FILE(VAMPWNCE.wav) \ +VCMI_SOUND_NAME(VIEW) VCMI_SOUND_FILE(VIEW.wav) \ +VCMI_SOUND_NAME(VISIONS) VCMI_SOUND_FILE(VISIONS.wav) \ +VCMI_SOUND_NAME(WALLHIT) VCMI_SOUND_FILE(WALLHIT.wav) \ +VCMI_SOUND_NAME(WALLMISS) VCMI_SOUND_FILE(WALLMISS.wav) \ +VCMI_SOUND_NAME(WATRWALK) VCMI_SOUND_FILE(WATRWALK.wav) \ +VCMI_SOUND_NAME(WEAKNESS) VCMI_SOUND_FILE(WEAKNESS.wav) \ +VCMI_SOUND_NAME(WELFAttack) VCMI_SOUND_FILE(WELFATTK.wav) \ +VCMI_SOUND_NAME(WELFDefend) VCMI_SOUND_FILE(WELFDFND.wav) \ +VCMI_SOUND_NAME(WELFKill) VCMI_SOUND_FILE(WELFKILL.wav) \ +VCMI_SOUND_NAME(WELFMove) VCMI_SOUND_FILE(WELFMOVE.wav) \ +VCMI_SOUND_NAME(WELFShot) VCMI_SOUND_FILE(WELFSHOT.wav) \ +VCMI_SOUND_NAME(WELFWNCE) VCMI_SOUND_FILE(WELFWNCE.wav) \ +VCMI_SOUND_NAME(WELMAttack) VCMI_SOUND_FILE(WELMATTK.wav) \ +VCMI_SOUND_NAME(WELMDefend) VCMI_SOUND_FILE(WELMDFND.wav) \ +VCMI_SOUND_NAME(WELMKill) VCMI_SOUND_FILE(WELMKILL.wav) \ +VCMI_SOUND_NAME(WELMMove) VCMI_SOUND_FILE(WELMMOVE.wav) \ +VCMI_SOUND_NAME(WELMWNCE) VCMI_SOUND_FILE(WELMWNCE.wav) \ +VCMI_SOUND_NAME(WGHTAttack) VCMI_SOUND_FILE(WGHTATTK.wav) \ +VCMI_SOUND_NAME(WGHTDefend) VCMI_SOUND_FILE(WGHTDFND.wav) \ +VCMI_SOUND_NAME(WGHTKill) VCMI_SOUND_FILE(WGHTKILL.wav) \ +VCMI_SOUND_NAME(WGHTMove) VCMI_SOUND_FILE(WGHTMOVE.wav) \ +VCMI_SOUND_NAME(WGHTWNCE) VCMI_SOUND_FILE(WGHTWNCE.wav) \ +VCMI_SOUND_NAME(WRTHAttack) VCMI_SOUND_FILE(WRTHATTK.wav) \ +VCMI_SOUND_NAME(WRTHDefend) VCMI_SOUND_FILE(WRTHDFND.wav) \ +VCMI_SOUND_NAME(WRTHKill) VCMI_SOUND_FILE(WRTHKILL.wav) \ +VCMI_SOUND_NAME(WRTHMove) VCMI_SOUND_FILE(WRTHMOVE.wav) \ +VCMI_SOUND_NAME(WRTHWNCE) VCMI_SOUND_FILE(WRTHWNCE.wav) \ +VCMI_SOUND_NAME(WUNCAttack) VCMI_SOUND_FILE(WUNCATTK.wav) \ +VCMI_SOUND_NAME(WUNCDefend) VCMI_SOUND_FILE(WUNCDFND.wav) \ +VCMI_SOUND_NAME(WUNCKill) VCMI_SOUND_FILE(WUNCKILL.wav) \ +VCMI_SOUND_NAME(WUNCMove) VCMI_SOUND_FILE(WUNCMOVE.wav) \ +VCMI_SOUND_NAME(WUNCShot) VCMI_SOUND_FILE(WUNCSHOT.wav) \ +VCMI_SOUND_NAME(WUNCWNCE) VCMI_SOUND_FILE(WUNCWNCE.wav) \ +VCMI_SOUND_NAME(WYVMAttack) VCMI_SOUND_FILE(WYVMATTK.wav) \ +VCMI_SOUND_NAME(WYVMDefend) VCMI_SOUND_FILE(WYVMDFND.wav) \ +VCMI_SOUND_NAME(WYVMKill) VCMI_SOUND_FILE(WYVMKILL.wav) \ +VCMI_SOUND_NAME(WYVMMove) VCMI_SOUND_FILE(WYVMMOVE.wav) \ +VCMI_SOUND_NAME(WYVMWNCE) VCMI_SOUND_FILE(WYVMWNCE.wav) \ +VCMI_SOUND_NAME(WYVNAttack) VCMI_SOUND_FILE(WYVNATTK.wav) \ +VCMI_SOUND_NAME(WYVNDefend) VCMI_SOUND_FILE(WYVNDFND.wav) \ +VCMI_SOUND_NAME(WYVNKill) VCMI_SOUND_FILE(WYVNKILL.wav) \ +VCMI_SOUND_NAME(WYVNMove) VCMI_SOUND_FILE(WYVNMOVE.wav) \ +VCMI_SOUND_NAME(WYVNWNCE) VCMI_SOUND_FILE(WYVNWNCE.wav) \ +VCMI_SOUND_NAME(YBMHAttack) VCMI_SOUND_FILE(YBMHATTK.wav) \ +VCMI_SOUND_NAME(YBMHDefend) VCMI_SOUND_FILE(YBMHDFND.wav) \ +VCMI_SOUND_NAME(YBMHKill) VCMI_SOUND_FILE(YBMHKILL.wav) \ +VCMI_SOUND_NAME(YBMHMove) VCMI_SOUND_FILE(YBMHMOVE.wav) \ +VCMI_SOUND_NAME(YBMHWNCE) VCMI_SOUND_FILE(YBMHWNCE.wav) \ +VCMI_SOUND_NAME(zelotAttack) VCMI_SOUND_FILE(ZELTATTK.wav) \ +VCMI_SOUND_NAME(zelotDefend) VCMI_SOUND_FILE(ZELTDFND.wav) \ +VCMI_SOUND_NAME(zelotKill) VCMI_SOUND_FILE(ZELTKILL.wav) \ +VCMI_SOUND_NAME(zelotMove) VCMI_SOUND_FILE(ZELTMOVE.wav) \ +VCMI_SOUND_NAME(zelotShot) VCMI_SOUND_FILE(ZELTSHOT.wav) \ +VCMI_SOUND_NAME(zelotWNCE) VCMI_SOUND_FILE(ZELTWNCE.wav) \ +VCMI_SOUND_NAME(ZMBLAttack) VCMI_SOUND_FILE(ZMBLATTK.wav) \ +VCMI_SOUND_NAME(ZMBLDefend) VCMI_SOUND_FILE(ZMBLDFND.wav) \ +VCMI_SOUND_NAME(ZMBLKill) VCMI_SOUND_FILE(ZMBLKILL.wav) \ +VCMI_SOUND_NAME(ZMBLMove) VCMI_SOUND_FILE(ZMBLMOVE.wav) \ +VCMI_SOUND_NAME(ZMBLWNCE) VCMI_SOUND_FILE(ZMBLWNCE.wav) \ +VCMI_SOUND_NAME(ZOMBAttack) VCMI_SOUND_FILE(ZOMBATTK.wav) \ +VCMI_SOUND_NAME(ZOMBDefend) VCMI_SOUND_FILE(ZOMBDFND.wav) \ +VCMI_SOUND_NAME(ZOMBKill) VCMI_SOUND_FILE(ZOMBKILL.wav) \ +VCMI_SOUND_NAME(ZOMBMove) VCMI_SOUND_FILE(ZOMBMOVE.wav) \ +VCMI_SOUND_NAME(ZOMBWNCE) VCMI_SOUND_FILE(ZOMBWNCE.wav) + +class soundBase +{ +public: + // Make a list of enums + // We must keep an entry 0 for an invalid or no sound. +#define VCMI_SOUND_NAME(x) x, +#define VCMI_SOUND_FILE(y) + enum soundID { + invalid=0, + sound_todo=1, // temp entry until code is fixed + VCMI_SOUND_LIST + sound_after_last + }; +#undef VCMI_SOUND_FILE +#undef VCMI_SOUND_NAME +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStack.cpp b/lib/CStack.cpp index c8e63313c..128b1e2bb 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -1,405 +1,406 @@ -/* - * CStack.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 "CStack.h" - -#include - -#include -#include - -#include "CGeneralTextHandler.h" -#include "battle/BattleInfo.h" -#include "spells/CSpellHandler.h" -#include "NetPacks.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -///CStack -CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, ui8 Side, const SlotID & S): - CBonusSystemNode(STACK_BATTLE), - base(Base), - ID(I), - type(Base->type), - baseAmount(Base->count), - owner(O), - slot(S), - side(Side) -{ - health.init(); //??? -} - -CStack::CStack(): - CBonusSystemNode(STACK_BATTLE), - owner(PlayerColor::NEUTRAL), - slot(SlotID(255)), - initialPosition(BattleHex()) -{ -} - -CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S): - CBonusSystemNode(STACK_BATTLE), - ID(I), - type(stack->type), - baseAmount(stack->count), - owner(O), - slot(S), - side(Side) -{ - health.init(); //??? -} - -void CStack::localInit(BattleInfo * battleInfo) -{ - battle = battleInfo; - assert(type); - - exportBonuses(); - if(base) //stack originating from "real" stack in garrison -> attach to it - { - attachTo(const_cast(*base)); - } - else //attach directly to obj to which stack belongs and creature type - { - CArmedInstance * army = battle->battleGetArmyObject(side); - assert(army); - attachTo(*army); - attachTo(const_cast(*type)); - } - 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 - position = initialPosition; -} - -ui32 CStack::level() const -{ - if(base) - return base->getLevel(); //creature or commander - else - return std::max(1, static_cast(unitType()->getLevel())); //war machine, clone etc -} - -si32 CStack::magicResistance() const -{ - auto magicResistance = AFactionMember::magicResistance(); - - si32 auraBonus = 0; - - for(const auto * one : battle->battleAdjacentUnits(this)) - { - if(one->unitOwner() == owner) - vstd::amax(auraBonus, one->valOfBonuses(BonusType::SPELL_RESISTANCE_AURA)); //max value - } - vstd::abetween(auraBonus, 0, 100); - vstd::abetween(magicResistance, 0, 100); - float castChance = (100 - magicResistance) * (100 - auraBonus)/100.0; - - return static_cast(100 - castChance); -} - -BattleHex::EDir CStack::destShiftDir() const -{ - if(doubleWide()) - { - if(side == BattleSide::ATTACKER) - return BattleHex::EDir::RIGHT; - else - return BattleHex::EDir::LEFT; - } - else - { - return BattleHex::EDir::NONE; - } -} - -std::vector CStack::activeSpells() const -{ - std::vector ret; - - std::stringstream cachingStr; - cachingStr << "!type_" << vstd::to_underlying(BonusType::NONE) << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT); - CSelector selector = Selector::sourceType()(BonusSource::SPELL_EFFECT) - .And(CSelector([](const Bonus * b)->bool - { - return b->type != BonusType::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure(); - })); - - TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); - for(const auto & it : *spellEffects) - { - if(!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects - ret.push_back(it->sid); - } - - return ret; -} - -CStack::~CStack() -{ - detachFromAll(); -} - -const CGHeroInstance * CStack::getMyHero() const -{ - if(base) - return dynamic_cast(base->armyObj); - else //we are attached directly? - for(const CBonusSystemNode * n : getParentNodes()) - if(n->getNodeType() == HERO) - return dynamic_cast(n); - - return nullptr; -} - -std::string CStack::nodeName() const -{ - std::ostringstream oss; - oss << owner.getStr(); - oss << " battle stack [" << ID << "]: " << getCount() << " of "; - if(type) - oss << type->getNamePluralTextID(); - else - oss << "[UNDEFINED TYPE]"; - - oss << " from slot " << slot; - if(base && base->armyObj) - oss << " of armyobj=" << base->armyObj->id.getNum(); - return oss.str(); -} - -void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const -{ - auto newState = acquireState(); - prepareAttacked(bsa, rand, newState); -} - -void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const std::shared_ptr & customState) -{ - auto initialCount = customState->getCount(); - - // compute damage and update bsa.damageAmount - customState->damage(bsa.damageAmount); - - bsa.killedAmount = initialCount - customState->getCount(); - - if(!customState->alive() && customState->isClone()) - { - bsa.flags |= BattleStackAttacked::CLONE_KILLED; - } - else if(!customState->alive()) //stack killed - { - bsa.flags |= BattleStackAttacked::KILLED; - - auto resurrectValue = customState->valOfBonuses(BonusType::REBIRTH); - - if(resurrectValue > 0 && customState->canCast()) //there must be casts left - { - double resurrectFactor = resurrectValue / 100.0; - - auto baseAmount = customState->unitBaseAmount(); - - double resurrectedRaw = baseAmount * resurrectFactor; - - auto resurrectedCount = static_cast(floor(resurrectedRaw)); - - auto resurrectedAdd = static_cast(baseAmount - (resurrectedCount / resurrectFactor)); - - auto rangeGen = rand.getInt64Range(0, 99); - - for(int32_t i = 0; i < resurrectedAdd; i++) - { - if(resurrectValue > rangeGen()) - resurrectedCount += 1; - } - - if(customState->hasBonusOfType(BonusType::REBIRTH, 1)) - { - // resurrect at least one Sacred Phoenix - vstd::amax(resurrectedCount, 1); - } - - if(resurrectedCount > 0) - { - customState->casts.use(); - bsa.flags |= BattleStackAttacked::REBIRTH; - int64_t toHeal = customState->getMaxHealth() * resurrectedCount; - //TODO: add one-battle rebirth? - customState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - customState->counterAttacks.use(customState->counterAttacks.available()); - } - } - } - - customState->save(bsa.newState.data); - bsa.newState.healthDelta = -bsa.damageAmount; - bsa.newState.id = customState->unitId(); - bsa.newState.operation = UnitChanges::EOperation::RESET_STATE; -} - -std::vector CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) -{ - int mask = 0; - std::vector res; - - if (!attackerPos.isValid()) - attackerPos = attacker->getPosition(); - if (!defenderPos.isValid()) - defenderPos = defender->getPosition(); - - BattleHex otherAttackerPos = attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1); - BattleHex otherDefenderPos = defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1); - - if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front - { - if((mask & 1) == 0) - { - mask |= 1; - res.push_back(defenderPos); - } - } - if (attacker->doubleWide() //back <=> front - && BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0) - { - if((mask & 1) == 0) - { - mask |= 1; - res.push_back(defenderPos); - } - } - if (defender->doubleWide()//front <=> back - && BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0) - { - if((mask & 2) == 0) - { - mask |= 2; - res.push_back(otherDefenderPos); - } - } - if (defender->doubleWide() && attacker->doubleWide()//back <=> back - && BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0) - { - if((mask & 2) == 0) - { - mask |= 2; - res.push_back(otherDefenderPos); - } - } - - return res; -} - -bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) -{ - 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 -} - -bool CStack::canBeHealed() const -{ - return getFirstHPleft() < static_cast(getMaxHealth()) && isValidTarget() && !hasBonusOfType(BonusType::SIEGE_WEAPON); -} - -bool CStack::isOnNativeTerrain() const -{ - //this code is called from CreatureTerrainLimiter::limit on battle start - auto res = nativeTerrain == ETerrainId::ANY_TERRAIN || nativeTerrain == battle->getTerrainType(); - return res; -} - -bool CStack::isOnTerrain(TerrainId terrain) const -{ - return battle->getTerrainType() == terrain; -} - -const CCreature * CStack::unitType() const -{ - return type; -} - -int32_t CStack::unitBaseAmount() const -{ - return baseAmount; -} - -const IBonusBearer* CStack::getBonusBearer() const -{ - return this; -} - -bool CStack::unitHasAmmoCart(const battle::Unit * unit) const -{ - for(const CStack * st : battle->stacks) - { - if(battle->battleMatchOwner(st, unit, true) && st->unitType()->getId() == CreatureID::AMMO_CART) - { - return st->alive(); - } - } - //ammo cart works during creature bank battle while not on battlefield - 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) - { - return true; - } - } - return false; //will be always false if trying to examine enemy hero in "special battle" -} - -PlayerColor CStack::unitEffectiveOwner(const battle::Unit * unit) const -{ - return battle->battleGetOwner(unit); -} - -uint32_t CStack::unitId() const -{ - return ID; -} - -ui8 CStack::unitSide() const -{ - return side; -} - -PlayerColor CStack::unitOwner() const -{ - return owner; -} - -SlotID CStack::unitSlot() const -{ - return slot; -} - -std::string CStack::getDescription() const -{ - return nodeName(); -} - -void CStack::spendMana(ServerCallback * server, const int spellCost) const -{ - if(spellCost != 1) - logGlobal->warn("Unexpected spell cost %d for creature", spellCost); - - BattleSetStackProperty ssp; - ssp.stackID = unitId(); - ssp.which = BattleSetStackProperty::CASTS; - ssp.val = -spellCost; - ssp.absolute = false; - server->apply(&ssp); -} - -VCMI_LIB_NAMESPACE_END +/* + * CStack.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 "CStack.h" + +#include + +#include +#include + +#include "CGeneralTextHandler.h" +#include "battle/BattleInfo.h" +#include "spells/CSpellHandler.h" +#include "networkPacks/PacksForClientBattle.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +///CStack +CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, ui8 Side, const SlotID & S): + CBonusSystemNode(STACK_BATTLE), + base(Base), + ID(I), + type(Base->type), + baseAmount(Base->count), + owner(O), + slot(S), + side(Side) +{ + health.init(); //??? +} + +CStack::CStack(): + CBonusSystemNode(STACK_BATTLE), + owner(PlayerColor::NEUTRAL), + slot(SlotID(255)), + initialPosition(BattleHex()) +{ +} + +CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S): + CBonusSystemNode(STACK_BATTLE), + ID(I), + type(stack->type), + baseAmount(stack->count), + owner(O), + slot(S), + side(Side) +{ + health.init(); //??? +} + +void CStack::localInit(BattleInfo * battleInfo) +{ + battle = battleInfo; + assert(type); + + exportBonuses(); + if(base) //stack originating from "real" stack in garrison -> attach to it + { + attachTo(const_cast(*base)); + } + else //attach directly to obj to which stack belongs and creature type + { + CArmedInstance * army = battle->battleGetArmyObject(side); + assert(army); + attachTo(*army); + attachTo(const_cast(*type)); + } + 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 + position = initialPosition; +} + +ui32 CStack::level() const +{ + if(base) + return base->getLevel(); //creature or commander + else + return std::max(1, static_cast(unitType()->getLevel())); //war machine, clone etc +} + +si32 CStack::magicResistance() const +{ + auto magicResistance = AFactionMember::magicResistance(); + + si32 auraBonus = 0; + + for(const auto * one : battle->battleAdjacentUnits(this)) + { + if(one->unitOwner() == owner) + vstd::amax(auraBonus, one->valOfBonuses(BonusType::SPELL_RESISTANCE_AURA)); //max value + } + vstd::abetween(auraBonus, 0, 100); + vstd::abetween(magicResistance, 0, 100); + float castChance = (100 - magicResistance) * (100 - auraBonus)/100.0; + + return static_cast(100 - castChance); +} + +BattleHex::EDir CStack::destShiftDir() const +{ + if(doubleWide()) + { + if(side == BattleSide::ATTACKER) + return BattleHex::EDir::RIGHT; + else + return BattleHex::EDir::LEFT; + } + else + { + return BattleHex::EDir::NONE; + } +} + +std::vector CStack::activeSpells() const +{ + std::vector ret; + + std::stringstream cachingStr; + cachingStr << "!type_" << vstd::to_underlying(BonusType::NONE) << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT); + CSelector selector = Selector::sourceType()(BonusSource::SPELL_EFFECT) + .And(CSelector([](const Bonus * b)->bool + { + return b->type != BonusType::NONE && b->sid.as().toSpell() && !b->sid.as().toSpell()->isAdventure(); + })); + + TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); + for(const auto & it : *spellEffects) + { + if(!vstd::contains(ret, it->sid.as())) //do not duplicate spells with multiple effects + ret.push_back(it->sid.as()); + } + + return ret; +} + +CStack::~CStack() +{ + detachFromAll(); +} + +const CGHeroInstance * CStack::getMyHero() const +{ + if(base) + return dynamic_cast(base->armyObj); + else //we are attached directly? + for(const CBonusSystemNode * n : getParentNodes()) + if(n->getNodeType() == HERO) + return dynamic_cast(n); + + return nullptr; +} + +std::string CStack::nodeName() const +{ + std::ostringstream oss; + oss << owner.toString(); + oss << " battle stack [" << ID << "]: " << getCount() << " of "; + if(type) + oss << type->getNamePluralTextID(); + else + oss << "[UNDEFINED TYPE]"; + + oss << " from slot " << slot; + if(base && base->armyObj) + oss << " of armyobj=" << base->armyObj->id.getNum(); + return oss.str(); +} + +void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const +{ + auto newState = acquireState(); + prepareAttacked(bsa, rand, newState); +} + +void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const std::shared_ptr & customState) +{ + auto initialCount = customState->getCount(); + + // compute damage and update bsa.damageAmount + customState->damage(bsa.damageAmount); + + bsa.killedAmount = initialCount - customState->getCount(); + + if(!customState->alive() && customState->isClone()) + { + bsa.flags |= BattleStackAttacked::CLONE_KILLED; + } + else if(!customState->alive()) //stack killed + { + bsa.flags |= BattleStackAttacked::KILLED; + + auto resurrectValue = customState->valOfBonuses(BonusType::REBIRTH); + + if(resurrectValue > 0 && customState->canCast()) //there must be casts left + { + double resurrectFactor = resurrectValue / 100.0; + + auto baseAmount = customState->unitBaseAmount(); + + double resurrectedRaw = baseAmount * resurrectFactor; + + auto resurrectedCount = static_cast(floor(resurrectedRaw)); + + auto resurrectedAdd = static_cast(baseAmount - (resurrectedCount / resurrectFactor)); + + auto rangeGen = rand.getInt64Range(0, 99); + + for(int32_t i = 0; i < resurrectedAdd; i++) + { + if(resurrectValue > rangeGen()) + resurrectedCount += 1; + } + + if(customState->hasBonusOfType(BonusType::REBIRTH, BonusCustomSubtype::rebirthSpecial)) + { + // resurrect at least one Sacred Phoenix + vstd::amax(resurrectedCount, 1); + } + + if(resurrectedCount > 0) + { + customState->casts.use(); + bsa.flags |= BattleStackAttacked::REBIRTH; + int64_t toHeal = customState->getMaxHealth() * resurrectedCount; + //TODO: add one-battle rebirth? + customState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + customState->counterAttacks.use(customState->counterAttacks.available()); + } + } + } + + customState->save(bsa.newState.data); + bsa.newState.healthDelta = -bsa.damageAmount; + bsa.newState.id = customState->unitId(); + bsa.newState.operation = UnitChanges::EOperation::RESET_STATE; +} + +std::vector CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) +{ + int mask = 0; + std::vector res; + + if (!attackerPos.isValid()) + attackerPos = attacker->getPosition(); + if (!defenderPos.isValid()) + defenderPos = defender->getPosition(); + + BattleHex otherAttackerPos = attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1); + BattleHex otherDefenderPos = defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1); + + if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front + { + if((mask & 1) == 0) + { + mask |= 1; + res.push_back(defenderPos); + } + } + if (attacker->doubleWide() //back <=> front + && BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0) + { + if((mask & 1) == 0) + { + mask |= 1; + res.push_back(defenderPos); + } + } + if (defender->doubleWide()//front <=> back + && BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0) + { + if((mask & 2) == 0) + { + mask |= 2; + res.push_back(otherDefenderPos); + } + } + if (defender->doubleWide() && attacker->doubleWide()//back <=> back + && BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0) + { + if((mask & 2) == 0) + { + mask |= 2; + res.push_back(otherDefenderPos); + } + } + + return res; +} + +bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) +{ + 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 +} + +bool CStack::canBeHealed() const +{ + return getFirstHPleft() < static_cast(getMaxHealth()) && isValidTarget() && !hasBonusOfType(BonusType::SIEGE_WEAPON); +} + +bool CStack::isOnNativeTerrain() const +{ + //this code is called from CreatureTerrainLimiter::limit on battle start + auto res = nativeTerrain == ETerrainId::ANY_TERRAIN || nativeTerrain == battle->getTerrainType(); + return res; +} + +bool CStack::isOnTerrain(TerrainId terrain) const +{ + return battle->getTerrainType() == terrain; +} + +const CCreature * CStack::unitType() const +{ + return type; +} + +int32_t CStack::unitBaseAmount() const +{ + return baseAmount; +} + +const IBonusBearer* CStack::getBonusBearer() const +{ + return this; +} + +bool CStack::unitHasAmmoCart(const battle::Unit * unit) const +{ + for(const CStack * st : battle->stacks) + { + if(battle->battleMatchOwner(st, unit, true) && st->unitType()->getId() == CreatureID::AMMO_CART) + { + return st->alive(); + } + } + //ammo cart works during creature bank battle while not on battlefield + 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) + { + return true; + } + } + return false; //will be always false if trying to examine enemy hero in "special battle" +} + +PlayerColor CStack::unitEffectiveOwner(const battle::Unit * unit) const +{ + return battle->battleGetOwner(unit); +} + +uint32_t CStack::unitId() const +{ + return ID; +} + +ui8 CStack::unitSide() const +{ + return side; +} + +PlayerColor CStack::unitOwner() const +{ + return owner; +} + +SlotID CStack::unitSlot() const +{ + return slot; +} + +std::string CStack::getDescription() const +{ + return nodeName(); +} + +void CStack::spendMana(ServerCallback * server, const int spellCost) const +{ + if(spellCost != 1) + logGlobal->warn("Unexpected spell cost %d for creature", spellCost); + + BattleSetStackProperty ssp; + ssp.battleID = battle->battleID; + ssp.stackID = unitId(); + ssp.which = BattleSetStackProperty::CASTS; + ssp.val = -spellCost; + ssp.absolute = false; + server->apply(&ssp); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStack.h b/lib/CStack.h index a2f944a8d..78e8eb750 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -1,150 +1,150 @@ -/* - * CStack.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 "JsonNode.h" -#include "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "CCreatureHandler.h" //todo: remove -#include "battle/BattleHex.h" -#include "mapObjects/CGHeroInstance.h" // for commander serialization - -#include "battle/CUnitState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct BattleStackAttacked; -class BattleInfo; - -//Represents STACK_BATTLE nodes -class DLL_LINKAGE CStack : public CBonusSystemNode, public battle::CUnitState, public battle::IUnitEnvironment -{ -private: - ui32 ID = -1; //unique ID of stack - const CCreature * type = nullptr; - 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; - - SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) - -public: - const CStackInstance * base = nullptr; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) - - 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(); - ~CStack(); - - std::string nodeName() const override; - - void localInit(BattleInfo * battleInfo); - std::string getName() const; //plural or singular - - bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines - bool isOnNativeTerrain() const; - bool isOnTerrain(TerrainId terrain) const; - - ui32 level() const; - si32 magicResistance() const override; //include aura of resistance - std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast - const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise - - static std::vector meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); - static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); - - BattleHex::EDir destShiftDir() const; - - void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmout filled - static void prepareAttacked(BattleStackAttacked & bsa, - vstd::RNG & rand, - const std::shared_ptr & customState); //requires bsa.damageAmout filled - - const CCreature * unitType() const override; - int32_t unitBaseAmount() const override; - - uint32_t unitId() const override; - ui8 unitSide() const override; - PlayerColor unitOwner() const override; - SlotID unitSlot() const override; - - std::string getDescription() const override; - - bool unitHasAmmoCart(const battle::Unit * unit) const override; - PlayerColor unitEffectiveOwner(const battle::Unit * unit) const override; - - void spendMana(ServerCallback * server, const int spellCost) const override; - - const IBonusBearer* getBonusBearer() const override; - - PlayerColor getOwner() const override - { - return this->owner; - } - - template void serialize(Handler & h, const int version) - { - //this assumes that stack objects is newly created - //stackState is not serialized here - assert(isIndependentNode()); - h & static_cast(*this); - h & type; - h & ID; - h & baseAmount; - h & owner; - h & slot; - h & side; - h & initialPosition; - - const CArmedInstance * army = (base ? base->armyObj : nullptr); - SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID()); - - if(h.saving) - { - h & army; - h & extSlot; - } - else - { - h & army; - h & extSlot; - - if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - const auto * hero = dynamic_cast(army); - assert(hero); - base = hero->commander; - } - else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT) - { - //no external slot possible, so no base stack - base = nullptr; - } - else if(!army || extSlot == SlotID() || !army->hasStackAtSlot(extSlot)) - { - base = nullptr; - logGlobal->warn("%s doesn't have a base stack!", type->getNameSingularTranslated()); - } - else - { - base = &army->getStack(extSlot); - } - } - } - -private: - const BattleInfo * battle; //do not serialize -}; - -VCMI_LIB_NAMESPACE_END +/* + * CStack.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 "JsonNode.h" +#include "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "CCreatureHandler.h" //todo: remove +#include "battle/BattleHex.h" +#include "mapObjects/CGHeroInstance.h" // for commander serialization + +#include "battle/CUnitState.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleStackAttacked; +class BattleInfo; + +//Represents STACK_BATTLE nodes +class DLL_LINKAGE CStack : public CBonusSystemNode, public battle::CUnitState, public battle::IUnitEnvironment +{ +private: + ui32 ID = -1; //unique ID of stack + const CCreature * type = nullptr; + 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; + + SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) + +public: + const CStackInstance * base = nullptr; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) + + 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(); + ~CStack(); + + std::string nodeName() const override; + + void localInit(BattleInfo * battleInfo); + std::string getName() const; //plural or singular + + bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines + bool isOnNativeTerrain() const; + bool isOnTerrain(TerrainId terrain) const; + + ui32 level() const; + si32 magicResistance() const override; //include aura of resistance + std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast + const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise + + static std::vector meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); + static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); + + BattleHex::EDir destShiftDir() const; + + void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmout filled + static void prepareAttacked(BattleStackAttacked & bsa, + vstd::RNG & rand, + const std::shared_ptr & customState); //requires bsa.damageAmout filled + + const CCreature * unitType() const override; + int32_t unitBaseAmount() const override; + + uint32_t unitId() const override; + ui8 unitSide() const override; + PlayerColor unitOwner() const override; + SlotID unitSlot() const override; + + std::string getDescription() const override; + + bool unitHasAmmoCart(const battle::Unit * unit) const override; + PlayerColor unitEffectiveOwner(const battle::Unit * unit) const override; + + void spendMana(ServerCallback * server, const int spellCost) const override; + + const IBonusBearer* getBonusBearer() const override; + + PlayerColor getOwner() const override + { + return this->owner; + } + + template void serialize(Handler & h, const int version) + { + //this assumes that stack objects is newly created + //stackState is not serialized here + assert(isIndependentNode()); + h & static_cast(*this); + h & type; + h & ID; + h & baseAmount; + h & owner; + h & slot; + h & side; + h & initialPosition; + + const CArmedInstance * army = (base ? base->armyObj : nullptr); + SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID()); + + if(h.saving) + { + h & army; + h & extSlot; + } + else + { + h & army; + h & extSlot; + + if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + const auto * hero = dynamic_cast(army); + assert(hero); + base = hero->commander; + } + else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT) + { + //no external slot possible, so no base stack + base = nullptr; + } + else if(!army || extSlot == SlotID() || !army->hasStackAtSlot(extSlot)) + { + base = nullptr; + logGlobal->warn("%s doesn't have a base stack!", type->getNameSingularTranslated()); + } + else + { + base = &army->getStack(extSlot); + } + } + } + +private: + const BattleInfo * battle; //do not serialize +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStopWatch.h b/lib/CStopWatch.h index 46e3cb65c..08167eb3f 100644 --- a/lib/CStopWatch.h +++ b/lib/CStopWatch.h @@ -1,68 +1,68 @@ -/* - * CStopWatch.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 - -#ifdef __FreeBSD__ - #include - #include - #include - #define TO_MS_DIVISOR (1000) -#else - #include - #define TO_MS_DIVISOR (CLOCKS_PER_SEC / 1000) -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class CStopWatch -{ - si64 start, last, mem; - -public: - CStopWatch() - : start(clock()) - { - last=clock(); - mem=0; - } - - si64 getDiff() //get diff in milliseconds - { - si64 ret = clock() - last; - last = clock(); - return ret / TO_MS_DIVISOR; - } - void update() - { - last=clock(); - } - void remember() - { - mem=clock(); - } - si64 memDif() - { - return (clock()-mem) / TO_MS_DIVISOR; - } - -private: - si64 clock() - { - #ifdef __FreeBSD__ // TODO: enable also for Apple? - struct rusage usage; - getrusage(RUSAGE_SELF, &usage); - return static_cast(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; - #else - return std::clock(); - #endif - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CStopWatch.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 + +#ifdef __FreeBSD__ + #include + #include + #include + #define TO_MS_DIVISOR (1000) +#else + #include + #define TO_MS_DIVISOR (CLOCKS_PER_SEC / 1000) +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +class CStopWatch +{ + si64 start, last, mem; + +public: + CStopWatch() + : start(clock()) + { + last=clock(); + mem=0; + } + + si64 getDiff() //get diff in milliseconds + { + si64 ret = clock() - last; + last = clock(); + return ret / TO_MS_DIVISOR; + } + void update() + { + last=clock(); + } + void remember() + { + mem=clock(); + } + si64 memDif() + { + return (clock()-mem) / TO_MS_DIVISOR; + } + +private: + si64 clock() + { + #ifdef __FreeBSD__ // TODO: enable also for Apple? + struct rusage usage; + getrusage(RUSAGE_SELF, &usage); + return static_cast(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; + #else + return std::clock(); + #endif + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CThreadHelper.cpp b/lib/CThreadHelper.cpp index e98a42274..66f3bf7f9 100644 --- a/lib/CThreadHelper.cpp +++ b/lib/CThreadHelper.cpp @@ -1,99 +1,101 @@ -/* - * CThreadHelper.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 "CThreadHelper.h" - -#ifdef VCMI_WINDOWS - #include -#elif defined(VCMI_HAIKU) - #include -#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && !defined(VCMI_HURD) - #include -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -CThreadHelper::CThreadHelper(std::vector> * Tasks, int Threads): - currentTask(0), - amount(static_cast(Tasks->size())), - tasks(Tasks), - threads(Threads) -{ -} -void CThreadHelper::run() -{ - boost::thread_group grupa; - for(int i=0;i lock(rtinm); - if((pom = currentTask) >= amount) - break; - else - ++currentTask; - } - (*tasks)[pom](); - } -} - -// set name for this thread. -// NOTE: on *nix string will be trimmed to 16 symbols -void setThreadName(const std::string &name) -{ -#ifdef VCMI_WINDOWS -#ifndef __GNUC__ - //follows http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx - const DWORD MS_VC_EXCEPTION=0x406D1388; -#pragma pack(push,8) - typedef struct tagTHREADNAME_INFO - { - DWORD dwType; // Must be 0x1000. - LPCSTR szName; // Pointer to name (in user addr space). - DWORD dwThreadID; // Thread ID (-1=caller thread). - DWORD dwFlags; // Reserved for future use, must be zero. - } THREADNAME_INFO; -#pragma pack(pop) - THREADNAME_INFO info; - info.dwType = 0x1000; - info.szName = name.c_str(); - info.dwThreadID = -1; - info.dwFlags = 0; - - - __try - { - RaiseException( MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info ); - } - __except(EXCEPTION_EXECUTE_HANDLER) - { - } -#else -//not supported -#endif - -#elif defined(__linux__) - prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); -#elif defined(VCMI_APPLE) - pthread_setname_np(name.c_str()); -#elif defined(VCMI_HAIKU) - rename_thread(find_thread(NULL), name.c_str()); -#endif -} - -VCMI_LIB_NAMESPACE_END +/* + * CThreadHelper.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 "CThreadHelper.h" + +#ifdef VCMI_WINDOWS + #include +#elif defined(VCMI_HAIKU) + #include +#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && !defined(VCMI_HURD) + #include +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +CThreadHelper::CThreadHelper(std::vector> * Tasks, int Threads): + currentTask(0), + amount(static_cast(Tasks->size())), + tasks(Tasks), + threads(Threads) +{ +} +void CThreadHelper::run() +{ + std::vector group; + for(int i=0;i lock(rtinm); + if((pom = currentTask) >= amount) + break; + else + ++currentTask; + } + (*tasks)[pom](); + } +} + +// set name for this thread. +// NOTE: on *nix string will be trimmed to 16 symbols +void setThreadName(const std::string &name) +{ +#ifdef VCMI_WINDOWS +#ifndef __GNUC__ + //follows http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx + const DWORD MS_VC_EXCEPTION=0x406D1388; +#pragma pack(push,8) + typedef struct tagTHREADNAME_INFO + { + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. + } THREADNAME_INFO; +#pragma pack(pop) + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = name.c_str(); + info.dwThreadID = -1; + info.dwFlags = 0; + + + __try + { + RaiseException( MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info ); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + } +#else +//not supported +#endif + +#elif defined(__linux__) + prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); +#elif defined(VCMI_APPLE) + pthread_setname_np(name.c_str()); +#elif defined(VCMI_HAIKU) + rename_thread(find_thread(NULL), name.c_str()); +#endif +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CThreadHelper.h b/lib/CThreadHelper.h index aaa3e67f3..86dcb7619 100644 --- a/lib/CThreadHelper.h +++ b/lib/CThreadHelper.h @@ -1,87 +1,87 @@ -/* - * CThreadHelper.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 - - -///DEPRECATED -/// Can assign CPU work to other threads/cores -class DLL_LINKAGE CThreadHelper -{ -public: - using Task = std::function; - CThreadHelper(std::vector > *Tasks, int Threads); - void run(); -private: - boost::mutex rtinm; - int currentTask, amount, threads; - std::vector *tasks; - - - void processTasks(); -}; - -template -class ThreadPool -{ -public: - using Task = std::function)>; - using Tasks = std::vector; - - ThreadPool(Tasks * tasks_, std::vector> context_) - : currentTask(0), - amount(tasks_->size()), - threads(context_.size()), - tasks(tasks_), - context(context_) - {} - - void run() - { - boost::thread_group grupa; - for(size_t i=0; i payload = context.at(i); - - grupa.create_thread(std::bind(&ThreadPool::processTasks, this, payload)); - } - - grupa.join_all(); - - //thread group deletes threads, do not free manually - } -private: - boost::mutex rtinm; - size_t currentTask, amount, threads; - Tasks * tasks; - std::vector> context; - - void processTasks(std::shared_ptr payload) - { - while(true) - { - size_t pom; - { - boost::unique_lock lock(rtinm); - if((pom = currentTask) >= amount) - break; - else - ++currentTask; - } - (*tasks)[pom](payload); - } - } -}; - - -void DLL_LINKAGE setThreadName(const std::string &name); - -VCMI_LIB_NAMESPACE_END +/* + * CThreadHelper.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 + + +///DEPRECATED +/// Can assign CPU work to other threads/cores +class DLL_LINKAGE CThreadHelper +{ +public: + using Task = std::function; + CThreadHelper(std::vector > *Tasks, int Threads); + void run(); +private: + boost::mutex rtinm; + int currentTask, amount, threads; + std::vector *tasks; + + + void processTasks(); +}; + +template +class ThreadPool +{ +public: + using Task = std::function)>; + using Tasks = std::vector; + + ThreadPool(Tasks * tasks_, std::vector> context_) + : currentTask(0), + amount(tasks_->size()), + threads(context_.size()), + tasks(tasks_), + context(context_) + {} + + void run() + { + std::vector group; + for(size_t i=0; i payload = context.at(i); + group.emplace_back(std::bind(&ThreadPool::processTasks, this, payload)); + } + + for (auto & thread : group) + thread.join(); + + //thread group deletes threads, do not free manually + } +private: + boost::mutex rtinm; + size_t currentTask, amount, threads; + Tasks * tasks; + std::vector> context; + + void processTasks(std::shared_ptr payload) + { + while(true) + { + size_t pom; + { + boost::unique_lock lock(rtinm); + if((pom = currentTask) >= amount) + break; + else + ++currentTask; + } + (*tasks)[pom](payload); + } + } +}; + + +void DLL_LINKAGE setThreadName(const std::string &name); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index ee47103db..153125538 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1,1275 +1,1293 @@ -/* - * 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 "CTownHandler.h" - -#include "VCMI_Lib.h" -#include "CGeneralTextHandler.h" -#include "JsonNode.h" -#include "StringConstants.h" -#include "CCreatureHandler.h" -#include "CModHandler.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 "ResourceSet.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.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 } -}; - -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 >= 0) - { - 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 >= 0 && 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; -} - -int32_t CFaction::getIconIndex() const -{ - return index; //??? -} - -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, 1, "CPRSMALL", info.towerIconSmall); - cb(index, 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(); -} - -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; -} - -EBoatId CFaction::getBoatType() const -{ - return boatType.toEnum(); -} - -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::getRandomNameTranslated(size_t index) const -{ - return VLC->generaltexth->translate(getRandomNameTextID(index)); -} - -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::EBuildingID 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()) -{ - randomFaction->town = randomTown; - randomTown->faction = randomFaction; - randomFaction->identifier = "random"; - randomFaction->modScope = "core"; -} - -CTownHandler::~CTownHandler() -{ - delete randomTown; -} - -JsonNode readBuilding(CLegacyConfigParser & parser) -{ - JsonNode ret; - JsonNode & cost = ret["cost"]; - - //note: this code will try to parse mithril as well but wil always return 0 for it - for(const std::string & resID : GameConstants::RESOURCE_NAMES) - cost[resID].Float() = parser.readNumber(); - - cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator - - parser.endLine(); - - return ret; -} - -TPropagatorPtr & CTownHandler::emptyPropagator() -{ - static TPropagatorPtr emptyProp(nullptr); - return emptyProp; -} - -std::vector CTownHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION); - - std::vector dest(dataSize); - objects.resize(dataSize); - - auto getBuild = [&](size_t town, size_t building) -> JsonNode & - { - return dest[town]["town"]["buildings"][EBuildingType::names[building]]; - }; - - CLegacyConfigParser parser("DATA/BUILDING.TXT"); - - parser.endLine(); // header - parser.endLine(); - - //Unique buildings - for (size_t town=0; town & bidsToLoad) const -{ - if (source.isNull()) - return; - - BuildingRequirementsHelper hlp; - hlp.building = building; - hlp.town = building->town; - hlp.json = source; - 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 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, PrimarySkill::SPELL_POWER); - break; - case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, PrimarySkill::ATTACK); - break; - case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, PrimarySkill::DEFENSE); - break; - case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, playerPropagator, 0); - break; - } - - if(b) - building->addNewBonus(b, building->buildingBonuses); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, int subtype) const -{ - return createBonus(build, type, val, emptyPropagator(), subtype); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype) const -{ - std::ostringstream descr; - descr << build->getNameTranslated(); - return createBonusImpl(build->bid, type, val, prop, descr.str(), subtype); -} - -std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building, - BonusType type, - int val, - TPropagatorPtr & prop, - const std::string & description, - int subtype) const -{ - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, description, subtype); - - if(prop) - b->addPropagator(prop); - - return b; -} - -void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) -{ - for(const auto & b : source.Vector()) - { - auto bonus = JsonUtils::parseBuildingBonus(b, building->bid, building->getNameTranslated()); - - if(bonus == nullptr) - continue; - - bonus->sid = Bonus::getSid32(building->town->faction->getIndex(), building->bid); - //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. - if(bonus->propagator != nullptr - && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) - bonus->addPropagator(emptyPropagator()); - building->addNewBonus(bonus, bonusList); - } -} - -void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source) -{ - assert(stringID.find(':') == std::string::npos); - assert(!source.meta.empty()); - - auto * ret = new CBuilding(); - ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); - ret->subId = BuildingSubID::NONE; - - if(ret->bid == BuildingID::NONE && !source["id"].isNull()) - { - // FIXME: A lot of false-positives with no clear way to handle them in mods - //logMod->warn("Building %s: id field is deprecated", stringID); - ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float()); - } - - if (ret->bid == BuildingID::NONE) - logMod->error("Building '%s' isn't recognized and won't work properly. Correct the typo or update VCMI.", stringID); - - ret->mode = ret->bid == BuildingID::GRAIL - ? CBuilding::BUILD_GRAIL - : getMappedValue(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); - - ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); - - ret->identifier = stringID; - ret->modScope = source.meta; - ret->town = town; - - VLC->generaltexth->registerString(source.meta, ret->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(source.meta, ret->getDescriptionTextID(), source["description"].String()); - - ret->resources = TResources(source["cost"]); - ret->produce = TResources(source["produce"]); - - if(ret->bid == BuildingID::TAVERN) - addBonusesForVanilaBuilding(ret); - else if(ret->bid.IsSpecialOrGrail()) - { - loadSpecialBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); - - if(ret->buildingBonuses.empty()) - { - ret->subId = getMappedValue(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); - addBonusesForVanilaBuilding(ret); - } - - loadSpecialBuildingBonuses(source["onVisitBonuses"], ret->onVisitBonuses, ret); - - if(!ret->onVisitBonuses.empty()) - { - if(ret->subId == BuildingSubID::NONE) - ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; - - for(auto & bonus : ret->onVisitBonuses) - bonus->sid = Bonus::getSid32(ret->town->faction->getIndex(), ret->bid); - } - - if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) - { - ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD; - ret->rewardableObjectInfo.init(source, ret->getBaseTextID()); - } - } - //MODS COMPATIBILITY FOR 0.96 - if(!ret->produce.nonZero()) - { - switch (ret->bid) { - 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; - } - } - } - } - loadBuildingRequirements(ret, source["requires"], requirementsToLoad); - - if(ret->bid.IsSpecialOrGrail()) - loadBuildingRequirements(ret, source["overrides"], overriddenBidsToLoad); - - if (!source["upgrades"].isNull()) - { - // building id and upgrades can't be the same - if(stringID == source["upgrades"].String()) - { - throw std::runtime_error(boost::str(boost::format("Building with ID '%s' of town '%s' can't be an upgrade of the same building.") % - stringID % ret->town->faction->getNameTranslated())); - } - - VLC->modh->identifiers.requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) - { - ret->upgrade = BuildingID(identifier); - }); - } - else - ret->upgrade = BuildingID::NONE; - - ret->town->buildings[ret->bid] = ret; - - registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid); -} - -void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) -{ - if(source.isStruct()) - { - for(const auto & node : source.Struct()) - { - if (!node.second.isNull()) - loadBuilding(town, node.first, node.second); - } - } -} - -void CTownHandler::loadStructure(CTown &town, const std::string & stringID, const JsonNode & source) const -{ - auto * ret = new CStructure(); - - ret->building = nullptr; - ret->buildable = nullptr; - - VLC->modh->identifiers.tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable - { - ret->building = town.buildings[BuildingID(identifier)]; - }); - - if (source["builds"].isNull()) - { - VLC->modh->identifiers.tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable - { - ret->building = town.buildings[BuildingID(identifier)]; - }); - } - else - { - VLC->modh->identifiers.requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable - { - ret->buildable = town.buildings[BuildingID(identifier)]; - }); - } - - ret->identifier = stringID; - ret->pos.x = static_cast(source["x"].Float()); - ret->pos.y = static_cast(source["y"].Float()); - ret->pos.z = static_cast(source["z"].Float()); - - ret->hiddenUpgrade = source["hidden"].Bool(); - ret->defName = source["animation"].String(); - ret->borderName = source["border"].String(); - ret->areaName = source["area"].String(); - - town.clientInfo.structures.emplace_back(ret); -} - -void CTownHandler::loadStructures(CTown &town, const JsonNode & source) const -{ - for(const auto & node : source.Struct()) - { - if (!node.second.isNull()) - loadStructure(town, node.first, node.second); - } -} - -void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const -{ - auto & dstSlots = town.clientInfo.hallSlots; - const auto & srcSlots = source.Vector(); - dstSlots.resize(srcSlots.size()); - - for(size_t i=0; imodh->identifiers.requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) - { - dst = BuildingID(identifier); - }); - } - } - } -} - -Point JsonToPoint(const JsonNode & node) -{ - if(!node.isStruct()) - return Point::makeInvalid(); - - Point ret; - ret.x = static_cast(node["x"].Float()); - ret.y = static_cast(node["y"].Float()); - return ret; -} - -void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const -{ - town.clientInfo.siegePrefix = source["imagePrefix"].String(); - town.clientInfo.towerIconSmall = source["towerIconSmall"].String(); - town.clientInfo.towerIconLarge = source["towerIconLarge"].String(); - - VLC->modh->identifiers.requestIdentifier("creature", source["shooter"], [&town](si32 creature) - { - auto crId = CreatureID(creature); - if((*VLC->creh)[crId]->animation.missleFrameAngles.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; - }); - - auto & pos = town.clientInfo.siegePositions; - pos.resize(21); - - pos[8] = JsonToPoint(source["towers"]["top"]["tower"]); - pos[17] = JsonToPoint(source["towers"]["top"]["battlement"]); - pos[20] = JsonToPoint(source["towers"]["top"]["creature"]); - - pos[2] = JsonToPoint(source["towers"]["keep"]["tower"]); - pos[15] = JsonToPoint(source["towers"]["keep"]["battlement"]); - pos[18] = JsonToPoint(source["towers"]["keep"]["creature"]); - - pos[3] = JsonToPoint(source["towers"]["bottom"]["tower"]); - pos[16] = JsonToPoint(source["towers"]["bottom"]["battlement"]); - pos[19] = JsonToPoint(source["towers"]["bottom"]["creature"]); - - pos[9] = JsonToPoint(source["gate"]["gate"]); - pos[10] = JsonToPoint(source["gate"]["arch"]); - - pos[7] = JsonToPoint(source["walls"]["upper"]); - pos[6] = JsonToPoint(source["walls"]["upperMid"]); - pos[5] = JsonToPoint(source["walls"]["bottomMid"]); - pos[4] = JsonToPoint(source["walls"]["bottom"]); - - pos[13] = JsonToPoint(source["moat"]["moat"]); - pos[14] = JsonToPoint(source["moat"]["bank"]); - - pos[11] = JsonToPoint(source["static"]["bottom"]); - pos[12] = JsonToPoint(source["static"]["top"]); - pos[1] = JsonToPoint(source["static"]["background"]); -} - -static void readIcon(JsonNode source, std::string & small, std::string & large) -{ - if (source.getType() == JsonNode::JsonType::DATA_STRUCT) // don't crash on old format - { - small = source["small"].String(); - large = source["large"].String(); - } -} - -void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const -{ - CTown::ClientInfo & info = town.clientInfo; - - readIcon(source["icons"]["village"]["normal"], info.iconSmall[0][0], info.iconLarge[0][0]); - readIcon(source["icons"]["village"]["built"], info.iconSmall[0][1], info.iconLarge[0][1]); - 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]); - - info.hallBackground = source["hallBackground"].String(); - info.musicTheme = source["musicTheme"].String(); - info.townBackground = source["townBackground"].String(); - info.guildWindow = source["guildWindow"].String(); - info.buildingsIcons = source["buildingsIcons"].String(); - - //left for back compatibility - will be removed later - if(!source["guildBackground"].String().empty()) - info.guildBackground = source["guildBackground"].String(); - else - info.guildBackground = "TPMAGE.bmp"; - if(!source["tavernVideo"].String().empty()) - info.tavernVideo = source["tavernVideo"].String(); - else - info.tavernVideo = "TAVERN.BIK"; - //end of legacy assignment - - loadTownHall(town, source["hallSlots"]); - loadStructures(town, source["structures"]); - loadSiegeScreen(town, source["siege"]); -} - -void CTownHandler::loadTown(CTown * town, const JsonNode & source) -{ - const auto * resIter = boost::find(GameConstants::RESOURCE_NAMES, source["primaryResource"].String()); - if(resIter == std::end(GameConstants::RESOURCE_NAMES)) - town->primaryRes = GameResID(EGameResID::WOOD_AND_ORE); //Wood + Ore - else - town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES)); - - warMachinesToLoad[town] = source["warMachine"]; - - 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()); - town->namesCount += 1; - } - - if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code - { - VLC->modh->identifiers.requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) - { - town->moatAbility = SpellID(ability); - }); - } - else - { - VLC->modh->identifiers.requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) - { - town->moatAbility = SpellID(ability); - }); - } - - // Horde building creature level - for(const JsonNode &node : source["horde"].Vector()) - town->hordeLvl[static_cast(town->hordeLvl.size())] = static_cast(node.Float()); - - // town needs to have exactly 2 horde entries. Validation will take care of 2+ entries - // but anything below 2 must be handled here - for (size_t i=source["horde"].Vector().size(); i<2; i++) - town->hordeLvl[static_cast(i)] = -1; - - const JsonVector & creatures = source["creatures"].Vector(); - - town->creatures.resize(creatures.size()); - - for (size_t i=0; i< creatures.size(); i++) - { - const JsonVector & level = creatures[i].Vector(); - - town->creatures[i].resize(level.size()); - - for (size_t j=0; jmodh->identifiers.requestIdentifier("creature", level[j], [=](si32 creature) - { - town->creatures[i][j] = CreatureID(creature); - }); - } - } - - town->defaultTavernChance = static_cast(source["defaultTavern"].Float()); - /// set chance of specific hero class to appear in this town - for(const auto & node : source["tavern"].Struct()) - { - int chance = static_cast(node.second.Float()); - - VLC->modh->identifiers.requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) - { - VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; - }); - } - - for(const auto & node : source["guildSpells"].Struct()) - { - int chance = static_cast(node.second.Float()); - - VLC->modh->identifiers.requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) - { - VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; - }); - } - - for(const JsonNode & d : source["adventureMap"]["dwellings"].Vector()) - { - town->dwellings.push_back(d["graphics"].String()); - town->dwellingNames.push_back(d["name"].String()); - } - - loadBuildings(town, source["buildings"]); - loadClientData(*town, source); -} - -void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const -{ - faction.puzzleMap.reserve(GameConstants::PUZZLE_MAP_PIECES); - - std::string prefix = source["prefix"].String(); - for(const JsonNode &piece : source["pieces"].Vector()) - { - size_t index = faction.puzzleMap.size(); - SPuzzleInfo spi; - - spi.x = static_cast(piece["x"].Float()); - spi.y = static_cast(piece["y"].Float()); - spi.whenUncovered = static_cast(piece["index"].Float()); - spi.number = static_cast(index); - - // filename calculation - std::ostringstream suffix; - suffix << std::setfill('0') << std::setw(2) << index; - - spi.filename = prefix + suffix.str(); - - faction.puzzleMap.push_back(spi); - } - 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) -{ - assert(identifier.find(':') == std::string::npos); - - auto * faction = new CFaction(); - - faction->index = static_cast(index); - faction->modScope = scope; - faction->identifier = identifier; - - VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); - - faction->creatureBg120 = source["creatureBackground"]["120px"].String(); - faction->creatureBg130 = source["creatureBackground"]["130px"].String(); - - faction->boatType = EBoatId::CASTLE; //Do not crash - if (!source["boat"].isNull()) - { - VLC->modh->identifiers.requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) - { - faction->boatType = BoatId(boatTypeID); - }); - } - - int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String()); - if (alignment == -1) - faction->alignment = EAlignment::NEUTRAL; - else - faction->alignment = static_cast(alignment); - - auto preferUndergound = source["preferUndergroundPlacement"]; - faction->preferUndergroundPlacement = preferUndergound.isNull() ? false : preferUndergound.Bool(); - - // NOTE: semi-workaround - normally, towns are supposed to have native terrains. - // Towns without one are exceptions. So, vcmi requires nativeTerrain to be defined - // But allows it to be defined with explicit value of "none" if town should not have native terrain - // This is better than allowing such terrain-less towns silently, leading to issues with RMG - faction->nativeTerrain = ETerrainId::NONE; - if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none") - { - VLC->modh->identifiers.requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ - faction->nativeTerrain = TerrainId(index); - - auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain); - - if (!terrain->isSurface() && !terrain->isUnderground()) - logMod->warn("Faction %s has terrain %s as native, but terrain is not suitable for either surface or subterranean layers!", faction->getJsonKey(), terrain->getJsonKey()); - }); - } - - if (!source["town"].isNull()) - { - faction->town = new CTown(); - faction->town->faction = faction; - loadTown(faction->town, source["town"]); - } - else - faction->town = nullptr; - - if (!source["puzzleMap"].isNull()) - loadPuzzle(*faction, source["puzzleMap"]); - - return faction; -} - -void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) -{ - auto * object = loadFromJson(scope, data, name, objects.size()); - - objects.emplace_back(object); - - if (object->town) - { - auto & info = object->town->clientInfo; - info.icons[0][0] = 8 + object->index * 4 + 0; - info.icons[0][1] = 8 + object->index * 4 + 1; - info.icons[1][0] = 8 + object->index * 4 + 2; - info.icons[1][1] = 8 + object->index * 4 + 3; - - VLC->modh->identifiers.requestIdentifier(scope, "object", "town", [=](si32 index) - { - // register town once objects are loaded - JsonNode config = data["town"]["mapObject"]; - config["faction"].String() = name; - config["faction"].meta = scope; - if (config.meta.empty())// MODS COMPATIBILITY FOR 0.96 - config.meta = scope; - VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); - - // MODS COMPATIBILITY FOR 0.96 - const auto & advMap = data["town"]["adventureMap"]; - if (!advMap.isNull()) - { - logMod->warn("Outdated town mod. Will try to generate valid templates out of fort"); - JsonNode config; - config["animation"] = advMap["castle"]; - VLC->objtypeh->getHandlerFor(index, object->index)->addTemplate(config); - } - }); - } - - registerObject(scope, "faction", name, object->index); -} - -void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto * object = loadFromJson(scope, data, name, index); - - if (objects.size() > index) - assert(objects[index] == nullptr); // ensure that this id was not loaded before - else - objects.resize(index + 1); - objects[index] = object; - - if (object->town) - { - auto & info = object->town->clientInfo; - info.icons[0][0] = (GameConstants::F_NUMBER + object->index) * 2 + 0; - info.icons[0][1] = (GameConstants::F_NUMBER + object->index) * 2 + 1; - info.icons[1][0] = object->index * 2 + 0; - info.icons[1][1] = object->index * 2 + 1; - - VLC->modh->identifiers.requestIdentifier(scope, "object", "town", [=](si32 index) - { - // register town once objects are loaded - JsonNode config = data["town"]["mapObject"]; - config["faction"].String() = name; - config["faction"].meta = scope; - VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); - }); - } - - registerObject(scope, "faction", name, object->index); -} - -void CTownHandler::loadRandomFaction() -{ - static const ResourceID randomFactionPath("config/factions/random.json"); - - JsonNode randomFactionJson(randomFactionPath); - randomFactionJson.setMeta(CModHandler::scopeBuiltin(), true); - loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); -} - -void CTownHandler::loadCustom() -{ - loadRandomFaction(); -} - -void CTownHandler::afterLoadFinalization() -{ - initializeRequirements(); - initializeOverridden(); - initializeWarMachines(); -} - -void CTownHandler::initializeRequirements() -{ - // must be done separately after all ID's are known - for (auto & requirement : requirementsToLoad) - { - requirement.building->requirements = CBuilding::TRequired(requirement.json, [&](const JsonNode & node) -> BuildingID - { - if (node.Vector().size() > 1) - { - logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); - logMod->error("Entry contains: "); - logMod->error(node.toJson()); - } - - auto index = VLC->modh->identifiers.getIdentifier(requirement.town->getBuildingScope(), node[0]); - - if (!index.has_value()) - { - logMod->error("Unknown building in town buildings: %s", node[0].String()); - return BuildingID::NONE; - } - return BuildingID(index.value()); - }); - } - 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->modh->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->modh->identifiers.getIdentifier("creature", creatureKey, false); - - if(ret) - { - const CCreature * creature = CreatureID(*ret).toCreature(); - - t->warMachine = creature->warMachine; - } - } - - warMachinesToLoad.clear(); -} - -std::vector CTownHandler::getDefaultAllowed() const -{ - std::vector allowedFactions; - allowedFactions.reserve(objects.size()); - for(auto town : objects) - { - allowedFactions.push_back(town->town != nullptr); - } - return allowedFactions; -} - -std::set CTownHandler::getAllowedFactions(bool withTown) const -{ - std::set allowedFactions; - std::vector allowed; - if (withTown) - allowed = getDefaultAllowed(); - else - allowed.resize( objects.size(), true); - - for (size_t i=0; i(i)); - - return allowedFactions; -} - -const std::vector & CTownHandler::getTypeNames() const -{ - static const std::vector typeNames = { "faction", "town" }; - return typeNames; -} - - -VCMI_LIB_NAMESPACE_END +/* + * 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 "CTownHandler.h" + +#include "VCMI_Lib.h" +#include "CGeneralTextHandler.h" +#include "JsonNode.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 "ResourceSet.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.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(); +} + +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::getRandomNameTranslated(size_t index) const +{ + return VLC->generaltexth->translate(getRandomNameTextID(index)); +} + +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()) +{ + randomFaction->town = randomTown; + randomTown->faction = randomFaction; + randomFaction->identifier = "random"; + randomFaction->modScope = "core"; +} + +CTownHandler::~CTownHandler() +{ + delete randomFaction; // will also delete randomTown +} + +JsonNode readBuilding(CLegacyConfigParser & parser) +{ + JsonNode ret; + JsonNode & cost = ret["cost"]; + + //note: this code will try to parse mithril as well but wil always return 0 for it + for(const std::string & resID : GameConstants::RESOURCE_NAMES) + cost[resID].Float() = parser.readNumber(); + + cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator + + parser.endLine(); + + return ret; +} + +TPropagatorPtr & CTownHandler::emptyPropagator() +{ + static TPropagatorPtr emptyProp(nullptr); + return emptyProp; +} + +std::vector CTownHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION); + + std::vector dest(dataSize); + objects.resize(dataSize); + + auto getBuild = [&](size_t town, size_t building) -> JsonNode & + { + return dest[town]["town"]["buildings"][EBuildingType::names[building]]; + }; + + CLegacyConfigParser parser(TextPath::builtin("DATA/BUILDING.TXT")); + + parser.endLine(); // header + parser.endLine(); + + //Unique buildings + for (size_t town=0; town & bidsToLoad) const +{ + if (source.isNull()) + return; + + BuildingRequirementsHelper hlp; + hlp.building = building; + hlp.town = building->town; + hlp.json = source; + 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 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, TPropagatorPtr & prop) const +{ + std::ostringstream descr; + descr << build->getNameTranslated(); + return createBonusImpl(build->bid, build->town->faction->getId(), type, val, prop, descr.str(), subtype); +} + +std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building, + const FactionID & faction, + BonusType type, + int val, + TPropagatorPtr & prop, + const std::string & description, + BonusSubtypeID subtype) const +{ + auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, BuildingTypeUniqueID(faction, building), subtype, description); + + if(prop) + b->addPropagator(prop); + + return b; +} + +void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) +{ + for(const auto & b : source.Vector()) + { + auto bonus = JsonUtils::parseBuildingBonus(b, building->town->faction->getId(), building->bid, building->getNameTranslated()); + + if(bonus == nullptr) + continue; + + bonus->sid = BonusSourceID(building->getUniqueTypeID()); + //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. + if(bonus->propagator != nullptr + && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) + bonus->addPropagator(emptyPropagator()); + building->addNewBonus(bonus, bonusList); + } +} + +void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source) +{ + assert(stringID.find(':') == std::string::npos); + assert(!source.meta.empty()); + + auto * ret = new CBuilding(); + ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); + ret->subId = BuildingSubID::NONE; + + if(ret->bid == BuildingID::NONE && !source["id"].isNull()) + { + // FIXME: A lot of false-positives with no clear way to handle them in mods + //logMod->warn("Building %s: id field is deprecated", stringID); + ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float()); + } + + if (ret->bid == BuildingID::NONE) + logMod->error("Building '%s' isn't recognized and won't work properly. Correct the typo or update VCMI.", stringID); + + ret->mode = ret->bid == BuildingID::GRAIL + ? CBuilding::BUILD_GRAIL + : getMappedValue(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); + + ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); + + ret->identifier = stringID; + ret->modScope = source.meta; + ret->town = town; + + VLC->generaltexth->registerString(source.meta, ret->getNameTextID(), source["name"].String()); + VLC->generaltexth->registerString(source.meta, ret->getDescriptionTextID(), source["description"].String()); + + ret->resources = TResources(source["cost"]); + ret->produce = TResources(source["produce"]); + + if(ret->bid == BuildingID::TAVERN) + addBonusesForVanilaBuilding(ret); + else if(ret->bid.IsSpecialOrGrail()) + { + loadSpecialBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); + + if(ret->buildingBonuses.empty()) + { + ret->subId = getMappedValue(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); + addBonusesForVanilaBuilding(ret); + } + + loadSpecialBuildingBonuses(source["onVisitBonuses"], ret->onVisitBonuses, ret); + + if(!ret->onVisitBonuses.empty()) + { + if(ret->subId == BuildingSubID::NONE) + ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; + + for(auto & bonus : ret->onVisitBonuses) + bonus->sid = BonusSourceID(ret->getUniqueTypeID()); + } + + if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) + { + ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD; + ret->rewardableObjectInfo.init(source, ret->getBaseTextID()); + } + } + //MODS COMPATIBILITY FOR 0.96 + if(!ret->produce.nonZero()) + { + 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; + } + } + } + } + loadBuildingRequirements(ret, source["requires"], requirementsToLoad); + + if(ret->bid.IsSpecialOrGrail()) + loadBuildingRequirements(ret, source["overrides"], overriddenBidsToLoad); + + if (!source["upgrades"].isNull()) + { + // building id and upgrades can't be the same + if(stringID == source["upgrades"].String()) + { + throw std::runtime_error(boost::str(boost::format("Building with ID '%s' of town '%s' can't be an upgrade of the same building.") % + stringID % ret->town->faction->getNameTranslated())); + } + + VLC->identifiers()->requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) + { + ret->upgrade = BuildingID(identifier); + }); + } + else + ret->upgrade = BuildingID::NONE; + + ret->town->buildings[ret->bid] = ret; + + registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum()); +} + +void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) +{ + if(source.isStruct()) + { + for(const auto & node : source.Struct()) + { + if (!node.second.isNull()) + loadBuilding(town, node.first, node.second); + } + } +} + +void CTownHandler::loadStructure(CTown &town, const std::string & stringID, const JsonNode & source) const +{ + auto * ret = new CStructure(); + + ret->building = nullptr; + ret->buildable = nullptr; + + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + { + ret->building = town.buildings[BuildingID(identifier)]; + }); + + if (source["builds"].isNull()) + { + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + { + ret->building = town.buildings[BuildingID(identifier)]; + }); + } + else + { + VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable + { + ret->buildable = town.buildings[BuildingID(identifier)]; + }); + } + + ret->identifier = stringID; + ret->pos.x = static_cast(source["x"].Float()); + ret->pos.y = static_cast(source["y"].Float()); + ret->pos.z = static_cast(source["z"].Float()); + + ret->hiddenUpgrade = source["hidden"].Bool(); + ret->defName = AnimationPath::fromJson(source["animation"]); + ret->borderName = ImagePath::fromJson(source["border"]); + ret->areaName = ImagePath::fromJson(source["area"]); + + town.clientInfo.structures.emplace_back(ret); +} + +void CTownHandler::loadStructures(CTown &town, const JsonNode & source) const +{ + for(const auto & node : source.Struct()) + { + if (!node.second.isNull()) + loadStructure(town, node.first, node.second); + } +} + +void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const +{ + auto & dstSlots = town.clientInfo.hallSlots; + const auto & srcSlots = source.Vector(); + dstSlots.resize(srcSlots.size()); + + for(size_t i=0; iidentifiers()->requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) + { + dst = BuildingID(identifier); + }); + } + } + } +} + +Point JsonToPoint(const JsonNode & node) +{ + if(!node.isStruct()) + return Point::makeInvalid(); + + Point ret; + ret.x = static_cast(node["x"].Float()); + ret.y = static_cast(node["y"].Float()); + return ret; +} + +void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const +{ + town.clientInfo.siegePrefix = source["imagePrefix"].String(); + town.clientInfo.towerIconSmall = source["towerIconSmall"].String(); + town.clientInfo.towerIconLarge = source["towerIconLarge"].String(); + + VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature) + { + auto crId = CreatureID(creature); + if((*VLC->creh)[crId]->animation.missleFrameAngles.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; + }); + + auto & pos = town.clientInfo.siegePositions; + pos.resize(21); + + pos[8] = JsonToPoint(source["towers"]["top"]["tower"]); + pos[17] = JsonToPoint(source["towers"]["top"]["battlement"]); + pos[20] = JsonToPoint(source["towers"]["top"]["creature"]); + + pos[2] = JsonToPoint(source["towers"]["keep"]["tower"]); + pos[15] = JsonToPoint(source["towers"]["keep"]["battlement"]); + pos[18] = JsonToPoint(source["towers"]["keep"]["creature"]); + + pos[3] = JsonToPoint(source["towers"]["bottom"]["tower"]); + pos[16] = JsonToPoint(source["towers"]["bottom"]["battlement"]); + pos[19] = JsonToPoint(source["towers"]["bottom"]["creature"]); + + pos[9] = JsonToPoint(source["gate"]["gate"]); + pos[10] = JsonToPoint(source["gate"]["arch"]); + + pos[7] = JsonToPoint(source["walls"]["upper"]); + pos[6] = JsonToPoint(source["walls"]["upperMid"]); + pos[5] = JsonToPoint(source["walls"]["bottomMid"]); + pos[4] = JsonToPoint(source["walls"]["bottom"]); + + pos[13] = JsonToPoint(source["moat"]["moat"]); + pos[14] = JsonToPoint(source["moat"]["bank"]); + + pos[11] = JsonToPoint(source["static"]["bottom"]); + pos[12] = JsonToPoint(source["static"]["top"]); + pos[1] = JsonToPoint(source["static"]["background"]); +} + +static void readIcon(JsonNode source, std::string & small, std::string & large) +{ + if (source.getType() == JsonNode::JsonType::DATA_STRUCT) // don't crash on old format + { + small = source["small"].String(); + large = source["large"].String(); + } +} + +void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const +{ + CTown::ClientInfo & info = town.clientInfo; + + readIcon(source["icons"]["village"]["normal"], info.iconSmall[0][0], info.iconLarge[0][0]); + readIcon(source["icons"]["village"]["built"], info.iconSmall[0][1], info.iconLarge[0][1]); + 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]); + + 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"]); + + info.guildBackground = ImagePath::fromJson(source["guildBackground"]); + info.tavernVideo = VideoPath::fromJson(source["tavernVideo"]); + + loadTownHall(town, source["hallSlots"]); + loadStructures(town, source["structures"]); + loadSiegeScreen(town, source["siege"]); +} + +void CTownHandler::loadTown(CTown * town, const JsonNode & source) +{ + const auto * resIter = boost::find(GameConstants::RESOURCE_NAMES, source["primaryResource"].String()); + if(resIter == std::end(GameConstants::RESOURCE_NAMES)) + town->primaryRes = GameResID(EGameResID::WOOD_AND_ORE); //Wood + Ore + else + town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES)); + + warMachinesToLoad[town] = source["warMachine"]; + + 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()); + town->namesCount += 1; + } + + if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code + { + VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) + { + town->moatAbility = SpellID(ability); + }); + } + else + { + VLC->identifiers()->requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) + { + town->moatAbility = SpellID(ability); + }); + } + + // Horde building creature level + for(const JsonNode &node : source["horde"].Vector()) + town->hordeLvl[static_cast(town->hordeLvl.size())] = static_cast(node.Float()); + + // town needs to have exactly 2 horde entries. Validation will take care of 2+ entries + // but anything below 2 must be handled here + for (size_t i=source["horde"].Vector().size(); i<2; i++) + town->hordeLvl[static_cast(i)] = -1; + + const JsonVector & creatures = source["creatures"].Vector(); + + town->creatures.resize(creatures.size()); + + for (size_t i=0; i< creatures.size(); i++) + { + const JsonVector & level = creatures[i].Vector(); + + town->creatures[i].resize(level.size()); + + for (size_t j=0; jidentifiers()->requestIdentifier("creature", level[j], [=](si32 creature) + { + town->creatures[i][j] = CreatureID(creature); + }); + } + } + + town->defaultTavernChance = static_cast(source["defaultTavern"].Float()); + /// set chance of specific hero class to appear in this town + for(const auto & node : source["tavern"].Struct()) + { + int chance = static_cast(node.second.Float()); + + VLC->identifiers()->requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) + { + VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; + }); + } + + for(const auto & node : source["guildSpells"].Struct()) + { + int chance = static_cast(node.second.Float()); + + VLC->identifiers()->requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) + { + VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; + }); + } + + for(const JsonNode & d : source["adventureMap"]["dwellings"].Vector()) + { + town->dwellings.push_back(d["graphics"].String()); + town->dwellingNames.push_back(d["name"].String()); + } + + loadBuildings(town, source["buildings"]); + loadClientData(*town, source); +} + +void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const +{ + faction.puzzleMap.reserve(GameConstants::PUZZLE_MAP_PIECES); + + std::string prefix = source["prefix"].String(); + for(const JsonNode &piece : source["pieces"].Vector()) + { + size_t index = faction.puzzleMap.size(); + SPuzzleInfo spi; + + spi.x = static_cast(piece["x"].Float()); + spi.y = static_cast(piece["y"].Float()); + spi.whenUncovered = static_cast(piece["index"].Float()); + spi.number = static_cast(index); + + // filename calculation + std::ostringstream suffix; + suffix << std::setfill('0') << std::setw(2) << index; + + spi.filename = ImagePath::builtinTODO(prefix + suffix.str()); + + faction.puzzleMap.push_back(spi); + } + 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) +{ + assert(identifier.find(':') == std::string::npos); + + auto * faction = new CFaction(); + + faction->index = static_cast(index); + faction->modScope = scope; + faction->identifier = identifier; + + VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); + + faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]); + faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]); + + faction->boatType = BoatId::CASTLE; //Do not crash + if (!source["boat"].isNull()) + { + VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) + { + faction->boatType = BoatId(boatTypeID); + }); + } + + int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String()); + if (alignment == -1) + faction->alignment = EAlignment::NEUTRAL; + else + faction->alignment = static_cast(alignment); + + auto preferUndergound = source["preferUndergroundPlacement"]; + faction->preferUndergroundPlacement = preferUndergound.isNull() ? false : preferUndergound.Bool(); + + // NOTE: semi-workaround - normally, towns are supposed to have native terrains. + // Towns without one are exceptions. So, vcmi requires nativeTerrain to be defined + // But allows it to be defined with explicit value of "none" if town should not have native terrain + // This is better than allowing such terrain-less towns silently, leading to issues with RMG + faction->nativeTerrain = ETerrainId::NONE; + if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none") + { + VLC->identifiers()->requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ + faction->nativeTerrain = TerrainId(index); + + auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain); + + if (!terrain->isSurface() && !terrain->isUnderground()) + logMod->warn("Faction %s has terrain %s as native, but terrain is not suitable for either surface or subterranean layers!", faction->getJsonKey(), terrain->getJsonKey()); + }); + } + + if (!source["town"].isNull()) + { + faction->town = new CTown(); + faction->town->faction = faction; + loadTown(faction->town, source["town"]); + } + else + faction->town = nullptr; + + if (!source["puzzleMap"].isNull()) + loadPuzzle(*faction, source["puzzleMap"]); + + return faction; +} + +void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) +{ + auto * object = loadFromJson(scope, data, name, objects.size()); + + objects.emplace_back(object); + + if (object->town) + { + auto & info = object->town->clientInfo; + info.icons[0][0] = 8 + object->index.getNum() * 4 + 0; + info.icons[0][1] = 8 + object->index.getNum() * 4 + 1; + info.icons[1][0] = 8 + object->index.getNum() * 4 + 2; + info.icons[1][1] = 8 + object->index.getNum() * 4 + 3; + + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) + { + // register town once objects are loaded + JsonNode config = data["town"]["mapObject"]; + config["faction"].String() = name; + config["faction"].meta = scope; + if (config.meta.empty())// MODS COMPATIBILITY FOR 0.96 + config.meta = scope; + VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); + + // MODS COMPATIBILITY FOR 0.96 + const auto & advMap = data["town"]["adventureMap"]; + if (!advMap.isNull()) + { + logMod->warn("Outdated town mod. Will try to generate valid templates out of fort"); + JsonNode config; + config["animation"] = advMap["castle"]; + VLC->objtypeh->getHandlerFor(index, object->index)->addTemplate(config); + } + }); + } + + registerObject(scope, "faction", name, object->index.getNum()); +} + +void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto * object = loadFromJson(scope, data, name, index); + + if (objects.size() > index) + assert(objects[index] == nullptr); // ensure that this id was not loaded before + else + objects.resize(index + 1); + objects[index] = object; + + if (object->town) + { + auto & info = object->town->clientInfo; + info.icons[0][0] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 0; + info.icons[0][1] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 1; + info.icons[1][0] = object->index.getNum() * 2 + 0; + info.icons[1][1] = object->index.getNum() * 2 + 1; + + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) + { + // register town once objects are loaded + JsonNode config = data["town"]["mapObject"]; + config["faction"].String() = name; + config["faction"].meta = scope; + VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); + }); + } + + registerObject(scope, "faction", name, object->index.getNum()); +} + +void CTownHandler::loadRandomFaction() +{ + JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json")); + randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); + loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); +} + +void CTownHandler::loadCustom() +{ + loadRandomFaction(); +} + +void CTownHandler::afterLoadFinalization() +{ + initializeRequirements(); + initializeOverridden(); + initializeWarMachines(); +} + +void CTownHandler::initializeRequirements() +{ + // must be done separately after all ID's are known + for (auto & requirement : requirementsToLoad) + { + requirement.building->requirements = CBuilding::TRequired(requirement.json, [&](const JsonNode & node) -> BuildingID + { + if (node.Vector().size() > 1) + { + logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); + logMod->error("Entry contains: "); + logMod->error(node.toJson()); + } + + auto index = VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node[0]); + + if (!index.has_value()) + { + logMod->error("Unknown building in town buildings: %s", node[0].String()); + return BuildingID::NONE; + } + return BuildingID(index.value()); + }); + } + 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) + if (town->town != nullptr) + allowedFactions.insert(town->getId()); + + return allowedFactions; +} + +std::set CTownHandler::getAllowedFactions(bool withTown) const +{ + if (withTown) + return getDefaultAllowed(); + + std::set result; + for(auto town : objects) + result.insert(town->getId()); + + return result; + +} + +const std::vector & CTownHandler::getTypeNames() const +{ + static const std::vector typeNames = { "faction", "town" }; + return typeNames; +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 66aec6196..5a8421b35 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -1,455 +1,363 @@ -/* - * 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" - -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) {}; - - 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; - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & town; - h & bid; - h & resources; - h & produce; - h & requirements; - h & upgrade; - h & mode; - h & subId; - h & height; - h & overrideBids; - h & buildingBonuses; - h & onVisitBonuses; - h & rewardableObjectInfo; - } - - 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; - std::string defName, borderName, areaName, 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) - template void serialize(Handler &h, const int version) - { - h & pos; - h & defName; - h & borderName; - h & areaName; - h & identifier; - h & building; - h & buildable; - h & hiddenUpgrade; - } -}; - -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) - std::string filename; //file with graphic of this puzzle - - template void serialize(Handler &h, const int version) - { - h & number; - h & x; - h & y; - h & whenUncovered; - h & filename; - } -}; - -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; - - /// 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(EBoatId::CASTLE); - - - CTown * town = nullptr; //NOTE: can be null - - std::string creatureBg120; - std::string 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; - - bool hasTown() const override; - TerrainId getNativeTerrain() const override; - EAlignment getAlignment() const override; - EBoatId getBoatType() const override; - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & index; - h & nativeTerrain; - h & boatType; - h & alignment; - h & town; - h & creatureBg120; - h & creatureBg130; - h & puzzleMap; - } -}; - -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::EBuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; - - std::string getRandomNameTranslated(size_t index) 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]; - std::string tavernVideo; - std::string musicTheme; - std::string townBackground; - std::string guildBackground; - std::string guildWindow; - std::string buildingsIcons; - std::string 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; - - template void serialize(Handler &h, const int version) - { - h & icons; - h & iconSmall; - h & iconLarge; - h & tavernVideo; - h & musicTheme; - h & townBackground; - h & guildBackground; - h & guildWindow; - h & buildingsIcons; - h & hallBackground; - h & hallSlots; - h & structures; - h & siegePrefix; - h & siegePositions; - h & siegeShooter; - h & towerIconSmall; - h & towerIconLarge; - } - } clientInfo; - - template void serialize(Handler &h, const int version) - { - h & namesCount; - h & faction; - h & creatures; - h & dwellings; - h & dwellingNames; - h & buildings; - h & hordeLvl; - h & mageLevel; - h & primaryRes; - h & warMachine; - h & clientInfo; - h & moatAbility; - h & defaultTavernChance; - } - -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 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, int subtype = -1) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype = -1) const; - std::shared_ptr createBonusImpl(const BuildingID & building, - BonusType type, - int val, - TPropagatorPtr & prop, - const std::string & description, - int subtype = -1) 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::vector getDefaultAllowed() const override; - std::set getAllowedFactions(bool withTown = true) const; - - static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); - - template void serialize(Handler &h, const int version) - { - h & objects; - h & randomTown; - } - -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 +/* + * 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; + + /// 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; + + 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 getRandomNameTranslated(size_t index) 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 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, TPropagatorPtr & prop) const; + std::shared_ptr createBonusImpl(const BuildingID & building, + const FactionID & faction, + BonusType type, + int val, + TPropagatorPtr & prop, + const std::string & description, + BonusSubtypeID subtype) 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 e6558e9f3..2c231f2b1 100644 --- a/lib/Color.h +++ b/lib/Color.h @@ -1,62 +1,62 @@ -/* - * Color.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 - -/// An object that represents RGBA color -class ColorRGBA -{ -public: - enum : uint8_t - { - ALPHA_OPAQUE = 255, - ALPHA_TRANSPARENT = 0, - }; - - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; - - //constructors - constexpr ColorRGBA() - :r(0) - ,g(0) - ,b(0) - ,a(0) - { - } - - constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) - : r(r) - , g(g) - , b(b) - , a(a) - {} - - constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b) - : r(r) - , g(g) - , b(b) - , a(ALPHA_OPAQUE) - {} - - template - void serialize(Handler &h, const int version) - { - h & r; - h & g; - h & b; - h & a; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Color.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 + +/// An object that represents RGBA color +class ColorRGBA +{ +public: + enum : uint8_t + { + ALPHA_OPAQUE = 255, + ALPHA_TRANSPARENT = 0, + }; + + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + + //constructors + constexpr ColorRGBA() + :r(0) + ,g(0) + ,b(0) + ,a(0) + { + } + + constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) + : r(r) + , g(g) + , b(b) + , a(a) + {} + + constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b) + : r(r) + , g(g) + , b(b) + , a(ALPHA_OPAQUE) + {} + + template + void serialize(Handler &h, const int version) + { + h & r; + h & g; + h & b; + h & a; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CondSh.h b/lib/CondSh.h index 0120a36fa..cc3540431 100644 --- a/lib/CondSh.h +++ b/lib/CondSh.h @@ -1,71 +1,71 @@ -/* - * CondSh.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 - -/// Used for multithreading, wraps boost functions -template struct CondSh -{ - T data; - boost::condition_variable cond; - boost::mutex mx; - - CondSh() : data(T()) {} - - CondSh(T t) : data(t) {} - - // set data - void set(T t) - { - boost::unique_lock lock(mx); - data = t; - } - - // set data and notify - void setn(T t) - { - set(t); - cond.notify_all(); - }; - - // get stored value - T get() - { - boost::unique_lock lock(mx); - return data; - } - - // waits until data is set to false - void waitWhileTrue() - { - boost::unique_lock un(mx); - while(data) - cond.wait(un); - } - - // waits while data is set to arg - void waitWhile(const T & t) - { - boost::unique_lock un(mx); - while(data == t) - cond.wait(un); - } - - // waits until data is set to arg - void waitUntil(const T & t) - { - boost::unique_lock un(mx); - while(data != t) - cond.wait(un); - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CondSh.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 + +/// Used for multithreading, wraps boost functions +template struct CondSh +{ + T data; + boost::condition_variable cond; + boost::mutex mx; + + CondSh() : data(T()) {} + + CondSh(T t) : data(t) {} + + // set data + void set(T t) + { + boost::unique_lock lock(mx); + data = t; + } + + // set data and notify + void setn(T t) + { + set(t); + cond.notify_all(); + }; + + // get stored value + T get() + { + boost::unique_lock lock(mx); + return data; + } + + // waits until data is set to false + void waitWhileTrue() + { + boost::unique_lock un(mx); + while(data) + cond.wait(un); + } + + // waits while data is set to arg + void waitWhile(const T & t) + { + boost::unique_lock un(mx); + while(data == t) + cond.wait(un); + } + + // waits until data is set to arg + void waitUntil(const T & t) + { + boost::unique_lock un(mx); + while(data != t) + cond.wait(un); + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ConstTransitivePtr.h b/lib/ConstTransitivePtr.h index 9867bf65e..e29ccbced 100644 --- a/lib/ConstTransitivePtr.h +++ b/lib/ConstTransitivePtr.h @@ -1,80 +1,80 @@ -/* - * ConstTransitivePtr.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 CGameHandler; - -VCMI_LIB_NAMESPACE_BEGIN - -template -class ConstTransitivePtr -{ - T *ptr = nullptr; - ConstTransitivePtr(const T *Ptr) - : ptr(const_cast(Ptr)) - {} -public: - ConstTransitivePtr(T *Ptr = nullptr) - : ptr(Ptr) - {} - ConstTransitivePtr(std::nullptr_t) - {} - const T& operator*() const - { - return *ptr; - } - T& operator*() - { - return *ptr; - } - operator const T*() const - { - return ptr; - } - T* get() - { - return ptr; - } - const T* get() const - { - return ptr; - } - operator T*() - { - return ptr; - } - T *operator->() - { - return ptr; - } - const T *operator->() const - { - return ptr; - } - const T*operator=(T *t) - { - return ptr = t; - } - - void dellNull() - { - delete ptr; - ptr = nullptr; - } - - template void serialize(Handler &h, const int version) - { - h & ptr; - } - - friend class ::CGameHandler; -}; - -VCMI_LIB_NAMESPACE_END +/* + * ConstTransitivePtr.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 CGameHandler; + +VCMI_LIB_NAMESPACE_BEGIN + +template +class ConstTransitivePtr +{ + T *ptr = nullptr; + ConstTransitivePtr(const T *Ptr) + : ptr(const_cast(Ptr)) + {} +public: + ConstTransitivePtr(T *Ptr = nullptr) + : ptr(Ptr) + {} + ConstTransitivePtr(std::nullptr_t) + {} + const T& operator*() const + { + return *ptr; + } + T& operator*() + { + return *ptr; + } + operator const T*() const + { + return ptr; + } + T* get() + { + return ptr; + } + const T* get() const + { + return ptr; + } + operator T*() + { + return ptr; + } + T *operator->() + { + return ptr; + } + const T *operator->() const + { + return ptr; + } + const T*operator=(T *t) + { + return ptr = t; + } + + void dellNull() + { + delete ptr; + ptr = nullptr; + } + + template void serialize(Handler &h, const int version) + { + h & ptr; + } + + friend class ::CGameHandler; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp deleted file mode 100644 index 5a1ffe19a..000000000 --- a/lib/GameConstants.cpp +++ /dev/null @@ -1,318 +0,0 @@ -/* - * GameConstants.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 - * - */ - -#define INSTANTIATE_BASE_FOR_ID_HERE - -#include "StdInc.h" - -#ifndef VCMI_NO_EXTRA_VERSION -#include "../Version.h" -#endif -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "VCMI_Lib.h" -#include "CArtHandler.h"//todo: remove -#include "CCreatureHandler.h"//todo: remove -#include "spells/CSpellHandler.h" //todo: remove -#include "CSkillHandler.h"//todo: remove -#include "StringConstants.h" -#include "CGeneralTextHandler.h" -#include "CModHandler.h"//todo: remove -#include "TerrainHandler.h" //TODO: remove -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1); -const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1); - -const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER = SlotID(-2); -const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3); -const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4); -const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5); - -const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252); -const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253); -const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254); -const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255); -const PlayerColor PlayerColor::PLAYER_LIMIT = PlayerColor(PLAYER_LIMIT_I); -const TeamID TeamID::NO_TEAM = TeamID(255); - -namespace GameConstants -{ -#ifdef VCMI_NO_EXTRA_VERSION - const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING; -#else - const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING "." + std::string{GIT_SHA1}; -#endif -} - -si32 HeroTypeID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string HeroTypeID::encode(const si32 index) -{ - return VLC->heroTypes()->getByIndex(index)->getJsonKey(); -} - -const CArtifact * ArtifactID::toArtifact() const -{ - return dynamic_cast(toArtifact(VLC->artifacts())); -} - -const Artifact * ArtifactID::toArtifact(const ArtifactService * service) const -{ - return service->getById(*this); -} - -si32 ArtifactID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "artifact", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string ArtifactID::encode(const si32 index) -{ - return VLC->artifacts()->getByIndex(index)->getJsonKey(); -} - -const CCreature * CreatureID::toCreature() const -{ - return VLC->creh->objects.at(*this); -} - -const Creature * CreatureID::toCreature(const CreatureService * creatures) const -{ - return creatures->getById(*this); -} - -si32 CreatureID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string CreatureID::encode(const si32 index) -{ - return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); -} - -const CSpell * SpellID::toSpell() const -{ - if(num < 0 || num >= VLC->spellh->objects.size()) - { - logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); - return nullptr; - } - return VLC->spellh->objects[*this]; -} - -const spells::Spell * SpellID::toSpell(const spells::Service * service) const -{ - return service->getById(*this); -} - -si32 SpellID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string SpellID::encode(const si32 index) -{ - return VLC->spells()->getByIndex(index)->getJsonKey(); -} - -bool PlayerColor::isValidPlayer() const -{ - return num < PLAYER_LIMIT_I; -} - -bool PlayerColor::isSpectator() const -{ - return num == 252; -} - -std::string PlayerColor::getStr(bool L10n) const -{ - std::string ret = "unnamed"; - if(isValidPlayer()) - { - if(L10n) - ret = VLC->generaltexth->colors[num]; - else - ret = GameConstants::PLAYER_COLOR_NAMES[num]; - } - else if(L10n) - { - ret = VLC->generaltexth->allTexts[508]; - ret[0] = std::tolower(ret[0]); - } - - return ret; -} - -std::string PlayerColor::getStrCap(bool L10n) const -{ - std::string ret = getStr(L10n); - ret[0] = std::toupper(ret[0]); - return ret; -} - -const FactionID FactionID::NONE = FactionID(-2); -const FactionID FactionID::DEFAULT = FactionID(-1); -const FactionID FactionID::CASTLE = FactionID(0); -const FactionID FactionID::RAMPART = FactionID(1); -const FactionID FactionID::TOWER = FactionID(2); -const FactionID FactionID::INFERNO = FactionID(3); -const FactionID FactionID::NECROPOLIS = FactionID(4); -const FactionID FactionID::DUNGEON = FactionID(5); -const FactionID FactionID::STRONGHOLD = FactionID(6); -const FactionID FactionID::FORTRESS = FactionID(7); -const FactionID FactionID::CONFLUX = FactionID(8); -const FactionID FactionID::NEUTRAL = FactionID(9); - -si32 FactionID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return FactionID::DEFAULT; -} - -std::string FactionID::encode(const si32 index) -{ - return VLC->factions()->getByIndex(index)->getJsonKey(); -} - -std::string FactionID::entityType() -{ - return "faction"; -} - - -si32 TerrainID::decode(const std::string & identifier) -{ - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return static_cast(ETerrainId::NONE); -} - -std::string TerrainID::encode(const si32 index) -{ - return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); -} - -std::string TerrainID::entityType() -{ - return "terrain"; -} - -std::ostream & operator<<(std::ostream & os, const EActionType actionType) -{ - static const std::map actionTypeToString = - { - {EActionType::END_TACTIC_PHASE, "End tactic phase"}, - {EActionType::INVALID, "Invalid"}, - {EActionType::NO_ACTION, "No action"}, - {EActionType::HERO_SPELL, "Hero spell"}, - {EActionType::WALK, "Walk"}, - {EActionType::DEFEND, "Defend"}, - {EActionType::RETREAT, "Retreat"}, - {EActionType::SURRENDER, "Surrender"}, - {EActionType::WALK_AND_ATTACK, "Walk and attack"}, - {EActionType::SHOOT, "Shoot"}, - {EActionType::WAIT, "Wait"}, - {EActionType::CATAPULT, "Catapult"}, - {EActionType::MONSTER_SPELL, "Monster spell"}, - {EActionType::BAD_MORALE, "Bad morale"}, - {EActionType::STACK_HEAL, "Stack heal"}, - }; - - auto it = actionTypeToString.find(actionType); - if (it == actionTypeToString.end()) return os << ""; - else return os << it->second; -} - -std::ostream & operator<<(std::ostream & os, const EPathfindingLayer & pathfindingLayer) -{ - static const std::map pathfinderLayerToString - { - #define DEFINE_ELEMENT(element) {EPathfindingLayer::element, #element} - DEFINE_ELEMENT(WRONG), - DEFINE_ELEMENT(AUTO), - DEFINE_ELEMENT(LAND), - DEFINE_ELEMENT(SAIL), - DEFINE_ELEMENT(WATER), - DEFINE_ELEMENT(AIR), - DEFINE_ELEMENT(NUM_LAYERS) - #undef DEFINE_ELEMENT - }; - - auto it = pathfinderLayerToString.find(pathfindingLayer.num); - if (it == pathfinderLayerToString.end()) return os << ""; - else return os << it->second; -} - -const BattleField BattleField::NONE; - -bool operator==(const BattleField & l, const BattleField & r) -{ - return l.num == r.num; -} - -bool operator!=(const BattleField & l, const BattleField & r) -{ - return l.num != r.num; -} - -bool operator<(const BattleField & l, const BattleField & r) -{ - return l.num < r.num; -} - -const BattleFieldInfo * BattleField::getInfo() const -{ - return VLC->battlefields()->getById(*this); -} - -const ObstacleInfo * Obstacle::getInfo() const -{ - return VLC->obstacles()->getById(*this); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/GameConstants.h b/lib/GameConstants.h index ab2aaed89..3659b5428 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1,1393 +1,22 @@ -/* - * GameConstants.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 "ConstTransitivePtr.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Artifact; -class ArtifactService; -class Creature; -class CreatureService; - -namespace spells -{ - class Spell; - class Service; -} - -class CArtifact; -class CArtifactInstance; -class CCreature; -class CHero; -class CSpell; -class CSkill; -class CGameInfoCallback; -class CNonConstInfoCallback; - -struct IdTag -{}; - -namespace GameConstants -{ - DLL_LINKAGE extern const std::string VCMI_VERSION; - - constexpr int PUZZLE_MAP_PIECES = 48; - - constexpr int MAX_HEROES_PER_PLAYER = 8; - constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; - - constexpr int ALL_PLAYERS = 255; //bitfield - - constexpr int CREATURES_PER_TOWN = 7; //without upgrades - constexpr int SPELL_LEVELS = 5; - constexpr int SPELL_SCHOOL_LEVELS = 4; - constexpr int DEFAULT_SCHOOLS = 4; - constexpr int CRE_LEVELS = 10; // number of creature experience levels - - constexpr int HERO_GOLD_COST = 2500; - constexpr int SPELLBOOK_GOLD_COST = 500; - constexpr int SKILL_GOLD_COST = 2000; - constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty - constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit - constexpr int ARMY_SIZE = 7; - constexpr int SKILL_PER_HERO = 8; - constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order - - constexpr int SKILL_QUANTITY=28; - constexpr int PRIMARY_SKILLS=4; - constexpr int RESOURCE_QUANTITY=8; - constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type - - // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase - constexpr int F_NUMBER = 9; - constexpr int ARTIFACTS_QUANTITY=171; - constexpr int HEROES_QUANTITY=156; - constexpr int SPELLS_QUANTITY=70; - constexpr int CREATURES_COUNT = 197; - - constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement - - constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits - - constexpr std::array POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; -} - -#define ID_LIKE_CLASS_COMMON(CLASS_NAME, ENUM_NAME) \ -constexpr CLASS_NAME(const CLASS_NAME & other) = default; \ -constexpr CLASS_NAME & operator=(const CLASS_NAME & other) = default; \ -explicit constexpr CLASS_NAME(si32 id) \ - : num(static_cast(id)) \ -{} \ -constexpr operator ENUM_NAME() const \ -{ \ - return num; \ -} \ -constexpr si32 getNum() const \ -{ \ - return static_cast(num); \ -} \ -constexpr ENUM_NAME toEnum() const \ -{ \ - return num; \ -} \ -template void serialize(Handler &h, const int version) \ -{ \ - h & num; \ -} \ -constexpr CLASS_NAME & advance(int i) \ -{ \ - num = static_cast(static_cast(num) + i); \ - return *this; \ -} - - -// Operators are performance-critical and to be inlined they must be in header -#define ID_LIKE_OPERATORS_INTERNAL(A, B, AN, BN) \ -STRONG_INLINE constexpr bool operator==(const A & a, const B & b) \ -{ \ - return AN == BN ; \ -} \ -STRONG_INLINE constexpr bool operator!=(const A & a, const B & b) \ -{ \ - return AN != BN ; \ -} \ -STRONG_INLINE constexpr bool operator<(const A & a, const B & b) \ -{ \ - return AN < BN ; \ -} \ -STRONG_INLINE constexpr bool operator<=(const A & a, const B & b) \ -{ \ - return AN <= BN ; \ -} \ -STRONG_INLINE constexpr bool operator>(const A & a, const B & b) \ -{ \ - return AN > BN ; \ -} \ -STRONG_INLINE constexpr bool operator>=(const A & a, const B & b) \ -{ \ - return AN >= BN ; \ -} - -#define ID_LIKE_OPERATORS(CLASS_NAME, ENUM_NAME) \ - ID_LIKE_OPERATORS_INTERNAL(CLASS_NAME, CLASS_NAME, a.num, b.num) \ - ID_LIKE_OPERATORS_INTERNAL(CLASS_NAME, ENUM_NAME, a.num, b) \ - ID_LIKE_OPERATORS_INTERNAL(ENUM_NAME, CLASS_NAME, a, b.num) - - -#define INSTID_LIKE_CLASS_COMMON(CLASS_NAME, NUMERIC_NAME) \ -public: \ -constexpr CLASS_NAME(const CLASS_NAME & other): \ - BaseForID(other) \ -{ \ -} \ -constexpr CLASS_NAME & operator=(const CLASS_NAME & other) = default; \ -constexpr CLASS_NAME & operator=(NUMERIC_NAME other) { num = other; return *this; }; \ -explicit constexpr CLASS_NAME(si32 id = -1) \ - : BaseForID(id) \ -{} - -template < typename Derived, typename NumericType> -class BaseForID : public IdTag -{ -protected: - NumericType num; - -public: - constexpr NumericType getNum() const - { - return num; - } - - //to make it more similar to IDLIKE - constexpr NumericType toEnum() const - { - return num; - } - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr explicit BaseForID(NumericType _num = -1) : - num(_num) - { - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr bool operator == (const BaseForID & b) const { return num == b.num; } - constexpr bool operator <= (const BaseForID & b) const { return num <= b.num; } - constexpr bool operator >= (const BaseForID & b) const { return num >= b.num; } - constexpr bool operator != (const BaseForID & b) const { return num != b.num; } - constexpr bool operator < (const BaseForID & b) const { return num < b.num; } - constexpr bool operator > (const BaseForID & b) const { return num > b.num; } - - constexpr BaseForID & operator++() { ++num; return *this; } - - constexpr operator NumericType() const - { - return num; - } -}; - -template < typename T> -class Identifier : public IdTag -{ -public: - using EnumType = T; - using NumericType = typename std::underlying_type::type; - -private: - NumericType num; - -public: - constexpr NumericType getNum() const - { - return num; - } - - constexpr EnumType toEnum() const - { - return static_cast(num); - } - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr explicit Identifier(NumericType _num = -1) - { - num = _num; - } - - /* implicit */constexpr Identifier(EnumType _num): - num(static_cast(_num)) - { - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr bool operator == (const Identifier & b) const { return num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return num != b.num; } - constexpr bool operator < (const Identifier & b) const { return num < b.num; } - constexpr bool operator > (const Identifier & b) const { return num > b.num; } - - constexpr Identifier & operator++() - { - ++num; - return *this; - } - - constexpr Identifier operator++(int) - { - Identifier ret(*this); - ++num; - return ret; - } - - constexpr operator NumericType() const - { - return num; - } -}; - - -template -std::ostream & operator << (std::ostream & os, BaseForID id); - -template -std::ostream & operator << (std::ostream & os, BaseForID id) -{ - //We use common type with short to force char and unsigned char to be promoted and formatted as numbers. - typedef typename std::common_type::type Number; - return os << static_cast(id.getNum()); -} - -template -std::ostream & operator << (std::ostream & os, Identifier id) -{ - //We use common type with short to force char and unsigned char to be promoted and formatted as numbers. - typedef typename std::common_type::NumericType>::type Number; - return os << static_cast(id.getNum()); -} - -class ArtifactInstanceID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(ArtifactInstanceID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class QueryID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(QueryID, si32) - - QueryID & operator++() - { - ++num; - return *this; - } -}; - -class ObjectInstanceID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(ObjectInstanceID, si32) - - DLL_LINKAGE static const ObjectInstanceID NONE; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class HeroClassID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(HeroClassID, si32) -}; - -class HeroTypeID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(HeroTypeID, si32) - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - - DLL_LINKAGE static const HeroTypeID NONE; -}; - -class SlotID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(SlotID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; - - DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; - DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; - } -}; - -class PlayerColor : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(PlayerColor, ui8) - - enum EPlayerColor - { - PLAYER_LIMIT_I = 8, - }; - - using Mask = uint8_t; - - DLL_LINKAGE static const PlayerColor SPECTATOR; //252 - DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 - DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) - DLL_LINKAGE static const PlayerColor NEUTRAL; //255 - DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map - - DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - DLL_LINKAGE bool isSpectator() const; - - DLL_LINKAGE std::string getStr(bool L10n = false) const; - DLL_LINKAGE std::string getStrCap(bool L10n = false) const; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class TeamID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(TeamID, ui8) - - DLL_LINKAGE static const TeamID NO_TEAM; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class TeleportChannelID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(TeleportChannelID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -// #ifndef INSTANTIATE_BASE_FOR_ID_HERE -// extern template std::ostream & operator << (std::ostream & os, BaseForID id); -// extern template std::ostream & operator << (std::ostream & os, BaseForID id); -// #endif - -// Enum declarations -namespace PrimarySkill -{ - enum PrimarySkill : int8_t { NONE = -1, ATTACK, DEFENSE, SPELL_POWER, KNOWLEDGE, - EXPERIENCE = 4}; //for some reason changePrimSkill uses it -} - -class SecondarySkill -{ -public: - enum ESecondarySkill - { - WRONG = -2, - DEFAULT = -1, - PATHFINDING = 0, ARCHERY, LOGISTICS, SCOUTING, DIPLOMACY, NAVIGATION, LEADERSHIP, WISDOM, MYSTICISM, - LUCK, BALLISTICS, EAGLE_EYE, NECROMANCY, ESTATES, FIRE_MAGIC, AIR_MAGIC, WATER_MAGIC, EARTH_MAGIC, - SCHOLAR, TACTICS, ARTILLERY, LEARNING, OFFENCE, ARMORER, INTELLIGENCE, SORCERY, RESISTANCE, - FIRST_AID, SKILL_SIZE - }; - - static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); - - SecondarySkill(ESecondarySkill _num = WRONG) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(SecondarySkill, ESecondarySkill) - - ESecondarySkill num; -}; - -ID_LIKE_OPERATORS(SecondarySkill, SecondarySkill::ESecondarySkill) - -enum class EAlignment : uint8_t { GOOD, EVIL, NEUTRAL }; - -namespace ETownType//deprecated -{ - enum ETownType - { - ANY = -1, - CASTLE, RAMPART, TOWER, INFERNO, NECROPOLIS, DUNGEON, STRONGHOLD, FORTRESS, CONFLUX, NEUTRAL - }; -} - -class FactionID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(FactionID, si32) - - DLL_LINKAGE static const FactionID NONE; - DLL_LINKAGE static const FactionID DEFAULT; - DLL_LINKAGE static const FactionID CASTLE; - DLL_LINKAGE static const FactionID RAMPART; - DLL_LINKAGE static const FactionID TOWER; - DLL_LINKAGE static const FactionID INFERNO; - DLL_LINKAGE static const FactionID NECROPOLIS; - DLL_LINKAGE static const FactionID DUNGEON; - DLL_LINKAGE static const FactionID STRONGHOLD; - DLL_LINKAGE static const FactionID FORTRESS; - DLL_LINKAGE static const FactionID CONFLUX; - DLL_LINKAGE static const FactionID NEUTRAL; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); - static std::string entityType(); -}; - -class TerrainID -{ - //Dummy class used only for serialization -public: - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - static std::string entityType(); -}; - -class BuildingID -{ -public: - //Quite useful as long as most of building mechanics hardcoded - // NOTE: all building with completely configurable mechanics will be removed from list - enum EBuildingID - { - DEFAULT = -50, - HORDE_PLACEHOLDER7 = -36, - HORDE_PLACEHOLDER6 = -35, - HORDE_PLACEHOLDER5 = -34, - HORDE_PLACEHOLDER4 = -33, - HORDE_PLACEHOLDER3 = -32, - HORDE_PLACEHOLDER2 = -31, - HORDE_PLACEHOLDER1 = -30, - NONE = -1, - FIRST_REGULAR_ID = 0, - MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, - TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, - VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, - RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, - SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, - 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_1 = DWELL_FIRST, - DWELL_LVL_7 = DWELL_LAST, - DWELL_LVL_1_UP = DWELL_UP_FIRST, - DWELL_LVL_7_UP = DWELL_UP_LAST, - - //Special buildings for towns. - LIGHTHOUSE = SPECIAL_1, - STABLES = SPECIAL_2, //Castle - BROTHERHOOD = SPECIAL_3, - - MYSTIC_POND = SPECIAL_1, - FOUNTAIN_OF_FORTUNE = SPECIAL_2, //Rampart - TREASURY = SPECIAL_3, - - ARTIFACT_MERCHANT = SPECIAL_1, - LOOKOUT_TOWER = SPECIAL_2, //Tower - LIBRARY = SPECIAL_3, - WALL_OF_KNOWLEDGE = SPECIAL_4, - - STORMCLOUDS = SPECIAL_2, - CASTLE_GATE = SPECIAL_3, //Inferno - ORDER_OF_FIRE = SPECIAL_4, - - COVER_OF_DARKNESS = SPECIAL_1, - NECROMANCY_AMPLIFIER = SPECIAL_2, //Necropolis - SKELETON_TRANSFORMER = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MANA_VORTEX = SPECIAL_2, - PORTAL_OF_SUMMON = SPECIAL_3, //Dungeon - BATTLE_ACADEMY = SPECIAL_4, - - ESCAPE_TUNNEL = SPECIAL_1, - FREELANCERS_GUILD = SPECIAL_2, //Stronghold - BALLISTA_YARD = SPECIAL_3, - HALL_OF_VALHALLA = SPECIAL_4, - - CAGE_OF_WARLORDS = SPECIAL_1, - GLYPHS_OF_FEAR = SPECIAL_2, // Fortress - BLOOD_OBELISK = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MAGIC_UNIVERSITY = SPECIAL_2, // Conflux - }; - - BuildingID(EBuildingID _num = NONE) : num(_num) - {} - - STRONG_INLINE - bool IsSpecialOrGrail() const - { - return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; - } - - ID_LIKE_CLASS_COMMON(BuildingID, EBuildingID) - - EBuildingID num; -}; - -ID_LIKE_OPERATORS(BuildingID, BuildingID::EBuildingID) - -namespace BuildingSubID -{ - enum EBuildingSubID - { - 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, - CUSTOM_VISITING_BONUS, - CUSTOM_VISITING_REWARD - }; -} - -namespace EMarketMode -{ - enum EMarketMode - { - RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, - ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, - MARTKET_AFTER_LAST_PLACEHOLDER - }; -} - -namespace MappedKeys -{ - - static const std::map BUILDING_NAMES_TO_TYPES = - { - { "special1", BuildingID::SPECIAL_1 }, - { "special2", BuildingID::SPECIAL_2 }, - { "special3", BuildingID::SPECIAL_3 }, - { "special4", BuildingID::SPECIAL_4 }, - { "grail", BuildingID::GRAIL }, - { "mageGuild1", BuildingID::MAGES_GUILD_1 }, - { "mageGuild2", BuildingID::MAGES_GUILD_2 }, - { "mageGuild3", BuildingID::MAGES_GUILD_3 }, - { "mageGuild4", BuildingID::MAGES_GUILD_4 }, - { "mageGuild5", BuildingID::MAGES_GUILD_5 }, - { "tavern", BuildingID::TAVERN }, - { "shipyard", BuildingID::SHIPYARD }, - { "fort", BuildingID::FORT }, - { "citadel", BuildingID::CITADEL }, - { "castle", BuildingID::CASTLE }, - { "villageHall", BuildingID::VILLAGE_HALL }, - { "townHall", BuildingID::TOWN_HALL }, - { "cityHall", BuildingID::CITY_HALL }, - { "capitol", BuildingID::CAPITOL }, - { "marketplace", BuildingID::MARKETPLACE }, - { "resourceSilo", BuildingID::RESOURCE_SILO }, - { "blacksmith", BuildingID::BLACKSMITH }, - { "horde1", BuildingID::HORDE_1 }, - { "horde1Upgr", BuildingID::HORDE_1_UPGR }, - { "horde2", BuildingID::HORDE_2 }, - { "horde2Upgr", BuildingID::HORDE_2_UPGR }, - { "ship", BuildingID::SHIP }, - { "dwellingLvl1", BuildingID::DWELL_LVL_1 }, - { "dwellingLvl2", BuildingID::DWELL_LVL_2 }, - { "dwellingLvl3", BuildingID::DWELL_LVL_3 }, - { "dwellingLvl4", BuildingID::DWELL_LVL_4 }, - { "dwellingLvl5", BuildingID::DWELL_LVL_5 }, - { "dwellingLvl6", BuildingID::DWELL_LVL_6 }, - { "dwellingLvl7", BuildingID::DWELL_LVL_7 }, - { "dwellingUpLvl1", BuildingID::DWELL_LVL_1_UP }, - { "dwellingUpLvl2", BuildingID::DWELL_LVL_2_UP }, - { "dwellingUpLvl3", BuildingID::DWELL_LVL_3_UP }, - { "dwellingUpLvl4", BuildingID::DWELL_LVL_4_UP }, - { "dwellingUpLvl5", BuildingID::DWELL_LVL_5_UP }, - { "dwellingUpLvl6", BuildingID::DWELL_LVL_6_UP }, - { "dwellingUpLvl7", BuildingID::DWELL_LVL_7_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 } - }; - - static const std::map MARKET_NAMES_TO_TYPES = - { - { "resource-resource", EMarketMode::RESOURCE_RESOURCE }, - { "resource-player", EMarketMode::RESOURCE_PLAYER }, - { "creature-resource", EMarketMode::CREATURE_RESOURCE }, - { "resource-artifact", EMarketMode::RESOURCE_ARTIFACT }, - { "artifact-resource", EMarketMode::ARTIFACT_RESOURCE }, - { "artifact-experience", EMarketMode::ARTIFACT_EXP }, - { "creature-experience", EMarketMode::CREATURE_EXP }, - { "creature-undead", EMarketMode::CREATURE_UNDEAD }, - { "resource-skill", EMarketMode::RESOURCE_SKILL }, - }; -} - -namespace EAiTactic -{ -enum EAiTactic -{ - NONE = -1, - RANDOM, - WARRIOR, - BUILDER, - EXPLORER -}; -} - -namespace EBuildingState -{ - enum EBuildingState - { - HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, - NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED - }; -} - -namespace ESpellCastProblem -{ - enum ESpellCastProblem - { - OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, - HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, - SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, - NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, - MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all - INVALID - }; -} - -namespace ECommander -{ - enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; - const int MAX_SKILL_LEVEL = 5; -} - -enum class EWallPart : int8_t -{ - INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, - KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, - PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ -}; - -enum class EWallState : int8_t -{ - NONE = -1, //no wall - DESTROYED, - DAMAGED, - INTACT, - REINFORCED, // walls in towns with castle -}; - -enum class EGateState : uint8_t -{ - NONE, - CLOSED, - BLOCKED, // gate is blocked in closed state, e.g. by creature - OPENED, - DESTROYED -}; - -namespace ESiegeHex -{ - enum ESiegeHex : si16 - { - DESTRUCTIBLE_WALL_1 = 29, - DESTRUCTIBLE_WALL_2 = 78, - DESTRUCTIBLE_WALL_3 = 130, - DESTRUCTIBLE_WALL_4 = 182, - GATE_BRIDGE = 94, - GATE_OUTER = 95, - GATE_INNER = 96 - }; -} - -namespace ETileType -{ - enum ETileType - { - FREE, - POSSIBLE, - BLOCKED, - USED - }; -} - -enum class ETeleportChannelType -{ - IMPASSABLE, - BIDIRECTIONAL, - UNIDIRECTIONAL, - MIXED -}; - -class Obj -{ -public: - enum EObj - { - NO_OBJ = -1, - ALTAR_OF_SACRIFICE [[deprecated]] = 2, - ANCHOR_POINT = 3, - ARENA = 4, - ARTIFACT = 5, - PANDORAS_BOX = 6, - BLACK_MARKET [[deprecated]] = 7, - BOAT = 8, - BORDERGUARD = 9, - KEYMASTER = 10, - BUOY = 11, - CAMPFIRE = 12, - CARTOGRAPHER = 13, - SWAN_POND = 14, - COVER_OF_DARKNESS = 15, - CREATURE_BANK = 16, - CREATURE_GENERATOR1 = 17, - CREATURE_GENERATOR2 = 18, - CREATURE_GENERATOR3 = 19, - CREATURE_GENERATOR4 = 20, - CURSED_GROUND1 = 21, - CORPSE = 22, - MARLETTO_TOWER = 23, - DERELICT_SHIP = 24, - DRAGON_UTOPIA = 25, - EVENT = 26, - EYE_OF_MAGI = 27, - FAERIE_RING = 28, - FLOTSAM = 29, - FOUNTAIN_OF_FORTUNE = 30, - FOUNTAIN_OF_YOUTH = 31, - GARDEN_OF_REVELATION = 32, - GARRISON = 33, - HERO = 34, - HILL_FORT = 35, - GRAIL = 36, - HUT_OF_MAGI = 37, - IDOL_OF_FORTUNE = 38, - LEAN_TO = 39, - LIBRARY_OF_ENLIGHTENMENT = 41, - LIGHTHOUSE = 42, - MONOLITH_ONE_WAY_ENTRANCE = 43, - MONOLITH_ONE_WAY_EXIT = 44, - MONOLITH_TWO_WAY = 45, - MAGIC_PLAINS1 = 46, - SCHOOL_OF_MAGIC = 47, - MAGIC_SPRING = 48, - MAGIC_WELL = 49, - MARKET_OF_TIME = 50, - MERCENARY_CAMP = 51, - MERMAID = 52, - MINE = 53, - MONSTER = 54, - MYSTICAL_GARDEN = 55, - OASIS = 56, - OBELISK = 57, - REDWOOD_OBSERVATORY = 58, - OCEAN_BOTTLE = 59, - PILLAR_OF_FIRE = 60, - STAR_AXIS = 61, - PRISON = 62, - PYRAMID = 63,//subtype 0 - WOG_OBJECT = 63,//subtype > 0 - RALLY_FLAG = 64, - RANDOM_ART = 65, - RANDOM_TREASURE_ART = 66, - RANDOM_MINOR_ART = 67, - RANDOM_MAJOR_ART = 68, - RANDOM_RELIC_ART = 69, - RANDOM_HERO = 70, - RANDOM_MONSTER = 71, - RANDOM_MONSTER_L1 = 72, - RANDOM_MONSTER_L2 = 73, - RANDOM_MONSTER_L3 = 74, - RANDOM_MONSTER_L4 = 75, - RANDOM_RESOURCE = 76, - RANDOM_TOWN = 77, - REFUGEE_CAMP = 78, - RESOURCE = 79, - SANCTUARY = 80, - SCHOLAR = 81, - SEA_CHEST = 82, - SEER_HUT = 83, - CRYPT = 84, - SHIPWRECK = 85, - SHIPWRECK_SURVIVOR = 86, - SHIPYARD = 87, - SHRINE_OF_MAGIC_INCANTATION = 88, - SHRINE_OF_MAGIC_GESTURE = 89, - SHRINE_OF_MAGIC_THOUGHT = 90, - SIGN = 91, - SIRENS = 92, - SPELL_SCROLL = 93, - STABLES = 94, - TAVERN = 95, - TEMPLE = 96, - DEN_OF_THIEVES = 97, - TOWN = 98, - TRADING_POST [[deprecated]] = 99, - LEARNING_STONE = 100, - TREASURE_CHEST = 101, - TREE_OF_KNOWLEDGE = 102, - SUBTERRANEAN_GATE = 103, - UNIVERSITY [[deprecated]] = 104, - WAGON = 105, - WAR_MACHINE_FACTORY = 106, - SCHOOL_OF_WAR = 107, - WARRIORS_TOMB = 108, - WATER_WHEEL = 109, - WATERING_HOLE = 110, - WHIRLPOOL = 111, - WINDMILL = 112, - WITCH_HUT = 113, - HOLE = 124, - RANDOM_MONSTER_L5 = 162, - RANDOM_MONSTER_L6 = 163, - RANDOM_MONSTER_L7 = 164, - BORDER_GATE = 212, - FREELANCERS_GUILD [[deprecated]] = 213, - HERO_PLACEHOLDER = 214, - QUEST_GUARD = 215, - RANDOM_DWELLING = 216, - RANDOM_DWELLING_LVL = 217, //subtype = creature level - RANDOM_DWELLING_FACTION = 218, //subtype = faction - GARRISON2 = 219, - ABANDONED_MINE = 220, - TRADING_POST_SNOW [[deprecated]] = 221, - CLOVER_FIELD = 222, - CURSED_GROUND2 = 223, - EVIL_FOG = 224, - FAVORABLE_WINDS = 225, - FIERY_FIELDS = 226, - HOLY_GROUNDS = 227, - LUCID_POOLS = 228, - MAGIC_CLOUDS = 229, - MAGIC_PLAINS2 = 230, - ROCKLANDS = 231, - }; - Obj(EObj _num = NO_OBJ) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(Obj, EObj) - - EObj num; -}; - -ID_LIKE_OPERATORS(Obj, Obj::EObj) - -enum class Road : int8_t -{ - NO_ROAD = 0, - FIRST_REGULAR_ROAD = 1, - DIRT_ROAD = 1, - GRAVEL_ROAD = 2, - COBBLESTONE_ROAD = 3, - ORIGINAL_ROAD_COUNT //+1 -}; - -enum class River : int8_t -{ - NO_RIVER = 0, - FIRST_REGULAR_RIVER = 1, - WATER_RIVER = 1, - ICY_RIVER = 2, - MUD_RIVER = 3, - LAVA_RIVER = 4, - ORIGINAL_RIVER_COUNT //+1 -}; - -namespace SecSkillLevel -{ - enum SecSkillLevel - { - NONE, - BASIC, - ADVANCED, - EXPERT, - LEVELS_SIZE - }; -} - -namespace Date -{ - enum EDateType - { - DAY = 0, - DAY_OF_WEEK = 1, - WEEK = 2, - MONTH = 3, - DAY_OF_MONTH - }; -} - -enum class EActionType : int32_t -{ - CANCEL = -3, - END_TACTIC_PHASE = -2, - INVALID = -1, - NO_ACTION = 0, - HERO_SPELL, - WALK, - DEFEND, - RETREAT, - SURRENDER, - WALK_AND_ATTACK, - SHOOT, - WAIT, - CATAPULT, - MONSTER_SPELL, - BAD_MORALE, - STACK_HEAL, -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EActionType actionType); - -class DLL_LINKAGE EDiggingStatus -{ -public: - enum EEDiggingStatus - { - UNKNOWN = -1, - CAN_DIG = 0, - LACK_OF_MOVEMENT, - WRONG_TERRAIN, - TILE_OCCUPIED, - BACKPACK_IS_FULL - }; - - EDiggingStatus(EEDiggingStatus _num = UNKNOWN) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(EDiggingStatus, EEDiggingStatus) - - EEDiggingStatus num; -}; - -ID_LIKE_OPERATORS(EDiggingStatus, EDiggingStatus::EEDiggingStatus) - -class DLL_LINKAGE EPathfindingLayer -{ -public: - enum EEPathfindingLayer : ui8 - { - LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO - }; - - EPathfindingLayer(EEPathfindingLayer _num = WRONG) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(EPathfindingLayer, EEPathfindingLayer) - - EEPathfindingLayer num; -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EPathfindingLayer & pathfindingLayer); - -ID_LIKE_OPERATORS(EPathfindingLayer, EPathfindingLayer::EEPathfindingLayer) - -namespace EPlayerStatus -{ - enum EStatus {WRONG = -1, INGAME, LOSER, WINNER}; -} - -namespace PlayerRelations -{ - enum PlayerRelations {ENEMIES, ALLIES, SAME_PLAYER}; -} - -class ArtifactPosition -{ -public: - enum EArtifactPosition - { - TRANSITION_POS = -3, - FIRST_AVAILABLE = -2, - PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack - HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 - RIGHT_RING, LEFT_RING, FEET, //8 - MISC1, MISC2, MISC3, MISC4, //12 - MACH1, MACH2, MACH3, MACH4, //16 - SPELLBOOK, MISC5, //18 - AFTER_LAST, - //cres - CREATURE_SLOT = 0, - COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST - }; - - static_assert (AFTER_LAST == 19, "incorrect number of artifact slots"); - - ArtifactPosition(EArtifactPosition _num = PRE_FIRST) : num(_num) - {} - - ArtifactPosition(std::string slotName); - - ID_LIKE_CLASS_COMMON(ArtifactPosition, EArtifactPosition) - - EArtifactPosition num; - - STRONG_INLINE EArtifactPosition operator+(const int arg) - { - return EArtifactPosition(static_cast(num) + arg); - } - STRONG_INLINE EArtifactPosition operator+(const EArtifactPosition & arg) - { - return EArtifactPosition(static_cast(num) + static_cast(arg)); - } -}; - -ID_LIKE_OPERATORS(ArtifactPosition, ArtifactPosition::EArtifactPosition) - -namespace GameConstants -{ - const auto BACKPACK_START = ArtifactPosition::AFTER_LAST; -} - -class ArtifactID -{ -public: - enum EArtifactID - { - NONE = -1, - SPELLBOOK = 0, - SPELL_SCROLL = 1, - GRAIL = 2, - CATAPULT = 3, - BALLISTA = 4, - AMMO_CART = 5, - FIRST_AID_TENT = 6, - //CENTAUR_AXE = 7, - //BLACKSHARD_OF_THE_DEAD_KNIGHT = 8, - VIAL_OF_DRAGON_BLOOD = 127, - ARMAGEDDONS_BLADE = 128, - TITANS_THUNDER = 135, - //CORNUCOPIA = 140, - //FIXME: the following is only true if WoG is enabled. Otherwise other mod artifacts will take these slots. - ART_SELECTION = 144, - ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 - AXE_OF_SMASHING = 146, - MITHRIL_MAIL = 147, - SWORD_OF_SHARPNESS = 148, - HELM_OF_IMMORTALITY = 149, - PENDANT_OF_SORCERY = 150, - BOOTS_OF_HASTE = 151, - BOW_OF_SEEKING = 152, - DRAGON_EYE_RING = 153 - //HARDENED_SHIELD = 154, - //SLAVAS_RING_OF_POWER = 155 - }; - - ArtifactID(EArtifactID _num = NONE) : num(_num) - {} - - DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - - ID_LIKE_CLASS_COMMON(ArtifactID, EArtifactID) - - EArtifactID num; - - struct hash - { - size_t operator()(const ArtifactID & aid) const - { - return std::hash()(aid.num); - } - }; -}; - -ID_LIKE_OPERATORS(ArtifactID, ArtifactID::EArtifactID) - -class CreatureID -{ -public: - enum ECreatureID - { - NONE = -1, - ARCHER = 2, - CAVALIER = 10, - CHAMPION = 11, - STONE_GOLEM = 32, - IRON_GOLEM = 33, - IMP = 42, - SKELETON = 56, - WALKING_DEAD = 58, - WIGHTS = 60, - LICHES = 64, - BONE_DRAGON = 68, - TROGLODYTES = 70, - MEDUSA = 76, - HYDRA = 110, - CHAOS_HYDRA = 111, - AIR_ELEMENTAL = 112, - EARTH_ELEMENTAL = 113, - FIRE_ELEMENTAL = 114, - WATER_ELEMENTAL = 115, - GOLD_GOLEM = 116, - DIAMOND_GOLEM = 117, - PSYCHIC_ELEMENTAL = 120, - MAGIC_ELEMENTAL = 121, - CATAPULT = 145, - BALLISTA = 146, - FIRST_AID_TENT = 147, - AMMO_CART = 148, - ARROW_TOWERS = 149 - }; - - CreatureID(ECreatureID _num = NONE) : num(_num) - {} - - DLL_LINKAGE const CCreature * toCreature() const; - DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; - - ID_LIKE_CLASS_COMMON(CreatureID, ECreatureID) - - ECreatureID num; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); -}; - -ID_LIKE_OPERATORS(CreatureID, CreatureID::ECreatureID) - -class SpellID -{ -public: - enum ESpellID - { - SPELLBOOK_PRESET = -3, - PRESET = -2, - NONE = -1, - SUMMON_BOAT=0, SCUTTLE_BOAT=1, VISIONS=2, VIEW_EARTH=3, DISGUISE=4, VIEW_AIR=5, - FLY=6, WATER_WALK=7, DIMENSION_DOOR=8, TOWN_PORTAL=9, - - QUICKSAND=10, LAND_MINE=11, FORCE_FIELD=12, FIRE_WALL=13, EARTHQUAKE=14, - MAGIC_ARROW=15, ICE_BOLT=16, LIGHTNING_BOLT=17, IMPLOSION=18, - CHAIN_LIGHTNING=19, FROST_RING=20, FIREBALL=21, INFERNO=22, - METEOR_SHOWER=23, DEATH_RIPPLE=24, DESTROY_UNDEAD=25, ARMAGEDDON=26, - SHIELD=27, AIR_SHIELD=28, FIRE_SHIELD=29, PROTECTION_FROM_AIR=30, - PROTECTION_FROM_FIRE=31, PROTECTION_FROM_WATER=32, - PROTECTION_FROM_EARTH=33, ANTI_MAGIC=34, DISPEL=35, MAGIC_MIRROR=36, - CURE=37, RESURRECTION=38, ANIMATE_DEAD=39, SACRIFICE=40, BLESS=41, - CURSE=42, BLOODLUST=43, PRECISION=44, WEAKNESS=45, STONE_SKIN=46, - DISRUPTING_RAY=47, PRAYER=48, MIRTH=49, SORROW=50, FORTUNE=51, - MISFORTUNE=52, HASTE=53, SLOW=54, SLAYER=55, FRENZY=56, - TITANS_LIGHTNING_BOLT=57, COUNTERSTRIKE=58, BERSERK=59, HYPNOTIZE=60, - FORGETFULNESS=61, BLIND=62, TELEPORT=63, REMOVE_OBSTACLE=64, CLONE=65, - SUMMON_FIRE_ELEMENTAL=66, SUMMON_EARTH_ELEMENTAL=67, SUMMON_WATER_ELEMENTAL=68, SUMMON_AIR_ELEMENTAL=69, - - STONE_GAZE=70, POISON=71, BIND=72, DISEASE=73, PARALYZE=74, AGE=75, DEATH_CLOUD=76, THUNDERBOLT=77, - DISPEL_HELPFUL_SPELLS=78, DEATH_STARE=79, ACID_BREATH_DEFENSE=80, ACID_BREATH_DAMAGE=81, - - FIRST_NON_SPELL = 70, AFTER_LAST = 82 - }; - - SpellID(ESpellID _num = NONE) : num(_num) - {} - - DLL_LINKAGE const CSpell * toSpell() const; //deprecated - DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; - - ID_LIKE_CLASS_COMMON(SpellID, ESpellID) - - ESpellID num; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); -}; - -ID_LIKE_OPERATORS(SpellID, SpellID::ESpellID) - -class BattleFieldInfo; -class BattleField : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(BattleField, si32) - - DLL_LINKAGE static const BattleField NONE; - - DLL_LINKAGE friend bool operator==(const BattleField & l, const BattleField & r); - DLL_LINKAGE friend bool operator!=(const BattleField & l, const BattleField & r); - DLL_LINKAGE friend bool operator<(const BattleField & l, const BattleField & r); - - DLL_LINKAGE const BattleFieldInfo * getInfo() const; -}; - -enum class EBoatId : int32_t -{ - NONE = -1, - NECROPOLIS = 0, - CASTLE, - FORTRESS -}; - -using BoatId = Identifier; - -enum class ETerrainId { - NATIVE_TERRAIN = -4, - ANY_TERRAIN = -3, - NONE = -1, - FIRST_REGULAR_TERRAIN = 0, - DIRT = 0, - SAND, - GRASS, - SNOW, - SWAMP, - ROUGH, - SUBTERRANEAN, - LAVA, - WATER, - ROCK, - ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK -}; - -using TerrainId = Identifier; -using RoadId = Identifier; -using RiverId = Identifier; - -class ObstacleInfo; -class Obstacle : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(Obstacle, si32) - - DLL_LINKAGE const ObstacleInfo * getInfo() const; -}; - -enum class ESpellSchool: int8_t -{ - ANY = -1, - AIR = 0, - FIRE = 1, - WATER = 2, - EARTH = 3, -}; - -using SpellSchool = Identifier; - -enum class EMetaclass: ui8 -{ - INVALID = 0, - ARTIFACT, - CREATURE, - FACTION, - EXPERIENCE, - HERO, - HEROCLASS, - LUCK, - MANA, - MORALE, - MOVEMENT, - OBJECT, - PRIMARY_SKILL, - SECONDARY_SKILL, - SPELL, - RESOURCE -}; - -enum class EHealLevel: ui8 -{ - HEAL, - RESURRECT, - OVERHEAL -}; - -enum class EHealPower : ui8 -{ - ONE_BATTLE, - PERMANENT -}; - -// Typedef declarations -using TExpType = si64; -using TQuantity = si32; - -using TRmgTemplateZoneId = int; - -#undef ID_LIKE_CLASS_COMMON -#undef ID_LIKE_OPERATORS -#undef ID_LIKE_OPERATORS_INTERNAL -#undef INSTID_LIKE_CLASS_COMMON - -VCMI_LIB_NAMESPACE_END +/* + * GameConstants.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/NumericConstants.h" +#include "constants/Enumerations.h" +#include "constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TExpType = si64; +using TQuantity = si32; +using TRmgTemplateZoneId = int; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 3a2969704..15a593e1b 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -1,128 +1,128 @@ -/* - * GameSettings.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 "GameSettings.h" -#include "JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool IGameSettings::getBoolean(EGameSettings option) const -{ - return getValue(option).Bool(); -} - -int64_t IGameSettings::getInteger(EGameSettings option) const -{ - return getValue(option).Integer(); -} - -double IGameSettings::getDouble(EGameSettings option) const -{ - return getValue(option).Float(); -} - -std::vector IGameSettings::getVector(EGameSettings option) const -{ - return getValue(option).convertTo>(); -} - -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; - }; - - 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::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_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::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, - {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, - }; - - for(const auto & option : optionPath) - { - const JsonNode & optionValue = input[option.group][option.key]; - size_t index = static_cast(option.setting); - - if(optionValue.isNull()) - continue; - - JsonUtils::mergeCopy(gameSettings[index], optionValue); - } -} - -const JsonNode & GameSettings::getValue(EGameSettings option) const -{ - assert(option < EGameSettings::OPTIONS_COUNT); - auto index = static_cast(option); - - assert(!gameSettings[index].isNull()); - return gameSettings[index]; -} - -VCMI_LIB_NAMESPACE_END +/* + * GameSettings.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 "GameSettings.h" +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool IGameSettings::getBoolean(EGameSettings option) const +{ + return getValue(option).Bool(); +} + +int64_t IGameSettings::getInteger(EGameSettings option) const +{ + return getValue(option).Integer(); +} + +double IGameSettings::getDouble(EGameSettings option) const +{ + return getValue(option).Float(); +} + +std::vector IGameSettings::getVector(EGameSettings option) const +{ + return getValue(option).convertTo>(); +} + +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; + }; + + 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::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_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::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, + {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, + }; + + for(const auto & option : optionPath) + { + const JsonNode & optionValue = input[option.group][option.key]; + size_t index = static_cast(option.setting); + + if(optionValue.isNull()) + continue; + + JsonUtils::mergeCopy(gameSettings[index], optionValue); + } +} + +const JsonNode & GameSettings::getValue(EGameSettings option) const +{ + assert(option < EGameSettings::OPTIONS_COUNT); + auto index = static_cast(option); + + assert(!gameSettings[index].isNull()); + return gameSettings[index]; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.h b/lib/GameSettings.h index eae22cbda..750c9abab 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -1,104 +1,104 @@ -/* - * GameSettings.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 -{ - 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, - 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_USE_MONOLITH_TWO_WAY, - PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, - PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, - PATHFINDER_USE_WHIRLPOOL, - TOWNS_BUILDINGS_PER_TURN_CAP, - TOWNS_STARTING_DWELLING_CHANCES, - COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, - - 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; - -public: - GameSettings(); - ~GameSettings(); - - void load(const JsonNode & input); - const JsonNode & getValue(EGameSettings option) const override; - - template - void serialize(Handler & h, const int version) - { - h & gameSettings; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * GameSettings.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 +{ + 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, + 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_USE_MONOLITH_TWO_WAY, + PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, + PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, + PATHFINDER_USE_WHIRLPOOL, + TOWNS_BUILDINGS_PER_TURN_CAP, + TOWNS_STARTING_DWELLING_CHANCES, + COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, + + 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; + +public: + GameSettings(); + ~GameSettings(); + + void load(const JsonNode & input); + const JsonNode & getValue(EGameSettings option) const override; + + template + void serialize(Handler & h, const int version) + { + h & gameSettings; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IBonusTypeHandler.h b/lib/IBonusTypeHandler.h index 60914bace..7604da02a 100644 --- a/lib/IBonusTypeHandler.h +++ b/lib/IBonusTypeHandler.h @@ -1,28 +1,30 @@ -/* - * IBonusTypeHandler.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 IBonusBearer; -struct Bonus; - -///High level interface for BonusTypeHandler - -class DLL_LINKAGE IBonusTypeHandler -{ -public: - virtual ~IBonusTypeHandler() = default; - - virtual std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const = 0; - virtual std::string bonusToGraphics(const std::shared_ptr & bonus) const = 0; -}; - -VCMI_LIB_NAMESPACE_END +/* + * IBonusTypeHandler.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 IBonusBearer; +struct Bonus; + +///High level interface for BonusTypeHandler + +class DLL_LINKAGE IBonusTypeHandler +{ +public: + virtual ~IBonusTypeHandler() = default; + + virtual std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const = 0; + virtual ImagePath bonusToGraphics(const std::shared_ptr & bonus) const = 0; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 8c5fc9e42..f40aebcd8 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -1,292 +1,276 @@ -/* - * IGameCallback.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 "IGameCallback.h" - -#include "CHeroHandler.h" // for CHeroHandler -#include "spells/CSpellHandler.h"// for CSpell -#include "CSkillHandler.h"// for CSkill -#include "NetPacks.h" -#include "CBonusTypeHandler.h" -#include "CModHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "bonuses/CBonusSystemNode.h" -#include "bonuses/Limiters.h" -#include "bonuses/Propagators.h" -#include "bonuses/Updaters.h" - -#include "serializer/CSerializer.h" // for SAVEGAME_MAGIC -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" -#include "serializer/CLoadIntegrityValidator.h" -#include "rmg/CMapGenOptions.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "mapObjects/CObjectHandler.h" -#include "mapObjects/ObjectTemplate.h" -#include "campaign/CampaignState.h" -#include "StartInfo.h" -#include "gameState/CGameState.h" -#include "gameState/CGameStateCampaign.h" -#include "gameState/TavernHeroesPool.h" -#include "mapping/CMap.h" -#include "CPlayerState.h" -#include "GameSettings.h" -#include "ScriptHandler.h" -#include "RoadHandler.h" -#include "RiverHandler.h" -#include "TerrainHandler.h" - -#include "serializer/Connection.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const -{ - std::vector floors; - floors.reserve(gs->map->levels()); - for(int b = 0; b < gs->map->levels(); ++b) - { - floors.push_back(b); - } - const TerrainTile * tinfo = nullptr; - for (auto zd : floors) - { - for (int xd = 0; xd < gs->map->width; xd++) - { - 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 - tiles.emplace_back(xd, yd, zd); - } - } - } -} - -void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, - const int3 & pos, - int radious, - std::optional player, - int mode, - int3::EDistanceFormula distanceFormula) const -{ - if(!!player && *player >= PlayerColor::PLAYER_LIMIT) - { - logGlobal->error("Illegal call to getTilesInRange!"); - return; - } - if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map - getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE); - else - { - const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); - for (int xd = std::max(pos.x - radious , 0); xd <= std::min(pos.x + radious, gs->map->width - 1); xd++) - { - for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) - { - int3 tilePos(xd,yd,pos.z); - double distance = pos.dist(tilePos, distanceFormula); - - if(distance <= radious) - { - if(!player - || (mode == 1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) - || (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) - ) - tiles.insert(int3(xd,yd,pos.z)); - } - } - } - } -} - -void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const -{ - if(!!Player && *Player >= PlayerColor::PLAYER_LIMIT) - { - logGlobal->error("Illegal call to getAllTiles !"); - return; - } - - std::vector floors; - if(level == -1) - { - for(int b = 0; b < gs->map->levels(); ++b) - { - floors.push_back(b); - } - } - else - floors.push_back(level); - - for(auto zd: floors) - { - for(int xd = 0; xd < gs->map->width; xd++) - { - for(int yd = 0; yd < gs->map->height; yd++) - { - bool isTileEligible = false; - - switch(tileFilterMode) - { - case MapTerrainFilterMode::NONE: - isTileEligible = true; - break; - case MapTerrainFilterMode::WATER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater(); - break; - case MapTerrainFilterMode::LAND: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand(); - break; - case MapTerrainFilterMode::LAND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible(); - break; - case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible(); - break; - } - - if(isTileEligible) - tiles.insert(int3(xd, yd, zd)); - } - } - } -} - -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const -{ - for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]); - for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]); - - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]); -} - -void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) -{ - for (ui32 i = 0; i < gs->map->allowedSpells.size(); i++) //spellh size appears to be greater (?) - { - const spells::Spell * spell = SpellID(i).toSpell(); - - if (!isAllowed(0, spell->getIndex())) - continue; - - if (level.has_value() && spell->getLevel() != level) - continue; - - out.push_back(spell->getId()); - } -} - -CGameState * CPrivilegedInfoCallback::gameState() -{ - return gs; -} - -template -void CPrivilegedInfoCallback::loadCommonState(Loader & in) -{ - logGlobal->info("Loading lib part of game..."); - in.checkMagicBytes(SAVEGAME_MAGIC); - - CMapHeader dum; - StartInfo * si = nullptr; - - logGlobal->info("\tReading header"); - in.serializer & dum; - - logGlobal->info("\tReading options"); - in.serializer & si; - - logGlobal->info("\tReading handlers"); - in.serializer & *VLC; - - logGlobal->info("\tReading gamestate"); - in.serializer & gs; -} - -template -void CPrivilegedInfoCallback::saveCommonState(Saver & out) const -{ - logGlobal->info("Saving lib part of game..."); - out.putMagicBytes(SAVEGAME_MAGIC); - logGlobal->info("\tSaving header"); - out.serializer & static_cast(*gs->map); - logGlobal->info("\tSaving options"); - out.serializer & gs->scenarioOps; - logGlobal->info("\tSaving handlers"); - out.serializer & *VLC; - logGlobal->info("\tSaving gamestate"); - out.serializer & gs; -} - -// hardly memory usage for `-gdwarf-4` flag -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadIntegrityValidator &); -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)) - return nullptr; - return &gs->map->getTile(pos); -} - -CGHeroInstance * CNonConstInfoCallback::getHero(const ObjectInstanceID & objid) -{ - return const_cast(CGameInfoCallback::getHero(objid)); -} - -CGTownInstance * CNonConstInfoCallback::getTown(const ObjectInstanceID & objid) -{ - return const_cast(CGameInfoCallback::getTown(objid)); -} - -TeamState * CNonConstInfoCallback::getTeam(const TeamID & teamID) -{ - return const_cast(CGameInfoCallback::getTeam(teamID)); -} - -TeamState * CNonConstInfoCallback::getPlayerTeam(const PlayerColor & color) -{ - return const_cast(CGameInfoCallback::getPlayerTeam(color)); -} - -PlayerState * CNonConstInfoCallback::getPlayerState(const PlayerColor & color, bool verbose) -{ - return const_cast(CGameInfoCallback::getPlayerState(color, verbose)); -} - -CArtifactInstance * CNonConstInfoCallback::getArtInstance(const ArtifactInstanceID & aid) -{ - return gs->map->artInstances.at(aid.num); -} - -CGObjectInstance * CNonConstInfoCallback::getObjInstance(const ObjectInstanceID & oid) -{ - return gs->map->objects.at(oid.num); -} - -CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID & oid) -{ - return dynamic_cast(getObjInstance(oid)); -} - -bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) -{ - //only server knows - logGlobal->error("isVisitCoveredByAnotherQuery call on client side"); - return false; -} - -VCMI_LIB_NAMESPACE_END +/* + * IGameCallback.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 "IGameCallback.h" + +#include "CHeroHandler.h" // for CHeroHandler +#include "spells/CSpellHandler.h"// for CSpell +#include "CSkillHandler.h"// for CSkill +#include "CBonusTypeHandler.h" +#include "BattleFieldHandler.h" +#include "ObstacleHandler.h" +#include "bonuses/CBonusSystemNode.h" +#include "bonuses/Limiters.h" +#include "bonuses/Propagators.h" +#include "bonuses/Updaters.h" + +#include "serializer/CLoadFile.h" +#include "serializer/CSaveFile.h" +#include "rmg/CMapGenOptions.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "mapObjects/CObjectHandler.h" +#include "mapObjects/ObjectTemplate.h" +#include "campaign/CampaignState.h" +#include "StartInfo.h" +#include "gameState/CGameState.h" +#include "gameState/CGameStateCampaign.h" +#include "gameState/TavernHeroesPool.h" +#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" +#include "CPlayerState.h" +#include "GameSettings.h" +#include "ScriptHandler.h" +#include "RoadHandler.h" +#include "RiverHandler.h" +#include "TerrainHandler.h" + +#include "serializer/Connection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const +{ + std::vector floors; + floors.reserve(gs->map->levels()); + for(int b = 0; b < gs->map->levels(); ++b) + { + floors.push_back(b); + } + const TerrainTile * tinfo = nullptr; + for (auto zd : floors) + { + for (int xd = 0; xd < gs->map->width; xd++) + { + 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 + tiles.emplace_back(xd, yd, zd); + } + } + } +} + +void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, + const int3 & pos, + int radious, + ETileVisibility mode, + std::optional player, + int3::EDistanceFormula distanceFormula) const +{ + if(!!player && !player->isValidPlayer()) + { + logGlobal->error("Illegal call to getTilesInRange!"); + return; + } + if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map + getAllTiles (tiles, player, -1, [](auto * tile){return true;}); + else + { + const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); + for (int xd = std::max(pos.x - radious , 0); xd <= std::min(pos.x + radious, gs->map->width - 1); xd++) + { + for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) + { + int3 tilePos(xd,yd,pos.z); + int distance = pos.dist(tilePos, distanceFormula); + + 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) + ) + tiles.insert(int3(xd,yd,pos.z)); + } + } + } + } +} + +void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, std::function filter) const +{ + if(!!Player && !Player->isValidPlayer()) + { + logGlobal->error("Illegal call to getAllTiles !"); + return; + } + + std::vector floors; + if(level == -1) + { + for(int b = 0; b < gs->map->levels(); ++b) + { + floors.push_back(b); + } + } + else + floors.push_back(level); + + for(auto zd: floors) + { + for(int xd = 0; xd < gs->map->width; xd++) + { + for(int yd = 0; yd < gs->map->height; yd++) + { + int3 coordinates(xd, yd, zd); + if (filter(getTile(coordinates))) + tiles.insert(coordinates); + } + } + } +} + +void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) +{ + for (int j = 0; j < 3 ; j++) + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); + 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_MAJOR).toArtifact()); +} + +void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) +{ + for (ui32 i = 0; i < gs->map->allowedSpells.size(); i++) //spellh size appears to be greater (?) + { + const spells::Spell * spell = SpellID(i).toSpell(); + + if (!isAllowed(spell->getId())) + continue; + + if (level.has_value() && spell->getLevel() != level) + continue; + + out.push_back(spell->getId()); + } +} + +CGameState * CPrivilegedInfoCallback::gameState() +{ + return gs; +} + +template +void CPrivilegedInfoCallback::loadCommonState(Loader & in) +{ + logGlobal->info("Loading lib part of game..."); + in.checkMagicBytes(SAVEGAME_MAGIC); + + CMapHeader dum; + StartInfo * si = nullptr; + ActiveModsInSaveList activeMods; + + logGlobal->info("\tReading header"); + in.serializer & dum; + + logGlobal->info("\tReading options"); + in.serializer & si; + + logGlobal->info("\tReading mod list"); + in.serializer & activeMods; + + logGlobal->info("\tReading gamestate"); + in.serializer & gs; +} + +template +void CPrivilegedInfoCallback::saveCommonState(Saver & out) const +{ + ActiveModsInSaveList activeMods; + + logGlobal->info("Saving lib part of game..."); + out.putMagicBytes(SAVEGAME_MAGIC); + logGlobal->info("\tSaving header"); + out.serializer & static_cast(*gs->map); + logGlobal->info("\tSaving options"); + out.serializer & gs->scenarioOps; + logGlobal->info("\tSaving mod list"); + out.serializer & activeMods; + logGlobal->info("\tSaving gamestate"); + 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)) + return nullptr; + return &gs->map->getTile(pos); +} + +CGHeroInstance * CNonConstInfoCallback::getHero(const ObjectInstanceID & objid) +{ + return const_cast(CGameInfoCallback::getHero(objid)); +} + +CGTownInstance * CNonConstInfoCallback::getTown(const ObjectInstanceID & objid) +{ + return const_cast(CGameInfoCallback::getTown(objid)); +} + +TeamState * CNonConstInfoCallback::getTeam(const TeamID & teamID) +{ + return const_cast(CGameInfoCallback::getTeam(teamID)); +} + +TeamState * CNonConstInfoCallback::getPlayerTeam(const PlayerColor & color) +{ + return const_cast(CGameInfoCallback::getPlayerTeam(color)); +} + +PlayerState * CNonConstInfoCallback::getPlayerState(const PlayerColor & color, bool verbose) +{ + return const_cast(CGameInfoCallback::getPlayerState(color, verbose)); +} + +CArtifactInstance * CNonConstInfoCallback::getArtInstance(const ArtifactInstanceID & aid) +{ + return gs->map->artInstances.at(aid.num); +} + +CGObjectInstance * CNonConstInfoCallback::getObjInstance(const ObjectInstanceID & oid) +{ + return gs->map->objects.at(oid.num); +} + +CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID & oid) +{ + return dynamic_cast(getObjInstance(oid)); +} + +bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) +{ + //only server knows + logGlobal->error("isVisitCoveredByAnotherQuery call on client side"); + return false; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 747d71d72..076cfd804 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -1,189 +1,180 @@ -/* - * IGameCallback.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 "CGameInfoCallback.h" // for CGameInfoCallback -#include "CRandomGenerator.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct SetMovePoints; -struct GiveBonus; -struct BlockingDialog; -struct TeleportDialog; -class MetaString; -struct StackLocation; -struct ArtifactLocation; -class CCreatureSet; -class CStackBasicDescriptor; -class CGCreature; - -namespace spells -{ - class Caster; -} - -#if SCRIPTING_ENABLED -namespace scripting -{ - class Pool; -} -#endif - - -class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback -{ -public: - enum class MapTerrainFilterMode - { - NONE = 0, - LAND = 1, - WATER = 2, - LAND_CARTOGRAPHER = 3, - UNDERGROUND_CARTOGRAPHER = 4 - }; - - CGameState *gameState(); - - //used for random spawns - void getFreeTiles(std::vector &tiles) const; - - //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed - void getTilesInRange(std::unordered_set & tiles, - const int3 & pos, - int radious, - std::optional player = std::optional(), - int mode = 0, - int3::EDistanceFormula formula = int3::DIST_2D) const; - - //returns all tiles on given level (-1 - both levels, otherwise number of level) - void getAllTiles(std::unordered_set &tiles, std::optional player = std::optional(), - int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const; - - //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant - void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; - 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 -}; - -class DLL_LINKAGE IGameEventCallback -{ -public: - virtual void setObjProperty(ObjectInstanceID objid, int prop, si64 val) = 0; - - virtual void showInfoDialog(InfoWindow * iw) = 0; - virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; - - virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; - virtual bool removeObject(const CGObjectInstance * obj)=0; - virtual void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) = 0; - virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; - virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::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 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 showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) =0; - virtual void giveResource(PlayerColor player, GameResID which, int val)=0; - virtual void giveResources(PlayerColor player, TResources resources)=0; - - virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0; - virtual void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) =0; - virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0; - virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0; - virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack - virtual bool eraseStack(const StackLocation &sl, bool forceRemoval = false) =0; - virtual bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) =0; - virtual bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) =0; //makes new stack or increases count of already existing - virtual void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) =0; //merges army from src do dst or opens a garrison window - virtual bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) = 0; - - 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 giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) = 0; - virtual void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) = 0; - virtual void removeArtifact(const ArtifactLocation &al) = 0; - virtual bool moveArtifact(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 bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; - virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; - virtual void giveHeroBonus(GiveBonus * bonus)=0; - virtual void setMovePoints(SetMovePoints * smp)=0; - 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)=0; - virtual void sendAndApply(CPackForClient * pack) = 0; - virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map - virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; - virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; - - virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; -}; - -class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback -{ -public: - //keep const version of callback accessible - using CGameInfoCallback::getPlayerState; - using CGameInfoCallback::getTeam; - using CGameInfoCallback::getPlayerTeam; - using CGameInfoCallback::getHero; - using CGameInfoCallback::getTown; - using CGameInfoCallback::getTile; - using CGameInfoCallback::getArtInstance; - using CGameInfoCallback::getObjInstance; - - PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true); - TeamState * getTeam(const TeamID & teamID); //get team by team ID - TeamState * getPlayerTeam(const PlayerColor & color); // get team by player color - CGHeroInstance * getHero(const ObjectInstanceID & objid); - CGTownInstance * getTown(const ObjectInstanceID & objid); - TerrainTile * getTile(const int3 & pos); - CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid); - CGObjectInstance * getObjInstance(const ObjectInstanceID & oid); - CArmedInstance * getArmyInstance(const ObjectInstanceID & oid); - - virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; -}; - -/// Interface class for handling general game logic and actions -class DLL_LINKAGE IGameCallback : public CPrivilegedInfoCallback, public IGameEventCallback -{ -public: - virtual ~IGameCallback(){}; - -#if SCRIPTING_ENABLED - virtual scripting::Pool * getGlobalContextPool() const = 0; -#endif - - //get info - virtual bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero); - - friend struct CPack; - friend struct CPackForClient; - friend struct CPackForServer; -}; - - -VCMI_LIB_NAMESPACE_END +/* + * IGameCallback.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 "CGameInfoCallback.h" // for CGameInfoCallback +#include "CRandomGenerator.h" +#include "networkPacks/ObjProperty.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct SetMovePoints; +struct GiveBonus; +struct BlockingDialog; +struct TeleportDialog; +struct StackLocation; +struct ArtifactLocation; +class CCreatureSet; +class CStackBasicDescriptor; +class CGCreature; +enum class EOpenWindowMode : uint8_t; + +namespace spells +{ + class Caster; +} + +#if SCRIPTING_ENABLED +namespace scripting +{ + class Pool; +} +#endif + + +class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback +{ +public: + CGameState *gameState(); + + //used for random spawns + void getFreeTiles(std::vector &tiles) const; + + //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed + void getTilesInRange(std::unordered_set & tiles, + const int3 & pos, + int radius, + ETileVisibility mode, + std::optional player = std::optional(), + int3::EDistanceFormula formula = int3::DIST_2D) const; + + //returns all tiles on given level (-1 - both levels, otherwise number of level) + void getAllTiles(std::unordered_set &tiles, std::optional player, 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 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 +}; + +class DLL_LINKAGE IGameEventCallback +{ +public: + virtual void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) = 0; + virtual void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) = 0; + + virtual void showInfoDialog(InfoWindow * iw) = 0; + virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; + + virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=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 setOwner(const CGObjectInstance * objid, PlayerColor owner)=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 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; + virtual void giveResource(PlayerColor player, GameResID which, int val)=0; + virtual void giveResources(PlayerColor player, TResources resources)=0; + + virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0; + virtual void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) =0; + virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0; + virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0; + virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack + virtual bool eraseStack(const StackLocation &sl, bool forceRemoval = false) =0; + virtual bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) =0; + virtual bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) =0; //makes new stack or increases count of already existing + virtual void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) =0; //merges army from src do dst or opens a garrison window + virtual bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) = 0; + + 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 moveArtifact(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 bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; + virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; + virtual void giveHeroBonus(GiveBonus * bonus)=0; + virtual void setMovePoints(SetMovePoints * smp)=0; + 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 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 castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; +}; + +class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback +{ +public: + //keep const version of callback accessible + using CGameInfoCallback::getPlayerState; + using CGameInfoCallback::getTeam; + using CGameInfoCallback::getPlayerTeam; + using CGameInfoCallback::getHero; + using CGameInfoCallback::getTown; + using CGameInfoCallback::getTile; + using CGameInfoCallback::getArtInstance; + using CGameInfoCallback::getObjInstance; + + PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true); + TeamState * getTeam(const TeamID & teamID); //get team by team ID + TeamState * getPlayerTeam(const PlayerColor & color); // get team by player color + CGHeroInstance * getHero(const ObjectInstanceID & objid); + CGTownInstance * getTown(const ObjectInstanceID & objid); + TerrainTile * getTile(const int3 & pos); + CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid); + CGObjectInstance * getObjInstance(const ObjectInstanceID & oid); + CArmedInstance * getArmyInstance(const ObjectInstanceID & oid); + + virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; +}; + +/// Interface class for handling general game logic and actions +class DLL_LINKAGE IGameCallback : public CPrivilegedInfoCallback, public IGameEventCallback +{ +public: + virtual ~IGameCallback(){}; + +#if SCRIPTING_ENABLED + virtual scripting::Pool * getGlobalContextPool() const = 0; +#endif + + //get info + virtual bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero); + + friend struct CPack; + friend struct CPackForClient; + friend struct CPackForServer; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 8d85df35e..9ee40945a 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -1,142 +1,142 @@ -/* - * IGameEventsReceiver.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 "battle/BattleHex.h" -#include "GameConstants.h" -#include "int3.h" - -class CCallback; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; -class CCreature; -class CArmedInstance; -struct StackLocation; -struct TryMoveHero; -struct ArtifactLocation; -class CGHeroInstance; -class IShipyard; -class CGDwelling; -struct Component; -class CStackInstance; -class CGBlackMarket; -class CGObjectInstance; -struct Bonus; -class IMarket; -struct SetObjectProperty; -struct PackageApplied; -class BattleAction; -struct BattleStackAttacked; -struct BattleResult; -struct BattleSpellCast; -struct CatapultAttack; -class CStack; -class CCreatureSet; -struct BattleAttack; -struct SetStackEffect; -struct BattleTriggerEffect; -struct CObstacleInstance; -struct CPackForServer; -class EVictoryLossCheckResult; -class MetaString; -class ObstacleChanges; -class UnitChanges; - -class DLL_LINKAGE IBattleEventsReceiver -{ -public: - virtual void actionFinished(const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero - virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero - virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack - virtual void battleStacksAttacked(const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) - virtual void battleEnd(const BattleResult *br, QueryID queryID){}; - virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied; - virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - virtual void battleLogMessage(const std::vector & lines){}; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport){}; - virtual void battleSpellCast(const BattleSpellCast *sc){}; - virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks - virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects - virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start - virtual void battleStart(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 battleUnitsChanged(const std::vector & units){}; - virtual void battleObstaclesChanged(const std::vector & obstacles){}; - virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack - virtual void battleGateStateChanged(const EGateState state){}; -}; - -class DLL_LINKAGE IGameEventsReceiver -{ -public: - virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what){}; //what: 1 - built, 2 - demolished - - virtual void battleResultsApplied(){}; //called when all effects of last battle are applied - - virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2){}; - - //artifacts operations - virtual void artifactPut(const ArtifactLocation &al){}; - virtual void artifactRemoved(const ArtifactLocation &al){}; - 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 askToAssembleArtifact(const ArtifactLocation & dst) {}; - - virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start){}; - virtual void heroCreated(const CGHeroInstance*){}; - virtual void heroInGarrisonChange(const CGTownInstance *town){}; - virtual void heroMoved(const TryMoveHero & details, bool verbose = true){}; - virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val){}; - virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val){}; - virtual void heroManaPointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after spell casts - virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement - virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){}; - 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){} - 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 showPuzzleMap(){}; - virtual void viewWorldMap(){}; - virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor){}; - virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor){}; - virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor){}; - virtual void showTavernWindow(const CGObjectInstance *townOrTavern){}; - virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; - virtual void showQuestLog(){}; - virtual void advmapSpellCast(const CGHeroInstance * caster, int spellID){}; //called when a hero casts a spell - virtual void tileHidden(const std::unordered_set &pos){}; - virtual void tileRevealed(const std::unordered_set &pos){}; - virtual void newObject(const CGObjectInstance * obj){}; //eg. ship built in shipyard - virtual void availableArtifactsChanged(const CGBlackMarket *bm = nullptr){}; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) - virtual void centerView (int3 pos, int focusTime){}; - virtual void availableCreaturesChanged(const CGDwelling *town){}; - virtual void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it - virtual void playerBonusChanged(const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it - virtual void requestSent(const CPackForServer *pack, int requestID){}; - virtual void requestRealized(PackageApplied *pa){}; - virtual void beforeObjectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged - virtual void objectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged - virtual void objectRemoved(const CGObjectInstance *obj){}; //eg. collected resource, picked artifact, beaten hero - virtual void objectRemovedAfter(){}; //eg. collected resource, picked artifact, beaten hero - virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle - virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game - virtual void playerStartsTurn(PlayerColor player){}; - - //TODO shouldn't be moved down the tree? - virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){}; -}; - -VCMI_LIB_NAMESPACE_END +/* + * IGameEventsReceiver.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/EInfoWindowMode.h" +#include "battle/BattleHex.h" +#include "GameConstants.h" +#include "int3.h" + +class CCallback; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; +class CCreature; +class CArmedInstance; +struct StackLocation; +struct TryMoveHero; +struct ArtifactLocation; +class CGHeroInstance; +class IShipyard; +class CGDwelling; +struct Component; +class CStackInstance; +class CGBlackMarket; +class CGObjectInstance; +struct Bonus; +class IMarket; +struct SetObjectProperty; +struct PackageApplied; +class BattleAction; +struct BattleStackAttacked; +struct BattleResult; +struct BattleSpellCast; +struct CatapultAttack; +class CStack; +class CCreatureSet; +struct BattleAttack; +struct SetStackEffect; +struct BattleTriggerEffect; +struct CObstacleInstance; +struct CPackForServer; +class EVictoryLossCheckResult; +class MetaString; +class ObstacleChanges; +class UnitChanges; + +class DLL_LINKAGE IBattleEventsReceiver +{ +public: + virtual void actionFinished(const BattleID & battleID, const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero + virtual void actionStarted(const BattleID & battleID, const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){}; + virtual void battleNewRoundFirst(const BattleID & battleID){}; //called at the beginning of each turn before changes are applied; + virtual void battleNewRound(const BattleID & battleID){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + virtual void battleLogMessage(const BattleID & battleID, const std::vector & lines){}; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport){}; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){}; + 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 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 + virtual void battleGateStateChanged(const BattleID & battleID, const EGateState state){}; +}; + +class DLL_LINKAGE IGameEventsReceiver +{ +public: + virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what){}; //what: 1 - built, 2 - demolished + + virtual void battleResultsApplied(){}; //called when all effects of last battle are applied + + virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2){}; + + //artifacts operations + virtual void artifactPut(const ArtifactLocation &al){}; + virtual void artifactRemoved(const ArtifactLocation &al){}; + 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 askToAssembleArtifact(const ArtifactLocation & dst) {}; + + virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start){}; + virtual void heroCreated(const CGHeroInstance*){}; + virtual void heroInGarrisonChange(const CGTownInstance *town){}; + virtual void heroMoved(const TryMoveHero & details, bool verbose = true){}; + virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val){}; + virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val){}; + virtual void heroManaPointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after spell casts + virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement + virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){}; + 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 showPuzzleMap(){}; + virtual void viewWorldMap(){}; + 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){}; + virtual void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) {}; + virtual void showQuestLog(){}; + virtual void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID){}; //called when a hero casts a spell + virtual void tileHidden(const std::unordered_set &pos){}; + virtual void tileRevealed(const std::unordered_set &pos){}; + virtual void newObject(const CGObjectInstance * obj){}; //eg. ship built in shipyard + virtual void availableArtifactsChanged(const CGBlackMarket *bm = nullptr){}; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) + virtual void centerView (int3 pos, int focusTime){}; + virtual void availableCreaturesChanged(const CGDwelling *town){}; + virtual void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it + virtual void playerBonusChanged(const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it + virtual void requestSent(const CPackForServer *pack, int requestID){}; + virtual void requestRealized(PackageApplied *pa){}; + virtual void beforeObjectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged + virtual void objectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged + virtual void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator){}; //eg. collected resource, picked artifact, beaten hero + virtual void objectRemovedAfter(){}; //eg. collected resource, picked artifact, beaten hero + virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle + virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game + virtual void playerStartsTurn(PlayerColor player){}; + virtual void playerEndsTurn(PlayerColor player){}; + + //TODO shouldn't be moved down the tree? + virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){}; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IHandlerBase.cpp b/lib/IHandlerBase.cpp index 81475050b..1512c0680 100644 --- a/lib/IHandlerBase.cpp +++ b/lib/IHandlerBase.cpp @@ -10,18 +10,20 @@ #include "StdInc.h" #include "IHandlerBase.h" -#include "CModHandler.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" +#include "modding/CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN std::string IHandlerBase::getScopeBuiltin() { - return CModHandler::scopeBuiltin(); + return ModScope::scopeBuiltin(); } void IHandlerBase::registerObject(const std::string & scope, const std::string & type_name, const std::string & name, si32 index) { - return VLC->modh->identifiers.registerObject(scope, type_name, name, index); + return VLC->identifiersHandler->registerObject(scope, type_name, name, index); } VCMI_LIB_NAMESPACE_END diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index f513f3593..8bf602887 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -44,13 +44,6 @@ public: /// allows handler to do post-loading step for validation or integration of loaded data virtual void afterLoadFinalization(){}; - /** - * Gets a list of objects that are allowed by default on maps - * - * @return a list of allowed objects, the index is the object id - */ - virtual std::vector getDefaultAllowed() const = 0; - virtual ~IHandlerBase(){} }; @@ -123,7 +116,7 @@ public: if(index < 0 || index >= objects.size()) { logMod->error("%s id %d is invalid", getTypeNames()[0], index); - throw std::runtime_error("internal error"); + throw std::runtime_error("Attempt to access invalid index " + std::to_string(index) + " of type " + getTypeNames().front()); } return objects[index]; diff --git a/lib/Interprocess.h b/lib/Interprocess.h deleted file mode 100644 index cafe9666f..000000000 --- a/lib/Interprocess.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Interprocess.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 - -VCMI_LIB_NAMESPACE_BEGIN - -struct ServerReady -{ - bool ready; - uint16_t port; //ui16? - boost::interprocess::interprocess_mutex mutex; - boost::interprocess::interprocess_condition cond; - - ServerReady() - { - ready = false; - port = 0; - } - - void waitTillReady() - { - boost::interprocess::scoped_lock slock(mutex); - while(!ready) - { - cond.wait(slock); - } - } - - void setToReadyAndNotify(const uint16_t Port) - { - { - boost::unique_lock lock(mutex); - ready = true; - port = Port; - } - cond.notify_all(); - } -}; - -struct SharedMemory -{ - std::string name; - boost::interprocess::shared_memory_object smo; - boost::interprocess::mapped_region * mr; - ServerReady * sr; - - SharedMemory(const std::string & Name, bool initialize = false) - : name(Name) - { - if(initialize) - { - //if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it - boost::interprocess::shared_memory_object::remove(name.c_str()); - } - smo = boost::interprocess::shared_memory_object(boost::interprocess::open_or_create, name.c_str(), boost::interprocess::read_write); - smo.truncate(sizeof(ServerReady)); - mr = new boost::interprocess::mapped_region(smo,boost::interprocess::read_write); - if(initialize) - sr = new(mr->get_address())ServerReady(); - else - sr = reinterpret_cast(mr->get_address()); - }; - - ~SharedMemory() - { - delete mr; - boost::interprocess::shared_memory_object::remove(name.c_str()); - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index f6212256f..8eaee0a25 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -1,1256 +1,1268 @@ -/* - * JsonDetail.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 "JsonDetail.h" - -#include "VCMI_Lib.h" -#include "TextOperations.h" -#include "CModHandler.h" - -#include "filesystem/Filesystem.h" -#include "ScopeGuard.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static const JsonNode nullNode; - -template -void JsonWriter::writeContainer(Iterator begin, Iterator end) -{ - if (begin == end) - return; - - prefix += '\t'; - - writeEntry(begin++); - - while (begin != end) - { - out << (compactMode ? ", " : ",\n"); - writeEntry(begin++); - } - - out << (compactMode ? "" : "\n"); - prefix.resize(prefix.size()-1); -} - -void JsonWriter::writeEntry(JsonMap::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->second.meta.empty()) - out << prefix << " // " << entry->second.meta << "\n"; - if(!entry->second.flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->second.flags, ", ") << "\n"; - out << prefix; - } - writeString(entry->first); - out << " : "; - writeNode(entry->second); -} - -void JsonWriter::writeEntry(JsonVector::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->meta.empty()) - out << prefix << " // " << entry->meta << "\n"; - if(!entry->flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->flags, ", ") << "\n"; - out << prefix; - } - writeNode(*entry); -} - -void JsonWriter::writeString(const std::string &string) -{ - static const std::string escaped = "\"\\\b\f\n\r\t/"; - - static const std::array escaped_code = {'\"', '\\', 'b', 'f', 'n', 'r', 't', '/'}; - - out <<'\"'; - size_t pos = 0; - size_t start = 0; - for (; poswarn("File %s is not a valid JSON file!", fileName); - logMod->warn(errors); - } - return root; -} - -bool JsonParser::isValid() -{ - return errors.empty(); -} - -bool JsonParser::extractSeparator() -{ - if (!extractWhitespace()) - return false; - - if ( input[pos] !=':') - return error("Separator expected"); - - pos++; - return true; -} - -bool JsonParser::extractValue(JsonNode &node) -{ - if (!extractWhitespace()) - return false; - - switch (input[pos]) - { - case '\"': return extractString(node); - case 'n' : return extractNull(node); - case 't' : return extractTrue(node); - case 'f' : return extractFalse(node); - case '{' : return extractStruct(node); - case '[' : return extractArray(node); - case '-' : return extractFloat(node); - default: - { - if (input[pos] >= '0' && input[pos] <= '9') - return extractFloat(node); - return error("Value expected!"); - } - } -} - -bool JsonParser::extractWhitespace(bool verbose) -{ - while (true) - { - while(pos < input.size() && static_cast(input[pos]) <= ' ') - { - if (input[pos] == '\n') - { - lineCount++; - lineStart = pos+1; - } - pos++; - } - if (pos >= input.size() || input[pos] != '/') - break; - - pos++; - if (pos == input.size()) - break; - if (input[pos] == '/') - pos++; - else - error("Comments must consist from two slashes!", true); - - while (pos < input.size() && input[pos] != '\n') - pos++; - } - - if (pos >= input.size() && verbose) - return error("Unexpected end of file!"); - return true; -} - -bool JsonParser::extractEscaping(std::string &str) -{ - switch(input[pos]) - { - break; case '\"': str += '\"'; - break; case '\\': str += '\\'; - break; case 'b': str += '\b'; - break; case 'f': str += '\f'; - break; case 'n': str += '\n'; - break; case 'r': str += '\r'; - break; case 't': str += '\t'; - break; case '/': str += '/'; - break; default: return error("Unknown escape sequence!", true); - } - return true; -} - -bool JsonParser::extractString(std::string &str) -{ - if (input[pos] != '\"') - return error("String expected!"); - pos++; - - size_t first = pos; - - while (pos != input.size()) - { - if (input[pos] == '\"') // Correct end of string - { - str.append( &input[first], pos-first); - pos++; - return true; - } - if (input[pos] == '\\') // Escaping - { - str.append( &input[first], pos-first); - pos++; - if (pos == input.size()) - break; - extractEscaping(str); - first = pos + 1; - } - 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 - { - str.append( &input[first], pos-first); - first = pos+1; - error("Illegal character in the string!", true); - } - pos++; - } - return error("Unterminated string!"); -} - -bool JsonParser::extractString(JsonNode &node) -{ - std::string str; - if (!extractString(str)) - return false; - - node.setType(JsonNode::JsonType::DATA_STRING); - node.String() = str; - return true; -} - -bool JsonParser::extractLiteral(const std::string &literal) -{ - if (literal.compare(0, literal.size(), &input[pos], literal.size()) != 0) - { - while (pos < input.size() && ((input[pos]>'a' && input[pos]<'z') - || (input[pos]>'A' && input[pos]<'Z'))) - pos++; - return error("Unknown literal found", true); - } - - pos += literal.size(); - return true; -} - -bool JsonParser::extractNull(JsonNode &node) -{ - if (!extractLiteral("null")) - return false; - - node.clear(); - return true; -} - -bool JsonParser::extractTrue(JsonNode &node) -{ - if (!extractLiteral("true")) - return false; - - node.Bool() = true; - return true; -} - -bool JsonParser::extractFalse(JsonNode &node) -{ - if (!extractLiteral("false")) - return false; - - node.Bool() = false; - return true; -} - -bool JsonParser::extractStruct(JsonNode &node) -{ - node.setType(JsonNode::JsonType::DATA_STRUCT); - pos++; - - if (!extractWhitespace()) - return false; - - //Empty struct found - if (input[pos] == '}') - { - pos++; - return true; - } - - while (true) - { - if (!extractWhitespace()) - return false; - - std::string key; - if (!extractString(key)) - return false; - - // split key string into actual key and meta-flags - std::vector keyAndFlags; - boost::split(keyAndFlags, key, boost::is_any_of("#")); - key = keyAndFlags[0]; - // check for unknown flags - helps with debugging - std::vector knownFlags = { "override" }; - for(int i = 1; i < keyAndFlags.size(); i++) - { - if(!vstd::contains(knownFlags, keyAndFlags[i])) - error("Encountered unknown flag #" + keyAndFlags[i], true); - } - - if (node.Struct().find(key) != node.Struct().end()) - error("Dublicated element encountered!", true); - - if (!extractSeparator()) - return false; - - if (!extractElement(node.Struct()[key], '}')) - return false; - - // flags from key string belong to referenced element - for(int i = 1; i < keyAndFlags.size(); i++) - node.Struct()[key].flags.push_back(keyAndFlags[i]); - - if (input[pos] == '}') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractArray(JsonNode &node) -{ - pos++; - node.setType(JsonNode::JsonType::DATA_VECTOR); - - if (!extractWhitespace()) - return false; - - //Empty array found - if (input[pos] == ']') - { - pos++; - return true; - } - - while (true) - { - //NOTE: currently 50% of time is this vector resizing. - //May be useful to use list during parsing and then swap() all items to vector - node.Vector().resize(node.Vector().size()+1); - - if (!extractElement(node.Vector().back(), ']')) - return false; - - if (input[pos] == ']') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractElement(JsonNode &node, char terminator) -{ - if (!extractValue(node)) - return false; - - if (!extractWhitespace()) - return false; - - bool comma = (input[pos] == ','); - if (comma ) - { - pos++; - if (!extractWhitespace()) - return false; - } - - if (input[pos] == terminator) - { - //FIXME: MOD COMPATIBILITY: Too many of these right now, re-enable later - //if (comma) - //error("Extra comma found!", true); - return true; - } - - if (!comma) - error("Comma expected!", true); - - return true; -} - -bool JsonParser::extractFloat(JsonNode &node) -{ - assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); - bool negative=false; - double result=0; - si64 integerPart = 0; - bool isFloat = false; - - if (input[pos] == '-') - { - pos++; - negative = true; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Number expected!"); - - //Extract integer part - while (input[pos] >= '0' && input[pos] <= '9') - { - integerPart = integerPart*10+(input[pos]-'0'); - pos++; - } - - result = static_cast(integerPart); - - if (input[pos] == '.') - { - //extract fractional part - isFloat = true; - pos++; - double fractMult = 0.1; - if (input[pos] < '0' || input[pos] > '9') - return error("Decimal part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - result = result + fractMult*(input[pos]-'0'); - fractMult /= 10; - pos++; - } - } - - if(input[pos] == 'e') - { - //extract exponential part - pos++; - isFloat = true; - bool powerNegative = false; - double power = 0; - - if(input[pos] == '-') - { - pos++; - powerNegative = true; - } - else if(input[pos] == '+') - { - pos++; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Exponential part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - power = power*10 + (input[pos]-'0'); - pos++; - } - - if(powerNegative) - power = -power; - - result *= std::pow(10, power); - } - - if(isFloat) - { - if(negative) - result = -result; - - node.setType(JsonNode::JsonType::DATA_FLOAT); - node.Float() = result; - } - else - { - if(negative) - integerPart = -integerPart; - - node.setType(JsonNode::JsonType::DATA_INTEGER); - node.Integer() = integerPart; - } - - return true; -} - -bool JsonParser::error(const std::string &message, bool warning) -{ - std::ostringstream stream; - std::string type(warning?" warning: ":" error: "); - - stream << "At line " << lineCount << ", position "< stringToType = -{ - {"null", JsonNode::JsonType::DATA_NULL}, - {"boolean", JsonNode::JsonType::DATA_BOOL}, - {"number", JsonNode::JsonType::DATA_FLOAT}, - {"string", JsonNode::JsonType::DATA_STRING}, - {"array", JsonNode::JsonType::DATA_VECTOR}, - {"object", JsonNode::JsonType::DATA_STRUCT} -}; - -namespace -{ - namespace Common - { - std::string emptyCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - // check is not needed - e.g. incorporated into another check - return ""; - } - - std::string notImplementedCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data) - { - return "Not implemented entry in schema"; - } - - std::string schemaListCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data, - const std::string & errorMsg, - const std::function & isValid) - { - std::string errors = "\n"; - size_t result = 0; - - for(const auto & schemaEntry : schema.Vector()) - { - std::string error = check(schemaEntry, data, validator); - if (error.empty()) - { - result++; - } - else - { - errors += error; - errors += "\n"; - } - } - if (isValid(result)) - return ""; - else - return validator.makeErrorMessage(errorMsg) + errors; - } - - std::string allOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&](size_t count) - { - return count == schema.Vector().size(); - }); - } - - std::string anyOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [&](size_t count) - { - return count > 0; - }); - } - - std::string oneOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [&](size_t count) - { - return count == 1; - }); - } - - std::string notCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (check(schema, data, validator).empty()) - return validator.makeErrorMessage("Successful validation against negative check"); - return ""; - } - - std::string enumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for(const auto & enumEntry : schema.Vector()) - { - if (data == enumEntry) - return ""; - } - return validator.makeErrorMessage("Key must have one of predefined values"); - } - - std::string typeCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - const auto & typeName = schema.String(); - auto it = stringToType.find(typeName); - if(it == stringToType.end()) - { - return validator.makeErrorMessage("Unknown type in schema:" + typeName); - } - - JsonNode::JsonType type = it->second; - - //FIXME: hack for integer values - if(data.isNumber() && type == JsonNode::JsonType::DATA_FLOAT) - return ""; - - if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) - return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); - return ""; - } - - std::string refCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string URI = schema.String(); - //node must be validated using schema pointed by this reference and not by data here - //Local reference. Turn it into more easy to handle remote ref - if (boost::algorithm::starts_with(URI, "#")) - URI = validator.usedSchemas.back() + URI; - return check(URI, data, validator); - } - - std::string formatCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - auto formats = Validation::getKnownFormats(); - std::string errors; - auto checker = formats.find(schema.String()); - if (checker != formats.end()) - { - std::string result = checker->second(data); - if (!result.empty()) - errors += validator.makeErrorMessage(result); - } - else - errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); - return errors; - } - } - - namespace String - { - std::string maxLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); - return ""; - } - - std::string minLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); - return ""; - } - } - - namespace Number - { - - std::string maximumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMaximum"].Bool()) - { - if (data.Float() >= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - else - { - if (data.Float() > schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - return ""; - } - - std::string minimumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMinimum"].Bool()) - { - if (data.Float() <= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - else - { - if (data.Float() < schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - return ""; - } - - std::string multipleOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - double result = data.Float() / schema.Float(); - if (floor(result) != result) - return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); - return ""; - } - } - - namespace Vector - { - std::string itemEntryCheck(Validation::ValidationData & validator, const JsonVector & items, const JsonNode & schema, size_t index) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().Float() = static_cast(index); - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - if (!schema.isNull()) - return check(schema, items[index], validator); - return ""; - } - - std::string itemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for (size_t i=0; i i) - errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); - } - else - { - errors += itemEntryCheck(validator, data.Vector(), schema, i); - } - } - return errors; - } - - std::string additionalItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - // "items" is struct or empty (defaults to empty struct) - validation always successful - const JsonNode & items = baseSchema["items"]; - if (items.getType() != JsonNode::JsonType::DATA_VECTOR) - return ""; - - for (size_t i=items.Vector().size(); i schema.Float()) - return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string uniqueItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (schema.Bool()) - { - for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) - { - auto itB = itA; - while (++itB != schema.Vector().end()) - { - if (*itA == *itB) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - } - return ""; - } - } - - namespace Struct - { - std::string maxPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string minPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); - return ""; - } - - std::string uniquePropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) - { - auto itB = itA; - while (++itB != data.Struct().end()) - { - if (itA->second == itB->second) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - return ""; - } - - std::string requiredCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & required : schema.Vector()) - { - if (data[required.String()].isNull()) - errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); - } - return errors; - } - - std::string dependenciesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & deps : schema.Struct()) - { - if (!data[deps.first].isNull()) - { - if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) - { - JsonVector depList = deps.second.Vector(); - for(auto & depEntry : depList) - { - if (data[depEntry.String()].isNull()) - errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); - } - } - else - { - if (!check(deps.second, data, validator).empty()) - errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); - } - } - } - return errors; - } - - std::string propertyEntryCheck(Validation::ValidationData & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().String() = nodeName; - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - // there is schema specifically for this item - if (!schema.isNull()) - return check(schema, node, validator); - return ""; - } - - std::string propertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - - for(const auto & entry : data.Struct()) - errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); - return errors; - } - - std::string additionalPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & entry : data.Struct()) - { - if (baseSchema["properties"].Struct().count(entry.first) == 0) - { - // try generic additionalItems schema - if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) - errors += propertyEntryCheck(validator, entry.second, schema, entry.first); - - // 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); - } - } - return errors; - } - } - - namespace Formats - { - bool testFilePresence(const std::string & scope, const ResourceID & resource) - { - std::set allowedScopes; - if(scope != CModHandler::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies - { - //NOTE: recursive dependencies are not allowed at the moment - update code if this changes - bool found = true; - allowedScopes = VLC->modh->getModDependencies(scope, found); - - if(!found) - return false; - - allowedScopes.insert(CModHandler::scopeBuiltin()); // all mods can use H3 files - } - allowedScopes.insert(scope); // mods can use their own files - - for(const auto & entry : allowedScopes) - { - if (CResourceHandler::get(entry)->existsResource(resource)) - return true; - } - return false; - } - - #define TEST_FILE(scope, prefix, file, type) \ - if (testFilePresence(scope, ResourceID(prefix + file, type))) \ - return "" - - std::string testAnimation(const std::string & path, const std::string & scope) - { - TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); - TEST_FILE(scope, "Sprites/", path, EResType::TEXT); - return "Animation file \"" + path + "\" was not found"; - } - - std::string textFile(const JsonNode & node) - { - TEST_FILE(node.meta, "", node.String(), EResType::TEXT); - return "Text file \"" + node.String() + "\" was not found"; - } - - std::string musicFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Music/", node.String(), EResType::SOUND); - TEST_FILE(node.meta, "", node.String(), EResType::SOUND); - return "Music file \"" + node.String() + "\" was not found"; - } - - std::string soundFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Sounds/", node.String(), EResType::SOUND); - return "Sound file \"" + node.String() + "\" was not found"; - } - - std::string defFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string animationFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string imageFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Data/", node.String(), EResType::IMAGE); - TEST_FILE(node.meta, "Sprites/", node.String(), EResType::IMAGE); - if (node.String().find(':') != std::string::npos) - return testAnimation(node.String().substr(0, node.String().find(':')), node.meta); - return "Image file \"" + node.String() + "\" was not found"; - } - - std::string videoFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Video/", node.String(), EResType::VIDEO); - return "Video file \"" + node.String() + "\" was not found"; - } - - #undef TEST_FILE - } - - Validation::TValidatorMap createCommonFields() - { - Validation::TValidatorMap ret; - - ret["format"] = Common::formatCheck; - ret["allOf"] = Common::allOfCheck; - ret["anyOf"] = Common::anyOfCheck; - ret["oneOf"] = Common::oneOfCheck; - ret["enum"] = Common::enumCheck; - ret["type"] = Common::typeCheck; - ret["not"] = Common::notCheck; - ret["$ref"] = Common::refCheck; - - // fields that don't need implementation - ret["title"] = Common::emptyCheck; - ret["$schema"] = Common::emptyCheck; - ret["default"] = Common::emptyCheck; - ret["description"] = Common::emptyCheck; - ret["definitions"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createStringFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maxLength"] = String::maxLengthCheck; - ret["minLength"] = String::minLengthCheck; - - ret["pattern"] = Common::notImplementedCheck; - return ret; - } - - Validation::TValidatorMap createNumberFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maximum"] = Number::maximumCheck; - ret["minimum"] = Number::minimumCheck; - ret["multipleOf"] = Number::multipleOfCheck; - - ret["exclusiveMaximum"] = Common::emptyCheck; - ret["exclusiveMinimum"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createVectorFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["items"] = Vector::itemsCheck; - ret["minItems"] = Vector::minItemsCheck; - ret["maxItems"] = Vector::maxItemsCheck; - ret["uniqueItems"] = Vector::uniqueItemsCheck; - ret["additionalItems"] = Vector::additionalItemsCheck; - return ret; - } - - Validation::TValidatorMap createStructFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["additionalProperties"] = Struct::additionalPropertiesCheck; - ret["uniqueProperties"] = Struct::uniquePropertiesCheck; - ret["maxProperties"] = Struct::maxPropertiesCheck; - ret["minProperties"] = Struct::minPropertiesCheck; - ret["dependencies"] = Struct::dependenciesCheck; - ret["properties"] = Struct::propertiesCheck; - ret["required"] = Struct::requiredCheck; - - ret["patternProperties"] = Common::notImplementedCheck; - return ret; - } - - Validation::TFormatMap createFormatMap() - { - Validation::TFormatMap ret; - ret["textFile"] = Formats::textFile; - ret["musicFile"] = Formats::musicFile; - ret["soundFile"] = Formats::soundFile; - ret["defFile"] = Formats::defFile; - ret["animationFile"] = Formats::animationFile; - ret["imageFile"] = Formats::imageFile; - ret["videoFile"] = Formats::videoFile; - - return ret; - } -} - -namespace Validation -{ - std::string ValidationData::makeErrorMessage(const std::string &message) - { - std::string errors; - errors += "At "; - if (!currentPath.empty()) - { - for(const JsonNode &path : currentPath) - { - errors += "/"; - if (path.getType() == JsonNode::JsonType::DATA_STRING) - errors += path.String(); - else - errors += std::to_string(static_cast(path.Float())); - } - } - else - errors += ""; - errors += "\n\t Error: " + message + "\n"; - return errors; - } - - std::string check(const std::string & schemaName, const JsonNode & data) - { - ValidationData validator; - return check(schemaName, data, validator); - } - - std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator) - { - validator.usedSchemas.push_back(schemaName); - auto onscopeExit = vstd::makeScopeGuard([&]() - { - validator.usedSchemas.pop_back(); - }); - return check(JsonUtils::getSchema(schemaName), data, validator); - } - - std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator) - { - const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); - std::string errors; - for(const auto & entry : schema.Struct()) - { - auto checker = knownFields.find(entry.first); - if (checker != knownFields.end()) - errors += checker->second(validator, schema, entry.second, data); - //else - // errors += validator.makeErrorMessage("Unknown entry in schema " + entry.first); - } - return errors; - } - - const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type) - { - static const TValidatorMap commonFields = createCommonFields(); - static const TValidatorMap numberFields = createNumberFields(); - static const TValidatorMap stringFields = createStringFields(); - static const TValidatorMap vectorFields = createVectorFields(); - static const TValidatorMap structFields = createStructFields(); - - switch (type) - { - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - return numberFields; - case JsonNode::JsonType::DATA_STRING: return stringFields; - case JsonNode::JsonType::DATA_VECTOR: return vectorFields; - case JsonNode::JsonType::DATA_STRUCT: return structFields; - default: return commonFields; - } - } - - const TFormatMap & getKnownFormats() - { - static TFormatMap knownFormats = createFormatMap(); - return knownFormats; - } - -} // Validation namespace - -VCMI_LIB_NAMESPACE_END +/* + * JsonDetail.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 "JsonDetail.h" + +#include "VCMI_Lib.h" +#include "TextOperations.h" + +#include "filesystem/Filesystem.h" +#include "modding/ModScope.h" +#include "modding/CModHandler.h" +#include "ScopeGuard.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static const JsonNode nullNode; + +template +void JsonWriter::writeContainer(Iterator begin, Iterator end) +{ + if (begin == end) + return; + + prefix += '\t'; + + writeEntry(begin++); + + while (begin != end) + { + out << (compactMode ? ", " : ",\n"); + writeEntry(begin++); + } + + out << (compactMode ? "" : "\n"); + prefix.resize(prefix.size()-1); +} + +void JsonWriter::writeEntry(JsonMap::const_iterator entry) +{ + if(!compactMode) + { + if (!entry->second.meta.empty()) + out << prefix << " // " << entry->second.meta << "\n"; + if(!entry->second.flags.empty()) + out << prefix << " // flags: " << boost::algorithm::join(entry->second.flags, ", ") << "\n"; + out << prefix; + } + writeString(entry->first); + out << " : "; + writeNode(entry->second); +} + +void JsonWriter::writeEntry(JsonVector::const_iterator entry) +{ + if(!compactMode) + { + if (!entry->meta.empty()) + out << prefix << " // " << entry->meta << "\n"; + if(!entry->flags.empty()) + out << prefix << " // flags: " << boost::algorithm::join(entry->flags, ", ") << "\n"; + out << prefix; + } + writeNode(*entry); +} + +void JsonWriter::writeString(const std::string &string) +{ + static const std::string escaped = "\"\\\b\f\n\r\t/"; + + static const std::array escaped_code = {'\"', '\\', 'b', 'f', 'n', 'r', 't', '/'}; + + out <<'\"'; + size_t pos = 0; + size_t start = 0; + for (; poswarn("File %s is not a valid JSON file!", fileName); + logMod->warn(errors); + } + return root; +} + +bool JsonParser::isValid() +{ + return errors.empty(); +} + +bool JsonParser::extractSeparator() +{ + if (!extractWhitespace()) + return false; + + if ( input[pos] !=':') + return error("Separator expected"); + + pos++; + return true; +} + +bool JsonParser::extractValue(JsonNode &node) +{ + if (!extractWhitespace()) + return false; + + switch (input[pos]) + { + case '\"': return extractString(node); + case 'n' : return extractNull(node); + case 't' : return extractTrue(node); + case 'f' : return extractFalse(node); + case '{' : return extractStruct(node); + case '[' : return extractArray(node); + case '-' : return extractFloat(node); + default: + { + if (input[pos] >= '0' && input[pos] <= '9') + return extractFloat(node); + return error("Value expected!"); + } + } +} + +bool JsonParser::extractWhitespace(bool verbose) +{ + while (true) + { + while(pos < input.size() && static_cast(input[pos]) <= ' ') + { + if (input[pos] == '\n') + { + lineCount++; + lineStart = pos+1; + } + pos++; + } + if (pos >= input.size() || input[pos] != '/') + break; + + pos++; + if (pos == input.size()) + break; + if (input[pos] == '/') + pos++; + else + error("Comments must consist from two slashes!", true); + + while (pos < input.size() && input[pos] != '\n') + pos++; + } + + if (pos >= input.size() && verbose) + return error("Unexpected end of file!"); + return true; +} + +bool JsonParser::extractEscaping(std::string &str) +{ + switch(input[pos]) + { + break; case '\"': str += '\"'; + break; case '\\': str += '\\'; + break; case 'b': str += '\b'; + break; case 'f': str += '\f'; + break; case 'n': str += '\n'; + break; case 'r': str += '\r'; + break; case 't': str += '\t'; + break; case '/': str += '/'; + break; default: return error("Unknown escape sequence!", true); + } + return true; +} + +bool JsonParser::extractString(std::string &str) +{ + if (input[pos] != '\"') + return error("String expected!"); + pos++; + + size_t first = pos; + + while (pos != input.size()) + { + if (input[pos] == '\"') // Correct end of string + { + str.append( &input[first], pos-first); + pos++; + return true; + } + if (input[pos] == '\\') // Escaping + { + str.append( &input[first], pos-first); + pos++; + if (pos == input.size()) + break; + extractEscaping(str); + first = pos + 1; + } + 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 + { + str.append( &input[first], pos-first); + first = pos+1; + error("Illegal character in the string!", true); + } + pos++; + } + return error("Unterminated string!"); +} + +bool JsonParser::extractString(JsonNode &node) +{ + std::string str; + if (!extractString(str)) + return false; + + node.setType(JsonNode::JsonType::DATA_STRING); + node.String() = str; + return true; +} + +bool JsonParser::extractLiteral(const std::string &literal) +{ + if (literal.compare(0, literal.size(), &input[pos], literal.size()) != 0) + { + while (pos < input.size() && ((input[pos]>'a' && input[pos]<'z') + || (input[pos]>'A' && input[pos]<'Z'))) + pos++; + return error("Unknown literal found", true); + } + + pos += literal.size(); + return true; +} + +bool JsonParser::extractNull(JsonNode &node) +{ + if (!extractLiteral("null")) + return false; + + node.clear(); + return true; +} + +bool JsonParser::extractTrue(JsonNode &node) +{ + if (!extractLiteral("true")) + return false; + + node.Bool() = true; + return true; +} + +bool JsonParser::extractFalse(JsonNode &node) +{ + if (!extractLiteral("false")) + return false; + + node.Bool() = false; + return true; +} + +bool JsonParser::extractStruct(JsonNode &node) +{ + node.setType(JsonNode::JsonType::DATA_STRUCT); + pos++; + + if (!extractWhitespace()) + return false; + + //Empty struct found + if (input[pos] == '}') + { + pos++; + return true; + } + + while (true) + { + if (!extractWhitespace()) + return false; + + std::string key; + if (!extractString(key)) + return false; + + // split key string into actual key and meta-flags + std::vector keyAndFlags; + boost::split(keyAndFlags, key, boost::is_any_of("#")); + key = keyAndFlags[0]; + // check for unknown flags - helps with debugging + std::vector knownFlags = { "override" }; + for(int i = 1; i < keyAndFlags.size(); i++) + { + if(!vstd::contains(knownFlags, keyAndFlags[i])) + error("Encountered unknown flag #" + keyAndFlags[i], true); + } + + if (node.Struct().find(key) != node.Struct().end()) + error("Dublicated element encountered!", true); + + if (!extractSeparator()) + return false; + + if (!extractElement(node.Struct()[key], '}')) + return false; + + // flags from key string belong to referenced element + for(int i = 1; i < keyAndFlags.size(); i++) + node.Struct()[key].flags.push_back(keyAndFlags[i]); + + if (input[pos] == '}') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractArray(JsonNode &node) +{ + pos++; + node.setType(JsonNode::JsonType::DATA_VECTOR); + + if (!extractWhitespace()) + return false; + + //Empty array found + if (input[pos] == ']') + { + pos++; + return true; + } + + while (true) + { + //NOTE: currently 50% of time is this vector resizing. + //May be useful to use list during parsing and then swap() all items to vector + node.Vector().resize(node.Vector().size()+1); + + if (!extractElement(node.Vector().back(), ']')) + return false; + + if (input[pos] == ']') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractElement(JsonNode &node, char terminator) +{ + if (!extractValue(node)) + return false; + + if (!extractWhitespace()) + return false; + + bool comma = (input[pos] == ','); + if (comma ) + { + pos++; + if (!extractWhitespace()) + return false; + } + + if (input[pos] == terminator) + { + //FIXME: MOD COMPATIBILITY: Too many of these right now, re-enable later + //if (comma) + //error("Extra comma found!", true); + return true; + } + + if (!comma) + error("Comma expected!", true); + + return true; +} + +bool JsonParser::extractFloat(JsonNode &node) +{ + assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); + bool negative=false; + double result=0; + si64 integerPart = 0; + bool isFloat = false; + + if (input[pos] == '-') + { + pos++; + negative = true; + } + + if (input[pos] < '0' || input[pos] > '9') + return error("Number expected!"); + + //Extract integer part + while (input[pos] >= '0' && input[pos] <= '9') + { + integerPart = integerPart*10+(input[pos]-'0'); + pos++; + } + + result = static_cast(integerPart); + + if (input[pos] == '.') + { + //extract fractional part + isFloat = true; + pos++; + double fractMult = 0.1; + if (input[pos] < '0' || input[pos] > '9') + return error("Decimal part expected!"); + + while (input[pos] >= '0' && input[pos] <= '9') + { + result = result + fractMult*(input[pos]-'0'); + fractMult /= 10; + pos++; + } + } + + if(input[pos] == 'e') + { + //extract exponential part + pos++; + isFloat = true; + bool powerNegative = false; + double power = 0; + + if(input[pos] == '-') + { + pos++; + powerNegative = true; + } + else if(input[pos] == '+') + { + pos++; + } + + if (input[pos] < '0' || input[pos] > '9') + return error("Exponential part expected!"); + + while (input[pos] >= '0' && input[pos] <= '9') + { + power = power*10 + (input[pos]-'0'); + pos++; + } + + if(powerNegative) + power = -power; + + result *= std::pow(10, power); + } + + if(isFloat) + { + if(negative) + result = -result; + + node.setType(JsonNode::JsonType::DATA_FLOAT); + node.Float() = result; + } + else + { + if(negative) + integerPart = -integerPart; + + node.setType(JsonNode::JsonType::DATA_INTEGER); + node.Integer() = integerPart; + } + + return true; +} + +bool JsonParser::error(const std::string &message, bool warning) +{ + std::ostringstream stream; + std::string type(warning?" warning: ":" error: "); + + stream << "At line " << lineCount << ", position "< stringToType = +{ + {"null", JsonNode::JsonType::DATA_NULL}, + {"boolean", JsonNode::JsonType::DATA_BOOL}, + {"number", JsonNode::JsonType::DATA_FLOAT}, + {"string", JsonNode::JsonType::DATA_STRING}, + {"array", JsonNode::JsonType::DATA_VECTOR}, + {"object", JsonNode::JsonType::DATA_STRUCT} +}; + +namespace +{ + namespace Common + { + std::string emptyCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + // check is not needed - e.g. incorporated into another check + return ""; + } + + std::string notImplementedCheck(Validation::ValidationData & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data) + { + return "Not implemented entry in schema"; + } + + std::string schemaListCheck(Validation::ValidationData & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data, + const std::string & errorMsg, + const std::function & isValid) + { + std::string errors = "\n"; + size_t result = 0; + + for(const auto & schemaEntry : schema.Vector()) + { + std::string error = check(schemaEntry, data, validator); + if (error.empty()) + { + result++; + } + else + { + errors += error; + errors += "\n"; + } + } + if (isValid(result)) + return ""; + else + return validator.makeErrorMessage(errorMsg) + errors; + } + + std::string allOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&](size_t count) + { + return count == schema.Vector().size(); + }); + } + + std::string anyOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [&](size_t count) + { + return count > 0; + }); + } + + std::string oneOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [&](size_t count) + { + return count == 1; + }); + } + + std::string notCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (check(schema, data, validator).empty()) + return validator.makeErrorMessage("Successful validation against negative check"); + return ""; + } + + std::string enumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + for(const auto & enumEntry : schema.Vector()) + { + if (data == enumEntry) + return ""; + } + return validator.makeErrorMessage("Key must have one of predefined values"); + } + + std::string typeCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + const auto & typeName = schema.String(); + auto it = stringToType.find(typeName); + if(it == stringToType.end()) + { + return validator.makeErrorMessage("Unknown type in schema:" + typeName); + } + + JsonNode::JsonType type = it->second; + + //FIXME: hack for integer values + if(data.isNumber() && type == JsonNode::JsonType::DATA_FLOAT) + return ""; + + if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) + return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); + return ""; + } + + std::string refCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string URI = schema.String(); + //node must be validated using schema pointed by this reference and not by data here + //Local reference. Turn it into more easy to handle remote ref + if (boost::algorithm::starts_with(URI, "#")) + { + const std::string name = validator.usedSchemas.back(); + const std::string nameClean = name.substr(0, name.find('#')); + URI = nameClean + URI; + } + return check(URI, data, validator); + } + + std::string formatCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + auto formats = Validation::getKnownFormats(); + std::string errors; + auto checker = formats.find(schema.String()); + if (checker != formats.end()) + { + if (data.isString()) + { + std::string result = checker->second(data); + if (!result.empty()) + errors += validator.makeErrorMessage(result); + } + else + { + errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); + } + } + else + errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); + return errors; + } + } + + namespace String + { + std::string maxLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.String().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); + return ""; + } + + std::string minLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.String().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); + return ""; + } + } + + namespace Number + { + + std::string maximumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (baseSchema["exclusiveMaximum"].Bool()) + { + if (data.Float() >= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + } + else + { + if (data.Float() > schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + } + return ""; + } + + std::string minimumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (baseSchema["exclusiveMinimum"].Bool()) + { + if (data.Float() <= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + } + else + { + if (data.Float() < schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + } + return ""; + } + + std::string multipleOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + double result = data.Float() / schema.Float(); + if (floor(result) != result) + return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); + return ""; + } + } + + namespace Vector + { + std::string itemEntryCheck(Validation::ValidationData & validator, const JsonVector & items, const JsonNode & schema, size_t index) + { + validator.currentPath.emplace_back(); + validator.currentPath.back().Float() = static_cast(index); + auto onExit = vstd::makeScopeGuard([&]() + { + validator.currentPath.pop_back(); + }); + + if (!schema.isNull()) + return check(schema, items[index], validator); + return ""; + } + + std::string itemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for (size_t i=0; i i) + errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); + } + else + { + errors += itemEntryCheck(validator, data.Vector(), schema, i); + } + } + return errors; + } + + std::string additionalItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + // "items" is struct or empty (defaults to empty struct) - validation always successful + const JsonNode & items = baseSchema["items"]; + if (items.getType() != JsonNode::JsonType::DATA_VECTOR) + return ""; + + for (size_t i=items.Vector().size(); i schema.Float()) + return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); + return ""; + } + + std::string uniqueItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (schema.Bool()) + { + for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) + { + auto itB = itA; + while (++itB != schema.Vector().end()) + { + if (*itA == *itB) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + } + return ""; + } + } + + namespace Struct + { + std::string maxPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.Struct().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); + return ""; + } + + std::string minPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.Struct().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); + return ""; + } + + std::string uniquePropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) + { + auto itB = itA; + while (++itB != data.Struct().end()) + { + if (itA->second == itB->second) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + return ""; + } + + std::string requiredCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & required : schema.Vector()) + { + if (data[required.String()].isNull()) + errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); + } + return errors; + } + + std::string dependenciesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & deps : schema.Struct()) + { + if (!data[deps.first].isNull()) + { + if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) + { + JsonVector depList = deps.second.Vector(); + for(auto & depEntry : depList) + { + if (data[depEntry.String()].isNull()) + errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); + } + } + else + { + if (!check(deps.second, data, validator).empty()) + errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); + } + } + } + return errors; + } + + std::string propertyEntryCheck(Validation::ValidationData & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) + { + validator.currentPath.emplace_back(); + validator.currentPath.back().String() = nodeName; + auto onExit = vstd::makeScopeGuard([&]() + { + validator.currentPath.pop_back(); + }); + + // there is schema specifically for this item + if (!schema.isNull()) + return check(schema, node, validator); + return ""; + } + + std::string propertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + + for(const auto & entry : data.Struct()) + errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); + return errors; + } + + std::string additionalPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & entry : data.Struct()) + { + if (baseSchema["properties"].Struct().count(entry.first) == 0) + { + // try generic additionalItems schema + if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) + errors += propertyEntryCheck(validator, entry.second, schema, entry.first); + + // 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); + } + } + return errors; + } + } + + namespace Formats + { + bool testFilePresence(const std::string & scope, const ResourcePath & resource) + { + std::set allowedScopes; + if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies + { + //NOTE: recursive dependencies are not allowed at the moment - update code if this changes + bool found = true; + allowedScopes = VLC->modh->getModDependencies(scope, found); + + if(!found) + return false; + + allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files + } + allowedScopes.insert(scope); // mods can use their own files + + for(const auto & entry : allowedScopes) + { + if (CResourceHandler::get(entry)->existsResource(resource)) + return true; + } + return false; + } + + #define TEST_FILE(scope, prefix, file, type) \ + if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ + return "" + + std::string testAnimation(const std::string & path, const std::string & scope) + { + TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); + TEST_FILE(scope, "Sprites/", path, EResType::JSON); + return "Animation file \"" + path + "\" was not found"; + } + + std::string textFile(const JsonNode & node) + { + TEST_FILE(node.meta, "", node.String(), EResType::JSON); + return "Text file \"" + node.String() + "\" was not found"; + } + + std::string musicFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Music/", node.String(), EResType::SOUND); + TEST_FILE(node.meta, "", node.String(), EResType::SOUND); + return "Music file \"" + node.String() + "\" was not found"; + } + + std::string soundFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Sounds/", node.String(), EResType::SOUND); + return "Sound file \"" + node.String() + "\" was not found"; + } + + std::string defFile(const JsonNode & node) + { + return testAnimation(node.String(), node.meta); + } + + std::string animationFile(const JsonNode & node) + { + return testAnimation(node.String(), node.meta); + } + + std::string imageFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Data/", node.String(), EResType::IMAGE); + TEST_FILE(node.meta, "Sprites/", node.String(), EResType::IMAGE); + if (node.String().find(':') != std::string::npos) + return testAnimation(node.String().substr(0, node.String().find(':')), node.meta); + return "Image file \"" + node.String() + "\" was not found"; + } + + std::string videoFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Video/", node.String(), EResType::VIDEO); + return "Video file \"" + node.String() + "\" was not found"; + } + + #undef TEST_FILE + } + + Validation::TValidatorMap createCommonFields() + { + Validation::TValidatorMap ret; + + ret["format"] = Common::formatCheck; + ret["allOf"] = Common::allOfCheck; + ret["anyOf"] = Common::anyOfCheck; + ret["oneOf"] = Common::oneOfCheck; + ret["enum"] = Common::enumCheck; + ret["type"] = Common::typeCheck; + ret["not"] = Common::notCheck; + ret["$ref"] = Common::refCheck; + + // fields that don't need implementation + ret["title"] = Common::emptyCheck; + ret["$schema"] = Common::emptyCheck; + ret["default"] = Common::emptyCheck; + ret["description"] = Common::emptyCheck; + ret["definitions"] = Common::emptyCheck; + return ret; + } + + Validation::TValidatorMap createStringFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["maxLength"] = String::maxLengthCheck; + ret["minLength"] = String::minLengthCheck; + + ret["pattern"] = Common::notImplementedCheck; + return ret; + } + + Validation::TValidatorMap createNumberFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["maximum"] = Number::maximumCheck; + ret["minimum"] = Number::minimumCheck; + ret["multipleOf"] = Number::multipleOfCheck; + + ret["exclusiveMaximum"] = Common::emptyCheck; + ret["exclusiveMinimum"] = Common::emptyCheck; + return ret; + } + + Validation::TValidatorMap createVectorFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["items"] = Vector::itemsCheck; + ret["minItems"] = Vector::minItemsCheck; + ret["maxItems"] = Vector::maxItemsCheck; + ret["uniqueItems"] = Vector::uniqueItemsCheck; + ret["additionalItems"] = Vector::additionalItemsCheck; + return ret; + } + + Validation::TValidatorMap createStructFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["additionalProperties"] = Struct::additionalPropertiesCheck; + ret["uniqueProperties"] = Struct::uniquePropertiesCheck; + ret["maxProperties"] = Struct::maxPropertiesCheck; + ret["minProperties"] = Struct::minPropertiesCheck; + ret["dependencies"] = Struct::dependenciesCheck; + ret["properties"] = Struct::propertiesCheck; + ret["required"] = Struct::requiredCheck; + + ret["patternProperties"] = Common::notImplementedCheck; + return ret; + } + + Validation::TFormatMap createFormatMap() + { + Validation::TFormatMap ret; + ret["textFile"] = Formats::textFile; + ret["musicFile"] = Formats::musicFile; + ret["soundFile"] = Formats::soundFile; + ret["defFile"] = Formats::defFile; + ret["animationFile"] = Formats::animationFile; + ret["imageFile"] = Formats::imageFile; + ret["videoFile"] = Formats::videoFile; + + return ret; + } +} + +namespace Validation +{ + std::string ValidationData::makeErrorMessage(const std::string &message) + { + std::string errors; + errors += "At "; + if (!currentPath.empty()) + { + for(const JsonNode &path : currentPath) + { + errors += "/"; + if (path.getType() == JsonNode::JsonType::DATA_STRING) + errors += path.String(); + else + errors += std::to_string(static_cast(path.Float())); + } + } + else + errors += ""; + errors += "\n\t Error: " + message + "\n"; + return errors; + } + + std::string check(const std::string & schemaName, const JsonNode & data) + { + ValidationData validator; + return check(schemaName, data, validator); + } + + std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator) + { + validator.usedSchemas.push_back(schemaName); + auto onscopeExit = vstd::makeScopeGuard([&]() + { + validator.usedSchemas.pop_back(); + }); + return check(JsonUtils::getSchema(schemaName), data, validator); + } + + std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator) + { + const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); + std::string errors; + for(const auto & entry : schema.Struct()) + { + auto checker = knownFields.find(entry.first); + if (checker != knownFields.end()) + errors += checker->second(validator, schema, entry.second, data); + //else + // errors += validator.makeErrorMessage("Unknown entry in schema " + entry.first); + } + return errors; + } + + const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type) + { + static const TValidatorMap commonFields = createCommonFields(); + static const TValidatorMap numberFields = createNumberFields(); + static const TValidatorMap stringFields = createStringFields(); + static const TValidatorMap vectorFields = createVectorFields(); + static const TValidatorMap structFields = createStructFields(); + + switch (type) + { + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + return numberFields; + case JsonNode::JsonType::DATA_STRING: return stringFields; + case JsonNode::JsonType::DATA_VECTOR: return vectorFields; + case JsonNode::JsonType::DATA_STRUCT: return structFields; + default: return commonFields; + } + } + + const TFormatMap & getKnownFormats() + { + static TFormatMap knownFormats = createFormatMap(); + return knownFormats; + } + +} // Validation namespace + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index a06e2aa3d..d49f1e8b4 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1,1503 +1,1625 @@ -/* - * JsonNode.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 "JsonNode.h" - -#include "ScopeGuard.h" - -#include "bonuses/BonusParams.h" -#include "bonuses/Bonus.h" -#include "bonuses/Limiters.h" -#include "bonuses/Propagators.h" -#include "bonuses/Updaters.h" -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" //for identifier resolution -#include "CModHandler.h" -#include "CGeneralTextHandler.h" -#include "JsonDetail.h" -#include "StringConstants.h" -#include "battle/BattleHex.h" - -namespace -{ -// to avoid duplicating const and non-const code -template -Node & resolvePointer(Node & in, const std::string & pointer) -{ - if(pointer.empty()) - return in; - assert(pointer[0] == '/'); - - size_t splitPos = pointer.find('/', 1); - - std::string entry = pointer.substr(1, splitPos - 1); - std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); - - if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) - { - if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string - throw std::runtime_error("Invalid Json pointer"); - - if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed - throw std::runtime_error("Invalid Json pointer"); - - auto index = boost::lexical_cast(entry); - - if (in.Vector().size() > index) - return in.Vector()[index].resolvePointer(remainer); - } - return in[entry].resolvePointer(remainer); -} -} - -VCMI_LIB_NAMESPACE_BEGIN - -using namespace JsonDetail; - -class LibClasses; -class CModHandler; - -static const JsonNode nullNode; - -JsonNode::JsonNode(JsonType Type): - type(JsonType::DATA_NULL) -{ - setType(Type); -} - -JsonNode::JsonNode(const char *data, size_t datasize): - type(JsonType::DATA_NULL) -{ - JsonParser parser(data, datasize); - *this = parser.parse(""); -} - -JsonNode::JsonNode(ResourceID && fileURI): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const ResourceID & fileURI): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const std::string & idx, const ResourceID & fileURI): -type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(ResourceID && fileURI, bool &isValidSyntax): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); - isValidSyntax = parser.isValid(); -} - -JsonNode::JsonNode(const JsonNode ©): - type(JsonType::DATA_NULL), - meta(copy.meta), - flags(copy.flags) -{ - setType(copy.getType()); - switch(type) - { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: Bool() = copy.Bool(); - break; case JsonType::DATA_FLOAT: Float() = copy.Float(); - break; case JsonType::DATA_STRING: String() = copy.String(); - break; case JsonType::DATA_VECTOR: Vector() = copy.Vector(); - break; case JsonType::DATA_STRUCT: Struct() = copy.Struct(); - break; case JsonType::DATA_INTEGER:Integer() = copy.Integer(); - } -} - -JsonNode::~JsonNode() -{ - setType(JsonType::DATA_NULL); -} - -void JsonNode::swap(JsonNode &b) -{ - using std::swap; - swap(meta, b.meta); - swap(data, b.data); - swap(type, b.type); - swap(flags, b.flags); -} - -JsonNode & JsonNode::operator =(JsonNode node) -{ - swap(node); - return *this; -} - -bool JsonNode::operator == (const JsonNode &other) const -{ - if (getType() == other.getType()) - { - switch(type) - { - case JsonType::DATA_NULL: return true; - case JsonType::DATA_BOOL: return Bool() == other.Bool(); - case JsonType::DATA_FLOAT: return Float() == other.Float(); - case JsonType::DATA_STRING: return String() == other.String(); - case JsonType::DATA_VECTOR: return Vector() == other.Vector(); - case JsonType::DATA_STRUCT: return Struct() == other.Struct(); - case JsonType::DATA_INTEGER:return Integer()== other.Integer(); - } - } - return false; -} - -bool JsonNode::operator != (const JsonNode &other) const -{ - return !(*this == other); -} - -JsonNode::JsonType JsonNode::getType() const -{ - return type; -} - -void JsonNode::setMeta(const std::string & metadata, bool recursive) -{ - meta = metadata; - if (recursive) - { - switch (type) - { - break; case JsonType::DATA_VECTOR: - { - for(auto & node : Vector()) - { - node.setMeta(metadata); - } - } - break; case JsonType::DATA_STRUCT: - { - for(auto & node : Struct()) - { - node.second.setMeta(metadata); - } - } - } - } -} - -void JsonNode::setType(JsonType Type) -{ - if (type == Type) - return; - - //float<->int conversion - if(type == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) - { - si64 converted = static_cast(data.Float); - type = Type; - data.Integer = converted; - return; - } - else if(type == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) - { - auto converted = static_cast(data.Integer); - type = Type; - data.Float = converted; - return; - } - - //Reset node to nullptr - if (Type != JsonType::DATA_NULL) - setType(JsonType::DATA_NULL); - - switch (type) - { - break; case JsonType::DATA_STRING: delete data.String; - break; case JsonType::DATA_VECTOR: delete data.Vector; - break; case JsonType::DATA_STRUCT: delete data.Struct; - break; default: - break; - } - //Set new node type - type = Type; - switch(type) - { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: data.Bool = false; - break; case JsonType::DATA_FLOAT: data.Float = 0; - break; case JsonType::DATA_STRING: data.String = new std::string(); - break; case JsonType::DATA_VECTOR: data.Vector = new JsonVector(); - break; case JsonType::DATA_STRUCT: data.Struct = new JsonMap(); - break; case JsonType::DATA_INTEGER: data.Integer = 0; - } -} - -bool JsonNode::isNull() const -{ - return type == JsonType::DATA_NULL; -} - -bool JsonNode::isNumber() const -{ - return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT; -} - -bool JsonNode::isString() const -{ - return type == JsonType::DATA_STRING; -} - -bool JsonNode::isVector() const -{ - return type == JsonType::DATA_VECTOR; -} - -bool JsonNode::isStruct() const -{ - return type == JsonType::DATA_STRUCT; -} - -bool JsonNode::containsBaseData() const -{ - switch(type) - { - case JsonType::DATA_NULL: - return false; - case JsonType::DATA_STRUCT: - for(const auto & elem : *data.Struct) - { - if(elem.second.containsBaseData()) - return true; - } - return false; - default: - //other types (including vector) cannot be extended via merge - return true; - } -} - -bool JsonNode::isCompact() const -{ - switch(type) - { - case JsonType::DATA_VECTOR: - for(JsonNode & elem : *data.Vector) - { - if(!elem.isCompact()) - return false; - } - return true; - case JsonType::DATA_STRUCT: - { - auto propertyCount = data.Struct->size(); - if(propertyCount == 0) - return true; - else if(propertyCount == 1) - return data.Struct->begin()->second.isCompact(); - } - return false; - default: - return true; - } -} - -bool JsonNode::TryBoolFromString(bool & success) const -{ - success = true; - if(type == JsonNode::JsonType::DATA_BOOL) - return Bool(); - - success = type == JsonNode::JsonType::DATA_STRING; - if(success) - { - auto boolParamStr = String(); - boost::algorithm::trim(boolParamStr); - boost::algorithm::to_lower(boolParamStr); - success = boolParamStr == "true"; - - if(success) - return true; - - success = boolParamStr == "false"; - } - return false; -} - -void JsonNode::clear() -{ - setType(JsonType::DATA_NULL); -} - -bool & JsonNode::Bool() -{ - setType(JsonType::DATA_BOOL); - return data.Bool; -} - -double & JsonNode::Float() -{ - setType(JsonType::DATA_FLOAT); - return data.Float; -} - -si64 & JsonNode::Integer() -{ - setType(JsonType::DATA_INTEGER); - return data.Integer; -} - -std::string & JsonNode::String() -{ - setType(JsonType::DATA_STRING); - return *data.String; -} - -JsonVector & JsonNode::Vector() -{ - setType(JsonType::DATA_VECTOR); - return *data.Vector; -} - -JsonMap & JsonNode::Struct() -{ - setType(JsonType::DATA_STRUCT); - return *data.Struct; -} - -const bool boolDefault = false; -bool JsonNode::Bool() const -{ - if (type == JsonType::DATA_NULL) - return boolDefault; - assert(type == JsonType::DATA_BOOL); - return data.Bool; -} - -const double floatDefault = 0; -double JsonNode::Float() const -{ - if(type == JsonType::DATA_NULL) - return floatDefault; - else if(type == JsonType::DATA_INTEGER) - return static_cast(data.Integer); - - assert(type == JsonType::DATA_FLOAT); - return data.Float; -} - -const si64 integetDefault = 0; -si64 JsonNode::Integer() const -{ - if(type == JsonType::DATA_NULL) - return integetDefault; - else if(type == JsonType::DATA_FLOAT) - return static_cast(data.Float); - - assert(type == JsonType::DATA_INTEGER); - return data.Integer; -} - -const std::string stringDefault = std::string(); -const std::string & JsonNode::String() const -{ - if (type == JsonType::DATA_NULL) - return stringDefault; - assert(type == JsonType::DATA_STRING); - return *data.String; -} - -const JsonVector vectorDefault = JsonVector(); -const JsonVector & JsonNode::Vector() const -{ - if (type == JsonType::DATA_NULL) - return vectorDefault; - assert(type == JsonType::DATA_VECTOR); - return *data.Vector; -} - -const JsonMap mapDefault = JsonMap(); -const JsonMap & JsonNode::Struct() const -{ - if (type == JsonType::DATA_NULL) - return mapDefault; - assert(type == JsonType::DATA_STRUCT); - return *data.Struct; -} - -JsonNode & JsonNode::operator[](const std::string & child) -{ - return Struct()[child]; -} - -const JsonNode & JsonNode::operator[](const std::string & child) const -{ - auto it = Struct().find(child); - if (it != Struct().end()) - return it->second; - return nullNode; -} - -JsonNode & JsonNode::operator[](size_t child) -{ - if (child >= Vector().size() ) - Vector().resize(child + 1); - return Vector()[child]; -} - -const JsonNode & JsonNode::operator[](size_t child) const -{ - if (child < Vector().size() ) - return Vector()[child]; - - return nullNode; -} - -const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const -{ - return ::resolvePointer(*this, jsonPointer); -} - -JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) -{ - return ::resolvePointer(*this, jsonPointer); -} - -std::string JsonNode::toJson(bool compact) const -{ - std::ostringstream out; - JsonWriter writer(out, compact); - writer.writeNode(*this); - return out.str(); -} - -///JsonUtils - -void JsonUtils::parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest) -{ - dest->val = static_cast(source[1].Float()); - resolveIdentifier(source[2],dest->subtype); - dest->additionalInfo = static_cast(source[3].Float()); - dest->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) - dest->turnsRemain = 0; -} - -std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) -{ - auto b = std::make_shared(); - std::string type = ability_vec[0].String(); - auto it = bonusNameMap.find(type); - if (it == bonusNameMap.end()) - { - logMod->error("Error: invalid ability type %s.", type); - return b; - } - b->type = it->second; - - parseTypedBonusShort(ability_vec, b); - return b; -} - -template -const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) -{ - static T defaultValue = T(); - if (!val->isNull()) - { - const std::string & type = val->String(); - auto it = map.find(type); - if (it == map.end()) - { - logMod->error("Error: invalid %s%s.", err, type); - return defaultValue; - } - else - { - return it->second; - } - } - else - return defaultValue; -} - -template -const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) -{ - if(val->isNumber()) - return static_cast(val->Integer()); - else - return parseByMap(map, val, err); -} - -void JsonUtils::resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name) -{ - const JsonNode &value = node[name]; - if (!value.isNull()) - { - switch (value.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(value.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(value.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(value, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for value of %s.", name); - } - } -} - -void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) -{ - const JsonNode & value = node["addInfo"]; - if (!value.isNull()) - { - switch (value.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(value.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(value.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(value, [&](si32 identifier) - { - var = identifier; - }); - break; - case JsonNode::JsonType::DATA_VECTOR: - { - const JsonVector & vec = value.Vector(); - var.resize(vec.size()); - for(int i = 0; i < vec.size(); i++) - { - switch(vec[i].getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var[i] = static_cast(vec[i].Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var[i] = static_cast(vec[i].Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(vec[i], [&var,i](si32 identifier) - { - var[i] = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for value of addInfo[%d].", i); - } - } - break; - } - default: - logMod->error("Error! Wrong identifier used for value of addInfo."); - } - } -} - -void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var) -{ - switch (node.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(node.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(node.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(node, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for identifier!"); - } -} - -std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) -{ - switch(limiter.getType()) - { - case JsonNode::JsonType::DATA_VECTOR: - { - const JsonVector & subLimiters = limiter.Vector(); - if(subLimiters.empty()) - { - logMod->warn("Warning: empty limiter list"); - return std::make_shared(); - } - std::shared_ptr result; - int offset = 1; - // determine limiter type and offset for sub-limiters - if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING) - { - const std::string & aggregator = subLimiters[0].String(); - if(aggregator == AllOfLimiter::aggregator) - result = std::make_shared(); - else if(aggregator == AnyOfLimiter::aggregator) - result = std::make_shared(); - else if(aggregator == NoneOfLimiter::aggregator) - result = std::make_shared(); - } - if(!result) - { - // collapse for single limiter without explicit aggregate operator - if(subLimiters.size() == 1) - return parseLimiter(subLimiters[0]); - // implicit aggregator must be allOf - result = std::make_shared(); - offset = 0; - } - if(subLimiters.size() == offset) - logMod->warn("Warning: empty sub-limiter list"); - for(int sl = offset; sl < subLimiters.size(); ++sl) - result->add(parseLimiter(subLimiters[sl])); - return result; - } - break; - case JsonNode::JsonType::DATA_STRING: //pre-defined limiters - return parseByMap(bonusLimiterMap, &limiter, "limiter type "); - break; - case JsonNode::JsonType::DATA_STRUCT: //customizable limiters - { - std::string limiterType = limiter["type"].String(); - const JsonVector & parameters = limiter["parameters"].Vector(); - if(limiterType == "CREATURE_TYPE_LIMITER") - { - std::shared_ptr creatureLimiter = std::make_shared(); - VLC->modh->identifiers.requestIdentifier("creature", parameters[0], [=](si32 creature) - { - creatureLimiter->setCreature(CreatureID(creature)); - }); - auto includeUpgrades = false; - - if(parameters.size() > 1) - { - bool success = true; - includeUpgrades = parameters[1].TryBoolFromString(success); - - if(!success) - logMod->error("Second parameter of '%s' limiter should be Bool", limiterType); - } - creatureLimiter->includeUpgrades = includeUpgrades; - return creatureLimiter; - } - else if(limiterType == "HAS_ANOTHER_BONUS_LIMITER") - { - std::string anotherBonusType = parameters[0].String(); - auto it = bonusNameMap.find(anotherBonusType); - if(it == bonusNameMap.end()) - { - logMod->error("Error: invalid ability type %s.", anotherBonusType); - } - else - { - std::shared_ptr bonusLimiter = std::make_shared(); - bonusLimiter->type = it->second; - auto findSource = [&](const JsonNode & parameter) - { - if(parameter.getType() == JsonNode::JsonType::DATA_STRUCT) - { - auto sourceIt = bonusSourceMap.find(parameter["type"].String()); - if(sourceIt != bonusSourceMap.end()) - { - bonusLimiter->source = sourceIt->second; - bonusLimiter->isSourceRelevant = true; - if(!parameter["id"].isNull()) { - resolveIdentifier(parameter["id"], bonusLimiter->sid); - bonusLimiter->isSourceIDRelevant = true; - } - } - } - return false; - }; - if(parameters.size() > 1) - { - if(findSource(parameters[1]) && parameters.size() == 2) - return bonusLimiter; - else - { - resolveIdentifier(parameters[1], bonusLimiter->subtype); - bonusLimiter->isSubtypeRelevant = true; - if(parameters.size() > 2) - findSource(parameters[2]); - } - } - return bonusLimiter; - } - } - else if(limiterType == "CREATURE_ALIGNMENT_LIMITER") - { - int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, parameters[0].String()); - if(alignment == -1) - logMod->error("Error: invalid alignment %s.", parameters[0].String()); - else - return std::make_shared(static_cast(alignment)); - } - else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat - { - std::shared_ptr factionLimiter = std::make_shared(); - VLC->modh->identifiers.requestIdentifier("faction", parameters[0], [=](si32 faction) - { - factionLimiter->faction = FactionID(faction); - }); - return factionLimiter; - } - else if(limiterType == "CREATURE_LEVEL_LIMITER") - { - auto levelLimiter = std::make_shared(); - if(!parameters.empty()) //If parameters is empty, level limiter works as CREATURES_ONLY limiter - { - levelLimiter->minLevel = parameters[0].Integer(); - if(parameters[1].isNumber()) - levelLimiter->maxLevel = parameters[1].Integer(); - } - return levelLimiter; - } - else if(limiterType == "CREATURE_TERRAIN_LIMITER") - { - std::shared_ptr terrainLimiter = std::make_shared(); - if(!parameters.empty()) - { - VLC->modh->identifiers.requestIdentifier("terrain", parameters[0], [=](si32 terrain) - { - //TODO: support limiters - //terrainLimiter->terrainType = terrain; - }); - } - return terrainLimiter; - } - else if(limiterType == "UNIT_ON_HEXES") { - auto hexLimiter = std::make_shared(); - if(!parameters.empty()) - { - for (const auto & parameter: parameters){ - if(parameter.isNumber()) - hexLimiter->applicableHexes.insert(BattleHex(parameter.Integer())); - } - } - return hexLimiter; - } - else - { - logMod->error("Error: invalid customizable limiter type %s.", limiterType); - } - } - break; - default: - break; - } - return nullptr; -} - -std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) -{ - auto b = std::make_shared(); - if (!parseBonus(ability, b.get())) - { - // caller code can not handle this case and presumes that returned bonus is always valid - logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); - - b->type = BonusType::NONE; - assert(0); // or throw? Game *should* work with dummy bonus - - return b; - } - return b; -} - -std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description) -{ - /* duration = BonusDuration::PERMANENT - source = BonusSource::TOWN_STRUCTURE - bonusType, val, subtype - get from json - */ - auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description, -1); - - if(!parseBonus(ability, b.get())) - return nullptr; - return b; -} - -static BonusParams convertDeprecatedBonus(const JsonNode &ability) -{ - if(vstd::contains(deprecatedBonusSet, ability["type"].String())) - { - logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toJson()); - auto params = BonusParams(ability["type"].String(), - ability["subtype"].isString() ? ability["subtype"].String() : "", - ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); - if(params.isConverted) - { - if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special - { - params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; - params.targetType = BonusSource::SECONDARY_SKILL; - } - - logMod->warn("Please, use this bonus:\n%s\nConverted sucessfully!", params.toJson().toJson()); - return params; - } - else - logMod->error("Cannot convert bonus!\n%s", ability.toJson()); - } - BonusParams ret; - ret.isConverted = false; - return ret; -} - -static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) -{ - switch(updaterJson.getType()) - { - case JsonNode::JsonType::DATA_STRING: - return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); - break; - case JsonNode::JsonType::DATA_STRUCT: - if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") - { - std::shared_ptr updater = std::make_shared(); - const JsonVector param = updaterJson["parameters"].Vector(); - updater->valPer20 = static_cast(param[0].Integer()); - if(param.size() > 1) - updater->stepSize = static_cast(param[1].Integer()); - return updater; - } - else if (updaterJson["type"].String() == "ARMY_MOVEMENT") - { - std::shared_ptr updater = std::make_shared(); - if(updaterJson["parameters"].isVector()) - { - const auto & param = updaterJson["parameters"].Vector(); - if(param.size() < 4) - logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); - else - { - updater->base = static_cast(param.at(0).Integer()); - updater->divider = static_cast(param.at(1).Integer()); - updater->multiplier = static_cast(param.at(2).Integer()); - updater->max = static_cast(param.at(3).Integer()); - } - return updater; - } - } - else - logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); - break; - } - return nullptr; -} - -bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) -{ - const JsonNode * value = nullptr; - - std::string type = ability["type"].String(); - auto it = bonusNameMap.find(type); - auto params = std::make_unique(false); - if (it == bonusNameMap.end()) - { - params = std::make_unique(convertDeprecatedBonus(ability)); - if(!params->isConverted) - { - logMod->error("Error: invalid ability type %s.", type); - return false; - } - b->type = params->type; - b->val = params->val.value_or(0); - b->valType = params->valueType.value_or(BonusValueType::ADDITIVE_VALUE); - if(params->targetType) - b->targetSourceType = params->targetType.value(); - } - else - b->type = it->second; - - resolveIdentifier(b->subtype, params->isConverted ? params->toJson() : ability, "subtype"); - - if(!params->isConverted) - { - b->val = static_cast(ability["val"].Float()); - - value = &ability["valueType"]; - if (!value->isNull()) - b->valType = static_cast(parseByMapN(bonusValueMap, value, "value type ")); - } - - b->stacking = ability["stacking"].String(); - - resolveAddInfo(b->additionalInfo, ability); - - b->turnsRemain = static_cast(ability["turns"].Float()); - - b->sid = static_cast(ability["sourceID"].Float()); - - if(!ability["description"].isNull()) - { - if (ability["description"].isString()) - b->description = ability["description"].String(); - if (ability["description"].isNumber()) - b->description = VLC->generaltexth->translate("core.arraytxt", ability["description"].Integer()); - } - - value = &ability["effectRange"]; - if (!value->isNull()) - b->effectRange = static_cast(parseByMapN(bonusLimitEffect, value, "effect range ")); - - value = &ability["duration"]; - if (!value->isNull()) - { - switch (value->getType()) - { - case JsonNode::JsonType::DATA_STRING: - b->duration = parseByMap(bonusDurationMap, value, "duration type "); - break; - case JsonNode::JsonType::DATA_VECTOR: - { - BonusDuration::Type dur = 0; - for (const JsonNode & d : value->Vector()) - dur |= parseByMapN(bonusDurationMap, &d, "duration type "); - b->duration = dur; - } - break; - default: - logMod->error("Error! Wrong bonus duration format."); - } - } - - value = &ability["sourceType"]; - if (!value->isNull()) - b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); - - value = &ability["targetSourceType"]; - if (!value->isNull()) - b->targetSourceType = static_cast(parseByMap(bonusSourceMap, value, "target type ")); - - value = &ability["limiters"]; - if (!value->isNull()) - b->limiter = parseLimiter(*value); - - value = &ability["propagator"]; - if (!value->isNull()) - { - //ALL_CREATURES old propagator compatibility - if(value->String() == "ALL_CREATURES") - { - logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter"); - b->addLimiter(std::make_shared()); - b->propagator = bonusPropagatorMap.at("GLOBAL_EFFECT"); - } - else - b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type "); - } - - value = &ability["updater"]; - if(!value->isNull()) - b->addUpdater(parseUpdater(*value)); - value = &ability["propagationUpdater"]; - if(!value->isNull()) - b->propagationUpdater = parseUpdater(*value); - return true; -} - -CSelector JsonUtils::parseSelector(const JsonNode & ability) -{ - CSelector ret = Selector::all; - - // Recursive parsers for anyOf, allOf, noneOf - const auto * value = &ability["allOf"]; - if(value->isVector()) - { - for(const auto & andN : value->Vector()) - ret = ret.And(parseSelector(andN)); - } - - value = &ability["anyOf"]; - if(value->isVector()) - { - CSelector base = Selector::none; - for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); - - ret = ret.And(base); - } - - value = &ability["noneOf"]; - if(value->isVector()) - { - CSelector base = Selector::none; - for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); - - ret = ret.And(base.Not()); - } - - // Actual selector parser - value = &ability["type"]; - if(value->isString()) - { - auto it = bonusNameMap.find(value->String()); - if(it != bonusNameMap.end()) - ret = ret.And(Selector::type()(it->second)); - } - value = &ability["subtype"]; - if(!value->isNull()) - { - TBonusSubtype subtype; - resolveIdentifier(subtype, ability, "subtype"); - ret = ret.And(Selector::subtype()(subtype)); - } - value = &ability["sourceType"]; - std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized - std::optional id = std::nullopt; - if(value->isString()) - { - auto it = bonusSourceMap.find(value->String()); - if(it != bonusSourceMap.end()) - src = it->second; - } - - value = &ability["sourceID"]; - if(!value->isNull()) - { - id = -1; - resolveIdentifier(*id, ability, "sourceID"); - } - - if(src && id) - ret = ret.And(Selector::source(*src, *id)); - else if(src) - ret = ret.And(Selector::sourceTypeSel(*src)); - - - value = &ability["targetSourceType"]; - if(value->isString()) - { - auto it = bonusSourceMap.find(value->String()); - if(it != bonusSourceMap.end()) - ret = ret.And(Selector::targetSourceType()(it->second)); - } - value = &ability["valueType"]; - if(value->isString()) - { - auto it = bonusValueMap.find(value->String()); - if(it != bonusValueMap.end()) - ret = ret.And(Selector::valueType(it->second)); - } - CAddInfo info; - value = &ability["addInfo"]; - if(!value->isNull()) - { - resolveAddInfo(info, ability["addInfo"]); - ret = ret.And(Selector::info()(info)); - } - value = &ability["effectRange"]; - if(value->isString()) - { - auto it = bonusLimitEffect.find(value->String()); - if(it != bonusLimitEffect.end()) - ret = ret.And(Selector::effectRange()(it->second)); - } - value = &ability["lastsTurns"]; - if(value->isNumber()) - ret = ret.And(Selector::turns(value->Integer())); - value = &ability["lastsDays"]; - if(value->isNumber()) - ret = ret.And(Selector::days(value->Integer())); - - return ret; -} - -//returns first Key with value equal to given one -template -Key reverseMapFirst(const Val & val, const std::map & map) -{ - for(auto it : map) - { - if(it.second == val) - { - return it.first; - } - } - assert(0); - return ""; -} - -static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) -{ - const JsonNode & fieldProps = schema["properties"][fieldName]; - -#if defined(VCMI_IOS) - if (!fieldProps["defaultIOS"].isNull()) - return fieldProps["defaultIOS"]; -#elif defined(VCMI_ANDROID) - if (!fieldProps["defaultAndroid"].isNull()) - return fieldProps["defaultAndroid"]; -#elif !defined(VCMI_MOBILE) - if (!fieldProps["defaultDesktop"].isNull()) - return fieldProps["defaultDesktop"]; -#endif - return fieldProps["default"]; -} - -static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) -{ - assert(schema["type"].String() == "object"); - - std::set foundEntries; - - for(const auto & entry : schema["required"].Vector()) - foundEntries.insert(entry.String()); - - vstd::erase_if(node.Struct(), [&](const auto & node){ - return !vstd::contains(foundEntries, node.first); - }); -} - -static void minimizeNode(JsonNode & node, const JsonNode & schema) -{ - if (schema["type"].String() != "object") - return; - - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - minimizeNode(node[name], schema["properties"][name]); - - if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) - node.Struct().erase(name); - } - eraseOptionalNodes(node, schema); -} - -static void maximizeNode(JsonNode & node, const JsonNode & schema) -{ - // "required" entry can only be found in object/struct - if (schema["type"].String() != "object") - return; - - // check all required entries that have default version - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - - if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) - node[name] = getDefaultValue(schema, name); - - maximizeNode(node[name], schema["properties"][name]); - } - - eraseOptionalNodes(node, schema); -} - -void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) -{ - minimizeNode(node, getSchema(schemaName)); -} - -void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) -{ - maximizeNode(node, getSchema(schemaName)); -} - -bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) -{ - std::string log = Validation::check(schemaName, node); - if (!log.empty()) - { - logMod->warn("Data in %s is invalid!", dataName); - logMod->warn(log); - logMod->trace("%s json: %s", dataName, node.toJson(true)); - } - return log.empty(); -} - -const JsonNode & getSchemaByName(const std::string & name) -{ - // cached schemas to avoid loading json data multiple times - static std::map loadedSchemas; - - if (vstd::contains(loadedSchemas, name)) - return loadedSchemas[name]; - - std::string filename = "config/schemas/" + name; - - if (CResourceHandler::get()->existsResource(ResourceID(filename))) - { - loadedSchemas[name] = JsonNode(ResourceID(filename)); - return loadedSchemas[name]; - } - - logMod->error("Error: missing schema with name %s!", name); - assert(0); - return nullNode; -} - -const JsonNode & JsonUtils::getSchema(const std::string & URI) -{ - size_t posColon = URI.find(':'); - size_t posHash = URI.find('#'); - std::string filename; - if(posColon == std::string::npos) - { - filename = URI.substr(0, posHash); - } - else - { - std::string protocolName = URI.substr(0, posColon); - filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; - if(protocolName != "vcmi") - { - logMod->error("Error: unsupported URI protocol for schema: %s", URI); - return nullNode; - } - } - - // check if json pointer if present (section after hash in string) - if(posHash == std::string::npos || posHash == URI.size() - 1) - return getSchemaByName(filename); - else - return getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); -} - -void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) -{ - if (dest.getType() == JsonNode::JsonType::DATA_NULL) - { - std::swap(dest, source); - return; - } - - switch (source.getType()) - { - case JsonNode::JsonType::DATA_NULL: - { - dest.clear(); - break; - } - 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: - { - std::swap(dest, source); - break; - } - case JsonNode::JsonType::DATA_STRUCT: - { - if(!ignoreOverride && vstd::contains(source.flags, "override")) - { - std::swap(dest, source); - } - else - { - if (copyMeta) - dest.meta = source.meta; - - //recursively merge all entries from struct - for(auto & node : source.Struct()) - merge(dest[node.first], node.second, ignoreOverride); - } - } - } -} - -void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) -{ - // uses copy created in stack to safely merge two nodes - merge(dest, source, ignoreOverride, copyMeta); -} - -void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) -{ - JsonNode inheritedNode(base); - merge(inheritedNode, descendant, true, true); - descendant.swap(inheritedNode); -} - -JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) -{ - if(nodes.empty()) - return nullNode; - - JsonNode result = nodes[0]; - for(int i = 1; i < nodes.size(); i++) - { - if(result.isNull()) - break; - result = JsonUtils::intersect(result, nodes[i], pruneEmpty); - } - return result; -} - -JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty) -{ - if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // intersect individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : a.Struct()) - { - if(vstd::contains(b.Struct(), property.first)) - { - JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second); - if(pruneEmpty && !propertyIntersect.containsBaseData()) - continue; - result[property.first] = propertyIntersect; - } - } - return result; - } - else - { - // not a struct - same or different, no middle ground - if(a == b) - return a; - } - return nullNode; -} - -JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base) -{ - auto addsInfo = [](JsonNode diff) -> bool - { - switch(diff.getType()) - { - case JsonNode::JsonType::DATA_NULL: - return false; - case JsonNode::JsonType::DATA_STRUCT: - return !diff.Struct().empty(); - default: - return true; - } - }; - - if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // subtract individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : node.Struct()) - { - if(vstd::contains(base.Struct(), property.first)) - { - const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second); - if(addsInfo(propertyDifference)) - result[property.first] = propertyDifference; - } - else - { - result[property.first] = property.second; - } - } - return result; - } - else - { - if(node == base) - return nullNode; - } - return node; -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files) -{ - bool isValid = false; - return assembleFromFiles(files, isValid); -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) -{ - isValid = true; - JsonNode result; - - for(const std::string & file : files) - { - bool isValidFile = false; - JsonNode section(ResourceID(file, EResType::TEXT), isValidFile); - merge(result, section); - isValid |= isValidFile; - } - return result; -} - -JsonNode JsonUtils::assembleFromFiles(const std::string & filename) -{ - JsonNode result; - ResourceID resID(filename, EResType::TEXT); - - for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) - { - // FIXME: some way to make this code more readable - auto stream = loader->load(resID); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - JsonNode section(reinterpret_cast(textData.get()), stream->getSize()); - merge(result, section); - } - return result; -} - -DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value) -{ - JsonNode node; - node.Bool() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::floatNode(double value) -{ - JsonNode node; - node.Float() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::stringNode(const std::string & value) -{ - JsonNode node; - node.String() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value) -{ - JsonNode node; - node.Integer() = value; - return node; -} - -VCMI_LIB_NAMESPACE_END +/* + * JsonNode.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 "JsonNode.h" + +#include "ScopeGuard.h" + +#include "bonuses/BonusParams.h" +#include "bonuses/Bonus.h" +#include "bonuses/Limiters.h" +#include "bonuses/Propagators.h" +#include "bonuses/Updaters.h" +#include "filesystem/Filesystem.h" +#include "modding/IdentifierStorage.h" +#include "VCMI_Lib.h" //for identifier resolution +#include "CGeneralTextHandler.h" +#include "JsonDetail.h" +#include "constants/StringConstants.h" +#include "battle/BattleHex.h" + +namespace +{ +// to avoid duplicating const and non-const code +template +Node & resolvePointer(Node & in, const std::string & pointer) +{ + if(pointer.empty()) + return in; + assert(pointer[0] == '/'); + + size_t splitPos = pointer.find('/', 1); + + std::string entry = pointer.substr(1, splitPos - 1); + std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); + + if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) + { + if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string + throw std::runtime_error("Invalid Json pointer"); + + if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed + throw std::runtime_error("Invalid Json pointer"); + + auto index = boost::lexical_cast(entry); + + if (in.Vector().size() > index) + return in.Vector()[index].resolvePointer(remainer); + } + return in[entry].resolvePointer(remainer); +} +} + +VCMI_LIB_NAMESPACE_BEGIN + +using namespace JsonDetail; + +class LibClasses; +class CModHandler; + +static const JsonNode nullNode; + +JsonNode::JsonNode(JsonType Type) +{ + setType(Type); +} + +JsonNode::JsonNode(const char *data, size_t datasize) +{ + JsonParser parser(data, datasize); + *this = parser.parse(""); +} + +JsonNode::JsonNode(const JsonPath & fileURI) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI) +{ + auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); + isValidSyntax = parser.isValid(); +} + +bool JsonNode::operator == (const JsonNode &other) const +{ + return data == other.data; +} + +bool JsonNode::operator != (const JsonNode &other) const +{ + return !(*this == other); +} + +JsonNode::JsonType JsonNode::getType() const +{ + return static_cast(data.index()); +} + +void JsonNode::setMeta(const std::string & metadata, bool recursive) +{ + meta = metadata; + if (recursive) + { + switch (getType()) + { + break; case JsonType::DATA_VECTOR: + { + for(auto & node : Vector()) + { + node.setMeta(metadata); + } + } + break; case JsonType::DATA_STRUCT: + { + for(auto & node : Struct()) + { + node.second.setMeta(metadata); + } + } + } + } +} + +void JsonNode::setType(JsonType Type) +{ + if (getType() == Type) + return; + + //float<->int conversion + if(getType() == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) + { + si64 converted = static_cast(std::get(data)); + data = JsonData(converted); + return; + } + else if(getType() == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) + { + double converted = static_cast(std::get(data)); + data = JsonData(converted); + return; + } + + //Set new node type + switch(Type) + { + break; case JsonType::DATA_NULL: data = JsonData(); + break; case JsonType::DATA_BOOL: data = JsonData(false); + break; case JsonType::DATA_FLOAT: data = JsonData(static_cast(0.0)); + break; case JsonType::DATA_STRING: data = JsonData(std::string()); + break; case JsonType::DATA_VECTOR: data = JsonData(JsonVector()); + break; case JsonType::DATA_STRUCT: data = JsonData(JsonMap()); + break; case JsonType::DATA_INTEGER: data = JsonData(static_cast(0)); + } +} + +bool JsonNode::isNull() const +{ + return getType() == JsonType::DATA_NULL; +} + +bool JsonNode::isNumber() const +{ + return getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT; +} + +bool JsonNode::isString() const +{ + return getType() == JsonType::DATA_STRING; +} + +bool JsonNode::isVector() const +{ + return getType() == JsonType::DATA_VECTOR; +} + +bool JsonNode::isStruct() const +{ + return getType() == JsonType::DATA_STRUCT; +} + +bool JsonNode::containsBaseData() const +{ + switch(getType()) + { + case JsonType::DATA_NULL: + return false; + case JsonType::DATA_STRUCT: + for(const auto & elem : Struct()) + { + if(elem.second.containsBaseData()) + return true; + } + return false; + default: + //other types (including vector) cannot be extended via merge + return true; + } +} + +bool JsonNode::isCompact() const +{ + switch(getType()) + { + case JsonType::DATA_VECTOR: + for(const JsonNode & elem : Vector()) + { + if(!elem.isCompact()) + return false; + } + return true; + case JsonType::DATA_STRUCT: + { + auto propertyCount = Struct().size(); + if(propertyCount == 0) + return true; + else if(propertyCount == 1) + return Struct().begin()->second.isCompact(); + } + return false; + default: + return true; + } +} + +bool JsonNode::TryBoolFromString(bool & success) const +{ + success = true; + if(getType() == JsonNode::JsonType::DATA_BOOL) + return Bool(); + + success = getType() == JsonNode::JsonType::DATA_STRING; + if(success) + { + auto boolParamStr = String(); + boost::algorithm::trim(boolParamStr); + boost::algorithm::to_lower(boolParamStr); + success = boolParamStr == "true"; + + if(success) + return true; + + success = boolParamStr == "false"; + } + return false; +} + +void JsonNode::clear() +{ + setType(JsonType::DATA_NULL); +} + +bool & JsonNode::Bool() +{ + setType(JsonType::DATA_BOOL); + return std::get(data); +} + +double & JsonNode::Float() +{ + setType(JsonType::DATA_FLOAT); + return std::get(data); +} + +si64 & JsonNode::Integer() +{ + setType(JsonType::DATA_INTEGER); + return std::get(data); +} + +std::string & JsonNode::String() +{ + setType(JsonType::DATA_STRING); + return std::get(data); +} + +JsonVector & JsonNode::Vector() +{ + setType(JsonType::DATA_VECTOR); + return std::get(data); +} + +JsonMap & JsonNode::Struct() +{ + setType(JsonType::DATA_STRUCT); + return std::get(data); +} + +const bool boolDefault = false; +bool JsonNode::Bool() const +{ + if (getType() == JsonType::DATA_NULL) + return boolDefault; + assert(getType() == JsonType::DATA_BOOL); + return std::get(data); +} + +const double floatDefault = 0; +double JsonNode::Float() const +{ + if(getType() == JsonType::DATA_NULL) + return floatDefault; + + if(getType() == JsonType::DATA_INTEGER) + return static_cast(std::get(data)); + + assert(getType() == JsonType::DATA_FLOAT); + return std::get(data); +} + +const si64 integetDefault = 0; +si64 JsonNode::Integer() const +{ + if(getType() == JsonType::DATA_NULL) + return integetDefault; + + if(getType() == JsonType::DATA_FLOAT) + return static_cast(std::get(data)); + + assert(getType() == JsonType::DATA_INTEGER); + return std::get(data); +} + +const std::string stringDefault = std::string(); +const std::string & JsonNode::String() const +{ + if (getType() == JsonType::DATA_NULL) + return stringDefault; + assert(getType() == JsonType::DATA_STRING); + return std::get(data); +} + +const JsonVector vectorDefault = JsonVector(); +const JsonVector & JsonNode::Vector() const +{ + if (getType() == JsonType::DATA_NULL) + return vectorDefault; + assert(getType() == JsonType::DATA_VECTOR); + return std::get(data); +} + +const JsonMap mapDefault = JsonMap(); +const JsonMap & JsonNode::Struct() const +{ + if (getType() == JsonType::DATA_NULL) + return mapDefault; + assert(getType() == JsonType::DATA_STRUCT); + return std::get(data); +} + +JsonNode & JsonNode::operator[](const std::string & child) +{ + return Struct()[child]; +} + +const JsonNode & JsonNode::operator[](const std::string & child) const +{ + auto it = Struct().find(child); + if (it != Struct().end()) + return it->second; + return nullNode; +} + +JsonNode & JsonNode::operator[](size_t child) +{ + if (child >= Vector().size() ) + Vector().resize(child + 1); + return Vector()[child]; +} + +const JsonNode & JsonNode::operator[](size_t child) const +{ + if (child < Vector().size() ) + return Vector()[child]; + + return nullNode; +} + +const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const +{ + return ::resolvePointer(*this, jsonPointer); +} + +JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) +{ + return ::resolvePointer(*this, jsonPointer); +} + +std::string JsonNode::toJson(bool compact) const +{ + std::ostringstream out; + JsonWriter writer(out, compact); + writer.writeNode(*this); + return out.str(); +} + +///JsonUtils + +static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const JsonNode & node) +{ + if (node.isNull()) + { + subtype = BonusSubtypeID(); + return; + } + + if (node.isNumber()) // Compatibility code for 1.3 or older + { + logMod->warn("Bonus subtype must be string! (%s)", node.meta); + subtype = BonusCustomSubtype(node.Integer()); + return; + } + + if (!node.isString()) + { + logMod->warn("Bonus subtype must be string! (%s)", node.meta); + subtype = BonusSubtypeID(); + return; + } + + switch (type) + { + case BonusType::MAGIC_SCHOOL_SKILL: + case BonusType::SPELL_DAMAGE: + case BonusType::SPELLS_OF_SCHOOL: + case BonusType::SPELL_DAMAGE_REDUCTION: + case BonusType::SPELL_SCHOOL_IMMUNITY: + case BonusType::NEGATIVE_EFFECTS_IMMUNITY: + { + VLC->identifiers()->requestIdentifier( "spellSchool", node, [&subtype](int32_t identifier) + { + subtype = SpellSchool(identifier); + }); + break; + } + case BonusType::NO_TERRAIN_PENALTY: + { + VLC->identifiers()->requestIdentifier( "terrain", node, [&subtype](int32_t identifier) + { + subtype = TerrainId(identifier); + }); + break; + } + case BonusType::PRIMARY_SKILL: + { + VLC->identifiers()->requestIdentifier( "primarySkill", node, [&subtype](int32_t identifier) + { + subtype = PrimarySkill(identifier); + }); + break; + } + case BonusType::IMPROVED_NECROMANCY: + case BonusType::HERO_GRANTS_ATTACKS: + case BonusType::BONUS_DAMAGE_CHANCE: + case BonusType::BONUS_DAMAGE_PERCENTAGE: + case BonusType::SPECIAL_UPGRADE: + case BonusType::HATE: + case BonusType::SUMMON_GUARDIANS: + case BonusType::MANUAL_CONTROL: + { + VLC->identifiers()->requestIdentifier( "creature", node, [&subtype](int32_t identifier) + { + subtype = CreatureID(identifier); + }); + break; + } + case BonusType::SPELL_IMMUNITY: + case BonusType::SPELL_DURATION: + case BonusType::SPECIAL_ADD_VALUE_ENCHANT: + case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: + case BonusType::SPECIAL_PECULIAR_ENCHANT: + case BonusType::SPECIAL_SPELL_LEV: + case BonusType::SPECIFIC_SPELL_DAMAGE: + case BonusType::SPELL: + case BonusType::OPENING_BATTLE_SPELL: + case BonusType::SPELL_LIKE_ATTACK: + case BonusType::CATAPULT: + case BonusType::CATAPULT_EXTRA_SHOTS: + case BonusType::HEALER: + case BonusType::SPELLCASTER: + case BonusType::ENCHANTER: + case BonusType::SPELL_AFTER_ATTACK: + case BonusType::SPELL_BEFORE_ATTACK: + case BonusType::SPECIFIC_SPELL_POWER: + case BonusType::ENCHANTED: + case BonusType::MORE_DAMAGE_FROM_SPELL: + case BonusType::NOT_ACTIVE: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier) + { + subtype = SpellID(identifier); + }); + break; + } + case BonusType::GENERATE_RESOURCE: + { + VLC->identifiers()->requestIdentifier( "resource", node, [&subtype](int32_t identifier) + { + subtype = GameResID(identifier); + }); + break; + } + case BonusType::MOVEMENT: + case BonusType::WATER_WALKING: + case BonusType::FLYING_MOVEMENT: + case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES: + case BonusType::CREATURE_DAMAGE: + case BonusType::FLYING: + case BonusType::GENERAL_DAMAGE_REDUCTION: + case BonusType::PERCENTAGE_DAMAGE_BOOST: + case BonusType::SOUL_STEAL: + case BonusType::TRANSMUTATION: + case BonusType::DESTRUCTION: + case BonusType::DEATH_STARE: + case BonusType::REBIRTH: + case BonusType::VISIONS: + case BonusType::SPELLS_OF_LEVEL: // spell level + case BonusType::CREATURE_GROWTH: // creature level + { + VLC->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier) + { + subtype = BonusCustomSubtype(identifier); + }); + break; + } + default: + for(const auto & i : bonusNameMap) + if(i.second == type) + logMod->warn("Bonus type %s does not supports subtypes!", i.first ); + subtype = BonusSubtypeID(); + } +} + +static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) +{ + if (node.isNull()) + { + sourceInstance = BonusCustomSource(); + return; + } + + if (node.isNumber()) // Compatibility code for 1.3 or older + { + logMod->warn("Bonus source must be string!"); + sourceInstance = BonusCustomSource(node.Integer()); + return; + } + + if (!node.isString()) + { + logMod->warn("Bonus source must be string!"); + sourceInstance = BonusCustomSource(); + return; + } + + switch (sourceType) + { + case BonusSource::ARTIFACT: + case BonusSource::ARTIFACT_INSTANCE: + { + VLC->identifiers()->requestIdentifier( "artifact", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = ArtifactID(identifier); + }); + break; + } + case BonusSource::OBJECT_TYPE: + { + VLC->identifiers()->requestIdentifier( "object", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = Obj(identifier); + }); + break; + } + case BonusSource::OBJECT_INSTANCE: + case BonusSource::HERO_BASE_SKILL: + sourceInstance = ObjectInstanceID(ObjectInstanceID::decode(node.String())); + break; + case BonusSource::CREATURE_ABILITY: + { + VLC->identifiers()->requestIdentifier( "creature", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = CreatureID(identifier); + }); + break; + } + case BonusSource::TERRAIN_OVERLAY: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = BattleField(identifier); + }); + break; + } + case BonusSource::SPELL_EFFECT: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = SpellID(identifier); + }); + break; + } + case BonusSource::TOWN_STRUCTURE: + assert(0); // TODO + sourceInstance = BuildingTypeUniqueID(); + break; + case BonusSource::SECONDARY_SKILL: + { + VLC->identifiers()->requestIdentifier( "secondarySkill", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = SecondarySkill(identifier); + }); + break; + } + case BonusSource::HERO_SPECIAL: + { + VLC->identifiers()->requestIdentifier( "hero", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = HeroTypeID(identifier); + }); + break; + } + case BonusSource::CAMPAIGN_BONUS: + sourceInstance = CampaignScenarioID(CampaignScenarioID::decode(node.String())); + break; + case BonusSource::ARMY: + case BonusSource::STACK_EXPERIENCE: + case BonusSource::COMMANDER: + case BonusSource::GLOBAL: + case BonusSource::TERRAIN_NATIVE: + case BonusSource::OTHER: + default: + sourceInstance = BonusSourceID(); + break; + } +} + +std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) +{ + auto b = std::make_shared(); + std::string type = ability_vec[0].String(); + auto it = bonusNameMap.find(type); + if (it == bonusNameMap.end()) + { + logMod->error("Error: invalid ability type %s.", type); + return b; + } + b->type = it->second; + + b->val = static_cast(ability_vec[1].Float()); + loadBonusSubtype(b->subtype, b->type, ability_vec[2]); + b->additionalInfo = static_cast(ability_vec[3].Float()); + b->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) + b->turnsRemain = 0; + return b; +} + +template +const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) +{ + static T defaultValue = T(); + if (!val->isNull()) + { + const std::string & type = val->String(); + auto it = map.find(type); + if (it == map.end()) + { + logMod->error("Error: invalid %s%s.", err, type); + return defaultValue; + } + else + { + return it->second; + } + } + else + return defaultValue; +} + +template +const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) +{ + if(val->isNumber()) + return static_cast(val->Integer()); + else + return parseByMap(map, val, err); +} + +void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) +{ + const JsonNode & value = node["addInfo"]; + if (!value.isNull()) + { + switch (value.getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var = static_cast(value.Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var = static_cast(value.Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) + { + var = identifier; + }); + break; + case JsonNode::JsonType::DATA_VECTOR: + { + const JsonVector & vec = value.Vector(); + var.resize(vec.size()); + for(int i = 0; i < vec.size(); i++) + { + switch(vec[i].getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var[i] = static_cast(vec[i].Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var[i] = static_cast(vec[i].Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(vec[i], [&var,i](si32 identifier) + { + var[i] = identifier; + }); + break; + default: + logMod->error("Error! Wrong identifier used for value of addInfo[%d].", i); + } + } + break; + } + default: + logMod->error("Error! Wrong identifier used for value of addInfo."); + } + } +} + +std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) +{ + switch(limiter.getType()) + { + case JsonNode::JsonType::DATA_VECTOR: + { + const JsonVector & subLimiters = limiter.Vector(); + if(subLimiters.empty()) + { + logMod->warn("Warning: empty limiter list"); + return std::make_shared(); + } + std::shared_ptr result; + int offset = 1; + // determine limiter type and offset for sub-limiters + if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING) + { + const std::string & aggregator = subLimiters[0].String(); + if(aggregator == AllOfLimiter::aggregator) + result = std::make_shared(); + else if(aggregator == AnyOfLimiter::aggregator) + result = std::make_shared(); + else if(aggregator == NoneOfLimiter::aggregator) + result = std::make_shared(); + } + if(!result) + { + // collapse for single limiter without explicit aggregate operator + if(subLimiters.size() == 1) + return parseLimiter(subLimiters[0]); + // implicit aggregator must be allOf + result = std::make_shared(); + offset = 0; + } + if(subLimiters.size() == offset) + logMod->warn("Warning: empty sub-limiter list"); + for(int sl = offset; sl < subLimiters.size(); ++sl) + result->add(parseLimiter(subLimiters[sl])); + return result; + } + break; + case JsonNode::JsonType::DATA_STRING: //pre-defined limiters + return parseByMap(bonusLimiterMap, &limiter, "limiter type "); + break; + case JsonNode::JsonType::DATA_STRUCT: //customizable limiters + { + std::string limiterType = limiter["type"].String(); + const JsonVector & parameters = limiter["parameters"].Vector(); + if(limiterType == "CREATURE_TYPE_LIMITER") + { + std::shared_ptr creatureLimiter = std::make_shared(); + VLC->identifiers()->requestIdentifier("creature", parameters[0], [=](si32 creature) + { + creatureLimiter->setCreature(CreatureID(creature)); + }); + auto includeUpgrades = false; + + if(parameters.size() > 1) + { + bool success = true; + includeUpgrades = parameters[1].TryBoolFromString(success); + + if(!success) + logMod->error("Second parameter of '%s' limiter should be Bool", limiterType); + } + creatureLimiter->includeUpgrades = includeUpgrades; + return creatureLimiter; + } + else if(limiterType == "HAS_ANOTHER_BONUS_LIMITER") + { + std::string anotherBonusType = parameters[0].String(); + auto it = bonusNameMap.find(anotherBonusType); + if(it == bonusNameMap.end()) + { + logMod->error("Error: invalid ability type %s.", anotherBonusType); + } + else + { + std::shared_ptr bonusLimiter = std::make_shared(); + bonusLimiter->type = it->second; + auto findSource = [&](const JsonNode & parameter) + { + if(parameter.getType() == JsonNode::JsonType::DATA_STRUCT) + { + auto sourceIt = bonusSourceMap.find(parameter["type"].String()); + if(sourceIt != bonusSourceMap.end()) + { + bonusLimiter->source = sourceIt->second; + bonusLimiter->isSourceRelevant = true; + if(!parameter["id"].isNull()) { + loadBonusSourceInstance(bonusLimiter->sid, bonusLimiter->source, parameter["id"]); + bonusLimiter->isSourceIDRelevant = true; + } + } + } + return false; + }; + if(parameters.size() > 1) + { + if(findSource(parameters[1]) && parameters.size() == 2) + return bonusLimiter; + else + { + loadBonusSubtype(bonusLimiter->subtype, bonusLimiter->type, parameters[1]); + bonusLimiter->isSubtypeRelevant = true; + if(parameters.size() > 2) + findSource(parameters[2]); + } + } + return bonusLimiter; + } + } + else if(limiterType == "CREATURE_ALIGNMENT_LIMITER") + { + int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, parameters[0].String()); + if(alignment == -1) + logMod->error("Error: invalid alignment %s.", parameters[0].String()); + else + return std::make_shared(static_cast(alignment)); + } + else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat + { + std::shared_ptr factionLimiter = std::make_shared(); + VLC->identifiers()->requestIdentifier("faction", parameters[0], [=](si32 faction) + { + factionLimiter->faction = FactionID(faction); + }); + return factionLimiter; + } + else if(limiterType == "CREATURE_LEVEL_LIMITER") + { + auto levelLimiter = std::make_shared(); + if(!parameters.empty()) //If parameters is empty, level limiter works as CREATURES_ONLY limiter + { + levelLimiter->minLevel = parameters[0].Integer(); + if(parameters[1].isNumber()) + levelLimiter->maxLevel = parameters[1].Integer(); + } + return levelLimiter; + } + else if(limiterType == "CREATURE_TERRAIN_LIMITER") + { + std::shared_ptr terrainLimiter = std::make_shared(); + if(!parameters.empty()) + { + VLC->identifiers()->requestIdentifier("terrain", parameters[0], [=](si32 terrain) + { + //TODO: support limiters + //terrainLimiter->terrainType = terrain; + }); + } + return terrainLimiter; + } + else if(limiterType == "UNIT_ON_HEXES") { + auto hexLimiter = std::make_shared(); + if(!parameters.empty()) + { + for (const auto & parameter: parameters){ + if(parameter.isNumber()) + hexLimiter->applicableHexes.insert(BattleHex(parameter.Integer())); + } + } + return hexLimiter; + } + else + { + logMod->error("Error: invalid customizable limiter type %s.", limiterType); + } + } + break; + default: + break; + } + return nullptr; +} + +std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) +{ + auto b = std::make_shared(); + if (!parseBonus(ability, b.get())) + { + // caller code can not handle this case and presumes that returned bonus is always valid + logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); + b->type = BonusType::NONE; + return b; + } + return b; +} + +std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description) +{ + /* duration = BonusDuration::PERMANENT + source = BonusSource::TOWN_STRUCTURE + bonusType, val, subtype - get from json + */ + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, BuildingTypeUniqueID(faction, building), description); + + if(!parseBonus(ability, b.get())) + return nullptr; + return b; +} + +static BonusParams convertDeprecatedBonus(const JsonNode &ability) +{ + if(vstd::contains(deprecatedBonusSet, ability["type"].String())) + { + logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toJson()); + auto params = BonusParams(ability["type"].String(), + ability["subtype"].isString() ? ability["subtype"].String() : "", + ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); + if(params.isConverted) + { + if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special + { + params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; + params.targetType = BonusSource::SECONDARY_SKILL; + } + + logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); + return params; + } + else + logMod->error("Cannot convert bonus!\n%s", ability.toJson()); + } + BonusParams ret; + ret.isConverted = false; + return ret; +} + +static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) +{ + switch(updaterJson.getType()) + { + case JsonNode::JsonType::DATA_STRING: + return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); + break; + case JsonNode::JsonType::DATA_STRUCT: + if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") + { + std::shared_ptr updater = std::make_shared(); + const JsonVector param = updaterJson["parameters"].Vector(); + updater->valPer20 = static_cast(param[0].Integer()); + if(param.size() > 1) + updater->stepSize = static_cast(param[1].Integer()); + return updater; + } + else if (updaterJson["type"].String() == "ARMY_MOVEMENT") + { + std::shared_ptr updater = std::make_shared(); + if(updaterJson["parameters"].isVector()) + { + const auto & param = updaterJson["parameters"].Vector(); + if(param.size() < 4) + logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); + else + { + updater->base = static_cast(param.at(0).Integer()); + updater->divider = static_cast(param.at(1).Integer()); + updater->multiplier = static_cast(param.at(2).Integer()); + updater->max = static_cast(param.at(3).Integer()); + } + return updater; + } + } + else + logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); + break; + } + return nullptr; +} + +bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) +{ + const JsonNode * value = nullptr; + + std::string type = ability["type"].String(); + auto it = bonusNameMap.find(type); + auto params = std::make_unique(false); + if (it == bonusNameMap.end()) + { + params = std::make_unique(convertDeprecatedBonus(ability)); + if(!params->isConverted) + { + logMod->error("Error: invalid ability type %s.", type); + return false; + } + b->type = params->type; + b->val = params->val.value_or(0); + b->valType = params->valueType.value_or(BonusValueType::ADDITIVE_VALUE); + if(params->targetType) + b->targetSourceType = params->targetType.value(); + } + else + b->type = it->second; + + loadBonusSubtype(b->subtype, b->type, params->isConverted ? params->toJson()["subtype"] : ability["subtype"]); + + if(!params->isConverted) + { + b->val = static_cast(ability["val"].Float()); + + value = &ability["valueType"]; + if (!value->isNull()) + b->valType = static_cast(parseByMapN(bonusValueMap, value, "value type ")); + } + + b->stacking = ability["stacking"].String(); + + resolveAddInfo(b->additionalInfo, ability); + + b->turnsRemain = static_cast(ability["turns"].Float()); + + if(!ability["description"].isNull()) + { + if (ability["description"].isString()) + b->description = ability["description"].String(); + if (ability["description"].isNumber()) + b->description = VLC->generaltexth->translate("core.arraytxt", ability["description"].Integer()); + } + + value = &ability["effectRange"]; + if (!value->isNull()) + b->effectRange = static_cast(parseByMapN(bonusLimitEffect, value, "effect range ")); + + value = &ability["duration"]; + if (!value->isNull()) + { + switch (value->getType()) + { + case JsonNode::JsonType::DATA_STRING: + b->duration = parseByMap(bonusDurationMap, value, "duration type "); + break; + case JsonNode::JsonType::DATA_VECTOR: + { + BonusDuration::Type dur = 0; + for (const JsonNode & d : value->Vector()) + dur |= parseByMapN(bonusDurationMap, &d, "duration type "); + b->duration = dur; + } + break; + default: + logMod->error("Error! Wrong bonus duration format."); + } + } + + value = &ability["sourceType"]; + if (!value->isNull()) + b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); + + if (!ability["sourceID"].isNull()) + loadBonusSourceInstance(b->sid, b->source, ability["sourceID"]); + + value = &ability["targetSourceType"]; + if (!value->isNull()) + b->targetSourceType = static_cast(parseByMap(bonusSourceMap, value, "target type ")); + + value = &ability["limiters"]; + if (!value->isNull()) + b->limiter = parseLimiter(*value); + + value = &ability["propagator"]; + if (!value->isNull()) + { + //ALL_CREATURES old propagator compatibility + if(value->String() == "ALL_CREATURES") + { + logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter"); + b->addLimiter(std::make_shared()); + b->propagator = bonusPropagatorMap.at("GLOBAL_EFFECT"); + } + else + b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type "); + } + + value = &ability["updater"]; + if(!value->isNull()) + b->addUpdater(parseUpdater(*value)); + value = &ability["propagationUpdater"]; + if(!value->isNull()) + b->propagationUpdater = parseUpdater(*value); + return true; +} + +CSelector JsonUtils::parseSelector(const JsonNode & ability) +{ + CSelector ret = Selector::all; + + // Recursive parsers for anyOf, allOf, noneOf + const auto * value = &ability["allOf"]; + if(value->isVector()) + { + for(const auto & andN : value->Vector()) + ret = ret.And(parseSelector(andN)); + } + + value = &ability["anyOf"]; + if(value->isVector()) + { + CSelector base = Selector::none; + for(const auto & andN : value->Vector()) + base = base.Or(parseSelector(andN)); + + ret = ret.And(base); + } + + value = &ability["noneOf"]; + if(value->isVector()) + { + CSelector base = Selector::none; + for(const auto & andN : value->Vector()) + base = base.Or(parseSelector(andN)); + + ret = ret.And(base.Not()); + } + + BonusType type = BonusType::NONE; + + // Actual selector parser + value = &ability["type"]; + if(value->isString()) + { + auto it = bonusNameMap.find(value->String()); + if(it != bonusNameMap.end()) + { + type = it->second; + ret = ret.And(Selector::type()(it->second)); + } + } + value = &ability["subtype"]; + if(!value->isNull() && type != BonusType::NONE) + { + BonusSubtypeID subtype; + loadBonusSubtype(subtype, type, ability); + ret = ret.And(Selector::subtype()(subtype)); + } + value = &ability["sourceType"]; + std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized + std::optional id = std::nullopt; + if(value->isString()) + { + auto it = bonusSourceMap.find(value->String()); + if(it != bonusSourceMap.end()) + src = it->second; + } + + value = &ability["sourceID"]; + if(!value->isNull() && src.has_value()) + { + loadBonusSourceInstance(*id, *src, ability); + } + + if(src && id) + ret = ret.And(Selector::source(*src, *id)); + else if(src) + ret = ret.And(Selector::sourceTypeSel(*src)); + + + value = &ability["targetSourceType"]; + if(value->isString()) + { + auto it = bonusSourceMap.find(value->String()); + if(it != bonusSourceMap.end()) + ret = ret.And(Selector::targetSourceType()(it->second)); + } + value = &ability["valueType"]; + if(value->isString()) + { + auto it = bonusValueMap.find(value->String()); + if(it != bonusValueMap.end()) + ret = ret.And(Selector::valueType(it->second)); + } + CAddInfo info; + value = &ability["addInfo"]; + if(!value->isNull()) + { + resolveAddInfo(info, ability["addInfo"]); + ret = ret.And(Selector::info()(info)); + } + value = &ability["effectRange"]; + if(value->isString()) + { + auto it = bonusLimitEffect.find(value->String()); + if(it != bonusLimitEffect.end()) + ret = ret.And(Selector::effectRange()(it->second)); + } + value = &ability["lastsTurns"]; + if(value->isNumber()) + ret = ret.And(Selector::turns(value->Integer())); + value = &ability["lastsDays"]; + if(value->isNumber()) + ret = ret.And(Selector::days(value->Integer())); + + return ret; +} + +//returns first Key with value equal to given one +template +Key reverseMapFirst(const Val & val, const std::map & map) +{ + for(auto it : map) + { + if(it.second == val) + { + return it.first; + } + } + assert(0); + return ""; +} + +static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) +{ + const JsonNode & fieldProps = schema["properties"][fieldName]; + +#if defined(VCMI_IOS) + if (!fieldProps["defaultIOS"].isNull()) + return fieldProps["defaultIOS"]; +#elif defined(VCMI_ANDROID) + if (!fieldProps["defaultAndroid"].isNull()) + return fieldProps["defaultAndroid"]; +#elif !defined(VCMI_MOBILE) + if (!fieldProps["defaultDesktop"].isNull()) + return fieldProps["defaultDesktop"]; +#endif + return fieldProps["default"]; +} + +static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) +{ + assert(schema["type"].String() == "object"); + + std::set foundEntries; + + for(const auto & entry : schema["required"].Vector()) + foundEntries.insert(entry.String()); + + vstd::erase_if(node.Struct(), [&](const auto & node){ + return !vstd::contains(foundEntries, node.first); + }); +} + +static void minimizeNode(JsonNode & node, const JsonNode & schema) +{ + if (schema["type"].String() != "object") + return; + + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + minimizeNode(node[name], schema["properties"][name]); + + if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) + node.Struct().erase(name); + } + eraseOptionalNodes(node, schema); +} + +static void maximizeNode(JsonNode & node, const JsonNode & schema) +{ + // "required" entry can only be found in object/struct + if (schema["type"].String() != "object") + return; + + // check all required entries that have default version + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + + if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) + node[name] = getDefaultValue(schema, name); + + maximizeNode(node[name], schema["properties"][name]); + } + + eraseOptionalNodes(node, schema); +} + +void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) +{ + minimizeNode(node, getSchema(schemaName)); +} + +void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) +{ + maximizeNode(node, getSchema(schemaName)); +} + +bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) +{ + std::string log = Validation::check(schemaName, node); + if (!log.empty()) + { + logMod->warn("Data in %s is invalid!", dataName); + logMod->warn(log); + logMod->trace("%s json: %s", dataName, node.toJson(true)); + } + return log.empty(); +} + +const JsonNode & getSchemaByName(const std::string & name) +{ + // cached schemas to avoid loading json data multiple times + static std::map loadedSchemas; + + if (vstd::contains(loadedSchemas, name)) + return loadedSchemas[name]; + + auto filename = JsonPath::builtin("config/schemas/" + name); + + if (CResourceHandler::get()->existsResource(filename)) + { + loadedSchemas[name] = JsonNode(filename); + return loadedSchemas[name]; + } + + logMod->error("Error: missing schema with name %s!", name); + assert(0); + return nullNode; +} + +const JsonNode & JsonUtils::getSchema(const std::string & URI) +{ + size_t posColon = URI.find(':'); + size_t posHash = URI.find('#'); + std::string filename; + if(posColon == std::string::npos) + { + filename = URI.substr(0, posHash); + } + else + { + std::string protocolName = URI.substr(0, posColon); + filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; + if(protocolName != "vcmi") + { + logMod->error("Error: unsupported URI protocol for schema: %s", URI); + return nullNode; + } + } + + // check if json pointer if present (section after hash in string) + if(posHash == std::string::npos || posHash == URI.size() - 1) + { + auto const & result = getSchemaByName(filename); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } + else + { + auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } +} + +void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) +{ + if (dest.getType() == JsonNode::JsonType::DATA_NULL) + { + std::swap(dest, source); + return; + } + + switch (source.getType()) + { + case JsonNode::JsonType::DATA_NULL: + { + dest.clear(); + break; + } + 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: + { + std::swap(dest, source); + break; + } + case JsonNode::JsonType::DATA_STRUCT: + { + if(!ignoreOverride && vstd::contains(source.flags, "override")) + { + std::swap(dest, source); + } + else + { + if (copyMeta) + dest.meta = source.meta; + + //recursively merge all entries from struct + for(auto & node : source.Struct()) + merge(dest[node.first], node.second, ignoreOverride); + } + } + } +} + +void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) +{ + // uses copy created in stack to safely merge two nodes + merge(dest, source, ignoreOverride, copyMeta); +} + +void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) +{ + JsonNode inheritedNode(base); + merge(inheritedNode, descendant, true, true); + std::swap(descendant, inheritedNode); +} + +JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) +{ + if(nodes.empty()) + return nullNode; + + JsonNode result = nodes[0]; + for(int i = 1; i < nodes.size(); i++) + { + if(result.isNull()) + break; + result = JsonUtils::intersect(result, nodes[i], pruneEmpty); + } + return result; +} + +JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty) +{ + if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT) + { + // intersect individual properties + JsonNode result(JsonNode::JsonType::DATA_STRUCT); + for(const auto & property : a.Struct()) + { + if(vstd::contains(b.Struct(), property.first)) + { + JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second); + if(pruneEmpty && !propertyIntersect.containsBaseData()) + continue; + result[property.first] = propertyIntersect; + } + } + return result; + } + else + { + // not a struct - same or different, no middle ground + if(a == b) + return a; + } + return nullNode; +} + +JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base) +{ + auto addsInfo = [](JsonNode diff) -> bool + { + switch(diff.getType()) + { + case JsonNode::JsonType::DATA_NULL: + return false; + case JsonNode::JsonType::DATA_STRUCT: + return !diff.Struct().empty(); + default: + return true; + } + }; + + if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT) + { + // subtract individual properties + JsonNode result(JsonNode::JsonType::DATA_STRUCT); + for(const auto & property : node.Struct()) + { + if(vstd::contains(base.Struct(), property.first)) + { + const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second); + if(addsInfo(propertyDifference)) + result[property.first] = propertyDifference; + } + else + { + result[property.first] = property.second; + } + } + return result; + } + else + { + if(node == base) + return nullNode; + } + return node; +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files) +{ + bool isValid = false; + return assembleFromFiles(files, isValid); +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) +{ + isValid = true; + JsonNode result; + + for(const auto & file : files) + { + bool isValidFile = false; + JsonNode section(JsonPath::builtinTODO(file), isValidFile); + merge(result, section); + isValid |= isValidFile; + } + return result; +} + +JsonNode JsonUtils::assembleFromFiles(const std::string & filename) +{ + JsonNode result; + JsonPath resID = JsonPath::builtinTODO(filename); + + for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) + { + // FIXME: some way to make this code more readable + auto stream = loader->load(resID); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + + JsonNode section(reinterpret_cast(textData.get()), stream->getSize()); + merge(result, section); + } + return result; +} + +DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value) +{ + JsonNode node; + node.Bool() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::floatNode(double value) +{ + JsonNode node; + node.Float() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::stringNode(const std::string & value) +{ + JsonNode node; + node.String() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value) +{ + JsonNode node; + node.Integer() = value; + return node; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.h b/lib/JsonNode.h index c1d39991e..47ae9a1a0 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -1,372 +1,323 @@ -/* - * JsonNode.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 JsonNode; -using JsonMap = std::map; -using JsonVector = std::vector; - -struct Bonus; -class CSelector; -class ResourceID; -class CAddInfo; -class ILimiter; - -class DLL_LINKAGE JsonNode -{ -public: - enum class JsonType - { - DATA_NULL, - DATA_BOOL, - DATA_FLOAT, - DATA_STRING, - DATA_VECTOR, - DATA_STRUCT, - DATA_INTEGER - }; - -private: - union JsonData - { - bool Bool; - double Float; - std::string* String; - JsonVector* Vector; - JsonMap* Struct; - si64 Integer; - }; - - JsonType type; - JsonData data; - -public: - /// free to use metadata fields - std::string meta; - // meta-flags like override - std::vector flags; - - //Create empty node - JsonNode(JsonType Type = JsonType::DATA_NULL); - //Create tree from Json-formatted input - explicit JsonNode(const char * data, size_t datasize); - //Create tree from JSON file - explicit JsonNode(ResourceID && fileURI); - explicit JsonNode(const ResourceID & fileURI); - explicit JsonNode(const std::string& idx, const ResourceID & fileURI); - explicit JsonNode(ResourceID && fileURI, bool & isValidSyntax); - //Copy c-tor - JsonNode(const JsonNode ©); - - ~JsonNode(); - - void swap(JsonNode &b); - JsonNode& operator =(JsonNode node); - - bool operator == (const JsonNode &other) const; - bool operator != (const JsonNode &other) const; - - void setMeta(const std::string & metadata, bool recursive = true); - - /// Convert node to another type. Converting to nullptr will clear all data - void setType(JsonType Type); - JsonType getType() const; - - bool isNull() const; - bool isNumber() const; - bool isString() const; - bool isVector() const; - bool isStruct() const; - /// true if node contains not-null data that cannot be extended via merging - /// used for generating common base node from multiple nodes (e.g. bonuses) - bool containsBaseData() const; - bool isCompact() const; - /// removes all data from node and sets type to null - void clear(); - - /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise - bool TryBoolFromString(bool & success) const; - - /// non-const accessors, node will change type on type mismatch - bool & Bool(); - double & Float(); - si64 & Integer(); - std::string & String(); - JsonVector & Vector(); - JsonMap & Struct(); - - /// const accessors, will cause assertion failure on type mismatch - bool Bool() const; - ///float and integer allowed - double Float() const; - ///only integer allowed - si64 Integer() const; - const std::string & String() const; - const JsonVector & Vector() const; - const JsonMap & Struct() const; - - /// returns resolved "json pointer" (string in format "/path/to/node") - const JsonNode & resolvePointer(const std::string & jsonPointer) const; - JsonNode & resolvePointer(const std::string & jsonPointer); - - /// convert json tree into specified type. Json tree must have same type as Type - /// Valid types: bool, string, any numeric, map and vector - /// example: convertTo< std::map< std::vector > >(); - template - Type convertTo() const; - - //operator [], for structs only - get child node by name - JsonNode & operator[](const std::string & child); - const JsonNode & operator[](const std::string & child) const; - - JsonNode & operator[](size_t child); - const JsonNode & operator[](size_t child) const; - - std::string toJson(bool compact = false) const; - - template void serialize(Handler &h, const int version) - { - h & meta; - h & flags; - h & type; - switch(type) - { - case JsonType::DATA_NULL: - break; - case JsonType::DATA_BOOL: - h & data.Bool; - break; - case JsonType::DATA_FLOAT: - h & data.Float; - break; - case JsonType::DATA_STRING: - h & data.String; - break; - case JsonType::DATA_VECTOR: - h & data.Vector; - break; - case JsonType::DATA_STRUCT: - h & data.Struct; - break; - case JsonType::DATA_INTEGER: - h & data.Integer; - break; - } - } -}; - -namespace JsonUtils -{ - /** - * @brief parse short bonus format, excluding type - * @note sets duration to Permament - */ - DLL_LINKAGE void parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest); - - /// - DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); - DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); - DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description); - DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); - DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); - DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); - DLL_LINKAGE void resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name); - DLL_LINKAGE void resolveIdentifier(const JsonNode & node, si32 & var); - DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will destroy data in source - */ - DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will preserve data stored in source by creating copy - */ - DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); - - /** @brief recursively merges descendant into copy of base node - * Result emulates inheritance semantic - */ - DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); - - /** - * @brief construct node representing the common structure of input nodes - * @param pruneEmpty - omit common properties whose intersection is empty - * different types: null - * struct: recursive intersect on common properties - * other: input if equal, null otherwise - */ - DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true); - DLL_LINKAGE JsonNode intersect(const std::vector & nodes, bool pruneEmpty = true); - - /** - * @brief construct node representing the difference "node - base" - * merging difference with base gives node - */ - DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base); - - /** - * @brief generate one Json structure from multiple files - * @param files - list of filenames with parts of json structure - */ - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); - - /// This version loads all files with same name (overridden by mods) - DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); - - /** - * @brief removes all nodes that are identical to default entry in schema - * @param node - JsonNode to minimize - * @param schemaName - name of schema to use - * @note for minimizing data must be valid against given schema - */ - DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); - /// opposed to minimize, adds all missing, required entries that have default value - DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); - - /** - * @brief validate node against specified schema - * @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 - */ - 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); - - /// for easy construction of JsonNodes; helps with inserting primitives into vector node - DLL_LINKAGE JsonNode boolNode(bool value); - DLL_LINKAGE JsonNode floatNode(double value); - DLL_LINKAGE JsonNode stringNode(const std::string & value); - DLL_LINKAGE JsonNode intNode(si64 value); -} - -namespace JsonDetail -{ - // conversion helpers for JsonNode::convertTo (partial template function instantiation is illegal in c++) - - template - struct JsonConvImpl; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T((int)node.Float()); - } - }; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T(node.Float()); - } - }; - - template - struct JsonConverter - { - static Type convert(const JsonNode & node) - { - ///this should be triggered only for numeric types and enums - static_assert(boost::mpl::or_, std::is_enum, boost::is_class >::value, "Unsupported type for JsonNode::convertTo()!"); - return JsonConvImpl, boost::is_class >::value >::convertImpl(node); - - } - }; - - template - struct JsonConverter > - { - static std::map convert(const JsonNode & node) - { - std::map ret; - for (const JsonMap::value_type & entry : node.Struct()) - { - ret.insert(entry.first, entry.second.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::set convert(const JsonNode & node) - { - std::set ret; - for(const JsonVector::value_type & entry : node.Vector()) - { - ret.insert(entry.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::vector convert(const JsonNode & node) - { - std::vector ret; - for (const JsonVector::value_type & entry: node.Vector()) - { - ret.push_back(entry.convertTo()); - } - return ret; - } - }; - - template<> - struct JsonConverter - { - static std::string convert(const JsonNode & node) - { - return node.String(); - } - }; - - template<> - struct JsonConverter - { - static bool convert(const JsonNode & node) - { - return node.Bool(); - } - }; -} - -template -Type JsonNode::convertTo() const -{ - return JsonDetail::JsonConverter::convert(*this); -} - -VCMI_LIB_NAMESPACE_END +/* + * JsonNode.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 "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +using JsonMap = std::map; +using JsonVector = std::vector; + +struct Bonus; +class CSelector; +class CAddInfo; +class ILimiter; + +class DLL_LINKAGE JsonNode +{ +public: + enum class JsonType + { + DATA_NULL, + DATA_BOOL, + DATA_FLOAT, + DATA_STRING, + DATA_VECTOR, + DATA_STRUCT, + DATA_INTEGER + }; + +private: + using JsonData = std::variant; + + JsonData data; + +public: + /// free to use metadata fields + std::string meta; + // meta-flags like override + std::vector flags; + + //Create empty node + JsonNode(JsonType Type = JsonType::DATA_NULL); + //Create tree from Json-formatted input + explicit JsonNode(const char * data, size_t datasize); + //Create tree from JSON file + explicit JsonNode(const JsonPath & fileURI); + explicit JsonNode(const std::string & modName, const JsonPath & fileURI); + explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); + + bool operator == (const JsonNode &other) const; + bool operator != (const JsonNode &other) const; + + void setMeta(const std::string & metadata, bool recursive = true); + + /// Convert node to another type. Converting to nullptr will clear all data + void setType(JsonType Type); + JsonType getType() const; + + bool isNull() const; + bool isNumber() const; + bool isString() const; + bool isVector() const; + bool isStruct() const; + /// true if node contains not-null data that cannot be extended via merging + /// used for generating common base node from multiple nodes (e.g. bonuses) + bool containsBaseData() const; + bool isCompact() const; + /// removes all data from node and sets type to null + void clear(); + + /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise + bool TryBoolFromString(bool & success) const; + + /// non-const accessors, node will change type on type mismatch + bool & Bool(); + double & Float(); + si64 & Integer(); + std::string & String(); + JsonVector & Vector(); + JsonMap & Struct(); + + /// const accessors, will cause assertion failure on type mismatch + bool Bool() const; + ///float and integer allowed + double Float() const; + ///only integer allowed + si64 Integer() const; + const std::string & String() const; + const JsonVector & Vector() const; + const JsonMap & Struct() const; + + /// returns resolved "json pointer" (string in format "/path/to/node") + const JsonNode & resolvePointer(const std::string & jsonPointer) const; + JsonNode & resolvePointer(const std::string & jsonPointer); + + /// convert json tree into specified type. Json tree must have same type as Type + /// Valid types: bool, string, any numeric, map and vector + /// example: convertTo< std::map< std::vector > >(); + template + Type convertTo() const; + + //operator [], for structs only - get child node by name + JsonNode & operator[](const std::string & child); + const JsonNode & operator[](const std::string & child) const; + + JsonNode & operator[](size_t child); + const JsonNode & operator[](size_t child) const; + + std::string toJson(bool compact = false) const; + + template void serialize(Handler &h, const int version) + { + h & meta; + h & flags; + h & data; + } +}; + +namespace JsonUtils +{ + DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); + DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); + DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description); + DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); + DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); + DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); + DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); + + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will destroy data in source + */ + DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); + + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will preserve data stored in source by creating copy + */ + DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); + + /** @brief recursively merges descendant into copy of base node + * Result emulates inheritance semantic + */ + DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); + + /** + * @brief construct node representing the common structure of input nodes + * @param pruneEmpty - omit common properties whose intersection is empty + * different types: null + * struct: recursive intersect on common properties + * other: input if equal, null otherwise + */ + DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true); + DLL_LINKAGE JsonNode intersect(const std::vector & nodes, bool pruneEmpty = true); + + /** + * @brief construct node representing the difference "node - base" + * merging difference with base gives node + */ + DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base); + + /** + * @brief generate one Json structure from multiple files + * @param files - list of filenames with parts of json structure + */ + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); + + /// This version loads all files with same name (overridden by mods) + DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); + + /** + * @brief removes all nodes that are identical to default entry in schema + * @param node - JsonNode to minimize + * @param schemaName - name of schema to use + * @note for minimizing data must be valid against given schema + */ + DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); + /// opposed to minimize, adds all missing, required entries that have default value + DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); + + /** + * @brief validate node against specified schema + * @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 + */ + 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); + + /// for easy construction of JsonNodes; helps with inserting primitives into vector node + DLL_LINKAGE JsonNode boolNode(bool value); + DLL_LINKAGE JsonNode floatNode(double value); + DLL_LINKAGE JsonNode stringNode(const std::string & value); + DLL_LINKAGE JsonNode intNode(si64 value); +} + +namespace JsonDetail +{ + // conversion helpers for JsonNode::convertTo (partial template function instantiation is illegal in c++) + + template + struct JsonConvImpl; + + template + struct JsonConvImpl + { + static T convertImpl(const JsonNode & node) + { + return T((int)node.Float()); + } + }; + + template + struct JsonConvImpl + { + static T convertImpl(const JsonNode & node) + { + return T(node.Float()); + } + }; + + template + struct JsonConverter + { + static Type convert(const JsonNode & node) + { + ///this should be triggered only for numeric types and enums + static_assert(std::is_arithmetic_v || std::is_enum_v || std::is_class_v, "Unsupported type for JsonNode::convertTo()!"); + return JsonConvImpl || std::is_class_v >::convertImpl(node); + + } + }; + + template + struct JsonConverter > + { + static std::map convert(const JsonNode & node) + { + std::map ret; + for (const JsonMap::value_type & entry : node.Struct()) + { + ret.insert(entry.first, entry.second.convertTo()); + } + return ret; + } + }; + + template + struct JsonConverter > + { + static std::set convert(const JsonNode & node) + { + std::set ret; + for(const JsonVector::value_type & entry : node.Vector()) + { + ret.insert(entry.convertTo()); + } + return ret; + } + }; + + template + struct JsonConverter > + { + static std::vector convert(const JsonNode & node) + { + std::vector ret; + for (const JsonVector::value_type & entry: node.Vector()) + { + ret.push_back(entry.convertTo()); + } + return ret; + } + }; + + template<> + struct JsonConverter + { + static std::string convert(const JsonNode & node) + { + return node.String(); + } + }; + + template<> + struct JsonConverter + { + static bool convert(const JsonNode & node) + { + return node.Bool(); + } + }; +} + +template +Type JsonNode::convertTo() const +{ + return JsonDetail::JsonConverter::convert(*this); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 222d8aacf..f99a678fb 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -15,172 +15,120 @@ #include "JsonNode.h" #include "CRandomGenerator.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "VCMI_Lib.h" -#include "CModHandler.h" #include "CArtHandler.h" #include "CCreatureHandler.h" #include "CCreatureSet.h" #include "spells/CSpellHandler.h" #include "CSkillHandler.h" +#include "CHeroHandler.h" #include "IGameCallback.h" +#include "gameState/CGameState.h" #include "mapObjects/IObjectInterface.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN namespace JsonRandom { - si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue) + si32 loadVariable(std::string variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) + { + if (value.empty() || value[0] != '@') + { + logMod->warn("Invalid syntax in load value! Can not load value from '%s'", value); + return defaultValue; + } + + std::string variableID = variableGroup + value; + + if (variables.count(variableID) == 0) + { + logMod->warn("Invalid syntax in load value! Unknown variable '%s'", value); + return defaultValue; + } + return variables.at(variableID); + } + + si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) { if(value.isNull()) return defaultValue; if(value.isNumber()) return static_cast(value.Float()); + if (value.isString()) + return loadVariable("number", value.String(), variables, defaultValue); + if(value.isVector()) { const auto & vector = value.Vector(); size_t index= rng.getIntRange(0, vector.size()-1)(); - return loadValue(vector[index], rng, 0); + return loadValue(vector[index], rng, variables, 0); } if(value.isStruct()) { if (!value["amount"].isNull()) - return static_cast(loadValue(value["amount"], rng, defaultValue)); - si32 min = static_cast(loadValue(value["min"], rng, 0)); - si32 max = static_cast(loadValue(value["max"], rng, 0)); + return static_cast(loadValue(value["amount"], rng, variables, defaultValue)); + si32 min = static_cast(loadValue(value["min"], rng, variables, 0)); + si32 max = static_cast(loadValue(value["max"], rng, variables, 0)); return rng.getIntRange(min, max)(); } return defaultValue; } - std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet) + template + IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { - if(value.isString()) - return value.String(); - - if(value.isStruct()) - { - if(!value["type"].isNull()) - return value["type"].String(); - - if(!value["anyOf"].isNull()) - return RandomGeneratorUtil::nextItem(value["anyOf"].Vector(), rng)->String(); - - if(!value["noneOf"].isNull()) - { - auto copyValuesSet = valuesSet; - for(auto & s : value["noneOf"].Vector()) - copyValuesSet.erase(s.String()); - - if(!copyValuesSet.empty()) - return *RandomGeneratorUtil::nextItem(copyValuesSet, rng); - } - } - - return valuesSet.empty() ? "" : *RandomGeneratorUtil::nextItem(valuesSet, rng); + if (value.empty() || value[0] != '@') + return IdentifierType(*VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value, variables, IdentifierType::NONE); } - TResources loadResources(const JsonNode & value, CRandomGenerator & rng) + template + IdentifierType decodeKey(const JsonNode & value, const Variables & variables) { - TResources ret; - - if (value.isVector()) - { - for (const auto & entry : value.Vector()) - ret += loadResource(entry, rng); - return ret; - } - - for (size_t i=0; iidentifiers()->getIdentifier(IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE); } - TResources loadResource(const JsonNode & value, CRandomGenerator & rng) + template<> + PlayerColor decodeKey(const JsonNode & value, const Variables & variables) { - std::set defaultResources(std::begin(GameConstants::RESOURCE_NAMES), std::end(GameConstants::RESOURCE_NAMES) - 1); //except mithril - - std::string resourceName = loadKey(value, rng, defaultResources); - si32 resourceAmount = loadValue(value, rng, 0); - si32 resourceID(VLC->modh->identifiers.getIdentifier(value.meta, "resource", resourceName).value()); - - TResources ret; - ret[resourceID] = resourceAmount; - return ret; + return PlayerColor(*VLC->identifiers()->getIdentifier("playerColor", value)); } - - std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng) + template<> + PrimarySkill decodeKey(const JsonNode & value, const Variables & variables) { - std::vector ret; - if(value.isStruct()) - { - for(const auto & name : PrimarySkill::names) - { - ret.push_back(loadValue(value[name], rng)); - } - } - if(value.isVector()) - { - ret.resize(GameConstants::PRIMARY_SKILLS, 0); - std::set defaultStats(std::begin(PrimarySkill::names), std::end(PrimarySkill::names)); - for(const auto & element : value.Vector()) - { - auto key = loadKey(element, rng, defaultStats); - defaultStats.erase(key); - int id = vstd::find_pos(PrimarySkill::names, key); - if(id != -1) - ret[id] += loadValue(element, rng); - } - } - return ret; + return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); } - std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng) + template<> + PrimarySkill decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { - std::map ret; - if(value.isStruct()) - { - for(const auto & pair : value.Struct()) - { - SecondarySkill id(VLC->modh->identifiers.getIdentifier(pair.second.meta, "skill", pair.first).value()); - ret[id] = loadValue(pair.second, rng); - } - } - if(value.isVector()) - { - std::set defaultSkills; - for(const auto & skill : VLC->skillh->objects) - { - IObjectInterface::cb->isAllowed(2, skill->getIndex()); - auto scopeAndName = vstd::splitStringToPair(skill->getJsonKey(), ':'); - if(scopeAndName.first == CModHandler::scopeBuiltin() || scopeAndName.first == value.meta) - defaultSkills.insert(scopeAndName.second); - else - defaultSkills.insert(skill->getJsonKey()); - } - - for(const auto & element : value.Vector()) - { - auto key = loadKey(element, rng, defaultSkills); - defaultSkills.erase(key); //avoid dupicates - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "skill", key)) - { - SecondarySkill id(identifier.value()); - ret[id] = loadValue(element, rng); - } - } - } - return ret; + if (value.empty() || value[0] != '@') + return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value)); + else + return PrimarySkill(loadVariable("primarySkill", value, variables, PrimarySkill::NONE.getNum())); } - ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng) + /// Method that allows type-specific object filtering + /// Default implementation is to accept all input objects + template + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return ArtifactID(VLC->modh->identifiers.getIdentifier("artifact", value).value()); + return valuesSet; + } + + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + { + assert(value.isStruct()); std::set allowedClasses; std::set allowedPositions; @@ -194,60 +142,59 @@ namespace JsonRandom allowedClasses.insert(CArtHandler::stringToClass(entry.String())); if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING) - allowedPositions.insert(ArtifactPosition(value["class"].String())); + allowedPositions.insert(ArtifactPosition::decode(value["class"].String())); else for(const auto & entry : value["slot"].Vector()) - allowedPositions.insert(ArtifactPosition(entry.String())); + allowedPositions.insert(ArtifactPosition::decode(entry.String())); - if (!value["minValue"].isNull()) minValue = static_cast(value["minValue"].Float()); - if (!value["maxValue"].isNull()) maxValue = static_cast(value["maxValue"].Float()); + if (!value["minValue"].isNull()) + minValue = static_cast(value["minValue"].Float()); + if (!value["maxValue"].isNull()) + maxValue = static_cast(value["maxValue"].Float()); - return VLC->arth->pickRandomArtifact(rng, [=](const ArtifactID & artID) -> bool + std::set result; + + for (auto const & artID : valuesSet) { - CArtifact * art = VLC->arth->objects[artID]; + const CArtifact * art = artID.toArtifact(); if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) - return false; + continue; if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) - return false; - - if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) - return false; + continue; + + if(!IObjectInterface::cb->isAllowed(art->getId())) + continue; if(!allowedPositions.empty()) { + bool positionAllowed = false; for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO)) { if(allowedPositions.count(pos)) - return true; + positionAllowed = true; } - return false; + + if (!positionAllowed) + continue; } - return true; - }); - } - std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng) - { - std::vector ret; - for (const JsonNode & entry : value.Vector()) - { - ret.push_back(loadArtifact(entry, rng)); + result.insert(artID); } - return ret; + return result; } - SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells) + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return SpellID(VLC->modh->identifiers.getIdentifier("spell", value).value()); + std::set result = valuesSet; if (!value["level"].isNull()) { - int32_t spellLevel = value["level"].Float(); + int32_t spellLevel = value["level"].Integer(); - vstd::erase_if(spells, [=](const SpellID & spell) + vstd::erase_if(result, [=](const SpellID & spell) { return VLC->spellh->getById(spell)->getLevel() != spellLevel; }); @@ -255,58 +202,309 @@ namespace JsonRandom if (!value["school"].isNull()) { - int32_t schoolID = VLC->modh->identifiers.getIdentifier("spellSchool", value["school"]).value(); + int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value(); - vstd::erase_if(spells, [=](const SpellID & spell) + vstd::erase_if(result, [=](const SpellID & spell) { - return !VLC->spellh->getById(spell)->hasSchool(ESpellSchool(schoolID)); + return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID)); }); } - - if (spells.empty()) - { - logMod->warn("Failed to select suitable random spell!"); - return SpellID::NONE; - } - return SpellID(*RandomGeneratorUtil::nextItem(spells, rng)); + return result; } - std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells) + template + std::set filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables) { - std::vector ret; - for (const JsonNode & entry : value.Vector()) + if(value.isString()) + return { decodeKey(value, variables) }; + + assert(value.isStruct()); + + if(value.isStruct()) { - ret.push_back(loadSpell(entry, rng, spells)); + if(!value["type"].isNull()) + return filterKeys(value["type"], valuesSet, variables); + + std::set filteredTypes = filterKeysTyped(value, valuesSet); + + if(!value["anyOf"].isNull()) + { + std::set filteredAnyOf; + for (auto const & entry : value["anyOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet, variables); + filteredAnyOf.insert(subset.begin(), subset.end()); + } + + vstd::erase_if(filteredTypes, [&](const IdentifierType & value) + { + return filteredAnyOf.count(value) == 0; + }); + } + + if(!value["noneOf"].isNull()) + { + for (auto const & entry : value["noneOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet, variables); + for (auto bannedEntry : subset ) + filteredTypes.erase(bannedEntry); + } + } + + return filteredTypes; + } + return valuesSet; + } + + TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + TResources ret; + + if (value.isVector()) + { + for (const auto & entry : value.Vector()) + ret += loadResource(entry, rng, variables); + return ret; + } + + for (size_t i=0; i defaultResources{ + GameResID::WOOD, + GameResID::MERCURY, + GameResID::ORE, + GameResID::SULFUR, + GameResID::CRYSTAL, + GameResID::GEMS, + GameResID::GOLD + }; + + std::set potentialPicks = filterKeys(value, defaultResources, variables); + GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + si32 resourceAmount = loadValue(value, rng, variables, 0); + + TResources ret; + ret[resourceID] = resourceAmount; + return ret; + } + + PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret(GameConstants::PRIMARY_SKILLS, 0); + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + + if(value.isStruct()) + { + for(const auto & pair : value.Struct()) + { + PrimarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[id.getNum()] += loadValue(pair.second, rng, variables); + } + } + if(value.isVector()) + { + for(const auto & element : value.Vector()) + { + std::set potentialPicks = filterKeys(element, defaultSkills, variables); + PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); + ret[skillID.getNum()] += loadValue(element, rng, variables); + } + } + return ret; + } + + SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills; + for(const auto & skill : VLC->skillh->objects) + if (IObjectInterface::cb->isAllowed(skill->getId())) + defaultSkills.insert(skill->getId()); + + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::map ret; + if(value.isStruct()) + { + for(const auto & pair : value.Struct()) + { + SecondarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[id] = loadValue(pair.second, rng, variables); + } + } + if(value.isVector()) + { + std::set defaultSkills; + for(const auto & skill : VLC->skillh->objects) + if (IObjectInterface::cb->isAllowed(skill->getId())) + defaultSkills.insert(skill->getId()); + + for(const auto & element : value.Vector()) + { + std::set potentialPicks = filterKeys(element, defaultSkills, variables); + SecondarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); //avoid dupicates + ret[skillID] = loadValue(element, rng, variables); + } + } + return ret; + } + + ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set allowedArts; + for (auto const * artifact : VLC->arth->allowedArtifacts) + allowedArts.insert(artifact->getId()); + + std::set potentialPicks = filterKeys(value, allowedArts, variables); + + return IObjectInterface::cb->gameState()->pickRandomArtifact(rng, potentialPicks); + } + + std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret; + for (const JsonNode & entry : value.Vector()) + { + ret.push_back(loadArtifact(entry, rng, variables)); + } + return ret; + } + + SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSpells; + for(const auto & spell : VLC->spellh->objects) + if (IObjectInterface::cb->isAllowed(spell->getId()) && !spell->isSpecial()) + defaultSpells.insert(spell->getId()); + + std::set potentialPicks = filterKeys(value, defaultSpells, variables); + + if (potentialPicks.empty()) + { + logMod->warn("Failed to select suitable random spell!"); + return SpellID::NONE; + } + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret; + for (const JsonNode & entry : value.Vector()) + { + ret.push_back(loadSpell(entry, rng, variables)); + } + return ret; + } + + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::vector ret; + std::set defaultPlayers; + for(size_t i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + defaultPlayers.insert(PlayerColor(i)); + + for(auto & entry : value.Vector()) + { + std::set potentialPicks = filterKeys(entry, defaultPlayers, variables); + ret.push_back(*RandomGeneratorUtil::nextItem(potentialPicks, rng)); + } + + return ret; + } + + std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroTypes()->getByIndex(VLC->identifiers()->getIdentifier("hero", entry.String()).value())->getId()); + } + return ret; + } + + std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroClasses()->getByIndex(VLC->identifiers()->getIdentifier("heroClass", entry.String()).value())->getId()); + } + return ret; + } + + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { CStackBasicDescriptor stack; - stack.type = VLC->creh->objects[VLC->modh->identifiers.getIdentifier("creature", value["type"]).value()]; - stack.count = loadValue(value, rng); + + std::set defaultCreatures; + for(const auto & creature : VLC->creh->objects) + if (!creature->special) + defaultCreatures.insert(creature->getId()); + + std::set potentialPicks = filterKeys(value, defaultCreatures, variables); + CreatureID pickedCreature; + + if (!potentialPicks.empty()) + pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + else + logMod->warn("Failed to select suitable random creature!"); + + stack.type = pickedCreature.toCreature(); + stack.count = loadValue(value, rng, variables); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade { - stack.type = VLC->creh->objects[*RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)]; + stack.type = RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)->toCreature(); } } return stack; } - std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng) + std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) { - ret.push_back(loadCreature(node, rng)); + ret.push_back(loadCreature(node, rng, variables)); } return ret; } - std::vector evaluateCreatures(const JsonNode & value) + std::vector evaluateCreatures(const JsonNode & value, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) @@ -320,25 +518,18 @@ namespace JsonRandom info.minAmount = static_cast(node["min"].Float()); info.maxAmount = static_cast(node["max"].Float()); } - const CCreature * crea = VLC->creh->objects[VLC->modh->identifiers.getIdentifier("creature", node["type"]).value()]; + const CCreature * crea = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", node["type"]).value()]; info.allowedCreatures.push_back(crea); if (node["upgradeChance"].Float() > 0) { for(const auto & creaID : crea->upgrades) - info.allowedCreatures.push_back(VLC->creh->objects[creaID]); + info.allowedCreatures.push_back(creaID.toCreature()); } ret.push_back(info); } return ret; } - //std::vector loadComponents(const JsonNode & value) - //{ - // std::vector ret; - // return ret; - // //TODO - //} - std::vector DLL_LINKAGE loadBonuses(const JsonNode & value) { std::vector ret; diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 2ac9e1d65..804c87e3e 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -24,6 +24,8 @@ class CStackBasicDescriptor; namespace JsonRandom { + using Variables = std::map; + struct DLL_LINKAGE RandomStackInfo { std::vector allowedCreatures; @@ -31,25 +33,30 @@ namespace JsonRandom si32 maxAmount; }; - DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0); - DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet = {}); - DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); - DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells = {}); - DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells = {}); + DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value); + DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + + DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); + + DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); - //DLL_LINKAGE std::vector loadComponents(const JsonNode & value); } VCMI_LIB_NAMESPACE_END diff --git a/lib/Languages.h b/lib/Languages.h index d00ce6ff7..2f7e41564 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -1,112 +1,172 @@ -/* - * Languages.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 - -namespace Languages -{ - -enum class ELanguages -{ - CZECH, - CHINESE, - ENGLISH, - FINNISH, - FRENCH, - GERMAN, - HUNGARIAN, - ITALIAN, - KOREAN, - POLISH, - PORTUGUESE, - RUSSIAN, - SPANISH, - SWEDISH, - TURKISH, - UKRAINIAN, - VIETNAMESE, - - // Pseudo-languages, that have no translations but can define H3 encoding to use - OTHER_CP1250, - OTHER_CP1251, - OTHER_CP1252, - - COUNT -}; - -struct Options -{ - /// string identifier (ascii, lower-case), e.g. "english" - std::string identifier; - - /// human-readable name of language in English - std::string nameEnglish; - - /// human-readable name of language in its own language - std::string nameNative; - - /// encoding that is used by H3 for this language - std::string encoding; - - /// primary IETF language tag - std::string tagIETF; - - /// VCMI supports translations into this language - bool hasTranslation = false; -}; - -inline const auto & getLanguageList() -{ - static const std::array languages - { { - { "czech", "Czech", "Čeština", "CP1250", "cs", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, - { "french", "French", "Français", "CP1252", "fr", true }, - { "german", "German", "Deutsch", "CP1252", "de", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", true }, - { "korean", "Korean", "한국어", "CP949", "ko", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding - - { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", false } - } }; - static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); - - return languages; -} - -inline const Options & getLanguageOptions(ELanguages language) -{ - assert(language < ELanguages::COUNT); - return getLanguageList()[static_cast(language)]; -} - -inline const Options & getLanguageOptions(const std::string & language) -{ - for(const auto & entry : getLanguageList()) - if(entry.identifier == language) - return entry; - - static const Options emptyValue; - assert(0); - return emptyValue; -} - -} +/* + * Languages.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 + +namespace Languages +{ + +enum class EPluralForms +{ + NONE, + VI_1, // Single plural form, (Vietnamese) + EN_2, // Two forms, singular used for one only (English) + FR_2, // Two forms, singular used for zero and one (French) + UK_3, // Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] (Ukrainian) + CZ_3, // Three forms, special cases for 1 and 2, 3, 4 (Czech) + PL_3, // Three forms, special case for one and some numbers ending in 2, 3, or 4 (Polish) +}; + +enum class ELanguages +{ + CZECH, + CHINESE, + ENGLISH, + FINNISH, + FRENCH, + GERMAN, + HUNGARIAN, + ITALIAN, + KOREAN, + POLISH, + PORTUGUESE, + RUSSIAN, + SPANISH, + SWEDISH, + TURKISH, + UKRAINIAN, + VIETNAMESE, + + // Pseudo-languages, that have no translations but can define H3 encoding to use + OTHER_CP1250, + OTHER_CP1251, + OTHER_CP1252, + + COUNT +}; + +struct Options +{ + /// string identifier (ascii, lower-case), e.g. "english" + std::string identifier; + + /// human-readable name of language in English + std::string nameEnglish; + + /// human-readable name of language in its own language + std::string nameNative; + + /// encoding that is used by H3 for this language + std::string encoding; + + /// primary IETF language tag + std::string tagIETF; + + /// Ruleset for plural forms in this language + EPluralForms pluralForms = EPluralForms::NONE; + + /// VCMI supports translations into this language + bool hasTranslation = false; +}; + +inline const auto & getLanguageList() +{ + static const std::array languages + { { + { "czech", "Czech", "Čeština", "CP1250", "cs", EPluralForms::CZ_3, true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", EPluralForms::EN_2, true }, + { "finnish", "Finnish", "Suomi", "CP1252", "fi", EPluralForms::EN_2, true }, + { "french", "French", "Français", "CP1252", "fr", EPluralForms::FR_2, true }, + { "german", "German", "Deutsch", "CP1252", "de", EPluralForms::EN_2, true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", EPluralForms::EN_2, true }, + { "italian", "Italian", "Italiano", "CP1250", "it", EPluralForms::EN_2, true }, + { "korean", "Korean", "한국어", "CP949", "ko", EPluralForms::VI_1, true }, + { "polish", "Polish", "Polski", "CP1250", "pl", EPluralForms::PL_3, true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", EPluralForms::UK_3, true }, + { "spanish", "Spanish", "Español", "CP1252", "es", EPluralForms::EN_2, true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", EPluralForms::EN_2, true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", EPluralForms::EN_2, true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", EPluralForms::UK_3, true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", EPluralForms::VI_1, true }, // Fan translation uses special encoding + + { "other_cp1250", "Other (East European)", "", "CP1250", "", EPluralForms::NONE, false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", EPluralForms::NONE, false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", EPluralForms::NONE, false } + } }; + static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); + + return languages; +} + +inline const Options & getLanguageOptions(ELanguages language) +{ + assert(language < ELanguages::COUNT); + return getLanguageList()[static_cast(language)]; +} + +inline const Options & getLanguageOptions(const std::string & language) +{ + for(const auto & entry : getLanguageList()) + if(entry.identifier == language) + return entry; + + static const Options emptyValue; + assert(0); + return emptyValue; +} + +template +inline constexpr int getPluralFormIndex(EPluralForms form, Numeric value) +{ + // Based on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + switch(form) + { + case EPluralForms::NONE: + case EPluralForms::VI_1: + return 0; + case EPluralForms::EN_2: + if (value == 1) + return 1; + return 2; + case EPluralForms::FR_2: + if (value == 1 || value == 0) + return 1; + return 2; + case EPluralForms::UK_3: + if (value % 10 == 1 && value % 100 != 11) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + case EPluralForms::CZ_3: + if (value == 1) + return 1; + if (value>=2 && value<=4) + return 2; + return 0; + case EPluralForms::PL_3: + if (value == 1) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + } + throw std::runtime_error("Invalid plural form enumeration received!"); +} + +template +inline std::string getPluralFormTextID(std::string languageName, Numeric value, std::string textID) +{ + int formIndex = getPluralFormIndex(getLanguageOptions(languageName).pluralForms, value); + return textID + '.' + std::to_string(formIndex); +} + +} diff --git a/lib/LoadProgress.cpp b/lib/LoadProgress.cpp index aa24a4c2e..70eadbdbb 100644 --- a/lib/LoadProgress.cpp +++ b/lib/LoadProgress.cpp @@ -18,6 +18,11 @@ Progress::Progress(): _progress(std::numeric_limits::min()) setupSteps(100); } +Progress::Progress(int steps): _progress(std::numeric_limits::min()) +{ + setupSteps(steps); +} + Type Progress::get() const { if(_step >= _maxSteps) @@ -82,3 +87,49 @@ void Progress::step(int count) _step += count; } } + +void ProgressAccumulator::include(const Progress & p) +{ + boost::unique_lock guard(_mx); + _progress.emplace_back(p); +} + +void ProgressAccumulator::exclude(const Progress & p) +{ + boost::unique_lock guard(_mx); + for(auto i = _progress.begin(); i != _progress.end(); ++i) + { + if(&i->get() == &p) + { + _accumulated += static_cast(p.get()) * p._maxSteps; + _steps += p._maxSteps; + _progress.erase(i); + return; + } + } +} + +bool ProgressAccumulator::finished() const +{ + boost::unique_lock guard(_mx); + for(auto i : _progress) + if(!i.get().finished()) + return false; + return true; +} + +Type ProgressAccumulator::get() const +{ + boost::unique_lock guard(_mx); + auto sum = _accumulated; + auto totalSteps = _steps; + for(auto p : _progress) + { + sum += static_cast(p.get().get()) * p.get()._maxSteps; + totalSteps += p.get()._maxSteps; + } + + if(totalSteps) + sum /= totalSteps; + return static_cast(sum); +} diff --git a/lib/LoadProgress.h b/lib/LoadProgress.h index 2911dab2a..1c3c21b0d 100644 --- a/lib/LoadProgress.h +++ b/lib/LoadProgress.h @@ -18,6 +18,8 @@ namespace Load using Type = unsigned char; +class ProgressAccumulator; + /* * Purpose of that class is to track progress of computations * Derive from this class if you want to translate user or system @@ -29,8 +31,9 @@ class DLL_LINKAGE Progress public: //Sets current state to 0. - //Amount of steps to finish progress will be equal to 100 + //Amount of steps to finish progress will be equal to 100 for default constructor Progress(); + Progress(int steps); virtual ~Progress() = default; //Returns current state of the progress @@ -67,5 +70,25 @@ public: private: std::atomic _progress, _target; std::atomic _step, _maxSteps; + + friend class ProgressAccumulator; }; + +class DLL_LINKAGE ProgressAccumulator +{ +public: + ProgressAccumulator() = default; + + void include(const Progress &); + void exclude(const Progress &); + + bool finished() const; + Type get() const; + +private: + mutable boost::mutex _mx; + long long _accumulated = 0, _steps = 0; + std::vector> _progress; +}; + } diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 78329a899..8fdb6d3a0 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -19,6 +19,7 @@ #include "VCMI_Lib.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "spells/CSpellHandler.h" +#include "serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -50,8 +51,11 @@ void MetaString::appendRawString(const std::string & value) void MetaString::appendTextID(const std::string & value) { - message.push_back(EMessage::APPEND_TEXTID_STRING); - stringsTextID.push_back(value); + if (!value.empty()) + { + message.push_back(EMessage::APPEND_TEXTID_STRING); + stringsTextID.push_back(value); + } } void MetaString::appendNumber(int64_t value) @@ -101,7 +105,7 @@ void MetaString::clear() bool MetaString::empty() const { - return message.empty(); + return message.empty() || toString().empty(); } std::string MetaString::getLocalString(const std::pair & txt) const @@ -111,74 +115,12 @@ std::string MetaString::getLocalString(const std::pair & txt) c switch(type) { - case EMetaText::ART_NAMES: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getNameTranslated(); - return "#!#"; - } - case EMetaText::ART_DESCR: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getDescriptionTranslated(); - return "#!#"; - } - case EMetaText::ART_EVNTS: - { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); - if(art) - return art->getEventTranslated(); - return "#!#"; - } - case EMetaText::CRE_PL_NAMES: - { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); - if(cre) - return cre->getNamePluralTranslated(); - return "#!#"; - } - case EMetaText::CRE_SING_NAMES: - { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); - if(cre) - return cre->getNameSingularTranslated(); - return "#!#"; - } - case EMetaText::MINE_NAMES: - { - return VLC->generaltexth->translate("core.minename", ser); - } - case EMetaText::MINE_EVNTS: - { - return VLC->generaltexth->translate("core.mineevnt", ser); - } - case EMetaText::SPELL_NAME: - { - const auto * spell = SpellID(ser).toSpell(VLC->spells()); - if(spell) - return spell->getNameTranslated(); - return "#!#"; - } - case EMetaText::OBJ_NAMES: - return VLC->objtypeh->getObjectName(ser, 0); - case EMetaText::SEC_SKILL_NAME: - return VLC->skillh->getByIndex(ser)->getNameTranslated(); case EMetaText::GENERAL_TXT: return VLC->generaltexth->translate("core.genrltxt", ser); - case EMetaText::RES_NAMES: - return VLC->generaltexth->translate("core.restypes", ser); case EMetaText::ARRAY_TXT: return VLC->generaltexth->translate("core.arraytxt", ser); - case EMetaText::CREGENS: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser); - case EMetaText::CREGENS4: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser); case EMetaText::ADVOB_TXT: return VLC->generaltexth->translate("core.advevent", ser); - case EMetaText::COLOR: - return VLC->generaltexth->translate("vcmi.capitalColors", ser); case EMetaText::JK_TXT: return VLC->generaltexth->translate("core.jktext", ser); default: @@ -286,20 +228,6 @@ DLL_LINKAGE std::string MetaString::buildList() const return lista; } -void MetaString::replaceCreatureName(const CreatureID & id, TQuantity count) //adds sing or plural name; -{ - if (count == 1) - replaceLocalString (EMetaText::CRE_SING_NAMES, id); - else - replaceLocalString (EMetaText::CRE_PL_NAMES, id); -} - -void MetaString::replaceCreatureName(const CStackBasicDescriptor & stack) -{ - assert(stack.type); //valid type - replaceCreatureName(stack.type->getId(), stack.count); -} - bool MetaString::operator == (const MetaString & other) const { return message == other.message && localStrings == other.localStrings && exactStrings == other.exactStrings && stringsTextID == other.stringsTextID && numbers == other.numbers; @@ -385,4 +313,94 @@ void MetaString::jsonDeserialize(const JsonNode & source) numbers.push_back(entry.Integer()); } +void MetaString::serializeJson(JsonSerializeFormat & handler) +{ + if(handler.saving) + jsonSerialize(const_cast(handler.getCurrent())); + + if(!handler.saving) + jsonDeserialize(handler.getCurrent()); +} + +void MetaString::appendName(const SpellID & id) +{ + appendTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::appendName(const PlayerColor & id) +{ + appendTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::appendName(const CreatureID & id, TQuantity count) +{ + if(count == 1) + appendNameSingular(id); + else + appendNamePlural(id); +} + +void MetaString::appendNameSingular(const CreatureID & id) +{ + appendTextID(id.toEntity(VLC)->getNameSingularTextID()); +} + +void MetaString::appendNamePlural(const CreatureID & id) +{ + appendTextID(id.toEntity(VLC)->getNamePluralTextID()); +} + +void MetaString::replaceName(const ArtifactID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::replaceName(const MapObjectID& id) +{ + replaceTextID(VLC->objtypeh->getObjectName(id, 0)); +} + +void MetaString::replaceName(const PlayerColor & id) +{ + replaceTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::replaceName(const SecondarySkill & id) +{ + replaceTextID(VLC->skillh->getById(id)->getNameTextID()); +} + +void MetaString::replaceName(const SpellID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameTextID()); +} + +void MetaString::replaceName(const GameResID& id) +{ + replaceTextID(TextIdentifier("core.restypes", id.getNum()).get()); +} + +void MetaString::replaceNameSingular(const CreatureID & id) +{ + replaceTextID(id.toEntity(VLC)->getNameSingularTextID()); +} + +void MetaString::replaceNamePlural(const CreatureID & id) +{ + replaceTextID(id.toEntity(VLC)->getNamePluralTextID()); +} + +void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing or plural name; +{ + if(count == 1) + replaceNameSingular(id); + else + replaceNamePlural(id); +} + +void MetaString::replaceName(const CStackBasicDescriptor & stack) +{ + replaceName(stack.type->getId(), stack.count); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/MetaString.h b/lib/MetaString.h index c7329f629..ff2ffca19 100644 --- a/lib/MetaString.h +++ b/lib/MetaString.h @@ -12,30 +12,24 @@ VCMI_LIB_NAMESPACE_BEGIN class JsonNode; +class ArtifactID; class CreatureID; class CStackBasicDescriptor; +class JsonSerializeFormat; +class MapObjectID; +class MapObjectSubID; +class PlayerColor; +class SecondarySkill; +class SpellID; +class GameResID; using TQuantity = si32; /// Strings classes that can be used as replacement in MetaString enum class EMetaText : uint8_t { GENERAL_TXT = 1, - OBJ_NAMES, - RES_NAMES, - ART_NAMES, ARRAY_TXT, - CRE_PL_NAMES, - CREGENS, - MINE_NAMES, - MINE_EVNTS, ADVOB_TXT, - ART_EVNTS, - SPELL_NAME, - SEC_SKILL_NAME, - CRE_SING_NAMES, - CREGENS4, - COLOR, - ART_DESCR, JK_TXT }; @@ -81,6 +75,12 @@ public: /// Appends specified number to resulting string void appendNumber(int64_t value); + void appendName(const SpellID& id); + void appendName(const PlayerColor& id); + void appendName(const CreatureID & id, TQuantity count); + void appendNameSingular(const CreatureID & id); + void appendNamePlural(const CreatureID & id); + /// Replaces first '%s' placeholder in string with specified local string void replaceLocalString(EMetaText type, ui32 serial); /// Replaces first '%s' placeholder in string with specified fixed, untranslated string @@ -92,10 +92,19 @@ public: /// Replaces first '%+d' placeholder in string with specified number using '+' sign as prefix void replacePositiveNumber(int64_t txt); + void replaceName(const ArtifactID & id); + void replaceName(const MapObjectID& id); + void replaceName(const PlayerColor& id); + void replaceName(const SecondarySkill& id); + void replaceName(const SpellID& id); + void replaceName(const GameResID& id); + /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CreatureID & id, TQuantity count); + void replaceName(const CreatureID & id, TQuantity count); + void replaceNameSingular(const CreatureID & id); + void replaceNamePlural(const CreatureID & id); /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CStackBasicDescriptor & stack); + void replaceName(const CStackBasicDescriptor & stack); /// erases any existing content in the string void clear(); @@ -113,6 +122,8 @@ public: void jsonSerialize(JsonNode & dest) const; void jsonDeserialize(const JsonNode & dest); + + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler & h, const int version) { diff --git a/lib/NetPacks.h b/lib/NetPacks.h deleted file mode 100644 index 735737da5..000000000 --- a/lib/NetPacks.h +++ /dev/null @@ -1,2648 +0,0 @@ -/* - * NetPacks.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 "ConstTransitivePtr.h" -#include "MetaString.h" -#include "ResourceSet.h" -#include "int3.h" - -#include "battle/BattleAction.h" -#include "battle/CObstacleInstance.h" -#include "gameState/EVictoryLossCheckResult.h" -#include "gameState/TavernSlot.h" -#include "gameState/QuestInfo.h" -#include "mapObjects/CGHeroInstance.h" -#include "mapping/CMapDefines.h" -#include "spells/ViewSpellInt.h" - -class CClient; -class CGameHandler; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGameState; -class CArtifact; -class CGObjectInstance; -class CArtifactInstance; -struct StackLocation; -struct ArtSlotInfo; -struct QuestInfo; -class IBattleState; - -// This one teleport-specific, but has to be available everywhere in callbacks and netpacks -// For now it's will be there till teleports code refactored and moved into own file -using TTeleportExitsList = std::vector>; - -struct DLL_LINKAGE Query : public CPackForClient -{ - QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) -}; - -struct StackLocation -{ - ConstTransitivePtr army; - SlotID slot; - - StackLocation() = default; - StackLocation(const CArmedInstance * Army, const SlotID & Slot) - : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - - DLL_LINKAGE const CStackInstance * getStack(); - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - } -}; - -/***********************************************************************************************************/ -struct DLL_LINKAGE PackageApplied : public CPackForClient -{ - PackageApplied() = default; - PackageApplied(ui8 Result) - : result(Result) - { - } - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK - ui32 packType = 0; //type id of applied package - ui32 requestID = 0; //an ID given by client to the request that was applied - PlayerColor player; - - template void serialize(Handler & h, const int version) - { - h & result; - h & packType; - h & requestID; - h & player; - } -}; - -struct DLL_LINKAGE SystemMessage : public CPackForClient -{ - SystemMessage(std::string Text) - : text(std::move(Text)) - { - } - SystemMessage() = default; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - std::string text; - - template void serialize(Handler & h, const int version) - { - h & text; - } -}; - -struct DLL_LINKAGE PlayerBlocked : public CPackForClient -{ - enum EReason { UPCOMING_BATTLE, ONGOING_MOVEMENT }; - enum EMode { BLOCKADE_STARTED, BLOCKADE_ENDED }; - - EReason reason = UPCOMING_BATTLE; - EMode startOrEnd = BLOCKADE_STARTED; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & reason; - h & startOrEnd; - h & player; - } -}; - -struct DLL_LINKAGE PlayerCheated : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - bool losingCheatCode = false; - bool winningCheatCode = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & losingCheatCode; - h & winningCheatCode; - } -}; - -struct DLL_LINKAGE YourTurn : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - std::optional daysWithoutCastle; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & daysWithoutCastle; - } -}; - -struct DLL_LINKAGE EntitiesChanged : public CPackForClient -{ - std::vector changes; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & changes; - } -}; - -struct DLL_LINKAGE SetResources : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - bool abs = true; //false - changes by value; 1 - sets to value - PlayerColor player; - TResources res; //res[resid] => res amount - - template void serialize(Handler & h, const int version) - { - h & abs; - h & player; - h & res; - } -}; - -struct DLL_LINKAGE SetPrimSkill : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 abs = 0; //0 - changes by value; 1 - sets to value - ObjectInstanceID id; - PrimarySkill::PrimarySkill which = PrimarySkill::ATTACK; - si64 val = 0; - - template void serialize(Handler & h, const int version) - { - h & abs; - h & id; - h & which; - h & val; - } -}; - -struct DLL_LINKAGE SetSecSkill : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 abs = 0; //0 - changes by value; 1 - sets to value - ObjectInstanceID id; - SecondarySkill which; - ui16 val = 0; - - template void serialize(Handler & h, const int version) - { - h & abs; - h & id; - h & which; - h & val; - } -}; - -struct DLL_LINKAGE HeroVisitCastle : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 flags = 0; //1 - start - ObjectInstanceID tid, hid; - - bool start() const //if hero is entering castle (if false - leaving) - { - return flags & 1; - } - - template void serialize(Handler & h, const int version) - { - h & flags; - h & tid; - h & hid; - } -}; - -struct DLL_LINKAGE ChangeSpells : public CPackForClient -{ - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 learn = 1; //1 - gives spell, 0 - takes - ObjectInstanceID hid; - std::set spells; - - template void serialize(Handler & h, const int version) - { - h & learn; - h & hid; - h & spells; - } -}; - -struct DLL_LINKAGE SetMana : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ObjectInstanceID hid; - si32 val = 0; - bool absolute = true; - - template void serialize(Handler & h, const int version) - { - h & val; - h & hid; - h & absolute; - } -}; - -struct DLL_LINKAGE SetMovePoints : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID hid; - si32 val = 0; - bool absolute = true; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & val; - h & hid; - h & absolute; - } -}; - -struct DLL_LINKAGE FoWChange : public CPackForClient -{ - void applyGs(CGameState * gs); - - std::unordered_set tiles; - PlayerColor player; - ui8 mode = 0; //mode==0 - hide, mode==1 - reveal - bool waitForDialogs = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tiles; - h & player; - h & mode; - h & waitForDialogs; - } -}; - -struct DLL_LINKAGE SetAvailableHero : public CPackForClient -{ - SetAvailableHero() - { - army.clearSlots(); - } - void applyGs(CGameState * gs); - - TavernHeroSlot slotID; - TavernSlotRole roleID; - PlayerColor player; - HeroTypeID hid; //HeroTypeID::NONE if no hero - CSimpleArmy army; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & slotID; - h & roleID; - h & player; - h & hid; - h & army; - } -}; - -struct DLL_LINKAGE GiveBonus : public CPackForClient -{ - enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; - - GiveBonus(ETarget Who = ETarget::HERO) - :who(Who) - { - } - - void applyGs(CGameState * gs); - - ETarget who = ETarget::HERO; //who receives bonus - si32 id = 0; //hero. town or player id - whoever receives it - Bonus bonus; - MetaString bdescr; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & bonus; - h & id; - h & bdescr; - h & who; - assert(id != -1); - } -}; - -struct DLL_LINKAGE ChangeObjPos : public CPackForClient -{ - void applyGs(CGameState * gs); - - /// Object to move - ObjectInstanceID objid; - /// New position of visitable tile of an object - int3 nPos; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & objid; - h & nPos; - } -}; - -struct DLL_LINKAGE PlayerEndsGame : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - EVictoryLossCheckResult victoryLossCheckResult; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & victoryLossCheckResult; - } -}; - -struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient -{ - void applyGs(CGameState * gs); - - std::vector players; - ui8 playerConnectionId; //PLAYER_AI for AI player - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & players; - h & playerConnectionId; - } -}; - -struct DLL_LINKAGE RemoveBonus : public CPackForClient -{ - RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) - :who(Who) - { - } - - void applyGs(CGameState * gs); - - GiveBonus::ETarget who; //who receives bonus - ui32 whoID = 0; //hero, town or player id - whoever loses bonus - - //vars to identify bonus: its source - ui8 source = 0; - ui32 id = 0; //source id - - //used locally: copy of removed bonus - Bonus bonus; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & source; - h & id; - h & who; - h & whoID; - } -}; - -struct DLL_LINKAGE SetCommanderProperty : public CPackForClient -{ - enum ECommanderProperty { ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL }; - - void applyGs(CGameState * gs); - - ObjectInstanceID heroid; - - ECommanderProperty which = ALIVE; - TExpType amount = 0; //0 for dead, >0 for alive - si32 additionalInfo = 0; //for secondary skills choice - Bonus accumulatedBonus; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroid; - h & which; - h & amount; - h & additionalInfo; - h & accumulatedBonus; - } -}; - -struct DLL_LINKAGE AddQuest : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - QuestInfo quest; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & quest; - } -}; - -struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient -{ - std::vector treasures, minors, majors, relics; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & treasures; - h & minors; - h & majors; - h & relics; - } -}; - -struct DLL_LINKAGE UpdateMapEvents : public CPackForClient -{ - std::list events; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & events; - } -}; - -struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient -{ - ObjectInstanceID town; - std::list events; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & town; - h & events; - } -}; - -struct DLL_LINKAGE ChangeFormation : public CPackForClient -{ - ObjectInstanceID hid; - ui8 formation = 0; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & hid; - h & formation; - } -}; - -struct DLL_LINKAGE RemoveObject : public CPackForClient -{ - RemoveObject() = default; - RemoveObject(const ObjectInstanceID & ID) - : id(ID) - { - } - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - ObjectInstanceID id; - - template void serialize(Handler & h, const int version) - { - h & id; - } -}; - -struct DLL_LINKAGE TryMoveHero : public CPackForClient -{ - void applyGs(CGameState * gs); - - enum EResult - { - FAILED, - SUCCESS, - TELEPORTATION, - BLOCKING_VISIT, - EMBARK, - DISEMBARK - }; - - ObjectInstanceID id; - ui32 movePoints = 0; - EResult result = FAILED; //uses EResult - int3 start, end; //h3m format - std::unordered_set fowRevealed; //revealed tiles - std::optional attackedFrom; // Set when stepping into endangered tile. - - virtual void visitTyped(ICPackVisitor & visitor) override; - - bool stopMovement() const - { - return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & result; - h & start; - h & end; - h & movePoints; - h & fowRevealed; - h & attackedFrom; - } -}; - -struct DLL_LINKAGE NewStructures : public CPackForClient -{ - void applyGs(CGameState * gs); - - ObjectInstanceID tid; - std::set bid; - si16 builded = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & bid; - h & builded; - } -}; - -struct DLL_LINKAGE RazeStructures : public CPackForClient -{ - void applyGs(CGameState * gs); - - ObjectInstanceID tid; - std::set bid; - si16 destroyed = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & bid; - h & destroyed; - } -}; - -struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID tid; - std::vector > > creatures; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & creatures; - } -}; - -struct DLL_LINKAGE SetHeroesInTown : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & visiting; - h & garrison; - } -}; - -struct DLL_LINKAGE HeroRecruited : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - HeroTypeID hid; //subID of hero - ObjectInstanceID tid; - ObjectInstanceID boatId; - int3 tile; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & hid; - h & tid; - h & boatId; - h & tile; - h & player; - } -}; - -struct DLL_LINKAGE GiveHero : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID id; //object id - ObjectInstanceID boatId; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & boatId; - h & player; - } -}; - -struct DLL_LINKAGE OpenWindow : public CPackForClient -{ - EOpenWindowMode window; - si32 id1 = -1; - si32 id2 = -1; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & window; - h & id1; - h & id2; - } -}; - -struct DLL_LINKAGE NewObject : public CPackForClient -{ - void applyGs(CGameState * gs); - - /// Object ID to create - Obj ID; - /// Object secondary ID to create - ui32 subID = 0; - /// Position of visitable tile of created object - int3 targetPos; - - ObjectInstanceID createdObjectID; //used locally, filled during applyGs - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & ID; - h & subID; - h & targetPos; - } -}; - -struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - si32 id = 0; //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) - std::vector arts; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & arts; - } -}; - -struct DLL_LINKAGE CGarrisonOperationPack : CPackForClient -{ -}; - -struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - TQuantity count; - bool absoluteValue; //if not -> count will be added (or subtracted if negative) - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & count; - h & absoluteValue; - } -}; - -struct DLL_LINKAGE SetStackType : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - CreatureID type; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & type; - } -}; - -struct DLL_LINKAGE EraseStack : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - } -}; - -struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack -{ - ObjectInstanceID srcArmy; - ObjectInstanceID dstArmy; - SlotID srcSlot; - SlotID dstSlot; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & srcArmy; - h & dstArmy; - h & srcSlot; - h & dstSlot; - } -}; - -struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - CreatureID type; - TQuantity count = 0; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & type; - h & count; - } -}; - -///moves creatures from src stack to dst slot, may be used for merging/splittint/moving stacks -struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack -{ - ObjectInstanceID srcArmy; - ObjectInstanceID dstArmy; - SlotID srcSlot; - SlotID dstSlot; - - TQuantity count; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & srcArmy; - h & dstArmy; - h & srcSlot; - h & dstSlot; - h & count; - } -}; - -struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack -{ - std::vector moves; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & moves; - } -}; - -struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack -{ - std::vector moves; - std::vector changes; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & moves; - h & changes; - } -}; - -struct GetEngagedHeroIds -{ - std::optional operator()(const ConstTransitivePtr & h) const - { - return h->id; - } - std::optional operator()(const ConstTransitivePtr & s) const - { - if(s->armyObj && s->armyObj->ID == Obj::HERO) - return s->armyObj->id; - return std::optional(); - } -}; - -struct DLL_LINKAGE CArtifactOperationPack : CPackForClient -{ -}; - -struct DLL_LINKAGE PutArtifact : CArtifactOperationPack -{ - PutArtifact() = default; - PutArtifact(ArtifactLocation * dst, bool askAssemble = true) - : al(*dst), askAssemble(askAssemble) - { - } - - ArtifactLocation al; - bool askAssemble = false; - ConstTransitivePtr art; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - h & askAssemble; - h & art; - } -}; - -struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack -{ - ConstTransitivePtr art; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & art; - } -}; - -struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack -{ - ArtifactLocation al; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - } -}; - -struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack -{ - MoveArtifact() = default; - MoveArtifact(ArtifactLocation * src, ArtifactLocation * dst, bool askAssemble = true) - : src(*src), dst(*dst), askAssemble(askAssemble) - { - } - ArtifactLocation src, dst; - bool askAssemble = true; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & src; - h & dst; - h & askAssemble; - } -}; - -struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack -{ - struct LinkedSlots - { - ArtifactPosition srcPos; - ArtifactPosition dstPos; - - LinkedSlots() = default; - LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos) - : srcPos(srcPos) - , dstPos(dstPos) - { - } - template void serialize(Handler & h, const int version) - { - h & srcPos; - h & dstPos; - } - }; - - TArtHolder srcArtHolder; - TArtHolder dstArtHolder; - - BulkMoveArtifacts() - : swap(false) - { - } - BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) - : srcArtHolder(std::move(std::move(srcArtHolder))) - , dstArtHolder(std::move(std::move(dstArtHolder))) - , swap(swap) - { - } - - void applyGs(CGameState * gs); - - std::vector artsPack0; - std::vector artsPack1; - bool swap; - CArtifactSet * getSrcHolderArtSet(); - CArtifactSet * getDstHolderArtSet(); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & artsPack0; - h & artsPack1; - h & srcArtHolder; - h & dstArtHolder; - h & swap; - } -}; - -struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack -{ - ArtifactLocation al; //where assembly will be put - CArtifact * builtArt; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - h & builtArt; - } -}; - -struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack -{ - ArtifactLocation al; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - } -}; - -struct DLL_LINKAGE HeroVisit : public CPackForClient -{ - PlayerColor player; - ObjectInstanceID heroId; - ObjectInstanceID objId; - - bool starting; //false -> ending - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & heroId; - h & objId; - h & starting; - } -}; - -struct DLL_LINKAGE NewTurn : public CPackForClient -{ - enum weekType { NORMAL, DOUBLE_GROWTH, BONUS_GROWTH, DEITYOFFIRE, PLAGUE, NO_ACTION }; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - struct Hero - { - ObjectInstanceID id; - ui32 move, mana; //id is a general serial id - template void serialize(Handler & h, const int version) - { - 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, const int version) - { - 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; - MetaString text; - std::vector components; - PlayerColor player; - ui16 soundID = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & type; - h & text; - h & components; - h & player; - h & soundID; - } - InfoWindow() = default; -}; - -namespace ObjProperty -{ - enum - { - OWNER = 1, BLOCKVIS = 2, PRIMARY_STACK_COUNT = 3, VISITORS = 4, VISITED = 5, ID = 6, AVAILABLE_CREATURE = 7, SUBID = 8, - MONSTER_COUNT = 10, MONSTER_POWER = 11, MONSTER_EXP = 12, MONSTER_RESTORE_TYPE = 13, MONSTER_REFUSED_JOIN, - - //town-specific - STRUCTURE_ADD_VISITING_HERO, STRUCTURE_CLEAR_VISITORS, STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state - BONUS_VALUE_FIRST, BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) - - //creature-bank specific - BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, - - //object with reward - REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED - }; -} - -struct DLL_LINKAGE SetObjectProperty : public CPackForClient -{ - void applyGs(CGameState * gs) const; - ObjectInstanceID id; - ui8 what = 0; // see ObjProperty enum - ui32 val = 0; - SetObjectProperty() = default; - SetObjectProperty(const ObjectInstanceID & ID, ui8 What, ui32 Val) - : id(ID) - , what(What) - , val(Val) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & what; - h & val; - } -}; - -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_REMOVE, // unmark visitor, reversed to ADD - VISITOR_CLEAR // clear all visitors from this object (object reset) - }; - ui32 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; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ChangeObjectVisitors() = default; - - ChangeObjectVisitors(ui32 mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) - : mode(mode) - , object(object) - , hero(heroID) - { - } - - template void serialize(Handler & h, const int version) - { - h & object; - h & hero; - h & mode; - } -}; - -struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient -{ - ObjectInstanceID heroId; - - /// Do not serialize, used by server only - std::vector skills; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroId; - } -}; - -struct DLL_LINKAGE HeroLevelUp : public Query -{ - PlayerColor player; - ObjectInstanceID heroId; - - PrimarySkill::PrimarySkill primskill = PrimarySkill::ATTACK; - std::vector skills; - - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & heroId; - h & primskill; - h & skills; - } -}; - -struct DLL_LINKAGE CommanderLevelUp : public Query -{ - PlayerColor player; - ObjectInstanceID heroId; - - std::vector skills; //0-5 - secondary skills, val-100 - special skill - - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & heroId; - h & skills; - } -}; - -//A dialog that requires making decision by player - it may contain components to choose between or has yes/no options -//Client responds with QueryReply, where answer: 0 - cancel pressed, choice doesn't matter; 1/2/... - first/second/... component selected and OK pressed -//Until sending reply player won't be allowed to take any actions -struct DLL_LINKAGE BlockingDialog : public Query -{ - enum { ALLOW_CANCEL = 1, SELECTION = 2 }; - MetaString text; - std::vector components; - PlayerColor player; - ui8 flags = 0; - ui16 soundID = 0; - - bool cancel() const - { - return flags & ALLOW_CANCEL; - } - bool selection() const - { - return flags & SELECTION; - } - - BlockingDialog(bool yesno, bool Selection) - { - if(yesno) flags |= ALLOW_CANCEL; - if(Selection) flags |= SELECTION; - } - BlockingDialog() = default; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & text; - h & components; - h & player; - h & flags; - h & soundID; - } -}; - -struct DLL_LINKAGE GarrisonDialog : public Query -{ - ObjectInstanceID objid, hid; - bool removableUnits = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & objid; - h & hid; - h & removableUnits; - } -}; - -struct DLL_LINKAGE ExchangeDialog : public Query -{ - PlayerColor player; - - ObjectInstanceID hero1; - ObjectInstanceID hero2; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & hero1; - h & hero2; - } -}; - -struct DLL_LINKAGE TeleportDialog : public Query -{ - TeleportDialog() = default; - - TeleportDialog(const PlayerColor & Player, const TeleportChannelID & Channel) - : player(Player) - , channel(Channel) - { - } - PlayerColor player; - TeleportChannelID channel; - TTeleportExitsList exits; - bool impassable = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & channel; - h & exits; - h & impassable; - } -}; - -struct DLL_LINKAGE MapObjectSelectDialog : public Query -{ - PlayerColor player; - Component icon; - MetaString title; - MetaString description; - std::vector objects; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & icon; - h & title; - h & description; - h & objects; - } -}; - -class BattleInfo; -struct DLL_LINKAGE BattleStart : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleInfo * info = nullptr; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & info; - } -}; - -struct DLL_LINKAGE BattleNextRound : public CPackForClient -{ - void applyGs(CGameState * gs) const; - si32 round = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & round; - } -}; - -struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ui32 stack = 0; - ui8 askPlayerInterface = true; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & stack; - h & askPlayerInterface; - } -}; - -struct DLL_LINKAGE BattleResultAccepted : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - struct HeroBattleResults - { - HeroBattleResults() - : hero(nullptr), army(nullptr), exp(0) {} - - CGHeroInstance * hero; - CArmedInstance * army; - TExpType exp; - - template void serialize(Handler & h, const int version) - { - h & hero; - h & army; - h & exp; - } - }; - std::array heroResult; - ui8 winnerSide; - - template void serialize(Handler & h, const int version) - { - h & heroResult; - h & winnerSide; - } -}; - -struct DLL_LINKAGE BattleResult : public Query -{ - enum EResult { NORMAL = 0, ESCAPE = 1, SURRENDER = 2 }; - - void applyFirstCl(CClient * cl); - - EResult result = 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 - std::set artifacts; //artifacts taken from loser to winner - currently unused - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & result; - h & winner; - h & casualties[0]; - h & casualties[1]; - h & exp; - h & artifacts; - } -}; - -struct DLL_LINKAGE BattleLogMessage : public CPackForClient -{ - std::vector lines; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & lines; - } -}; - -struct DLL_LINKAGE BattleStackMoved : public CPackForClient -{ - ui32 stack = 0; - std::vector tilesToMove; - int distance = 0; - bool teleporting = false; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & stack; - h & tilesToMove; - h & distance; - h & teleporting; - } -}; - -struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - std::vector changedStacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & changedStacks; - } -}; - -struct BattleStackAttacked -{ - DLL_LINKAGE void applyGs(CGameState * gs); - DLL_LINKAGE void applyBattle(IBattleState * battleState); - - ui32 stackAttacked = 0, attackerID = 0; - ui32 killedAmount = 0; - int64_t damageAmount = 0; - UnitChanges newState; - enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; - ui32 flags = 0; //uses EFlags (above) - SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set - - bool killed() const//if target stack was killed - { - return flags & KILLED || flags & CLONE_KILLED; - } - bool cloneKilled() const - { - return flags & CLONE_KILLED; - } - bool isSecondary() const//if stack was not a primary target (receives no spell effects) - { - return flags & SECONDARY; - } - ///Attacked with spell (SPELL_LIKE_ATTACK) - bool isSpell() const - { - return flags & SPELL_EFFECT; - } - bool willRebirth() const//resurrection, e.g. Phoenix - { - return flags & REBIRTH; - } - bool fireShield() const - { - return flags & FIRE_SHIELD; - } - - template void serialize(Handler & h, const int version) - { - h & stackAttacked; - h & attackerID; - h & newState; - h & flags; - h & killedAmount; - h & damageAmount; - h & spellID; - } - bool operator<(const BattleStackAttacked & b) const - { - return stackAttacked < b.stackAttacked; - } -}; - -struct DLL_LINKAGE BattleAttack : public CPackForClient -{ - void applyGs(CGameState * gs); - BattleUnitsChanged attackerChanges; - - std::vector bsa; - ui32 stackAttacking = 0; - ui32 flags = 0; //uses Eflags (below) - enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; - - BattleHex tile; - SpellID spellID = SpellID::NONE; //for SPELL_LIKE - - bool shot() const//distance attack - decrease number of shots - { - return flags & SHOT; - } - bool counter() const//is it counterattack? - { - return flags & COUNTER; - } - bool lucky() const - { - return flags & LUCKY; - } - bool unlucky() const - { - return flags & UNLUCKY; - } - bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg - { - return flags & BALLISTA_DOUBLE_DMG; - } - bool deathBlow() const - { - return flags & DEATH_BLOW; - } - bool spellLike() const - { - return flags & SPELL_LIKE; - } - bool lifeDrain() const - { - return flags & LIFE_DRAIN; - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & bsa; - h & stackAttacking; - h & flags; - h & tile; - h & spellID; - h & attackerChanges; - } -}; - -struct DLL_LINKAGE StartAction : public CPackForClient -{ - StartAction() = default; - StartAction(BattleAction act) - : ba(std::move(act)) - { - } - void applyFirstCl(CClient * cl); - void applyGs(CGameState * gs); - - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & ba; - } -}; - -struct DLL_LINKAGE EndAction : public CPackForClient -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - } -}; - -struct DLL_LINKAGE BattleSpellCast : public CPackForClient -{ - void applyGs(CGameState * gs) const; - bool activeCast = true; - ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender - SpellID spellID; //id of spell - ui8 manaGained = 0; //mana channeling ability - BattleHex tile; //destination tile (may not be set in some global/mass spells - std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) - std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) - std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) - si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID - bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & side; - h & spellID; - h & manaGained; - h & tile; - h & affectedCres; - h & resistedCres; - h & reflectedCres; - h & casterStack; - h & castByHero; - h & activeCast; - } -}; - -struct DLL_LINKAGE SetStackEffect : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - std::vector>> toAdd; - std::vector>> toUpdate; - std::vector>> toRemove; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & toAdd; - h & toUpdate; - h & toRemove; - } -}; - -struct DLL_LINKAGE StacksInjured : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - std::vector stacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & stacks; - } -}; - -struct DLL_LINKAGE BattleResultsApplied : public CPackForClient -{ - PlayerColor player1, player2; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player1; - h & player2; - } -}; - -struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - std::vector changes; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & changes; - } -}; - -struct DLL_LINKAGE CatapultAttack : public CPackForClient -{ - struct AttackInfo - { - si16 destinationTile; - EWallPart attackedPart; - ui8 damageDealt; - - template void serialize(Handler & h, const int version) - { - h & destinationTile; - h & attackedPart; - h & damageDealt; - } - }; - - CatapultAttack(); - ~CatapultAttack() override; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - std::vector< AttackInfo > attackedParts; - int attacker = -1; //if -1, then a spell caused this - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & attackedParts; - h & attacker; - } -}; - -struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient -{ - enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; - - void applyGs(CGameState * gs) const; - - int stackID = 0; - BattleStackProperty which = CASTS; - int val = 0; - int absolute = 0; - - template void serialize(Handler & h, const int version) - { - h & stackID; - h & which; - h & val; - h & absolute; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -///activated at the beginning of turn -struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient -{ - void applyGs(CGameState * gs) const; //effect - - int stackID = 0; - int effect = 0; //use corresponding Bonus type - int val = 0; - int additionalInfo = 0; - - template void serialize(Handler & h, const int version) - { - h & stackID; - h & effect; - h & val; - h & additionalInfo; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - EGateState state = EGateState::NONE; - template void serialize(Handler & h, const int version) - { - h & state; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient -{ - ObjectInstanceID casterID; - SpellID spellID; - template void serialize(Handler & h, const int version) - { - h & casterID; - h & spellID; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient -{ - PlayerColor player; - bool showTerrain; // TODO: send terrain state - - std::vector objectPositions; - - template void serialize(Handler & h, const int version) - { - h & player; - h & showTerrain; - h & objectPositions; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE EndTurn : public CPackForServer -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - } -}; - -struct DLL_LINKAGE DismissHero : public CPackForServer -{ - DismissHero() = default; - DismissHero(const ObjectInstanceID & HID) - : hid(HID) - { - } - ObjectInstanceID hid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - } -}; - -struct DLL_LINKAGE MoveHero : public CPackForServer -{ - MoveHero() = default; - MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) - : dest(Dest) - , hid(HID) - , transit(Transit) - { - } - int3 dest; - ObjectInstanceID hid; - bool transit = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - h & transit; - } -}; - -struct DLL_LINKAGE CastleTeleportHero : public CPackForServer -{ - CastleTeleportHero() = default; - CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) - : dest(Dest) - , hid(HID) - , source(Source) - { - } - ObjectInstanceID dest; - ObjectInstanceID hid; - si8 source = 0; //who give teleporting, 1=castle gate - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - } -}; - -struct DLL_LINKAGE ArrangeStacks : public CPackForServer -{ - ArrangeStacks() = default; - ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) - : what(W) - , p1(P1) - , p2(P2) - , id1(ID1) - , id2(ID2) - , val(VAL) - { - } - - ui8 what = 0; //1 - swap; 2 - merge; 3 - split - SlotID p1, p2; //positions of first and second stack - ObjectInstanceID id1, id2; //ids of objects with garrison - si32 val = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & what; - h & p1; - h & p2; - h & id1; - h & id2; - h & val; - } -}; - -struct DLL_LINKAGE BulkMoveArmy : public CPackForServer -{ - SlotID srcSlot; - ObjectInstanceID srcArmy; - ObjectInstanceID destArmy; - - BulkMoveArmy() = default; - - BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) - : srcArmy(srcArmy) - , destArmy(destArmy) - , srcSlot(srcSlot) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcSlot; - h & srcArmy; - h & destArmy; - } -}; - -struct DLL_LINKAGE BulkSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - si32 amount = 0; - - BulkSplitStack() = default; - - BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) - : src(src) - , srcOwner(srcOwner) - , amount(howMany) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - h & amount; - } -}; - -struct DLL_LINKAGE BulkMergeStacks : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkMergeStacks() = default; - - BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkSmartSplitStack() = default; - - BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE DisbandCreature : public CPackForServer -{ - DisbandCreature() = default; - DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) - : pos(Pos) - , id(ID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - } -}; - -struct DLL_LINKAGE BuildStructure : public CPackForServer -{ - BuildStructure() = default; - BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) - : tid(TID) - , bid(BID) - { - } - ObjectInstanceID tid; //town id - BuildingID bid; //structure id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & bid; - } -}; - -struct DLL_LINKAGE RazeStructure : public BuildStructure -{ - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE RecruitCreatures : public CPackForServer -{ - RecruitCreatures() = default; - RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) - : tid(TID) - , dst(DST) - , crid(CRID) - , amount(Amount) - , level(Level) - { - } - ObjectInstanceID tid; //dwelling id, or town - ObjectInstanceID dst; //destination ID, e.g. hero - CreatureID crid; - ui32 amount = 0; //creature amount - si32 level = 0; //dwelling level to buy from, -1 if any - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & dst; - h & crid; - h & amount; - h & level; - } -}; - -struct DLL_LINKAGE UpgradeCreature : public CPackForServer -{ - UpgradeCreature() = default; - UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) - : pos(Pos) - , id(ID) - , cid(CRID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - CreatureID cid; //id of type to which we want make upgrade - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - h & cid; - } -}; - -struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer -{ - GarrisonHeroSwap() = default; - GarrisonHeroSwap(const ObjectInstanceID & TID) - : tid(TID) - { - } - ObjectInstanceID tid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - } -}; - -struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer -{ - ArtifactLocation src, dst; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & dst; - } -}; - -struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer -{ - ObjectInstanceID srcHero; - ObjectInstanceID dstHero; - bool swap = false; - - BulkExchangeArtifacts() = default; - BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap) - : srcHero(srcHero) - , dstHero(dstHero) - , swap(swap) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcHero; - h & dstHero; - h & swap; - } -}; - -struct DLL_LINKAGE AssembleArtifacts : public CPackForServer -{ - AssembleArtifacts() = default; - AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) - : heroID(_heroID) - , artifactSlot(_artifactSlot) - , assemble(_assemble) - , assembleTo(_assembleTo) - { - } - ObjectInstanceID heroID; - ArtifactPosition artifactSlot; - bool assemble = false; // True to assemble artifact, false to disassemble. - ArtifactID assembleTo; // Artifact to assemble into. - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & heroID; - h & artifactSlot; - h & assemble; - h & assembleTo; - } -}; - -struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer -{ - EraseArtifactByClient() = default; - EraseArtifactByClient(const ArtifactLocation & al) - : al(al) - { - } - ArtifactLocation al; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & al; - } -}; - -struct DLL_LINKAGE BuyArtifact : public CPackForServer -{ - BuyArtifact() = default; - BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) - : hid(HID) - , aid(AID) - { - } - ObjectInstanceID hid; - ArtifactID aid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & aid; - } -}; - -struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer -{ - ObjectInstanceID marketId; - ObjectInstanceID heroId; - - EMarketMode::EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; - std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] - std::vector val; //units of sold resource - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & marketId; - h & heroId; - h & mode; - h & r1; - h & r2; - h & val; - } -}; - -struct DLL_LINKAGE SetFormation : public CPackForServer -{ - SetFormation() = default; - ; - SetFormation(const ObjectInstanceID & HID, ui8 Formation) - : hid(HID) - , formation(Formation) - { - } - ObjectInstanceID hid; - ui8 formation = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & formation; - } -}; - -struct DLL_LINKAGE HireHero : public CPackForServer -{ - HireHero() = default; - HireHero(HeroTypeID HID, const ObjectInstanceID & TID) - : hid(HID) - , tid(TID) - { - } - HeroTypeID hid; //available hero serial - ObjectInstanceID tid; //town (tavern) id - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & tid; - h & player; - } -}; - -struct DLL_LINKAGE BuildBoat : public CPackForServer -{ - ObjectInstanceID objid; //where player wants to buy a boat - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & objid; - } -}; - -struct DLL_LINKAGE QueryReply : public CPackForServer -{ - QueryReply() = default; - QueryReply(const QueryID & QID, const JsonNode & Reply) - : qid(QID) - , reply(Reply) - { - } - QueryID qid; - PlayerColor player; - JsonNode reply; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & qid; - h & player; - h & reply; - } -}; - -struct DLL_LINKAGE MakeAction : public CPackForServer -{ - MakeAction() = default; - MakeAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - } -}; - -struct DLL_LINKAGE MakeCustomAction : public CPackForServer -{ - MakeCustomAction() = default; - MakeCustomAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - } -}; - -struct DLL_LINKAGE DigWithHero : public CPackForServer -{ - ObjectInstanceID id; //digging hero id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & id; - } -}; - -struct DLL_LINKAGE CastAdvSpell : public CPackForServer -{ - ObjectInstanceID hid; //hero id - SpellID sid; //spell id - int3 pos; //selected tile (not always used) - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & sid; - h & pos; - } -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE SaveGame : public CPackForServer -{ - SaveGame() = default; - SaveGame(std::string Fname) - : fname(std::move(Fname)) - { - } - std::string fname; - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & fname; - } -}; - -struct DLL_LINKAGE PlayerMessage : public CPackForServer -{ - PlayerMessage() = default; - PlayerMessage(std::string Text, const ObjectInstanceID & obj) - : text(std::move(Text)) - , currObj(obj) - { - } - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - std::string text; - ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & text; - h & currObj; - } -}; - -struct DLL_LINKAGE PlayerMessageClient : public CPackForClient -{ - PlayerMessageClient() = default; - PlayerMessageClient(const PlayerColor & Player, std::string Text) - : player(Player) - , text(std::move(Text)) - { - } - virtual void visitTyped(ICPackVisitor & visitor) override; - - PlayerColor player; - std::string text; - - template void serialize(Handler & h, const int version) - { - h & player; - h & text; - } -}; - -struct DLL_LINKAGE CenterView : public CPackForClient -{ - PlayerColor player; - int3 pos; - ui32 focusTime = 0; //ms - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & pos; - h & player; - h & focusTime; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacksBase.h b/lib/NetPacksBase.h deleted file mode 100644 index e7e7cb635..000000000 --- a/lib/NetPacksBase.h +++ /dev/null @@ -1,286 +0,0 @@ -/* - * NetPacksBase.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 "ConstTransitivePtr.h" -#include "GameConstants.h" -#include "JsonNode.h" - -class CClient; -class CGameHandler; -class CLobbyScreen; -class CServerHandler; -class CVCMIServer; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGameState; -class CConnection; -class CStackBasicDescriptor; -class CGHeroInstance; -class CStackInstance; -class CArmedInstance; -class CArtifactSet; -class CBonusSystemNode; -struct ArtSlotInfo; - -class ICPackVisitor; - -enum class EInfoWindowMode : uint8_t -{ - AUTO, - MODAL, - INFO -}; - -enum class EOpenWindowMode : uint8_t -{ - EXCHANGE_WINDOW, - RECRUITMENT_FIRST, - RECRUITMENT_ALL, - SHIPYARD_WINDOW, - THIEVES_GUILD, - UNIVERSITY_WINDOW, - HILL_FORT_WINDOW, - MARKET_WINDOW, - PUZZLE_MAP, - TAVERN_WINDOW -}; - -struct DLL_LINKAGE CPack -{ - std::shared_ptr c; // Pointer to connection that pack received from - - CPack() = default; - virtual ~CPack() = default; - - template void serialize(Handler &h, const int version) - { - logNetwork->error("CPack serialized... this should not happen!"); - assert(false && "CPack serialized"); - } - - void applyGs(CGameState * gs) - {} - - void visit(ICPackVisitor & cpackVisitor); - -protected: - /// - /// For basic types of netpacks hierarchy like CPackForClient. Called first. - /// - virtual void visitBasic(ICPackVisitor & cpackVisitor); - - /// - /// For leaf types of netpacks hierarchy. Called after visitBasic. - /// - virtual void visitTyped(ICPackVisitor & cpackVisitor); -}; - -struct DLL_LINKAGE CPackForClient : public CPack -{ -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct DLL_LINKAGE CPackForServer : public CPack -{ - mutable PlayerColor player = PlayerColor::NEUTRAL; - mutable si32 requestID; - - template void serialize(Handler &h, const int version) - { - h & player; - h & requestID; - } - -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct DLL_LINKAGE CPackForLobby : public CPack -{ - virtual bool isForServer() const; - -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct Component -{ - enum class EComponentType : uint8_t - { - PRIM_SKILL, - SEC_SKILL, - RESOURCE, - CREATURE, - ARTIFACT, - EXPERIENCE, - SPELL, - MORALE, - LUCK, - BUILDING, - HERO_PORTRAIT, - FLAG, - INVALID //should be last - }; - EComponentType id = EComponentType::INVALID; - ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels - si32 val = 0; // + give; - take - si16 when = 0; // 0 - now; +x - within x days; -x - per x days - - template void serialize(Handler &h, const int version) - { - h & id; - h & subtype; - h & val; - h & when; - } - Component() = default; - DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); - Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) - :id(Type),subtype(Subtype),val(Val),when(When) - { - } -}; - -using TArtHolder = std::variant, ConstTransitivePtr>; - -struct ArtifactLocation -{ - TArtHolder artHolder;//TODO: identify holder by id - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; - - ArtifactLocation() - : artHolder(ConstTransitivePtr()) - { - } - template - ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) - : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) - : artHolder(std::move(std::move(ArtHolder))) - , slot(Slot) - { - } - - template - bool isHolder(const T *t) const - { - if(auto ptrToT = std::get>(artHolder)) - { - return ptrToT == t; - } - return false; - } - - DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) - - DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner - DLL_LINKAGE PlayerColor owningPlayer() const; - DLL_LINKAGE CArtifactSet *getHolderArtSet(); - DLL_LINKAGE CBonusSystemNode *getHolderNode(); - DLL_LINKAGE CArtifactSet *getHolderArtSet() const; - DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; - - DLL_LINKAGE const CArtifactInstance *getArt() const; - DLL_LINKAGE CArtifactInstance *getArt(); - DLL_LINKAGE const ArtSlotInfo *getSlot() const; - template void serialize(Handler &h, const int version) - { - h & artHolder; - h & slot; - } -}; - -class EntityChanges -{ -public: - Metatype metatype = Metatype::UNKNOWN; - int32_t entityIndex = 0; - JsonNode data; - template void serialize(Handler & h, const int version) - { - h & metatype; - h & entityIndex; - h & data; - } -}; - -class BattleChanges -{ -public: - enum class EOperation : si8 - { - ADD, - RESET_STATE, - UPDATE, - REMOVE, - }; - - JsonNode data; - EOperation operation = EOperation::RESET_STATE; - - BattleChanges() = default; - BattleChanges(EOperation operation_) - : operation(operation_) - { - } -}; - -class UnitChanges : public BattleChanges -{ -public: - uint32_t id = 0; - int64_t healthDelta = 0; - - UnitChanges() = default; - UnitChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_) - , id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & healthDelta; - h & data; - h & operation; - } -}; - -class ObstacleChanges : public BattleChanges -{ -public: - uint32_t id = 0; - - ObstacleChanges() = default; - - ObstacleChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_), - id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & data; - h & operation; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index b2f522456..f85eae74c 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -10,7 +10,8 @@ #include "StdInc.h" #include "ObstacleHandler.h" #include "BattleFieldHandler.h" -#include "CModHandler.h" +#include "modding/IdentifierStorage.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -89,12 +90,12 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js auto * info = new ObstacleInfo(Obstacle(index), identifier); - info->animation = json["animation"].String(); + info->animation = AnimationPath::fromJson(json["animation"]); info->width = json["width"].Integer(); info->height = json["height"].Integer(); for(const auto & t : json["allowedTerrains"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", t, [info](int32_t identifier){ + VLC->identifiers()->requestIdentifier("terrain", t, [info](int32_t identifier){ info->allowedTerrains.emplace_back(identifier); }); } @@ -105,8 +106,6 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js info->isAbsoluteObstacle = json["absolute"].Bool(); info->isForegroundObstacle = json["foreground"].Bool(); - objects.emplace_back(info); - return info; } @@ -115,11 +114,6 @@ std::vector ObstacleHandler::loadLegacyData() return {}; } -std::vector ObstacleHandler::getDefaultAllowed() const -{ - return {}; -} - const std::vector & ObstacleHandler::getTypeNames() const { static const std::vector types = { "obstacle" }; diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 09bd52fd2..e47d1c0ea 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -14,6 +14,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "battle/BattleHex.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -31,7 +32,9 @@ public: Obstacle obstacle; si32 iconIndex; std::string identifier; - std::string appearSound, appearAnimation, animation; + AudioPath appearSound; + AnimationPath appearAnimation; + AnimationPath animation; std::vector allowedTerrains; std::vector allowedSpecialBfields; @@ -51,23 +54,6 @@ public: std::vector getBlocked(BattleHex hex) const; //returns vector of hexes blocked by obstacle when it's placed on hex 'hex' bool isAppropriate(const TerrainId terrainType, const BattleField & specialBattlefield) const; - - template void serialize(Handler &h, const int version) - { - h & obstacle; - h & iconIndex; - h & identifier; - h & animation; - h & appearAnimation; - h & appearSound; - h & allowedTerrains; - h & allowedSpecialBfields; - h & isAbsoluteObstacle; - h & isForegroundObstacle; - h & width; - h & height; - h & blockedTiles; - } }; class DLL_LINKAGE ObstacleService : public EntityServiceT @@ -85,12 +71,6 @@ public: const std::vector & getTypeNames() const override; std::vector loadLegacyData() override; - std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/Point.h b/lib/Point.h index 0d8827562..36f7a330c 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -1,127 +1,132 @@ -/* - * Point.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 int3; - -// A point with x/y coordinate, used mostly for graphic rendering -class Point -{ -public: - int x, y; - - //constructors - constexpr Point() : x(0), y(0) - { - } - - constexpr Point(int X, int Y) - : x(X) - , y(Y) - { - } - - constexpr static Point makeInvalid() - { - return Point(std::numeric_limits::min(), std::numeric_limits::min()); - } - - explicit DLL_LINKAGE Point(const int3 &a); - - template - constexpr Point operator+(const T &b) const - { - return Point(x+b.x,y+b.y); - } - - template - constexpr Point operator/(const T &div) const - { - return Point(x/div, y/div); - } - - template - constexpr Point operator*(const T &mul) const - { - return Point(x*mul, y*mul); - } - - constexpr Point operator*(const Point &b) const - { - return Point(x*b.x,y*b.y); - } - - template - constexpr Point& operator+=(const T &b) - { - x += b.x; - y += b.y; - return *this; - } - - constexpr Point operator-() const - { - return Point(-x, -y); - } - - template - constexpr Point operator-(const T &b) const - { - return Point(x - b.x, y - b.y); - } - - template - constexpr Point& operator-=(const T &b) - { - x -= b.x; - y -= b.y; - return *this; - } - - template constexpr Point& operator=(const T &t) - { - x = t.x; - y = t.y; - return *this; - } - template constexpr bool operator==(const T &t) const - { - return x == t.x && y == t.y; - } - template constexpr bool operator!=(const T &t) const - { - return !(*this == t); - } - - constexpr bool isValid() const - { - return x > std::numeric_limits::min() && y > std::numeric_limits::min(); - } - - constexpr int lengthSquared() const - { - return x * x + y * y; - } - - int length() const - { - return std::sqrt(lengthSquared()); - } - - template - void serialize(Handler &h, const int version) - { - h & x; - h & y; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Point.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 int3; + +// A point with x/y coordinate, used mostly for graphic rendering +class Point +{ +public: + int x, y; + + //constructors + constexpr Point() : x(0), y(0) + { + } + + constexpr Point(int X, int Y) + : x(X) + , y(Y) + { + } + + constexpr static Point makeInvalid() + { + return Point(std::numeric_limits::min(), std::numeric_limits::min()); + } + + explicit DLL_LINKAGE Point(const int3 &a); + + template + constexpr Point operator+(const T &b) const + { + return Point(x+b.x,y+b.y); + } + + template + constexpr Point operator/(const T &div) const + { + return Point(x/div, y/div); + } + + template + constexpr Point operator*(const T &mul) const + { + return Point(x*mul, y*mul); + } + + constexpr Point operator*(const Point &b) const + { + return Point(x*b.x,y*b.y); + } + + template + constexpr Point& operator+=(const T &b) + { + x += b.x; + y += b.y; + return *this; + } + + constexpr Point operator-() const + { + return Point(-x, -y); + } + + template + constexpr Point operator-(const T &b) const + { + return Point(x - b.x, y - b.y); + } + + template + constexpr Point& operator-=(const T &b) + { + x -= b.x; + y -= b.y; + return *this; + } + + template constexpr Point& operator=(const T &t) + { + x = t.x; + y = t.y; + return *this; + } + template constexpr bool operator==(const T &t) const + { + return x == t.x && y == t.y; + } + template constexpr bool operator!=(const T &t) const + { + return !(*this == t); + } + + constexpr bool isValid() const + { + return x > std::numeric_limits::min() && y > std::numeric_limits::min(); + } + + constexpr int lengthSquared() const + { + return x * x + y * y; + } + + int length() const + { + return std::sqrt(lengthSquared()); + } + + double angle() const + { + return std::atan2(y, x); // rad + } + + template + void serialize(Handler &h, const int version) + { + h & x; + h & y; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Rect.cpp b/lib/Rect.cpp index 1dfcd8fbc..db5b7a176 100644 --- a/lib/Rect.cpp +++ b/lib/Rect.cpp @@ -1,147 +1,147 @@ -/* - * Rect.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 "Rect.h" -#include "int3.h" - -VCMI_LIB_NAMESPACE_BEGIN - -Point::Point(const int3 & a) - : x(a.x) - , y(a.y) -{ -} - -/// Returns rect union - rect that covers both this rect and provided rect -Rect Rect::include(const Rect & other) const -{ - Point topLeft{ - std::min(this->left(), other.left()), - std::min(this->top(), other.top()) - }; - - Point bottomRight{ - std::max(this->right(), other.right()), - std::max(this->bottom(), other.bottom()) - }; - - return Rect(topLeft, bottomRight - topLeft); -} - -Rect Rect::createCentered( const Point & around, const Point & dimensions ) -{ - return Rect(around - dimensions/2, dimensions); -} - -Rect Rect::createAround(const Rect &r, int width) -{ - return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); -} - -Rect Rect::createCentered( const Rect & rect, const Point & dimensions) -{ - return createCentered(rect.center(), dimensions); -} - -bool Rect::intersectionTest(const Rect & other) const -{ - // this rect is above other rect - if(this->bottom() < other.top()) - return false; - - // this rect is below other rect - if(this->top() > other.bottom() ) - return false; - - // this rect is to the left of other rect - if(this->right() < other.left()) - return false; - - // this rect is to the right of other rect - if(this->left() > other.right()) - return false; - - return true; -} - -/// Algorithm to test whether line segment between points line1-line2 will intersect with -/// rectangle specified by top-left and bottom-right points -/// Note that in order to avoid floating point rounding errors algorithm uses integers with no divisions -bool Rect::intersectionTest(const Point & line1, const Point & line2) const -{ - // check whether segment is located to the left of our rect - if (line1.x < left() && line2.x < left()) - return false; - - // check whether segment is located to the right of our rect - if (line1.x > right() && line2.x > right()) - return false; - - // check whether segment is located on top of our rect - if (line1.y < top() && line2.y < top()) - return false; - - // check whether segment is located below of our rect - if (line1.y > bottom() && line2.y > bottom()) - return false; - - Point vector { line2.x - line1.x, line2.y - line1.y}; - - // compute position of corners relative to our line - int tlTest = vector.y*topLeft().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); - int trTest = vector.y*bottomRight().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); - int blTest = vector.y*topLeft().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); - int brTest = vector.y*bottomRight().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); - - // if all points are on the left of our line then there is no intersection - if ( tlTest > 0 && trTest > 0 && blTest > 0 && brTest > 0 ) - return false; - - // if all points are on the right of our line then there is no intersection - if ( tlTest < 0 && trTest < 0 && blTest < 0 && brTest < 0 ) - return false; - - // if all previous checks failed, this means that there is an intersection between line and AABB - return true; -} - - -Rect Rect::intersect(const Rect & other) const -{ - if(intersectionTest(other)) - { - Point topLeft{ - std::max(this->left(), other.left()), - std::max(this->top(), other.top()) - }; - - Point bottomRight{ - std::min(this->right(), other.right()), - std::min(this->bottom(), other.bottom()) - }; - - return Rect(topLeft, bottomRight - topLeft); - } - else - { - return Rect(); - } -} - -int Rect::distanceTo(const Point & target) const -{ - int distanceX = std::max({left() - target.x, 0, target.x - right()}); - int distanceY = std::max({top() - target.y, 0, target.y - bottom()}); - - return Point(distanceX, distanceY).length(); -} - -VCMI_LIB_NAMESPACE_END +/* + * Rect.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 "Rect.h" +#include "int3.h" + +VCMI_LIB_NAMESPACE_BEGIN + +Point::Point(const int3 & a) + : x(a.x) + , y(a.y) +{ +} + +/// Returns rect union - rect that covers both this rect and provided rect +Rect Rect::include(const Rect & other) const +{ + Point topLeft{ + std::min(this->left(), other.left()), + std::min(this->top(), other.top()) + }; + + Point bottomRight{ + std::max(this->right(), other.right()), + std::max(this->bottom(), other.bottom()) + }; + + return Rect(topLeft, bottomRight - topLeft); +} + +Rect Rect::createCentered( const Point & around, const Point & dimensions ) +{ + return Rect(around - dimensions/2, dimensions); +} + +Rect Rect::createAround(const Rect &r, int width) +{ + return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); +} + +Rect Rect::createCentered( const Rect & rect, const Point & dimensions) +{ + return createCentered(rect.center(), dimensions); +} + +bool Rect::intersectionTest(const Rect & other) const +{ + // this rect is above other rect + if(this->bottom() < other.top()) + return false; + + // this rect is below other rect + if(this->top() > other.bottom() ) + return false; + + // this rect is to the left of other rect + if(this->right() < other.left()) + return false; + + // this rect is to the right of other rect + if(this->left() > other.right()) + return false; + + return true; +} + +/// Algorithm to test whether line segment between points line1-line2 will intersect with +/// rectangle specified by top-left and bottom-right points +/// Note that in order to avoid floating point rounding errors algorithm uses integers with no divisions +bool Rect::intersectionTest(const Point & line1, const Point & line2) const +{ + // check whether segment is located to the left of our rect + if (line1.x < left() && line2.x < left()) + return false; + + // check whether segment is located to the right of our rect + if (line1.x > right() && line2.x > right()) + return false; + + // check whether segment is located on top of our rect + if (line1.y < top() && line2.y < top()) + return false; + + // check whether segment is located below of our rect + if (line1.y > bottom() && line2.y > bottom()) + return false; + + Point vector { line2.x - line1.x, line2.y - line1.y}; + + // compute position of corners relative to our line + int tlTest = vector.y*topLeft().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); + int trTest = vector.y*bottomRight().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); + int blTest = vector.y*topLeft().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); + int brTest = vector.y*bottomRight().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); + + // if all points are on the left of our line then there is no intersection + if ( tlTest > 0 && trTest > 0 && blTest > 0 && brTest > 0 ) + return false; + + // if all points are on the right of our line then there is no intersection + if ( tlTest < 0 && trTest < 0 && blTest < 0 && brTest < 0 ) + return false; + + // if all previous checks failed, this means that there is an intersection between line and AABB + return true; +} + + +Rect Rect::intersect(const Rect & other) const +{ + if(intersectionTest(other)) + { + Point topLeft{ + std::max(this->left(), other.left()), + std::max(this->top(), other.top()) + }; + + Point bottomRight{ + std::min(this->right(), other.right()), + std::min(this->bottom(), other.bottom()) + }; + + return Rect(topLeft, bottomRight - topLeft); + } + else + { + return Rect(); + } +} + +int Rect::distanceTo(const Point & target) const +{ + int distanceX = std::max({left() - target.x, 0, target.x - right()}); + int distanceY = std::max({top() - target.y, 0, target.y - bottom()}); + + return Point(distanceX, distanceY).length(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Rect.h b/lib/Rect.h index 50d514844..9aaa3578e 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -1,170 +1,175 @@ -/* - * Rect.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 "Point.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Rectangle class, which have a position and a size -class Rect -{ -public: - int x; - int y; - int w; - int h; - - Rect() - { - x = y = w = h = -1; - } - Rect(int X, int Y, int W, int H) - { - x = X; - y = Y; - w = W; - h = H; - } - Rect(const Point & position, const Point & size) - { - x = position.x; - y = position.y; - w = size.x; - h = size.y; - } - Rect(const Rect& r) = default; - - DLL_LINKAGE static Rect createCentered( const Point & around, const Point & size ); - DLL_LINKAGE static Rect createCentered( const Rect & target, const Point & size ); - DLL_LINKAGE static Rect createAround(const Rect &r, int borderWidth); - - bool isInside(int qx, int qy) const - { - if (qx > x && qxy && qy - void serialize(Handler &h, const int version) - { - h & x; - h & y; - h & w; - h & this->h; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Rect.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 "Point.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Rectangle class, which have a position and a size +class Rect +{ +public: + int x; + int y; + int w; + int h; + + Rect() + { + x = y = w = h = -1; + } + Rect(int X, int Y, int W, int H) + { + x = X; + y = Y; + w = W; + h = H; + } + Rect(const Point & position, const Point & size) + { + x = position.x; + y = position.y; + w = size.x; + h = size.y; + } + Rect(const Rect& r) = default; + + DLL_LINKAGE static Rect createCentered( const Point & around, const Point & size ); + DLL_LINKAGE static Rect createCentered( const Rect & target, const Point & size ); + DLL_LINKAGE static Rect createAround(const Rect &r, int borderWidth); + + bool isInside(int qx, int qy) const + { + if (qx > x && qxy && qy + void serialize(Handler &h, const int version) + { + h & x; + h & y; + h & w; + h & this->h; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index 50f2c48b6..ab786c7fd 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -1,169 +1,156 @@ -/* - * ResourceSet.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 "GameConstants.h" -#include "ResourceSet.h" -#include "StringConstants.h" -#include "JsonNode.h" -#include "serializer/JsonSerializeFormat.h" -#include "mapObjects/CObjectHandler.h" -#include "VCMI_Lib.h" - -VCMI_LIB_NAMESPACE_BEGIN - -ResourceSet::ResourceSet(const JsonNode & node) -{ - for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); -} - -ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, - TResource gems, TResource gold, TResource mithril) -{ - container[GameResID(EGameResID::WOOD)] = wood; - container[GameResID(EGameResID::MERCURY)] = mercury; - container[GameResID(EGameResID::ORE)] = ore; - container[GameResID(EGameResID::SULFUR)] = sulfur; - container[GameResID(EGameResID::CRYSTAL)] = crystal; - container[GameResID(EGameResID::GEMS)] = gems; - container[GameResID(EGameResID::GOLD)] = gold; - container[GameResID(EGameResID::MITHRIL)] = mithril; -} - -void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) -{ - if(handler.saving && !nonZero()) - return; - auto s = handler.enterStruct(fieldName); - - //TODO: add proper support for mithril to map format - for(int idx = 0; idx < GameConstants::RESOURCE_QUANTITY - 1; idx ++) - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], this->operator[](idx), 0); -} - -bool ResourceSet::nonZero() const -{ - for(const auto & elem : *this) - if(elem) - return true; - - return false; -} - -void ResourceSet::amax(const TResourceCap &val) -{ - for(auto & elem : *this) - vstd::amax(elem, val); -} - -void ResourceSet::amin(const TResourceCap &val) -{ - for(auto & elem : *this) - vstd::amin(elem, val); -} - -void ResourceSet::positive() -{ - for(auto & elem : *this) - vstd::amax(elem, 0); -} - -static bool canAfford(const ResourceSet &res, const ResourceSet &price) -{ - assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY); - for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - if(price[i] > res[i]) - return false; - - return true; -} - -bool ResourceSet::canBeAfforded(const ResourceSet &res) const -{ - return VCMI_LIB_WRAP_NAMESPACE(canAfford(res, *this)); -} - -bool ResourceSet::canAfford(const ResourceSet &price) const -{ - return VCMI_LIB_WRAP_NAMESPACE(canAfford(*this, price)); -} - -TResourceCap ResourceSet::marketValue() const -{ - TResourceCap total = 0; - for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - total += static_cast(VLC->objh->resVals[i]) * static_cast(operator[](i)); - return total; -} - -std::string ResourceSet::toString() const -{ - std::ostringstream out; - out << "["; - for(auto it = begin(); it != end(); ++it) - { - out << *it; - if(std::prev(end()) != it) out << ", "; - } - out << "]"; - return out.str(); -} - -bool ResourceSet::nziterator::valid() const -{ - return cur.resType < GameConstants::RESOURCE_QUANTITY && cur.resVal; -} - -ResourceSet::nziterator ResourceSet::nziterator::operator++() -{ - advance(); - return *this; -} - -ResourceSet::nziterator ResourceSet::nziterator::operator++(int) -{ - nziterator ret = *this; - advance(); - return ret; -} - -const ResourceSet::nziterator::ResEntry& ResourceSet::nziterator::operator*() const -{ - return cur; -} - -const ResourceSet::nziterator::ResEntry * ResourceSet::nziterator::operator->() const -{ - return &cur; -} - -void ResourceSet::nziterator::advance() -{ - do - { - ++cur.resType; - } while(cur.resType < GameConstants::RESOURCE_QUANTITY && !(cur.resVal=rs[cur.resType])); - - if(cur.resType >= GameConstants::RESOURCE_QUANTITY) - cur.resVal = -1; -} - -ResourceSet::nziterator::nziterator(const ResourceSet &RS) - : rs(RS) -{ - cur.resType = EGameResID::WOOD; - cur.resVal = rs[EGameResID::WOOD]; - - if(!valid()) - advance(); -} - -VCMI_LIB_NAMESPACE_END +/* + * ResourceSet.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 "GameConstants.h" +#include "ResourceSet.h" +#include "constants/StringConstants.h" +#include "JsonNode.h" +#include "serializer/JsonSerializeFormat.h" +#include "mapObjects/CObjectHandler.h" +#include "VCMI_Lib.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ResourceSet::ResourceSet(const JsonNode & node) +{ + for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); +} + +void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) +{ + if(handler.saving && !nonZero()) + return; + auto s = handler.enterStruct(fieldName); + + //TODO: add proper support for mithril to map format + for(int idx = 0; idx < GameConstants::RESOURCE_QUANTITY - 1; idx ++) + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], this->operator[](idx), 0); +} + +bool ResourceSet::nonZero() const +{ + for(const auto & elem : *this) + if(elem) + return true; + + return false; +} + +void ResourceSet::amax(const TResourceCap &val) +{ + for(auto & elem : *this) + vstd::amax(elem, val); +} + +void ResourceSet::amin(const TResourceCap &val) +{ + for(auto & elem : *this) + vstd::amin(elem, val); +} + +void ResourceSet::positive() +{ + for(auto & elem : *this) + vstd::amax(elem, 0); +} + +static bool canAfford(const ResourceSet &res, const ResourceSet &price) +{ + assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + if(price[i] > res[i]) + return false; + + return true; +} + +bool ResourceSet::canBeAfforded(const ResourceSet &res) const +{ + return VCMI_LIB_WRAP_NAMESPACE(canAfford(res, *this)); +} + +bool ResourceSet::canAfford(const ResourceSet &price) const +{ + return VCMI_LIB_WRAP_NAMESPACE(canAfford(*this, price)); +} + +TResourceCap ResourceSet::marketValue() const +{ + TResourceCap total = 0; + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + total += static_cast(VLC->objh->resVals[i]) * static_cast(operator[](i)); + return total; +} + +std::string ResourceSet::toString() const +{ + std::ostringstream out; + out << "["; + for(auto it = begin(); it != end(); ++it) + { + out << *it; + if(std::prev(end()) != it) out << ", "; + } + out << "]"; + return out.str(); +} + +bool ResourceSet::nziterator::valid() const +{ + return cur.resType < GameResID::COUNT && cur.resVal; +} + +ResourceSet::nziterator ResourceSet::nziterator::operator++() +{ + advance(); + return *this; +} + +ResourceSet::nziterator ResourceSet::nziterator::operator++(int) +{ + nziterator ret = *this; + advance(); + return ret; +} + +const ResourceSet::nziterator::ResEntry& ResourceSet::nziterator::operator*() const +{ + return cur; +} + +const ResourceSet::nziterator::ResEntry * ResourceSet::nziterator::operator->() const +{ + return &cur; +} + +void ResourceSet::nziterator::advance() +{ + do + { + ++cur.resType; + } while(cur.resType < GameResID::COUNT && !(cur.resVal=rs[cur.resType])); + + if(cur.resType >= GameResID::COUNT) + cur.resVal = -1; +} + +ResourceSet::nziterator::nziterator(const ResourceSet &RS) + : rs(RS) +{ + cur.resType = EGameResID::WOOD; + cur.resVal = rs[EGameResID::WOOD]; + + if(!valid()) + advance(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 6d879f715..ad844b230 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -1,237 +1,226 @@ -/* - * ResourceSet.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 - -using TResource = int32_t; -using TResourceCap = int64_t; //to avoid overflow when adding integers. Signed values are easier to control. - -class JsonNode; -class JsonSerializeFormat; - -class ResourceSet; - -enum class EGameResID : int8_t -{ - WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - - WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 -}; - -using GameResID = Identifier; - -//class to be representing a vector of resource -class ResourceSet -{ -private: - std::array container; -public: - // read resources set from json. Format example: { "gold": 500, "wood":5 } - DLL_LINKAGE ResourceSet(const JsonNode & node); - DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0, - TResource gems = 0, TResource gold = 0, TResource mithril = 0); - - -#define scalarOperator(OPSIGN) \ - ResourceSet& operator OPSIGN ## =(const TResource &rhs) \ - { \ - for(auto i = 0; i < container.size(); i++) \ - container.at(i) OPSIGN ## = rhs; \ - \ - return *this; \ - } - -#define vectorOperator(OPSIGN) \ - ResourceSet& operator OPSIGN ## =(const ResourceSet &rhs) \ - { \ - for(auto i = 0; i < container.size(); i++) \ - container.at(i) OPSIGN ## = rhs[i]; \ - \ - return *this; \ - } - -#define twoOperands(OPSIGN, RHS_TYPE) \ - friend ResourceSet operator OPSIGN(ResourceSet lhs, const RHS_TYPE &rhs) \ - { \ - lhs OPSIGN ## = rhs; \ - return lhs; \ - } - - scalarOperator(+) - scalarOperator(-) - scalarOperator(*) - scalarOperator(/) - vectorOperator(+) - vectorOperator(-) - twoOperands(+, TResource) - twoOperands(-, TResource) - twoOperands(*, TResource) - twoOperands(/, TResource) - twoOperands(+, ResourceSet) - twoOperands(-, ResourceSet) - - -#undef scalarOperator -#undef vectorOperator -#undef twoOperands - - using const_reference = decltype(container)::const_reference; - using value_type = decltype(container)::value_type; - using const_iterator = decltype(container)::const_iterator; - using iterator = decltype(container)::iterator; - - // Array-like interface - TResource & operator[](GameResID index) - { - return operator[](index.getNum()); - } - - const TResource & operator[](GameResID index) const - { - return operator[](index.getNum()); - } - - TResource & operator[](size_t index) - { - return container.at(index); - } - - const TResource & operator[](size_t index) const - { - return container.at(index); - } - - bool empty () const - { - for(const auto & res : *this) - if(res) - return false; - - return true; - } - - // C++ range-based for support - auto begin () -> decltype (container.begin()) - { - return container.begin(); - } - - auto end () -> decltype (container.end()) - { - return container.end(); - } - - auto begin () const -> decltype (container.cbegin()) - { - return container.cbegin(); - } - - auto end () const -> decltype (container.cend()) - { - return container.cend(); - } - - auto size () const -> decltype (container.size()) - { - return container.size(); - } - - //to be used for calculations of type "how many units of sth can I afford?" - int operator/(const ResourceSet &rhs) - { - int ret = INT_MAX; - for(int i = 0; i < container.size(); i++) - if(rhs[i]) - vstd::amin(ret, container.at(i) / rhs[i]); - - return ret; - } - - ResourceSet & operator=(const TResource &rhs) - { - for(int & i : container) - i = rhs; - - return *this; - } - - ResourceSet operator-() const - { - ResourceSet ret; - for(int i = 0; i < container.size(); i++) - ret[i] = -container.at(i); - return ret; - } - - bool operator==(const ResourceSet &rhs) const - { - 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, const int version) - { - h & container; - } - - DLL_LINKAGE void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); - - 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 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; - DLL_LINKAGE TResourceCap marketValue() const; - - DLL_LINKAGE std::string toString() const; - - //special iterator of iterating over non-zero resources in set - class DLL_LINKAGE nziterator - { - struct ResEntry - { - GameResID resType; - TResourceCap resVal; - } cur; - const ResourceSet &rs; - void advance(); - - public: - nziterator(const ResourceSet &RS); - bool valid() const; - nziterator operator++(); - nziterator operator++(int); - const ResEntry& operator*() const; - const ResEntry* operator->() const; - }; - - -}; - -using TResources = ResourceSet; - - -VCMI_LIB_NAMESPACE_END +/* + * ResourceSet.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 + +using TResource = int32_t; +using TResourceCap = int64_t; //to avoid overflow when adding integers. Signed values are easier to control. + +class JsonNode; +class JsonSerializeFormat; + +class ResourceSet; + +//class to be representing a vector of resource +class ResourceSet +{ +private: + std::array container = {}; +public: + // read resources set from json. Format example: { "gold": 500, "wood":5 } + DLL_LINKAGE ResourceSet(const JsonNode & node); + DLL_LINKAGE ResourceSet() = default; + + +#define scalarOperator(OPSIGN) \ + ResourceSet& operator OPSIGN ## =(const TResource &rhs) \ + { \ + for(auto i = 0; i < container.size(); i++) \ + container.at(i) OPSIGN ## = rhs; \ + \ + return *this; \ + } + +#define vectorOperator(OPSIGN) \ + ResourceSet& operator OPSIGN ## =(const ResourceSet &rhs) \ + { \ + for(auto i = 0; i < container.size(); i++) \ + container.at(i) OPSIGN ## = rhs[i]; \ + \ + return *this; \ + } + +#define twoOperands(OPSIGN, RHS_TYPE) \ + friend ResourceSet operator OPSIGN(ResourceSet lhs, const RHS_TYPE &rhs) \ + { \ + lhs OPSIGN ## = rhs; \ + return lhs; \ + } + + scalarOperator(+) + scalarOperator(-) + scalarOperator(*) + scalarOperator(/) + vectorOperator(+) + vectorOperator(-) + twoOperands(+, TResource) + twoOperands(-, TResource) + twoOperands(*, TResource) + twoOperands(/, TResource) + twoOperands(+, ResourceSet) + twoOperands(-, ResourceSet) + + +#undef scalarOperator +#undef vectorOperator +#undef twoOperands + + using const_reference = decltype(container)::const_reference; + using value_type = decltype(container)::value_type; + using const_iterator = decltype(container)::const_iterator; + using iterator = decltype(container)::iterator; + + // Array-like interface + TResource & operator[](GameResID index) + { + return operator[](index.getNum()); + } + + const TResource & operator[](GameResID index) const + { + return operator[](index.getNum()); + } + + TResource & operator[](size_t index) + { + return container.at(index); + } + + const TResource & operator[](size_t index) const + { + return container.at(index); + } + + bool empty () const + { + for(const auto & res : *this) + if(res) + return false; + + return true; + } + + // C++ range-based for support + auto begin () -> decltype (container.begin()) + { + return container.begin(); + } + + auto end () -> decltype (container.end()) + { + return container.end(); + } + + auto begin () const -> decltype (container.cbegin()) + { + return container.cbegin(); + } + + auto end () const -> decltype (container.cend()) + { + return container.cend(); + } + + auto size () const -> decltype (container.size()) + { + return container.size(); + } + + //to be used for calculations of type "how many units of sth can I afford?" + int operator/(const ResourceSet &rhs) + { + int ret = INT_MAX; + for(int i = 0; i < container.size(); i++) + if(rhs[i]) + vstd::amin(ret, container.at(i) / rhs[i]); + + return ret; + } + + ResourceSet & operator=(const TResource &rhs) + { + for(int & i : container) + i = rhs; + + return *this; + } + + ResourceSet operator-() const + { + ResourceSet ret; + for(int i = 0; i < container.size(); i++) + ret[i] = -container.at(i); + return ret; + } + + bool operator==(const ResourceSet &rhs) const + { + 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, const int version) + { + h & container; + } + + DLL_LINKAGE void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); + + 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 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; + DLL_LINKAGE TResourceCap marketValue() const; + + DLL_LINKAGE std::string toString() const; + + //special iterator of iterating over non-zero resources in set + class DLL_LINKAGE nziterator + { + struct ResEntry + { + GameResID resType; + TResourceCap resVal; + } cur; + const ResourceSet &rs; + void advance(); + + public: + nziterator(const ResourceSet &RS); + bool valid() const; + nziterator operator++(); + nziterator operator++(int); + const ResEntry& operator*() const; + const ResEntry* operator->() const; + }; + + +}; + +using TResources = ResourceSet; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index ba7344dd0..5154505b4 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -10,9 +10,9 @@ #include "StdInc.h" #include "RiverHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,7 +36,7 @@ RiverType * RiverTypeHandler::loadFromJson( info->id = RiverId(index); info->identifier = identifier; info->modScope = scope; - info->tilesFilename = json["tilesFilename"].String(); + info->tilesFilename = AnimationPath::fromJson(json["tilesFilename"]); info->shortIdentifier = json["shortIdentifier"].String(); info->deltaName = json["delta"].String(); @@ -68,11 +68,6 @@ std::vector RiverTypeHandler::loadLegacyData() return {}; } -std::vector RiverTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RiverType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index 22fad8660..c070a53a8 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -14,6 +14,7 @@ #include #include "GameConstants.h" #include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,12 +24,6 @@ struct DLL_LINKAGE RiverPaletteAnimation int32_t start; /// total numbers of colors to cycle int32_t length; - - template void serialize(Handler& h, const int version) - { - h & start; - h & length; - } }; class DLL_LINKAGE RiverType : public EntityT @@ -49,23 +44,13 @@ public: std::string getNameTextID() const override; std::string getNameTranslated() const override; - std::string tilesFilename; + AnimationPath tilesFilename; std::string shortIdentifier; std::string deltaName; std::vector paletteAnimation; RiverType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & deltaName; - h & id; - h & paletteAnimation; - } }; class DLL_LINKAGE RiverTypeService : public EntityServiceT @@ -86,12 +71,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index afaa1e282..85380d460 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -10,9 +10,9 @@ #include "StdInc.h" #include "RoadHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -36,7 +36,7 @@ RoadType * RoadTypeHandler::loadFromJson( info->id = RoadId(index); info->identifier = identifier; info->modScope = scope; - info->tilesFilename = json["tilesFilename"].String(); + info->tilesFilename = AnimationPath::fromJson(json["tilesFilename"]); info->shortIdentifier = json["shortIdentifier"].String(); info->movementCost = json["moveCost"].Integer(); @@ -59,11 +59,6 @@ std::vector RoadTypeHandler::loadLegacyData() return {}; } -std::vector RoadTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RoadType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index 156d7635d..f58d5b9bb 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -14,6 +14,7 @@ #include #include "GameConstants.h" #include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,20 +36,11 @@ public: std::string getNameTextID() const override; std::string getNameTranslated() const override; - std::string tilesFilename; + AnimationPath tilesFilename; std::string shortIdentifier; ui8 movementCost; RoadType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & id; - h & movementCost; - } }; class DLL_LINKAGE RoadTypeService : public EntityServiceT @@ -69,12 +61,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index 0fa1416af..5fe39407c 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -17,7 +17,6 @@ #include "CGameInterface.h" #include "CScriptingModule.h" -#include "CModHandler.h" #include "VCMIDirs.h" #include "serializer/JsonDeserializer.h" @@ -91,7 +90,7 @@ void ScriptImpl::serializeJson(vstd::CLoggerBase * logger, JsonSerializeFormat & { resolveHost(); - ResourceID sourcePathId("SCRIPTS/" + sourcePath); + ResourcePath sourcePathId("SCRIPTS/" + sourcePath); auto rawData = CResourceHandler::get()->load(sourcePathId)->readAll(); @@ -116,7 +115,7 @@ void ScriptImpl::serializeJsonState(JsonSerializeFormat & handler) void ScriptImpl::resolveHost() { - ResourceID sourcePathId(sourcePath); + ResourcePath sourcePathId(sourcePath); if(sourcePathId.getType() == EResType::ERM) host = owner->erm; @@ -218,11 +217,6 @@ const Script * ScriptHandler::resolveScript(const std::string & name) const } } -std::vector ScriptHandler::getDefaultAllowed() const -{ - return std::vector(); -} - std::vector ScriptHandler::loadLegacyData() { return std::vector(); diff --git a/lib/ScriptHandler.h b/lib/ScriptHandler.h index e2ff3a7e4..a0177fc65 100644 --- a/lib/ScriptHandler.h +++ b/lib/ScriptHandler.h @@ -97,7 +97,6 @@ public: const Script * resolveScript(const std::string & name) const; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; ScriptPtr loadFromJson(vstd::CLoggerBase * logger, const std::string & scope, const JsonNode & json, const std::string & identifier) const; diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index a28edc8b7..cf1a70112 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -11,20 +11,41 @@ #include "StartInfo.h" #include "CGeneralTextHandler.h" -#include "CModHandler.h" +#include "CTownHandler.h" +#include "CHeroHandler.h" #include "VCMI_Lib.h" #include "rmg/CMapGenOptions.h" #include "mapping/CMapInfo.h" #include "campaign/CampaignState.h" #include "mapping/CMapHeader.h" #include "mapping/CMapService.h" +#include "modding/ModIncompatibility.h" VCMI_LIB_NAMESPACE_BEGIN PlayerSettings::PlayerSettings() - : bonus(RANDOM), castle(NONE), hero(RANDOM), heroPortrait(RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false) + : bonus(PlayerStartingBonus::RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false) { +} +FactionID PlayerSettings::getCastleValidated() const +{ + if (!castle.isValid()) + return FactionID(0); + if (castle.getNum() < VLC->townh->size() && VLC->townh->objects[castle.getNum()]->town != nullptr) + return castle; + + return FactionID(0); +} + +HeroTypeID PlayerSettings::getHeroValidated() const +{ + if (!hero.isValid()) + return HeroTypeID(0); + if (hero.getNum() < VLC->heroh->size()) + return hero; + + return HeroTypeID(0); } bool PlayerSettings::isControlledByAI() const @@ -41,7 +62,7 @@ PlayerSettings & StartInfo::getIthPlayersSettings(const PlayerColor & no) { if(playerInfos.find(no) != playerInfos.end()) return playerInfos[no]; - logGlobal->error("Cannot find info about player %s. Throwing...", no.getStr()); + logGlobal->error("Cannot find info about player %s. Throwing...", no.toString()); throw std::runtime_error("Cannot find info about player"); } const PlayerSettings & StartInfo::getIthPlayersSettings(const PlayerColor & no) const @@ -62,8 +83,8 @@ PlayerSettings * StartInfo::getPlayersSettings(const ui8 connectedPlayerId) std::string StartInfo::getCampaignName() const { - if(!campState->getName().empty()) - return campState->getName(); + if(!campState->getNameTranslated().empty()) + return campState->getNameTranslated(); else return VLC->generaltexth->allTexts[508]; } @@ -74,12 +95,12 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.529")); auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); - CModHandler::Incompatibility::ModList modList; + ModIncompatibility::ModListWithVersion modList; for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); + modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) - throw CModHandler::Incompatibility(std::move(modList)); + throw ModIncompatibility(modList); //there must be at least one human player before game can be started std::map::const_iterator i; @@ -196,15 +217,15 @@ ui8 LobbyInfo::clientFirstId(int clientId) const return 0; } -PlayerInfo & LobbyInfo::getPlayerInfo(int color) +PlayerInfo & LobbyInfo::getPlayerInfo(PlayerColor color) { - return mi->mapHeader->players[color]; + return mi->mapHeader->players[color.getNum()]; } TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color) { - if(color < PlayerColor::PLAYER_LIMIT) - return getPlayerInfo(color.getNum()).team; + if(color.isValidPlayer()) + return getPlayerInfo(color).team; else return TeamID::NO_TEAM; } diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 31b4dfe15..86b5d6815 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -1,180 +1,215 @@ -/* - * StartInfo.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 "campaign/CampaignConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CMapGenOptions; -class CampaignState; -class CMapInfo; -struct PlayerInfo; -class PlayerColor; -struct SharedMemory; - -/// Struct which describes the name, the color, the starting bonus of a player -struct DLL_LINKAGE PlayerSettings -{ - enum { PLAYER_AI = 0 }; // for use in playerID - - enum Ebonus { - NONE = -2, - RANDOM = -1, - ARTIFACT = 0, - GOLD = 1, - RESOURCE = 2 - }; - - Ebonus bonus; - FactionID castle; - HeroTypeID hero; - HeroTypeID heroPortrait; //-1 if default, else ID - - std::string heroName; - PlayerColor color; //from 0 - - enum EHandicap {NO_HANDICAP, MILD, SEVERE}; - EHandicap handicap;//0-no, 1-mild, 2-severe - - std::string name; - std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids - bool compOnly; //true if this player is a computer only player; required for RMG - template - void serialize(Handler &h, const int version) - { - h & castle; - h & hero; - h & heroPortrait; - h & heroName; - h & bonus; - h & color; - h & handicap; - h & name; - h & connectedPlayerIDs; - h & compOnly; - } - - PlayerSettings(); - bool isControlledByAI() const; - bool isControlledByHuman() const; -}; - -/// Struct which describes the difficulty, the turn time,.. of a heroes match. -struct DLL_LINKAGE StartInfo -{ - enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; - - EMode mode; - ui8 difficulty; //0=easy; 4=impossible - - 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 - ui8 turnTime; //in minutes, 0=unlimited - std::string mapname; // empty for random map, otherwise name of the map or savegame - bool createRandomMap() const { return mapGenOptions != nullptr; } - std::shared_ptr mapGenOptions; - - std::shared_ptr campState; - - PlayerSettings & getIthPlayersSettings(const PlayerColor & no); - const PlayerSettings & getIthPlayersSettings(const PlayerColor & no) const; - PlayerSettings * getPlayersSettings(const ui8 connectedPlayerId); - - // TODO: Must be client-side - std::string getCampaignName() const; - - template - void serialize(Handler &h, const int version) - { - h & mode; - h & difficulty; - h & playerInfos; - h & seedToBeUsed; - h & seedPostInit; - h & mapfileChecksum; - h & turnTime; - h & mapname; - h & mapGenOptions; - h & campState; - } - - StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), turnTime(0) - { - - } -}; - -struct ClientPlayer -{ - int connection; - std::string name; - - template void serialize(Handler &h, const int version) - { - h & connection; - h & name; - } -}; - -struct DLL_LINKAGE LobbyState -{ - std::shared_ptr si; - std::shared_ptr mi; - std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" - int hostClientId; - // TODO: Campaign-only and we don't really need either of them. - // Before start both go into CCampaignState that is part of StartInfo - CampaignScenarioID campaignMap; - int campaignBonus; - - LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(CampaignScenarioID::NONE), campaignBonus(-1) {} - - template void serialize(Handler &h, const int version) - { - h & si; - h & mi; - h & playerNames; - h & hostClientId; - h & campaignMap; - h & campaignBonus; - } -}; - -struct DLL_LINKAGE LobbyInfo : public LobbyState -{ - boost::mutex stateMutex; - std::string uuid; - std::shared_ptr shm; - - LobbyInfo() {} - - void verifyStateBeforeStart(bool ignoreNoHuman = false) const; - - bool isClientHost(int clientId) const; - std::set getAllClientPlayers(int clientId); - std::vector getConnectedPlayerIdsForClient(int clientId) const; - - // Helpers for lobby state access - std::set clientHumanColors(int clientId); - PlayerColor clientFirstColor(int clientId) const; - bool isClientColor(int clientId, const PlayerColor & color) const; - ui8 clientFirstId(int clientId) const; // Used by chat only! - PlayerInfo & getPlayerInfo(int color); - TeamID getPlayerTeamId(const PlayerColor & color); -}; - - -VCMI_LIB_NAMESPACE_END +/* + * StartInfo.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 "vstd/DateUtils.h" + +#include "GameConstants.h" +#include "TurnTimerInfo.h" +#include "campaign/CampaignConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CMapGenOptions; +class CampaignState; +class CMapInfo; +struct PlayerInfo; +class PlayerColor; + +struct DLL_LINKAGE SimturnsInfo +{ + /// Minimal number of turns that must be played simultaneously even if contact has been detected + int requiredTurns = 0; + /// Maximum number of turns that might be played simultaneously unless contact is detected + int optionalTurns = 0; + /// If set to true, human and 1 AI can act at the same time + bool allowHumanWithAI = false; + + bool operator == (const SimturnsInfo & other) const + { + return requiredTurns == other.requiredTurns && + optionalTurns == other.optionalTurns && + allowHumanWithAI == other.allowHumanWithAI; + } + + template + void serialize(Handler &h, const int version) + { + h & requiredTurns; + h & optionalTurns; + h & allowHumanWithAI; + } +}; + +enum class PlayerStartingBonus : int8_t +{ + RANDOM = -1, + ARTIFACT = 0, + GOLD = 1, + RESOURCE = 2 +}; + +/// Struct which describes the name, the color, the starting bonus of a player +struct DLL_LINKAGE PlayerSettings +{ + enum { PLAYER_AI = 0 }; // for use in playerID + + PlayerStartingBonus bonus; + FactionID castle; + HeroTypeID hero; + HeroTypeID heroPortrait; //-1 if default, else ID + + std::string heroNameTextId; + PlayerColor color; //from 0 - + enum EHandicap {NO_HANDICAP, MILD, SEVERE}; + EHandicap handicap;//0-no, 1-mild, 2-severe + + std::string name; + std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids + bool compOnly; //true if this player is a computer only player; required for RMG + template + void serialize(Handler &h, const int version) + { + h & castle; + h & hero; + h & heroPortrait; + h & heroNameTextId; + h & bonus; + h & color; + h & handicap; + h & name; + h & connectedPlayerIDs; + h & compOnly; + } + + PlayerSettings(); + bool isControlledByAI() const; + bool isControlledByHuman() const; + + FactionID getCastleValidated() const; + HeroTypeID getHeroValidated() const; +}; + +/// Struct which describes the difficulty, the turn time,.. of a heroes match. +struct DLL_LINKAGE StartInfo +{ + enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; + + EMode mode; + ui8 difficulty; //0=easy; 4=impossible + + 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; + std::string fileURI; + SimturnsInfo simturnsInfo; + TurnTimerInfo turnTimerInfo; + std::string mapname; // empty for random map, otherwise name of the map or savegame + bool createRandomMap() const { return mapGenOptions != nullptr; } + std::shared_ptr mapGenOptions; + + std::shared_ptr campState; + + PlayerSettings & getIthPlayersSettings(const PlayerColor & no); + const PlayerSettings & getIthPlayersSettings(const PlayerColor & no) const; + PlayerSettings * getPlayersSettings(const ui8 connectedPlayerId); + + // TODO: Must be client-side + std::string getCampaignName() const; + + template + void serialize(Handler &h, const int version) + { + h & mode; + h & difficulty; + h & playerInfos; + h & seedToBeUsed; + h & seedPostInit; + h & mapfileChecksum; + h & startTimeIso8601; + h & fileURI; + h & simturnsInfo; + h & turnTimerInfo; + h & mapname; + h & mapGenOptions; + h & campState; + } + + StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), + mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))), fileURI("") + { + + } +}; + +struct ClientPlayer +{ + int connection; + std::string name; + + template void serialize(Handler &h, const int version) + { + h & connection; + h & name; + } +}; + +struct DLL_LINKAGE LobbyState +{ + std::shared_ptr si; + std::shared_ptr mi; + std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" + int hostClientId; + // TODO: Campaign-only and we don't really need either of them. + // Before start both go into CCampaignState that is part of StartInfo + CampaignScenarioID campaignMap; + int campaignBonus; + + LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(CampaignScenarioID::NONE), campaignBonus(-1) {} + + template void serialize(Handler &h, const int version) + { + h & si; + h & mi; + h & playerNames; + h & hostClientId; + h & campaignMap; + h & campaignBonus; + } +}; + +struct DLL_LINKAGE LobbyInfo : public LobbyState +{ + boost::mutex stateMutex; + std::string uuid; + + LobbyInfo() {} + + void verifyStateBeforeStart(bool ignoreNoHuman = false) const; + + bool isClientHost(int clientId) const; + std::set getAllClientPlayers(int clientId); + std::vector getConnectedPlayerIdsForClient(int clientId) const; + + // Helpers for lobby state access + std::set clientHumanColors(int clientId); + PlayerColor clientFirstColor(int clientId) const; + bool isClientColor(int clientId, const PlayerColor & color) const; + ui8 clientFirstId(int clientId) const; // Used by chat only! + PlayerInfo & getPlayerInfo(PlayerColor color); + TeamID getPlayerTeamId(const PlayerColor & color); +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/StdInc.cpp b/lib/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/lib/StdInc.cpp +++ b/lib/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/lib/StdInc.h b/lib/StdInc.h index 25d8caf3d..82dfac8ad 100644 --- a/lib/StdInc.h +++ b/lib/StdInc.h @@ -1,7 +1,7 @@ -#pragma once - -#include "../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. +#pragma once + +#include "../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. diff --git a/lib/StringConstants.h b/lib/StringConstants.h deleted file mode 100644 index ff0facbd8..000000000 --- a/lib/StringConstants.h +++ /dev/null @@ -1,125 +0,0 @@ -/* - * StringConstants.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 - -/// -/// String ID which are pointless to move to config file - these types are mostly hardcoded -/// -namespace GameConstants -{ - const std::string RESOURCE_NAMES [RESOURCE_QUANTITY] = { - "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" - }; - - const std::string PLAYER_COLOR_NAMES [PlayerColor::PLAYER_LIMIT_I] = { - "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" - }; - - const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; -} - -namespace PrimarySkill -{ - const std::string names [GameConstants::PRIMARY_SKILLS] = { "attack", "defence", "spellpower", "knowledge" }; -} - -namespace NSecondarySkill -{ - const std::string names [GameConstants::SKILL_QUANTITY] = - { - "pathfinding", "archery", "logistics", "scouting", "diplomacy", // 5 - "navigation", "leadership", "wisdom", "mysticism", "luck", // 10 - "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", // 15 - "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", // 20 - "artillery", "learning", "offence", "armorer", "intelligence", // 25 - "sorcery", "resistance", "firstAid" - }; - - const std::vector levels = - { - "none", "basic", "advanced", "expert" - }; -} - -namespace EBuildingType -{ - const std::string names [44] = - { - "mageGuild1", "mageGuild2", "mageGuild3", "mageGuild4", "mageGuild5", // 5 - "tavern", "shipyard", "fort", "citadel", "castle", // 10 - "villageHall", "townHall", "cityHall", "capitol", "marketplace", // 15 - "resourceSilo", "blacksmith", "special1", "horde1", "horde1Upgr", // 20 - "ship", "special2", "special3", "special4", "horde2", // 25 - "horde2Upgr", "grail", "extraTownHall", "extraCityHall", "extraCapitol", // 30 - "dwellingLvl1", "dwellingLvl2", "dwellingLvl3", "dwellingLvl4", "dwellingLvl5", // 35 - "dwellingLvl6", "dwellingLvl7", "dwellingUpLvl1", "dwellingUpLvl2", "dwellingUpLvl3", // 40 - "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7" - }; -} - -namespace ETownType -{ - const std::string names [GameConstants::F_NUMBER] = - { - "castle", "rampart", "tower", - "inferno", "necropolis", "dungeon", - "stronghold", "fortress", "conflux" - }; -} - -namespace NArtifactPosition -{ - const std::string namesHero [19] = - { - "head", "shoulders", "neck", "rightHand", "leftHand", "torso", //5 - "rightRing", "leftRing", "feet", //8 - "misc1", "misc2", "misc3", "misc4", //12 - "mach1", "mach2", "mach3", "mach4", //16 - "spellbook", "misc5" //18 - }; - - const std::string namesCreature[1] = - { - "creature1" - }; - - const std::string namesCommander[6] = - { - "commander1", "commander2", "commander3", "commander4", "commander5", "commander6", - }; - - - const std::string backpack = "backpack"; -} - -namespace NMetaclass -{ - const std::string names [16] = - { - "", - "artifact", "creature", "faction", "experience", "hero", - "heroClass", "luck", "mana", "morale", "movement", - "object", "primarySkill", "secondarySkill", "spell", "resource" - }; -} - -namespace NPathfindingLayer -{ - const std::string names[EPathfindingLayer::NUM_LAYERS] = - { - "land", "sail", "water", "air" - }; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index fbdcc48df..805f41362 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "TerrainHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" +#include "modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,10 +27,10 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const info->identifier = identifier; info->modScope = scope; info->moveCost = static_cast(json["moveCost"].Integer()); - info->musicFilename = json["music"].String(); - info->tilesFilename = json["tiles"].String(); - info->horseSound = json["horseSound"].String(); - info->horseSoundPenalty = json["horseSoundPenalty"].String(); + info->musicFilename = 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(); @@ -57,7 +58,6 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const { //Set bits const auto & s = node.String(); - if (s == "LAND") info->passabilityType |= TerrainType::PassabilityType::LAND; if (s == "WATER") info->passabilityType |= TerrainType::PassabilityType::WATER; if (s == "ROCK") info->passabilityType |= TerrainType::PassabilityType::ROCK; if (s == "SURFACE") info->passabilityType |= TerrainType::PassabilityType::SURFACE; @@ -67,7 +67,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const info->river = River::NO_RIVER; if(!json["river"].isNull()) { - VLC->modh->identifiers.requestIdentifier("river", json["river"], [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("river", json["river"], [info](int32_t identifier) { info->river = RiverId(identifier); }); @@ -87,7 +87,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const for(const auto & t : json["battleFields"].Vector()) { - VLC->modh->identifiers.requestIdentifier("battlefield", t, [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("battlefield", t, [info](int32_t identifier) { info->battleFields.emplace_back(identifier); }); @@ -95,7 +95,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const for(const auto & t : json["prohibitTransitions"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", t, [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("terrain", t, [info](int32_t identifier) { info->prohibitTransitions.emplace_back(identifier); }); @@ -105,7 +105,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const if(!json["rockTerrain"].isNull()) { - VLC->modh->identifiers.requestIdentifier("terrain", json["rockTerrain"], [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("terrain", json["rockTerrain"], [info](int32_t identifier) { info->rockTerrain = TerrainId(identifier); }); @@ -126,7 +126,7 @@ std::vector TerrainTypeHandler::loadLegacyData() objects.resize(dataSize); - CLegacyConfigParser terrainParser("DATA/TERRNAME.TXT"); + CLegacyConfigParser terrainParser(TextPath::builtin("DATA/TERRNAME.TXT")); std::vector result; do @@ -140,11 +140,6 @@ std::vector TerrainTypeHandler::loadLegacyData() return result; } -std::vector TerrainTypeHandler::getDefaultAllowed() const -{ - return {}; -} - bool TerrainType::isLand() const { return !isWater(); @@ -155,9 +150,14 @@ bool TerrainType::isWater() const return passabilityType & PassabilityType::WATER; } +bool TerrainType::isRock() const +{ + return passabilityType & PassabilityType::ROCK; +} + bool TerrainType::isPassable() const { - return !(passabilityType & PassabilityType::ROCK); + return !isRock(); } bool TerrainType::isSurface() const @@ -170,16 +170,6 @@ bool TerrainType::isUnderground() const return passabilityType & PassabilityType::SUBTERRANEAN; } -bool TerrainType::isSurfaceCartographerCompatible() const -{ - return isSurface(); -} - -bool TerrainType::isUndergroundCartographerCompatible() const -{ - return isLand() && isPassable() && !isSurface(); -} - bool TerrainType::isTransitionRequired() const { return transitionRequired; diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 5a10f1afa..68103abd1 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -15,6 +15,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "Color.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +44,7 @@ class DLL_LINKAGE TerrainType : public EntityT enum PassabilityType : ui8 { - LAND = 1, + //LAND = 1, WATER = 2, SURFACE = 4, SUBTERRANEAN = 8, @@ -66,11 +67,11 @@ public: ColorRGBA minimapBlocked; ColorRGBA minimapUnblocked; std::string shortIdentifier; - std::string musicFilename; - std::string tilesFilename; + AudioPath musicFilename; + AnimationPath tilesFilename; std::string terrainViewPatterns; - std::string horseSound; - std::string horseSoundPenalty; + AudioPath horseSound; + AudioPath horseSoundPenalty; std::vector paletteAnimation; @@ -83,36 +84,13 @@ public: bool isLand() const; bool isWater() const; + bool isRock() const; + bool isPassable() const; + bool isSurface() const; bool isUnderground() const; bool isTransitionRequired() const; - bool isSurfaceCartographerCompatible() const; - bool isUndergroundCartographerCompatible() const; - - template void serialize(Handler &h, const int version) - { - h & battleFields; - h & prohibitTransitions; - h & minimapBlocked; - h & minimapUnblocked; - h & modScope; - h & identifier; - h & musicFilename; - h & tilesFilename; - h & shortIdentifier; - h & terrainViewPatterns; - h & rockTerrain; - h & river; - h & paletteAnimation; - - h & id; - h & moveCost; - h & horseSound; - h & horseSoundPenalty; - h & passabilityType; - h & transitionRequired; - } }; class DLL_LINKAGE TerrainTypeService : public EntityServiceT @@ -131,12 +109,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index f97851140..df4771210 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -1,207 +1,213 @@ -/* - * TextOperations.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 "TextOperations.h" - -#include "CGeneralTextHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -size_t TextOperations::getUnicodeCharacterSize(char firstByte) -{ - // length of utf-8 character can be determined from 1st byte by counting number of highest bits set to 1: - // 0xxxxxxx -> 1 - ASCII chars - // 110xxxxx -> 2 - // 1110xxxx -> 3 - // 11110xxx -> 4 - last allowed in current standard - - auto value = static_cast(firstByte); - - if ((value & 0b10000000) == 0) - return 1; // ASCII - - if ((value & 0b11100000) == 0b11000000) - return 2; - - if ((value & 0b11110000) == 0b11100000) - return 3; - - if ((value & 0b11111000) == 0b11110000) - return 4; - - assert(0);// invalid unicode sequence - return 4; -} - -bool TextOperations::isValidUnicodeCharacter(const char * character, size_t maxSize) -{ - assert(maxSize > 0); - - auto value = static_cast(character[0]); - - // ASCII - if ( value < 0b10000000) - return maxSize > 0; - - // can't be first byte in UTF8 - if (value < 0b11000000) - return false; - - // above maximum allowed in standard (UTF codepoints are capped at 0x0010FFFF) - if (value > 0b11110000) - return false; - - // first character must follow rules checked in getUnicodeCharacterSize - size_t size = getUnicodeCharacterSize(character[0]); - - if (size > maxSize) - return false; - - // remaining characters must have highest bit set to 1 - for (size_t i = 1; i < size; i++) - { - auto characterValue = static_cast(character[i]); - if (characterValue < 0b10000000) - return false; - } - return true; -} - -bool TextOperations::isValidASCII(const std::string & text) -{ - for (const char & ch : text) - if (static_cast(ch) >= 0x80 ) - return false; - return true; -} - -bool TextOperations::isValidASCII(const char * data, size_t size) -{ - for (size_t i=0; i(data[i]) >= 0x80 ) - return false; - return true; -} - -bool TextOperations::isValidUnicodeString(const std::string & text) -{ - for (size_t i=0; i(data[0]) & 0b1111111; - case 2: - return - ((static_cast(data[0]) & 0b11111 ) << 6) + - ((static_cast(data[1]) & 0b111111) << 0) ; - case 3: - return - ((static_cast(data[0]) & 0b1111 ) << 12) + - ((static_cast(data[1]) & 0b111111) << 6) + - ((static_cast(data[2]) & 0b111111) << 0) ; - case 4: - return - ((static_cast(data[0]) & 0b111 ) << 18) + - ((static_cast(data[1]) & 0b111111) << 12) + - ((static_cast(data[2]) & 0b111111) << 6) + - ((static_cast(data[3]) & 0b111111) << 0) ; - } - - assert(0); - return 0; -} - -uint32_t TextOperations::getUnicodeCodepoint(char data, const std::string & encoding ) -{ - std::string stringNative(1, data); - std::string stringUnicode = toUnicode(stringNative, encoding); - - if (stringUnicode.empty()) - return 0; - - return getUnicodeCodepoint(stringUnicode.data(), stringUnicode.size()); -} - -std::string TextOperations::toUnicode(const std::string &text, const std::string &encoding) -{ - return boost::locale::conv::to_utf(text, encoding); -} - -std::string TextOperations::fromUnicode(const std::string &text, const std::string &encoding) -{ - return boost::locale::conv::from_utf(text, encoding); -} - -void TextOperations::trimRightUnicode(std::string & text, const size_t amount) -{ - if(text.empty()) - return; - //todo: more efficient algorithm - for(int i = 0; i< amount; i++){ - auto b = text.begin(); - auto e = text.end(); - size_t lastLen = 0; - size_t len = 0; - while (b != e) { - lastLen = len; - size_t n = getUnicodeCharacterSize(*b); - - if(!isValidUnicodeCharacter(&(*b),e-b)) - { - logGlobal->error("Invalid UTF8 sequence"); - break;//invalid sequence will be trimmed - } - - len += n; - b += n; - } - - text.resize(lastLen); - } -} - -std::string TextOperations::escapeString(std::string input) -{ - boost::replace_all(input, "\\", "\\\\"); - boost::replace_all(input, "\n", "\\n"); - boost::replace_all(input, "\r", "\\r"); - boost::replace_all(input, "\t", "\\t"); - boost::replace_all(input, "\"", "\\\""); - - return input; -} - -VCMI_LIB_NAMESPACE_END +/* + * TextOperations.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 "TextOperations.h" + +#include "CGeneralTextHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +size_t TextOperations::getUnicodeCharacterSize(char firstByte) +{ + // length of utf-8 character can be determined from 1st byte by counting number of highest bits set to 1: + // 0xxxxxxx -> 1 - ASCII chars + // 110xxxxx -> 2 + // 1110xxxx -> 3 + // 11110xxx -> 4 - last allowed in current standard + + auto value = static_cast(firstByte); + + if ((value & 0b10000000) == 0) + return 1; // ASCII + + if ((value & 0b11100000) == 0b11000000) + return 2; + + if ((value & 0b11110000) == 0b11100000) + return 3; + + if ((value & 0b11111000) == 0b11110000) + return 4; + + assert(0);// invalid unicode sequence + return 4; +} + +bool TextOperations::isValidUnicodeCharacter(const char * character, size_t maxSize) +{ + assert(maxSize > 0); + + auto value = static_cast(character[0]); + + // ASCII + if ( value < 0b10000000) + return maxSize > 0; + + // can't be first byte in UTF8 + if (value < 0b11000000) + return false; + + // above maximum allowed in standard (UTF codepoints are capped at 0x0010FFFF) + if (value > 0b11110000) + return false; + + // first character must follow rules checked in getUnicodeCharacterSize + size_t size = getUnicodeCharacterSize(character[0]); + + if (size > maxSize) + return false; + + // remaining characters must have highest bit set to 1 + for (size_t i = 1; i < size; i++) + { + auto characterValue = static_cast(character[i]); + if (characterValue < 0b10000000) + return false; + } + return true; +} + +bool TextOperations::isValidASCII(const std::string & text) +{ + for (const char & ch : text) + if (static_cast(ch) >= 0x80 ) + return false; + return true; +} + +bool TextOperations::isValidASCII(const char * data, size_t size) +{ + for (size_t i=0; i(data[i]) >= 0x80 ) + return false; + return true; +} + +bool TextOperations::isValidUnicodeString(const std::string & text) +{ + for (size_t i=0; i(data[0]) & 0b1111111; + case 2: + return + ((static_cast(data[0]) & 0b11111 ) << 6) + + ((static_cast(data[1]) & 0b111111) << 0) ; + case 3: + return + ((static_cast(data[0]) & 0b1111 ) << 12) + + ((static_cast(data[1]) & 0b111111) << 6) + + ((static_cast(data[2]) & 0b111111) << 0) ; + case 4: + return + ((static_cast(data[0]) & 0b111 ) << 18) + + ((static_cast(data[1]) & 0b111111) << 12) + + ((static_cast(data[2]) & 0b111111) << 6) + + ((static_cast(data[3]) & 0b111111) << 0) ; + } + + assert(0); + return 0; +} + +uint32_t TextOperations::getUnicodeCodepoint(char data, const std::string & encoding ) +{ + std::string stringNative(1, data); + std::string stringUnicode = toUnicode(stringNative, encoding); + + if (stringUnicode.empty()) + return 0; + + return getUnicodeCodepoint(stringUnicode.data(), stringUnicode.size()); +} + +std::string TextOperations::toUnicode(const std::string &text, const std::string &encoding) +{ + return boost::locale::conv::to_utf(text, encoding); +} + +std::string TextOperations::fromUnicode(const std::string &text, const std::string &encoding) +{ + return boost::locale::conv::from_utf(text, encoding); +} + +void TextOperations::trimRightUnicode(std::string & text, const size_t amount) +{ + if(text.empty()) + return; + //todo: more efficient algorithm + for(int i = 0; i< amount; i++){ + auto b = text.begin(); + auto e = text.end(); + size_t lastLen = 0; + size_t len = 0; + while (b != e) { + lastLen = len; + size_t n = getUnicodeCharacterSize(*b); + + if(!isValidUnicodeCharacter(&(*b),e-b)) + { + logGlobal->error("Invalid UTF8 sequence"); + break;//invalid sequence will be trimmed + } + + len += n; + b += n; + } + + text.resize(lastLen); + } +} + +size_t TextOperations::getUnicodeCharactersCount(const std::string & text) +{ + std::wstring_convert, char32_t> conv; + return conv.from_bytes(text).size(); +} + +std::string TextOperations::escapeString(std::string input) +{ + boost::replace_all(input, "\\", "\\\\"); + boost::replace_all(input, "\n", "\\n"); + boost::replace_all(input, "\r", "\\r"); + boost::replace_all(input, "\t", "\\t"); + boost::replace_all(input, "\"", "\\\""); + + return input; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.h b/lib/TextOperations.h index 9173260a1..73768cef3 100644 --- a/lib/TextOperations.h +++ b/lib/TextOperations.h @@ -1,80 +1,83 @@ -/* - * TextOperations.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 - -/// Namespace that provides utilites for unicode support (UTF-8) -namespace TextOperations -{ - /// returns 32-bit UTF codepoint for UTF-8 character symbol - uint32_t DLL_LINKAGE getUnicodeCodepoint(const char *data, size_t maxSize); - - /// returns 32-bit UTF codepoint for character symbol in selected single-byte encoding - uint32_t DLL_LINKAGE getUnicodeCodepoint(char data, const std::string & encoding ); - - /// returns length (in bytes) of UTF-8 character starting from specified character - 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) - bool DLL_LINKAGE isValidUnicodeCharacter(const char * character, size_t maxSize); - - /// returns true if text contains valid ASCII-string - /// Note that since UTF-8 extends ASCII, any ASCII string is also UTF-8 string - bool DLL_LINKAGE isValidASCII(const std::string & text); - bool DLL_LINKAGE isValidASCII(const char * data, size_t size); - - /// test if text contains valid UTF-8 sequence - bool DLL_LINKAGE isValidUnicodeString(const std::string & text); - bool DLL_LINKAGE isValidUnicodeString(const char * data, size_t size); - - /// converts text to UTF-8 from specified encoding or from one specified in settings - std::string DLL_LINKAGE toUnicode(const std::string & text, const std::string & encoding); - - /// converts text from unicode to specified encoding or to one specified in settings - /// NOTE: usage of these functions should be avoided if possible - std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding); - - ///delete specified amount of UTF-8 characters from right - DLL_LINKAGE void trimRightUnicode(std::string & text, size_t amount = 1); - - /// converts number into string using metric system prefixes, e.g. 'k' or 'M' to keep resulting strings within specified size - /// Note that resulting string may have more symbols than digits: minus sign and prefix symbol - template - inline std::string formatMetric(Arithmetic number, int maxDigits); - - /// replaces all symbols that normally need escaping with appropriate escape sequences - std::string escapeString(std::string input); -}; - - - -template -inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits) -{ - Arithmetic max = std::pow(10, maxDigits); - if (std::abs(number) < max) - return std::to_string(number); - - std::string symbols = " kMGTPE"; - auto iter = symbols.begin(); - - while (std::abs(number) >= max) - { - number /= 1000; - iter++; - - assert(iter != symbols.end());//should be enough even for int64 - } - return std::to_string(number) + *iter; -} - -VCMI_LIB_NAMESPACE_END +/* + * TextOperations.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 + +/// Namespace that provides utilites for unicode support (UTF-8) +namespace TextOperations +{ + /// returns 32-bit UTF codepoint for UTF-8 character symbol + uint32_t DLL_LINKAGE getUnicodeCodepoint(const char *data, size_t maxSize); + + /// returns 32-bit UTF codepoint for character symbol in selected single-byte encoding + uint32_t DLL_LINKAGE getUnicodeCodepoint(char data, const std::string & encoding ); + + /// returns length (in bytes) of UTF-8 character starting from specified character + 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) + bool DLL_LINKAGE isValidUnicodeCharacter(const char * character, size_t maxSize); + + /// returns true if text contains valid ASCII-string + /// Note that since UTF-8 extends ASCII, any ASCII string is also UTF-8 string + bool DLL_LINKAGE isValidASCII(const std::string & text); + bool DLL_LINKAGE isValidASCII(const char * data, size_t size); + + /// test if text contains valid UTF-8 sequence + bool DLL_LINKAGE isValidUnicodeString(const std::string & text); + bool DLL_LINKAGE isValidUnicodeString(const char * data, size_t size); + + /// converts text to UTF-8 from specified encoding or from one specified in settings + std::string DLL_LINKAGE toUnicode(const std::string & text, const std::string & encoding); + + /// converts text from unicode to specified encoding or to one specified in settings + /// NOTE: usage of these functions should be avoided if possible + std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding); + + ///delete specified amount of UTF-8 characters from right + DLL_LINKAGE void trimRightUnicode(std::string & text, size_t amount = 1); + + /// give back amount of unicode characters + size_t DLL_LINKAGE getUnicodeCharactersCount(const std::string & text); + + /// converts number into string using metric system prefixes, e.g. 'k' or 'M' to keep resulting strings within specified size + /// Note that resulting string may have more symbols than digits: minus sign and prefix symbol + template + inline std::string formatMetric(Arithmetic number, int maxDigits); + + /// replaces all symbols that normally need escaping with appropriate escape sequences + std::string escapeString(std::string input); +}; + + + +template +inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits) +{ + Arithmetic max = std::pow(10, maxDigits); + if (std::abs(number) < max) + return std::to_string(number); + + std::string symbols = " kMGTPE"; + auto iter = symbols.begin(); + + while (std::abs(number) >= max) + { + number /= 1000; + iter++; + + assert(iter != symbols.end());//should be enough even for int64 + } + return std::to_string(number) + *iter; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp new file mode 100644 index 000000000..1b0785ecd --- /dev/null +++ b/lib/TurnTimerInfo.cpp @@ -0,0 +1,25 @@ +/* + * TurnTimerInfo.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 "TurnTimerInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool TurnTimerInfo::isEnabled() const +{ + return turnTimer > 0 || baseTimer > 0; +} + +bool TurnTimerInfo::isBattleEnabled() const +{ + return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h new file mode 100644 index 000000000..0f98df4b8 --- /dev/null +++ b/lib/TurnTimerInfo.h @@ -0,0 +1,55 @@ +/* + * TurnTimerInfo.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 TurnTimerInfo +{ + int turnTimer = 0; //in ms, counts down when player is making his turn on adventure map + int baseTimer = 0; //in ms, counts down only when turn timer runs out + int battleTimer = 0; //in ms, counts down during battles when creature timer runs out + int unitTimer = 0; //in ms, counts down when player is choosing action in battle + + bool accumulatingTurnTimer = false; + bool accumulatingUnitTimer = false; + + bool isActive = false; //is being counting down + bool isBattle = false; //indicator for current timer mode + + bool isEnabled() const; + bool isBattleEnabled() const; + + bool operator == (const TurnTimerInfo & other) const + { + return turnTimer == other.turnTimer && + baseTimer == other.baseTimer && + battleTimer == other.battleTimer && + unitTimer == other.unitTimer && + accumulatingTurnTimer == other.accumulatingTurnTimer && + accumulatingUnitTimer == other.accumulatingUnitTimer; + } + + template + void serialize(Handler &h, const int version) + { + h & turnTimer; + h & baseTimer; + h & battleTimer; + h & unitTimer; + h & accumulatingTurnTimer; + h & accumulatingUnitTimer; + h & isActive; + h & isBattle; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/UnlockGuard.h b/lib/UnlockGuard.h index 39fb8ebb8..034979ff8 100644 --- a/lib/UnlockGuard.h +++ b/lib/UnlockGuard.h @@ -1,120 +1,120 @@ -/* - * UnlockGuard.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 - -namespace vstd -{ - namespace detail - { - template - class unlock_policy - { - protected: - void unlock(Mutex &m) - { - m.unlock(); - } - void lock(Mutex &m) - { - m.lock(); - } - }; - - template - class unlock_shared_policy - { - protected: - void unlock(Mutex &m) - { - m.unlock_shared(); - } - void lock(Mutex &m) - { - m.lock_shared(); - } - }; - } - - - //similar to boost::lock_guard but UNlocks for the scope + assertions - template > - class unlock_guard : LockingPolicy - { - private: - Mutex* m; - - explicit unlock_guard(unlock_guard&); - unlock_guard& operator=(unlock_guard&); - public: - explicit unlock_guard(Mutex& m_): - m(&m_) - { - this->unlock(*m); - } - - unlock_guard() - { - m = nullptr; - } - - unlock_guard(unlock_guard &&other) - : m(other.m) - { - other.m = nullptr; - } - - void release() - { - m = nullptr; - } - - ~unlock_guard() - { - if(m) - this->lock(*m); - } - }; - - template - unlock_guard > makeUnlockGuard(Mutex &m_) - { - return unlock_guard >(m_); - } - template - unlock_guard > makeEmptyGuard(Mutex &) - { - return unlock_guard >(); - } - template - unlock_guard > makeUnlockGuardIf(Mutex &m_, bool shallUnlock) - { - return shallUnlock - ? makeUnlockGuard(m_) - : unlock_guard >(); - } - template - unlock_guard > makeUnlockSharedGuard(Mutex &m_) - { - return unlock_guard >(m_); - } - template - unlock_guard > makeUnlockSharedGuardIf(Mutex &m_, bool shallUnlock) - { - return shallUnlock - ? makeUnlockSharedGuard(m_) - : unlock_guard >(); - } - - using unlock_shared_guard = unlock_guard>; -} - -VCMI_LIB_NAMESPACE_END +/* + * UnlockGuard.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 + +namespace vstd +{ + namespace detail + { + template + class unlock_policy + { + protected: + void unlock(Mutex &m) + { + m.unlock(); + } + void lock(Mutex &m) + { + m.lock(); + } + }; + + template + class unlock_shared_policy + { + protected: + void unlock(Mutex &m) + { + m.unlock_shared(); + } + void lock(Mutex &m) + { + m.lock_shared(); + } + }; + } + + + //similar to boost::lock_guard but UNlocks for the scope + assertions + template > + class unlock_guard : LockingPolicy + { + private: + Mutex* m; + + explicit unlock_guard(unlock_guard&); + unlock_guard& operator=(unlock_guard&); + public: + explicit unlock_guard(Mutex& m_): + m(&m_) + { + this->unlock(*m); + } + + unlock_guard() + { + m = nullptr; + } + + unlock_guard(unlock_guard &&other) + : m(other.m) + { + other.m = nullptr; + } + + void release() + { + m = nullptr; + } + + ~unlock_guard() + { + if(m) + this->lock(*m); + } + }; + + template + unlock_guard > makeUnlockGuard(Mutex &m_) + { + return unlock_guard >(m_); + } + template + unlock_guard > makeEmptyGuard(Mutex &) + { + return unlock_guard >(); + } + template + unlock_guard > makeUnlockGuardIf(Mutex &m_, bool shallUnlock) + { + return shallUnlock + ? makeUnlockGuard(m_) + : unlock_guard >(); + } + template + unlock_guard > makeUnlockSharedGuard(Mutex &m_) + { + return unlock_guard >(m_); + } + template + unlock_guard > makeUnlockSharedGuardIf(Mutex &m_, bool shallUnlock) + { + return shallUnlock + ? makeUnlockSharedGuard(m_) + : unlock_guard >(); + } + + using unlock_shared_guard = unlock_guard>; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 7199916a5..f297e5dff 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -1,333 +1,335 @@ -/* - * VCMI_Lib.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 "VCMI_Lib.h" - -#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 "CModHandler.h" -#include "IGameEventsReceiver.h" -#include "CStopWatch.h" -#include "VCMIDirs.h" -#include "filesystem/Filesystem.h" -#include "CConsoleHandler.h" -#include "rmg/CRmgTemplateStorage.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "mapObjects/CObjectHandler.h" -#include "mapping/CMapEditManager.h" -#include "ScriptHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "GameSettings.h" - -VCMI_LIB_NAMESPACE_BEGIN - -LibClasses * VLC = nullptr; - -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) -{ - console = Console; - VLC = new LibClasses(); - VLC->loadFilesystem(extractArchives); - settings.init(); - VLC->loadModFilesystem(onlyEssential); - -} - -DLL_LINKAGE void loadDLLClasses(bool onlyEssential) -{ - VLC->init(onlyEssential); -} - -const ArtifactService * LibClasses::artifacts() const -{ - return arth; -} - -const CreatureService * LibClasses::creatures() const -{ - return creh; -} - -const FactionService * LibClasses::factions() const -{ - return townh; -} - -const HeroClassService * LibClasses::heroClasses() const -{ - return &heroh->classes; -} - -const HeroTypeService * LibClasses::heroTypes() const -{ - return heroh; -} - -#if SCRIPTING_ENABLED -const scripting::Service * LibClasses::scripts() const -{ - return scriptHandler; -} -#endif - -const spells::Service * LibClasses::spells() const -{ - return spellh; -} - -const SkillService * LibClasses::skills() const -{ - return skillh; -} - -const IBonusTypeHandler * LibClasses::getBth() const -{ - return bth; -} - -const spells::effects::Registry * LibClasses::spellEffects() const -{ - return spells::effects::GlobalRegistry::get(); -} - -spells::effects::Registry * LibClasses::spellEffects() -{ - return spells::effects::GlobalRegistry::get(); -} - -const BattleFieldService * LibClasses::battlefields() const -{ - return battlefieldsHandler; -} - -const ObstacleService * LibClasses::obstacles() const -{ - return obstacleHandler; -} - -const IGameSettings * LibClasses::settings() const -{ - return settingsHandler; -} - -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: - heroh->classes.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; - - CResourceHandler::initialize(); - logGlobal->info("\tInitialization: %d ms", loadTime.getDiff()); - - CResourceHandler::load("config/filesystem.json", extractArchives); - logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); -} - -void LibClasses::loadModFilesystem(bool onlyEssential) -{ - CStopWatch loadTime; - modh = new CModHandler(); - modh->loadMods(onlyEssential); - 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(Handler *&handler, const std::string &name, CStopWatch &timer) -{ - handler = new Handler(); - logHandlerLoaded(name, timer); -} - -void LibClasses::init(bool onlyEssential) -{ - CStopWatch pomtime; - CStopWatch totalTime; - - createHandler(settingsHandler, "Game Settings", pomtime); - 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(arth, "Artifact", pomtime); - - createHandler(creh, "Creature", pomtime); - - createHandler(townh, "Town", 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?) - -#if SCRIPTING_ENABLED - createHandler(scriptHandler, "Script", pomtime); -#endif - - createHandler(battlefieldsHandler, "Battlefields", pomtime); - - createHandler(obstacleHandler, "Obstacles", pomtime); - - logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); - - modh->load(); - - modh->afterLoad(onlyEssential); - - //FIXME: make sure that everything is ok after game restart - //TODO: This should be done every time mod config changes -} - -void LibClasses::clear() -{ - delete heroh; - delete arth; - delete creh; - delete townh; - delete objh; - delete objtypeh; - delete spellh; - delete skillh; - delete modh; - delete bth; - delete tplh; - delete terviewh; -#if SCRIPTING_ENABLED - delete scriptHandler; -#endif - delete battlefieldsHandler; - delete generaltexth; - makeNull(); -} - -void LibClasses::makeNull() -{ - generaltexth = nullptr; - heroh = nullptr; - arth = nullptr; - creh = nullptr; - townh = nullptr; - objh = nullptr; - objtypeh = nullptr; - spellh = nullptr; - skillh = nullptr; - modh = nullptr; - bth = nullptr; - tplh = nullptr; - terviewh = nullptr; -#if SCRIPTING_ENABLED - scriptHandler = nullptr; -#endif - battlefieldsHandler = nullptr; -} - -LibClasses::LibClasses() -{ - //init pointers to handlers - makeNull(); -} - -void LibClasses::callWhenDeserializing() -{ - //FIXME: check if any of these are needed - //generaltexth = new CGeneralTextHandler(); - //generaltexth->load(); - //arth->load(true); - //modh->recreateHandlers(); - //modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config -} - -#if SCRIPTING_ENABLED -void LibClasses::scriptsLoaded() -{ - scriptHandler->performRegistration(this); -} -#endif - -LibClasses::~LibClasses() -{ - clear(); -} - -std::shared_ptr LibClasses::getContent() const -{ - return modh->content; -} - -void LibClasses::setContent(std::shared_ptr content) -{ - modh->content = std::move(content); -} - -VCMI_LIB_NAMESPACE_END +/* + * VCMI_Lib.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 "VCMI_Lib.h" + +#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 "modding/CModHandler.h" +#include "modding/CModInfo.h" +#include "modding/IdentifierStorage.h" +#include "modding/CModVersion.h" +#include "IGameEventsReceiver.h" +#include "CStopWatch.h" +#include "VCMIDirs.h" +#include "filesystem/Filesystem.h" +#include "CConsoleHandler.h" +#include "rmg/CRmgTemplateStorage.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "mapObjects/CObjectHandler.h" +#include "mapping/CMapEditManager.h" +#include "ScriptHandler.h" +#include "BattleFieldHandler.h" +#include "ObstacleHandler.h" +#include "GameSettings.h" + +VCMI_LIB_NAMESPACE_BEGIN + +LibClasses * VLC = nullptr; + +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) +{ + console = Console; + VLC = new LibClasses(); + VLC->loadFilesystem(extractArchives); + settings.init("config/settings.json", "vcmi:settings"); + persistentStorage.init("config/persistentStorage.json", ""); + VLC->loadModFilesystem(onlyEssential); + +} + +DLL_LINKAGE void loadDLLClasses(bool onlyEssential) +{ + VLC->init(onlyEssential); +} + +const ArtifactService * LibClasses::artifacts() const +{ + return arth; +} + +const CreatureService * LibClasses::creatures() const +{ + return creh; +} + +const FactionService * LibClasses::factions() const +{ + return townh; +} + +const HeroClassService * LibClasses::heroClasses() const +{ + return &heroh->classes; +} + +const HeroTypeService * LibClasses::heroTypes() const +{ + return heroh; +} + +#if SCRIPTING_ENABLED +const scripting::Service * LibClasses::scripts() const +{ + return scriptHandler; +} +#endif + +const spells::Service * LibClasses::spells() const +{ + return spellh; +} + +const SkillService * LibClasses::skills() const +{ + return skillh; +} + +const IBonusTypeHandler * LibClasses::getBth() const +{ + return bth; +} + +const CIdentifierStorage * LibClasses::identifiers() const +{ + return identifiersHandler; +} + +const spells::effects::Registry * LibClasses::spellEffects() const +{ + return spells::effects::GlobalRegistry::get(); +} + +spells::effects::Registry * LibClasses::spellEffects() +{ + return spells::effects::GlobalRegistry::get(); +} + +const BattleFieldService * LibClasses::battlefields() const +{ + return battlefieldsHandler; +} + +const ObstacleService * LibClasses::obstacles() const +{ + return obstacleHandler; +} + +const IGameSettings * LibClasses::settings() const +{ + return settingsHandler; +} + +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: + heroh->classes.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; + + CResourceHandler::initialize(); + logGlobal->info("\tInitialization: %d ms", loadTime.getDiff()); + + CResourceHandler::load("config/filesystem.json", extractArchives); + logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); +} + +void LibClasses::loadModFilesystem(bool onlyEssential) +{ + CStopWatch loadTime; + modh = new CModHandler(); + identifiersHandler = new CIdentifierStorage(); + modh->loadMods(onlyEssential); + 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(Handler *&handler, const std::string &name, CStopWatch &timer) +{ + handler = new Handler(); + logHandlerLoaded(name, timer); +} + +void LibClasses::init(bool onlyEssential) +{ + CStopWatch pomtime; + CStopWatch totalTime; + + createHandler(settingsHandler, "Game Settings", pomtime); + 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(arth, "Artifact", pomtime); + createHandler(creh, "Creature", pomtime); + createHandler(townh, "Town", 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?) +#if SCRIPTING_ENABLED + createHandler(scriptHandler, "Script", pomtime); +#endif + createHandler(battlefieldsHandler, "Battlefields", pomtime); + createHandler(obstacleHandler, "Obstacles", pomtime); + logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); + + modh->load(); + modh->afterLoad(onlyEssential); +} + +void LibClasses::clear() +{ + delete heroh; + delete arth; + delete creh; + delete townh; + delete objh; + delete objtypeh; + delete spellh; + delete skillh; + delete modh; + delete bth; + delete tplh; + delete terviewh; +#if SCRIPTING_ENABLED + delete scriptHandler; +#endif + delete battlefieldsHandler; + delete generaltexth; + delete identifiersHandler; + delete obstacleHandler; + delete terrainTypeHandler; + delete riverTypeHandler; + delete roadTypeHandler; + delete settingsHandler; + makeNull(); +} + +void LibClasses::makeNull() +{ + generaltexth = nullptr; + heroh = nullptr; + arth = nullptr; + creh = nullptr; + townh = nullptr; + objh = nullptr; + objtypeh = nullptr; + spellh = nullptr; + skillh = nullptr; + modh = nullptr; + bth = nullptr; + tplh = nullptr; + terviewh = nullptr; +#if SCRIPTING_ENABLED + scriptHandler = nullptr; +#endif + battlefieldsHandler = nullptr; + identifiersHandler = nullptr; + obstacleHandler = nullptr; + terrainTypeHandler = nullptr; + riverTypeHandler = nullptr; + roadTypeHandler = nullptr; + settingsHandler = nullptr; +} + +LibClasses::LibClasses() +{ + //init pointers to handlers + makeNull(); +} + +void LibClasses::callWhenDeserializing() +{ + //FIXME: check if any of these are needed + //generaltexth = new CGeneralTextHandler(); + //generaltexth->load(); + //arth->load(true); + //modh->recreateHandlers(); + //modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config +} + +#if SCRIPTING_ENABLED +void LibClasses::scriptsLoaded() +{ + scriptHandler->performRegistration(this); +} +#endif + +LibClasses::~LibClasses() +{ + clear(); +} + +std::shared_ptr LibClasses::getContent() const +{ + return modh->content; +} + +void LibClasses::setContent(std::shared_ptr content) +{ + modh->content = std::move(content); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index 5b95e4fb8..dc9912e3b 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -1,172 +1,131 @@ -/* - * VCMI_Lib.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 CConsoleHandler; -class CArtHandler; -class CHeroHandler; -class CCreatureHandler; -class CSpellHandler; -class CSkillHandler; -class CBuildingHandler; -class CObjectHandler; -class CObjectClassesHandler; -class CTownHandler; -class CGeneralTextHandler; -class CModHandler; -class CContentHandler; -class BattleFieldHandler; -class IBonusTypeHandler; -class CBonusTypeHandler; -class TerrainTypeHandler; -class RoadTypeHandler; -class RiverTypeHandler; -class ObstacleHandler; -class CTerrainViewPatternConfig; -class CRmgTemplateStorage; -class IHandlerBase; -class IGameSettings; -class GameSettings; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class ScriptHandler; -} -#endif - - -/// Loads and constructs several handlers -class DLL_LINKAGE LibClasses : public Services -{ - CBonusTypeHandler * bth; - - void callWhenDeserializing(); //should be called only by serialize !!! - void makeNull(); //sets all handler pointers to null - std::shared_ptr getContent() const; - void setContent(std::shared_ptr content); - -public: - bool IS_AI_ENABLED = false; //unused? - - const ArtifactService * artifacts() const override; - const CreatureService * creatures() const override; - const FactionService * factions() const override; - const HeroClassService * heroClasses() const override; - const HeroTypeService * heroTypes() const override; -#if SCRIPTING_ENABLED - const scripting::Service * scripts() const override; -#endif - const spells::Service * spells() const override; - 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 spells::effects::Registry * spellEffects() const override; - spells::effects::Registry * spellEffects() override; - - const IBonusTypeHandler * getBth() const; //deprecated - - CArtHandler * arth; - CHeroHandler * heroh; - CCreatureHandler * creh; - CSpellHandler * spellh; - CSkillHandler * skillh; - CObjectHandler * objh; - CObjectClassesHandler * objtypeh; - CTownHandler * townh; - CGeneralTextHandler * generaltexth; - CModHandler * modh; - - TerrainTypeHandler * terrainTypeHandler; - RoadTypeHandler * roadTypeHandler; - RiverTypeHandler * riverTypeHandler; - - CTerrainViewPatternConfig * terviewh; - CRmgTemplateStorage * tplh; - BattleFieldHandler * battlefieldsHandler; - ObstacleHandler * obstacleHandler; - GameSettings * settingsHandler; -#if SCRIPTING_ENABLED - scripting::ScriptHandler * scriptHandler; -#endif - - LibClasses(); //c-tor, loads .lods and NULLs handlers - ~LibClasses(); - void init(bool onlyEssential); //uses standard config file - void clear(); //deletes all handlers and its data - - // basic initialization. should be called before init(). Can also extract original H3 archives - void loadFilesystem(bool extractArchives); - void loadModFilesystem(bool onlyEssential); - -#if SCRIPTING_ENABLED - void scriptsLoaded(); -#endif - - template void serialize(Handler &h, const int version) - { -#if SCRIPTING_ENABLED - h & scriptHandler;//must be first (or second after modh), it can modify factories other handlers depends on - if(!h.saving) - { - scriptsLoaded(); - } -#endif - - h & settingsHandler; - h & heroh; - h & arth; - h & creh; - h & townh; - h & objh; - h & objtypeh; - h & spellh; - h & skillh; - h & battlefieldsHandler; - h & obstacleHandler; - h & roadTypeHandler; - h & riverTypeHandler; - h & terrainTypeHandler; - - if(!h.saving) - { - //modh will be changed and modh->content will be empty after deserialization - auto content = getContent(); - h & modh; - setContent(content); - } - else - h & modh; - - h & IS_AI_ENABLED; - h & bth; - - if(!h.saving) - { - callWhenDeserializing(); - } - } -}; - -extern DLL_LINKAGE LibClasses * VLC; - -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); -DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); - - -VCMI_LIB_NAMESPACE_END +/* + * VCMI_Lib.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 CConsoleHandler; +class CArtHandler; +class CHeroHandler; +class CCreatureHandler; +class CSpellHandler; +class CSkillHandler; +class CBuildingHandler; +class CObjectHandler; +class CObjectClassesHandler; +class CTownHandler; +class CGeneralTextHandler; +class CModHandler; +class CContentHandler; +class BattleFieldHandler; +class IBonusTypeHandler; +class CBonusTypeHandler; +class TerrainTypeHandler; +class RoadTypeHandler; +class RiverTypeHandler; +class ObstacleHandler; +class CTerrainViewPatternConfig; +class CRmgTemplateStorage; +class IHandlerBase; +class IGameSettings; +class GameSettings; +class CIdentifierStorage; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class ScriptHandler; +} +#endif + + +/// Loads and constructs several handlers +class DLL_LINKAGE LibClasses : public Services +{ + CBonusTypeHandler * bth; + + void callWhenDeserializing(); //should be called only by serialize !!! + void makeNull(); //sets all handler pointers to null + std::shared_ptr getContent() const; + void setContent(std::shared_ptr content); + +public: + bool IS_AI_ENABLED = false; //unused? + + const ArtifactService * artifacts() const override; + const CreatureService * creatures() const override; + const FactionService * factions() const override; + const HeroClassService * heroClasses() const override; + const HeroTypeService * heroTypes() const override; +#if SCRIPTING_ENABLED + const scripting::Service * scripts() const override; +#endif + const spells::Service * spells() const override; + 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 spells::effects::Registry * spellEffects() const override; + spells::effects::Registry * spellEffects() override; + + const IBonusTypeHandler * getBth() const; //deprecated + const CIdentifierStorage * identifiers() const; + + CArtHandler * arth; + CHeroHandler * heroh; + CCreatureHandler * creh; + CSpellHandler * spellh; + CSkillHandler * skillh; + CObjectHandler * objh; + CObjectClassesHandler * objtypeh; + CTownHandler * townh; + CGeneralTextHandler * generaltexth; + CModHandler * modh; + + TerrainTypeHandler * terrainTypeHandler; + RoadTypeHandler * roadTypeHandler; + RiverTypeHandler * riverTypeHandler; + CIdentifierStorage * identifiersHandler; + + CTerrainViewPatternConfig * terviewh; + CRmgTemplateStorage * tplh; + BattleFieldHandler * battlefieldsHandler; + ObstacleHandler * obstacleHandler; + GameSettings * settingsHandler; +#if SCRIPTING_ENABLED + scripting::ScriptHandler * scriptHandler; +#endif + + LibClasses(); //c-tor, loads .lods and NULLs handlers + ~LibClasses(); + void init(bool onlyEssential); //uses standard config file + void clear(); //deletes all handlers and its data + + // basic initialization. should be called before init(). Can also extract original H3 archives + void loadFilesystem(bool extractArchives); + void loadModFilesystem(bool onlyEssential); + +#if SCRIPTING_ENABLED + void scriptsLoaded(); +#endif +}; + +extern DLL_LINKAGE LibClasses * VLC; + +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); +DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_lib.cbp b/lib/VCMI_lib.cbp index 2195ab5c5..31c3ccba1 100644 --- a/lib/VCMI_lib.cbp +++ b/lib/VCMI_lib.cbp @@ -127,6 +127,7 @@ + diff --git a/lib/VCMI_lib.vcxproj b/lib/VCMI_lib.vcxproj index 2598a5bc6..060a9d38b 100644 --- a/lib/VCMI_lib.vcxproj +++ b/lib/VCMI_lib.vcxproj @@ -320,10 +320,12 @@
    + + diff --git a/lib/VCMI_lib.vcxproj.filters b/lib/VCMI_lib.vcxproj.filters index a8541fe9d..08cd5df48 100644 --- a/lib/VCMI_lib.vcxproj.filters +++ b/lib/VCMI_lib.vcxproj.filters @@ -393,6 +393,9 @@ registerTypes + + vstd + vstd @@ -467,6 +470,9 @@ Header Files + + Header Files + Header Files diff --git a/lib/battle/AccessibilityInfo.cpp b/lib/battle/AccessibilityInfo.cpp index 8d5f8067d..a7a29dae5 100644 --- a/lib/battle/AccessibilityInfo.cpp +++ b/lib/battle/AccessibilityInfo.cpp @@ -1,53 +1,53 @@ -/* - * AccessibilityInfo.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 "AccessibilityInfo.h" -#include "BattleHex.h" -#include "Unit.h" -#include "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, ui8 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) - return false; - return true; -} - -bool AccessibilityInfo::accessible(BattleHex tile, const battle::Unit * stack) const -{ - return accessible(tile, stack->doubleWide(), stack->unitSide()); -} - -bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, ui8 side) const -{ - // All hexes that stack would cover if standing on tile have to be accessible. - //do not use getHexes for speed reasons - if(!tile.isValid()) - return false; - if(!tileAccessibleWithGate(tile, side)) - return false; - - if(doubleWide) - { - auto otherHex = battle::Unit::occupiedHex(tile, doubleWide, side); - if(!otherHex.isValid()) - return false; - if(!tileAccessibleWithGate(otherHex, side)) - return false; - } - - return true; -} - -VCMI_LIB_NAMESPACE_END +/* + * AccessibilityInfo.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 "AccessibilityInfo.h" +#include "BattleHex.h" +#include "Unit.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, ui8 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) + return false; + return true; +} + +bool AccessibilityInfo::accessible(BattleHex tile, const battle::Unit * stack) const +{ + return accessible(tile, stack->doubleWide(), stack->unitSide()); +} + +bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, ui8 side) const +{ + // All hexes that stack would cover if standing on tile have to be accessible. + //do not use getHexes for speed reasons + if(!tile.isValid()) + return false; + if(!tileAccessibleWithGate(tile, side)) + return false; + + if(doubleWide) + { + auto otherHex = battle::Unit::occupiedHex(tile, doubleWide, side); + if(!otherHex.isValid()) + return false; + if(!tileAccessibleWithGate(otherHex, side)) + return false; + } + + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/AccessibilityInfo.h b/lib/battle/AccessibilityInfo.h index c84c1346d..dc3648412 100644 --- a/lib/battle/AccessibilityInfo.h +++ b/lib/battle/AccessibilityInfo.h @@ -1,45 +1,45 @@ -/* - * AccessibilityInfo.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 "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace battle -{ - class Unit; -} - -//Accessibility is property of hex in battle. It doesn't depend on stack, side's perspective and so on. -enum class EAccessibility -{ - ACCESSIBLE, - ALIVE_STACK, - OBSTACLE, - DESTRUCTIBLE_WALL, - GATE, //sieges -> gate opens only for defender stacks - UNAVAILABLE, //indestructible wall parts, special battlefields (like boat-to-boat) - SIDE_COLUMN //used for first and last columns of hexes that are unavailable but wat machines can stand there -}; - - -using TAccessibilityArray = std::array; - -struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray -{ - 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 - private: - bool tileAccessibleWithGate(BattleHex tile, ui8 side) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * AccessibilityInfo.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 "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace battle +{ + class Unit; +} + +//Accessibility is property of hex in battle. It doesn't depend on stack, side's perspective and so on. +enum class EAccessibility +{ + ACCESSIBLE, + ALIVE_STACK, + OBSTACLE, + DESTRUCTIBLE_WALL, + GATE, //sieges -> gate opens only for defender stacks + UNAVAILABLE, //indestructible wall parts, special battlefields (like boat-to-boat) + SIDE_COLUMN //used for first and last columns of hexes that are unavailable but wat machines can stand there +}; + + +using TAccessibilityArray = std::array; + +struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray +{ + 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 + private: + bool tileAccessibleWithGate(BattleHex tile, ui8 side) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index c0d1f8d9c..e021cc1ba 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -1,203 +1,244 @@ -/* - * BattleAction.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 "BattleAction.h" -#include "Unit.h" -#include "CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static const int32_t INVALID_UNIT_ID = -1000; - -BattleAction::BattleAction(): - side(-1), - stackNumber(-1), - actionType(EActionType::INVALID), - actionSubtype(-1) -{ -} - -BattleAction BattleAction::makeHeal(const battle::Unit * healer, const battle::Unit * healed) -{ - BattleAction ba; - ba.side = healer->unitSide(); - ba.actionType = EActionType::STACK_HEAL; - ba.stackNumber = healer->unitId(); - ba.aimToUnit(healed); - return ba; -} - -BattleAction BattleAction::makeDefend(const battle::Unit * stack) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::DEFEND; - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack) -{ - BattleAction ba; - ba.side = stack->unitSide(); //FIXME: will it fail if stack mind controlled? - ba.actionType = EActionType::WALK_AND_ATTACK; - ba.stackNumber = stack->unitId(); - ba.aimToHex(attackFrom); - ba.aimToHex(destination); - if(returnAfterAttack && stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) - ba.aimToHex(stack->getPosition()); - return ba; -} - -BattleAction BattleAction::makeWait(const battle::Unit * stack) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::WAIT; - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeShotAttack(const battle::Unit * shooter, const battle::Unit * target) -{ - BattleAction ba; - ba.side = shooter->unitSide(); - ba.actionType = EActionType::SHOOT; - ba.stackNumber = shooter->unitId(); - ba.aimToUnit(target); - return ba; -} - -BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID) -{ - BattleAction ba; - ba.actionType = EActionType::MONSTER_SPELL; - ba.actionSubtype = spellID; - ba.setTarget(target); - ba.side = stack->unitSide(); - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::WALK; - ba.stackNumber = stack->unitId(); - ba.aimToHex(dest); - return ba; -} - -BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::END_TACTIC_PHASE; - return ba; -} - -BattleAction BattleAction::makeSurrender(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::SURRENDER; - return ba; -} - -BattleAction BattleAction::makeRetreat(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::RETREAT; - return ba; -} - -std::string BattleAction::toString() const -{ - std::stringstream actionTypeStream; - actionTypeStream << actionType; - - std::stringstream targetStream; - - for(const DestinationInfo & info : target) - { - if(info.unitValue == INVALID_UNIT_ID) - { - targetStream << info.hexValue; - } - else - { - targetStream << info.unitValue; - targetStream << "@"; - targetStream << info.hexValue; - } - targetStream << ","; - } - - boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); - fmt % static_cast(side) % stackNumber % actionTypeStream.str() % actionSubtype % targetStream.str(); - return fmt.str(); -} - -void BattleAction::aimToHex(const BattleHex & destination) -{ - DestinationInfo info; - info.hexValue = destination; - info.unitValue = INVALID_UNIT_ID; - - target.push_back(info); -} - -void BattleAction::aimToUnit(const battle::Unit * destination) -{ - DestinationInfo info; - info.hexValue = destination->getPosition(); - info.unitValue = destination->unitId(); - - target.push_back(info); -} - -battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const -{ - battle::Target ret; - - for(const auto & destination : target) - { - if(destination.unitValue == INVALID_UNIT_ID) - ret.emplace_back(destination.hexValue); - else - ret.emplace_back(cb->battleGetUnitByID(destination.unitValue)); - } - - return ret; -} - -void BattleAction::setTarget(const battle::Target & target_) -{ - target.clear(); - for(const auto & destination : target_) - { - if(destination.unitValue == nullptr) - aimToHex(destination.hexValue); - else - aimToUnit(destination.unitValue); - } -} - - -std::ostream & operator<<(std::ostream & os, const BattleAction & ba) -{ - os << ba.toString(); - return os; -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleAction.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 "BattleAction.h" +#include "Unit.h" +#include "CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static const int32_t INVALID_UNIT_ID = -1000; + +BattleAction::BattleAction(): + side(-1), + stackNumber(-1), + actionType(EActionType::NO_ACTION) +{ +} + +BattleAction BattleAction::makeHeal(const battle::Unit * healer, const battle::Unit * healed) +{ + BattleAction ba; + ba.side = healer->unitSide(); + ba.actionType = EActionType::STACK_HEAL; + ba.stackNumber = healer->unitId(); + ba.aimToUnit(healed); + return ba; +} + +BattleAction BattleAction::makeDefend(const battle::Unit * stack) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::DEFEND; + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack) +{ + BattleAction ba; + ba.side = stack->unitSide(); //FIXME: will it fail if stack mind controlled? + ba.actionType = EActionType::WALK_AND_ATTACK; + ba.stackNumber = stack->unitId(); + ba.aimToHex(attackFrom); + ba.aimToHex(destination); + if(returnAfterAttack && stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) + ba.aimToHex(stack->getPosition()); + return ba; +} + +BattleAction BattleAction::makeWait(const battle::Unit * stack) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::WAIT; + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeShotAttack(const battle::Unit * shooter, const battle::Unit * target) +{ + BattleAction ba; + ba.side = shooter->unitSide(); + ba.actionType = EActionType::SHOOT; + ba.stackNumber = shooter->unitId(); + ba.aimToUnit(target); + return ba; +} + +BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID) +{ + BattleAction ba; + ba.actionType = EActionType::MONSTER_SPELL; + ba.spell = spellID; + ba.setTarget(target); + ba.side = stack->unitSide(); + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::WALK; + ba.stackNumber = stack->unitId(); + ba.aimToHex(dest); + return ba; +} + +BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::END_TACTIC_PHASE; + return ba; +} + +BattleAction BattleAction::makeSurrender(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::SURRENDER; + return ba; +} + +BattleAction BattleAction::makeRetreat(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::RETREAT; + return ba; +} + +std::string BattleAction::toString() const +{ + std::stringstream targetStream; + + for(const DestinationInfo & info : target) + { + if(info.unitValue == INVALID_UNIT_ID) + { + targetStream << info.hexValue; + } + else + { + targetStream << info.unitValue; + targetStream << "@"; + targetStream << info.hexValue; + } + targetStream << ","; + } + + boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); + fmt % static_cast(side) % stackNumber % static_cast(actionType) % spell.getNum() % targetStream.str(); + return fmt.str(); +} + +void BattleAction::aimToHex(const BattleHex & destination) +{ + DestinationInfo info; + info.hexValue = destination; + info.unitValue = INVALID_UNIT_ID; + + target.push_back(info); +} + +void BattleAction::aimToUnit(const battle::Unit * destination) +{ + DestinationInfo info; + info.hexValue = destination->getPosition(); + info.unitValue = destination->unitId(); + + target.push_back(info); +} + +battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const +{ + battle::Target ret; + + for(const auto & destination : target) + { + if(destination.unitValue == INVALID_UNIT_ID) + ret.emplace_back(destination.hexValue); + else + ret.emplace_back(cb->battleGetUnitByID(destination.unitValue)); + } + + return ret; +} + +void BattleAction::setTarget(const battle::Target & target_) +{ + target.clear(); + for(const auto & destination : target_) + { + if(destination.unitValue == nullptr) + aimToHex(destination.hexValue); + else + aimToUnit(destination.unitValue); + } +} + +bool BattleAction::isUnitAction() const +{ + static const std::array actions = { + EActionType::NO_ACTION, + EActionType::WALK, + EActionType::WAIT, + EActionType::DEFEND, + EActionType::WALK_AND_ATTACK, + EActionType::SHOOT, + EActionType::CATAPULT, + EActionType::MONSTER_SPELL, + EActionType::BAD_MORALE, + EActionType::STACK_HEAL + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isSpellAction() const +{ + static const std::array actions = { + EActionType::HERO_SPELL, + EActionType::MONSTER_SPELL + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isBattleEndAction() const +{ + static const std::array actions = { + EActionType::RETREAT, + EActionType::SURRENDER + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isTacticsAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::END_TACTIC_PHASE, + EActionType::RETREAT, + EActionType::SURRENDER + }; + return vstd::contains(actions, actionType); +} + +std::ostream & operator<<(std::ostream & os, const BattleAction & ba) +{ + os << ba.toString(); + return os; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index db65ac37e..8fc5d0bfc 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -1,81 +1,85 @@ -/* - * BattleAction.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 "Destination.h" -#include "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBattleInfoCallback; - -namespace battle -{ - class Unit; -} - -/// A struct which handles battle actions like defending, walking,... - represents a creature stack in a battle -class DLL_LINKAGE BattleAction -{ -public: - ui8 side; //who made this action - ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, - EActionType actionType; //use ActionType enum for values - - si32 actionSubtype; - - BattleAction(); - - static BattleAction makeHeal(const battle::Unit * healer, const battle::Unit * healed); - static BattleAction makeDefend(const battle::Unit * stack); - static BattleAction makeWait(const battle::Unit * stack); - static BattleAction makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack = true); - 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); - - std::string toString() const; - - void aimToHex(const BattleHex & destination); - void aimToUnit(const battle::Unit * destination); - - battle::Target getTarget(const CBattleInfoCallback * cb) const; - void setTarget(const battle::Target & target_); - - template void serialize(Handler & h, const int version) - { - h & side; - h & stackNumber; - h & actionType; - h & actionSubtype; - h & target; - } -private: - - struct DestinationInfo - { - int32_t unitValue; - BattleHex hexValue; - - template void serialize(Handler & h, const int version) - { - h & unitValue; - h & hexValue; - } - }; - - std::vector target; -}; - -DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleAction & ba); //todo: remove - -VCMI_LIB_NAMESPACE_END +/* + * BattleAction.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 "Destination.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBattleInfoCallback; + +namespace battle +{ + class Unit; +} + +/// A struct which handles battle actions like defending, walking,... - represents a creature stack in a battle +class DLL_LINKAGE BattleAction +{ +public: + ui8 side; //who made this action + ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, + EActionType actionType; //use ActionType enum for values + + SpellID spell; + + BattleAction(); + + static BattleAction makeHeal(const battle::Unit * healer, const battle::Unit * healed); + static BattleAction makeDefend(const battle::Unit * stack); + static BattleAction makeWait(const battle::Unit * stack); + static BattleAction makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack = true); + 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); + + bool isTacticsAction() const; + bool isUnitAction() const; + bool isSpellAction() const; + bool isBattleEndAction() const; + std::string toString() const; + + void aimToHex(const BattleHex & destination); + void aimToUnit(const battle::Unit * destination); + + battle::Target getTarget(const CBattleInfoCallback * cb) const; + void setTarget(const battle::Target & target_); + + template void serialize(Handler & h, const int version) + { + h & side; + h & stackNumber; + h & actionType; + h & spell; + h & target; + } +private: + + struct DestinationInfo + { + int32_t unitValue; + BattleHex hexValue; + + template void serialize(Handler & h, const int version) + { + h & unitValue; + h & hexValue; + } + }; + + std::vector target; +}; + +DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleAction & ba); //todo: remove + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAttackInfo.cpp b/lib/battle/BattleAttackInfo.cpp index 2746dc7cd..1e887b53d 100644 --- a/lib/battle/BattleAttackInfo.cpp +++ b/lib/battle/BattleAttackInfo.cpp @@ -1,34 +1,34 @@ -/* - * BattleAttackInfo.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 "BattleAttackInfo.h" -#include "CUnitState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BattleAttackInfo::BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting) - : attacker(Attacker), - defender(Defender), - shooting(Shooting), - attackerPos(BattleHex::INVALID), - defenderPos(BattleHex::INVALID), - chargeDistance(chargeDistance) -{} - -BattleAttackInfo BattleAttackInfo::reverse() const -{ - BattleAttackInfo ret(defender, attacker, 0, false); - - ret.defenderPos = attackerPos; - ret.attackerPos = defenderPos; - return ret; -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleAttackInfo.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 "BattleAttackInfo.h" +#include "CUnitState.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleAttackInfo::BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting) + : attacker(Attacker), + defender(Defender), + shooting(Shooting), + attackerPos(BattleHex::INVALID), + defenderPos(BattleHex::INVALID), + chargeDistance(chargeDistance) +{} + +BattleAttackInfo BattleAttackInfo::reverse() const +{ + BattleAttackInfo ret(defender, attacker, 0, false); + + ret.defenderPos = attackerPos; + ret.attackerPos = defenderPos; + return ret; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAttackInfo.h b/lib/battle/BattleAttackInfo.h index d221581fb..eef4f40c3 100644 --- a/lib/battle/BattleAttackInfo.h +++ b/lib/battle/BattleAttackInfo.h @@ -1,41 +1,41 @@ -/* - * BattleAttackInfo.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" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace battle -{ - class Unit; - class CUnitState; -} - -struct DLL_LINKAGE BattleAttackInfo -{ - const battle::Unit * attacker; - const battle::Unit * defender; - - BattleHex attackerPos; - BattleHex defenderPos; - - int chargeDistance = 0; - bool shooting = false; - bool luckyStrike = false; - bool unluckyStrike = false; - bool deathBlow = false; - bool doubleDamage = false; - - BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting); - BattleAttackInfo reverse() const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * BattleAttackInfo.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" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace battle +{ + class Unit; + class CUnitState; +} + +struct DLL_LINKAGE BattleAttackInfo +{ + const battle::Unit * attacker; + const battle::Unit * defender; + + BattleHex attackerPos; + BattleHex defenderPos; + + int chargeDistance = 0; + bool shooting = false; + bool luckyStrike = false; + bool unluckyStrike = false; + bool deathBlow = false; + bool doubleDamage = false; + + BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting); + BattleAttackInfo reverse() const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleHex.cpp b/lib/battle/BattleHex.cpp index 4c934f79c..f6eaf319f 100644 --- a/lib/battle/BattleHex.cpp +++ b/lib/battle/BattleHex.cpp @@ -1,245 +1,245 @@ -/* - * BattleHex.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 "BattleHex.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BattleHex::BattleHex() : hex(INVALID) {} - -BattleHex::BattleHex(si16 _hex) : hex(_hex) {} - -BattleHex::BattleHex(si16 x, si16 y) -{ - setXY(x, y); -} - -BattleHex::BattleHex(std::pair xy) -{ - setXY(xy); -} - -BattleHex::operator si16() const -{ - return hex; -} - -bool BattleHex::isValid() const -{ - return hex >= 0 && hex < GameConstants::BFIELD_SIZE; -} - -bool BattleHex::isAvailable() const -{ - return isValid() && getX() > 0 && getX() < GameConstants::BFIELD_WIDTH-1; -} - -void BattleHex::setX(si16 x) -{ - setXY(x, getY()); -} - -void BattleHex::setY(si16 y) -{ - setXY(getX(), y); -} - -void BattleHex::setXY(si16 x, si16 y, bool hasToBeValid) -{ - if(hasToBeValid) - { - if(x < 0 || x >= GameConstants::BFIELD_WIDTH || y < 0 || y >= GameConstants::BFIELD_HEIGHT) - throw std::runtime_error("Valid hex required"); - } - - hex = x + y * GameConstants::BFIELD_WIDTH; -} - -void BattleHex::setXY(std::pair xy) -{ - setXY(xy.first, xy.second); -} - -si16 BattleHex::getX() const -{ - return hex % GameConstants::BFIELD_WIDTH; -} - -si16 BattleHex::getY() const -{ - return hex / GameConstants::BFIELD_WIDTH; -} - -std::pair BattleHex::getXY() const -{ - return std::make_pair(getX(), getY()); -} - -BattleHex& BattleHex::moveInDirection(EDir dir, bool hasToBeValid) -{ - si16 x = getX(); - si16 y = getY(); - switch(dir) - { - case TOP_LEFT: - setXY((y%2) ? x-1 : x, y-1, hasToBeValid); - break; - case TOP_RIGHT: - setXY((y%2) ? x : x+1, y-1, hasToBeValid); - break; - case RIGHT: - setXY(x+1, y, hasToBeValid); - break; - case BOTTOM_RIGHT: - setXY((y%2) ? x : x+1, y+1, hasToBeValid); - break; - case BOTTOM_LEFT: - setXY((y%2) ? x-1 : x, y+1, hasToBeValid); - break; - case LEFT: - setXY(x-1, y, hasToBeValid); - break; - case NONE: - break; - default: - throw std::runtime_error("Disaster: wrong direction in BattleHex::operator+=!\n"); - break; - } - return *this; -} - -BattleHex &BattleHex::operator+=(BattleHex::EDir dir) -{ - return moveInDirection(dir); -} - -BattleHex BattleHex::cloneInDirection(BattleHex::EDir dir, bool hasToBeValid) const -{ - BattleHex result(hex); - result.moveInDirection(dir, hasToBeValid); - return result; -} - -BattleHex BattleHex::operator+(BattleHex::EDir dir) const -{ - return cloneInDirection(dir); -} - -std::vector BattleHex::neighbouringTiles() const -{ - std::vector ret; - ret.reserve(6); - for(auto dir : hexagonalDirections()) - checkAndPush(cloneInDirection(dir, false), ret); - return ret; -} - -std::vector BattleHex::allNeighbouringTiles() const -{ - std::vector ret; - ret.resize(6); - - for(auto dir : hexagonalDirections()) - ret[dir] = cloneInDirection(dir, false); - - return ret; -} - -BattleHex::EDir BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) -{ - for(auto dir : hexagonalDirections()) - if(hex2 == hex1.cloneInDirection(dir, false)) - return dir; - return NONE; -} - -uint8_t BattleHex::getDistance(BattleHex hex1, BattleHex hex2) -{ - int y1 = hex1.getY(); - int y2 = hex2.getY(); - - // FIXME: why there was * 0.5 instead of / 2? - int x1 = static_cast(hex1.getX() + y1 / 2); - int x2 = static_cast(hex2.getX() + y2 / 2); - - int xDst = x2 - x1; - int yDst = y2 - y1; - - if ((xDst >= 0 && yDst >= 0) || (xDst < 0 && yDst < 0)) - return std::max(std::abs(xDst), std::abs(yDst)); - - return std::abs(xDst) + std::abs(yDst); -} - -void BattleHex::checkAndPush(BattleHex tile, std::vector & ret) -{ - if(tile.isAvailable()) - ret.push_back(tile); -} - -BattleHex BattleHex::getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities) -{ - std::vector sortedTiles (possibilities.begin(), possibilities.end()); //set can't be sorted properly :( - BattleHex initialHex = BattleHex(initialPos); - auto compareDistance = [initialHex](const BattleHex left, const BattleHex right) -> bool - { - return initialHex.getDistance (initialHex, left) < initialHex.getDistance (initialHex, right); - }; - boost::sort (sortedTiles, compareDistance); //closest tiles at front - int closestDistance = initialHex.getDistance(initialPos, sortedTiles.front()); //sometimes closest tiles can be many hexes away - auto notClosest = [closestDistance, initialPos](const BattleHex here) -> bool - { - return closestDistance < here.getDistance (initialPos, here); - }; - vstd::erase_if(sortedTiles, notClosest); //only closest tiles are interesting - auto compareHorizontal = [side, initialPos](const BattleHex left, const BattleHex right) -> bool - { - if(left.getX() != right.getX()) - { - if(side == BattleSide::ATTACKER) - return left.getX() > right.getX(); //find furthest right - else - return left.getX() < right.getX(); //find furthest left - } - else - { - //Prefer tiles in the same row. - return std::abs(left.getY() - initialPos.getY()) < std::abs(right.getY() - initialPos.getY()); - } - }; - boost::sort (sortedTiles, compareHorizontal); - return sortedTiles.front(); -} - -std::ostream & operator<<(std::ostream & os, const BattleHex & hex) -{ - return os << boost::str(boost::format("{BattleHex: x '%d', y '%d', hex '%d'}") % hex.getX() % hex.getY() % hex.hex); -} - -static BattleHex::NeighbouringTilesCache calculateNeighbouringTiles() -{ - BattleHex::NeighbouringTilesCache ret; - ret.resize(GameConstants::BFIELD_SIZE); - - for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) - { - auto hexes = BattleHex(hex).neighbouringTiles(); - - size_t index = 0; - for(auto neighbour : hexes) - ret[hex].at(index++) = neighbour; - } - - return ret; -} - -const BattleHex::NeighbouringTilesCache BattleHex::neighbouringTilesCache = calculateNeighbouringTiles(); - -VCMI_LIB_NAMESPACE_END +/* + * BattleHex.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 "BattleHex.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleHex::BattleHex() : hex(INVALID) {} + +BattleHex::BattleHex(si16 _hex) : hex(_hex) {} + +BattleHex::BattleHex(si16 x, si16 y) +{ + setXY(x, y); +} + +BattleHex::BattleHex(std::pair xy) +{ + setXY(xy); +} + +BattleHex::operator si16() const +{ + return hex; +} + +bool BattleHex::isValid() const +{ + return hex >= 0 && hex < GameConstants::BFIELD_SIZE; +} + +bool BattleHex::isAvailable() const +{ + return isValid() && getX() > 0 && getX() < GameConstants::BFIELD_WIDTH-1; +} + +void BattleHex::setX(si16 x) +{ + setXY(x, getY()); +} + +void BattleHex::setY(si16 y) +{ + setXY(getX(), y); +} + +void BattleHex::setXY(si16 x, si16 y, bool hasToBeValid) +{ + if(hasToBeValid) + { + if(x < 0 || x >= GameConstants::BFIELD_WIDTH || y < 0 || y >= GameConstants::BFIELD_HEIGHT) + throw std::runtime_error("Valid hex required"); + } + + hex = x + y * GameConstants::BFIELD_WIDTH; +} + +void BattleHex::setXY(std::pair xy) +{ + setXY(xy.first, xy.second); +} + +si16 BattleHex::getX() const +{ + return hex % GameConstants::BFIELD_WIDTH; +} + +si16 BattleHex::getY() const +{ + return hex / GameConstants::BFIELD_WIDTH; +} + +std::pair BattleHex::getXY() const +{ + return std::make_pair(getX(), getY()); +} + +BattleHex& BattleHex::moveInDirection(EDir dir, bool hasToBeValid) +{ + si16 x = getX(); + si16 y = getY(); + switch(dir) + { + case TOP_LEFT: + setXY((y%2) ? x-1 : x, y-1, hasToBeValid); + break; + case TOP_RIGHT: + setXY((y%2) ? x : x+1, y-1, hasToBeValid); + break; + case RIGHT: + setXY(x+1, y, hasToBeValid); + break; + case BOTTOM_RIGHT: + setXY((y%2) ? x : x+1, y+1, hasToBeValid); + break; + case BOTTOM_LEFT: + setXY((y%2) ? x-1 : x, y+1, hasToBeValid); + break; + case LEFT: + setXY(x-1, y, hasToBeValid); + break; + case NONE: + break; + default: + throw std::runtime_error("Disaster: wrong direction in BattleHex::operator+=!\n"); + break; + } + return *this; +} + +BattleHex &BattleHex::operator+=(BattleHex::EDir dir) +{ + return moveInDirection(dir); +} + +BattleHex BattleHex::cloneInDirection(BattleHex::EDir dir, bool hasToBeValid) const +{ + BattleHex result(hex); + result.moveInDirection(dir, hasToBeValid); + return result; +} + +BattleHex BattleHex::operator+(BattleHex::EDir dir) const +{ + return cloneInDirection(dir); +} + +std::vector BattleHex::neighbouringTiles() const +{ + std::vector ret; + ret.reserve(6); + for(auto dir : hexagonalDirections()) + checkAndPush(cloneInDirection(dir, false), ret); + return ret; +} + +std::vector BattleHex::allNeighbouringTiles() const +{ + std::vector ret; + ret.resize(6); + + for(auto dir : hexagonalDirections()) + ret[dir] = cloneInDirection(dir, false); + + return ret; +} + +BattleHex::EDir BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) +{ + for(auto dir : hexagonalDirections()) + if(hex2 == hex1.cloneInDirection(dir, false)) + return dir; + return NONE; +} + +uint8_t BattleHex::getDistance(BattleHex hex1, BattleHex hex2) +{ + int y1 = hex1.getY(); + int y2 = hex2.getY(); + + // FIXME: why there was * 0.5 instead of / 2? + int x1 = static_cast(hex1.getX() + y1 / 2); + int x2 = static_cast(hex2.getX() + y2 / 2); + + int xDst = x2 - x1; + int yDst = y2 - y1; + + if ((xDst >= 0 && yDst >= 0) || (xDst < 0 && yDst < 0)) + return std::max(std::abs(xDst), std::abs(yDst)); + + return std::abs(xDst) + std::abs(yDst); +} + +void BattleHex::checkAndPush(BattleHex tile, std::vector & ret) +{ + if(tile.isAvailable()) + ret.push_back(tile); +} + +BattleHex BattleHex::getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities) +{ + std::vector sortedTiles (possibilities.begin(), possibilities.end()); //set can't be sorted properly :( + BattleHex initialHex = BattleHex(initialPos); + auto compareDistance = [initialHex](const BattleHex left, const BattleHex right) -> bool + { + return initialHex.getDistance (initialHex, left) < initialHex.getDistance (initialHex, right); + }; + boost::sort (sortedTiles, compareDistance); //closest tiles at front + int closestDistance = initialHex.getDistance(initialPos, sortedTiles.front()); //sometimes closest tiles can be many hexes away + auto notClosest = [closestDistance, initialPos](const BattleHex here) -> bool + { + return closestDistance < here.getDistance (initialPos, here); + }; + vstd::erase_if(sortedTiles, notClosest); //only closest tiles are interesting + auto compareHorizontal = [side, initialPos](const BattleHex left, const BattleHex right) -> bool + { + if(left.getX() != right.getX()) + { + if(side == BattleSide::ATTACKER) + return left.getX() > right.getX(); //find furthest right + else + return left.getX() < right.getX(); //find furthest left + } + else + { + //Prefer tiles in the same row. + return std::abs(left.getY() - initialPos.getY()) < std::abs(right.getY() - initialPos.getY()); + } + }; + boost::sort (sortedTiles, compareHorizontal); + return sortedTiles.front(); +} + +std::ostream & operator<<(std::ostream & os, const BattleHex & hex) +{ + return os << boost::str(boost::format("{BattleHex: x '%d', y '%d', hex '%d'}") % hex.getX() % hex.getY() % hex.hex); +} + +static BattleHex::NeighbouringTilesCache calculateNeighbouringTiles() +{ + BattleHex::NeighbouringTilesCache ret; + ret.resize(GameConstants::BFIELD_SIZE); + + for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) + { + auto hexes = BattleHex(hex).neighbouringTiles(); + + size_t index = 0; + for(auto neighbour : hexes) + ret[hex].at(index++) = neighbour; + } + + return ret; +} + +const BattleHex::NeighbouringTilesCache BattleHex::neighbouringTilesCache = calculateNeighbouringTiles(); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 060962780..69de2b811 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -1,118 +1,126 @@ -/* - * BattleHex.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 - -//TODO: change to enum class - -namespace BattleSide -{ - enum Type - { - ATTACKER = 0, - DEFENDER = 1 - }; -} - -namespace GameConstants -{ - const int BFIELD_WIDTH = 17; - const int BFIELD_HEIGHT = 11; - 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 -{ - // helpers for siege - static constexpr si16 CASTLE_CENTRAL_TOWER = -2; - static constexpr si16 CASTLE_BOTTOM_TOWER = -3; - static constexpr si16 CASTLE_UPPER_TOWER = -4; - - // hexes for interaction with heroes - static constexpr si16 HERO_ATTACKER = 0; - static constexpr si16 HERO_DEFENDER = GameConstants::BFIELD_WIDTH - 1; - - // helpers for rendering - static constexpr si16 HEX_BEFORE_ALL = std::numeric_limits::min(); - static constexpr si16 HEX_AFTER_ALL = std::numeric_limits::max(); - - si16 hex; - static constexpr si16 INVALID = -1; - enum EDir - { - NONE = -1, - - TOP_LEFT, - TOP_RIGHT, - RIGHT, - BOTTOM_RIGHT, - BOTTOM_LEFT, - LEFT, - - //Note: unused by BattleHex class, used by other code - TOP, - BOTTOM - }; - - BattleHex(); - BattleHex(si16 _hex); - BattleHex(si16 x, si16 y); - BattleHex(std::pair xy); - operator si16() const; - bool isValid() const; - bool isAvailable() const; //valid position not in first or last column - void setX(si16 x); - void setY(si16 y); - void setXY(si16 x, si16 y, bool hasToBeValid = true); - void setXY(std::pair xy); - si16 getX() const; - si16 getY() const; - std::pair getXY() const; - BattleHex& moveInDirection(EDir dir, bool hasToBeValid = true); - BattleHex& operator+=(EDir dir); - BattleHex cloneInDirection(EDir dir, bool hasToBeValid = true) const; - BattleHex operator+(EDir dir) const; - - /// returns all valid neighbouring tiles - std::vector neighbouringTiles() const; - - /// returns all tiles, unavailable tiles will be set as invalid - /// order of returned tiles matches EDir enim - std::vector allNeighbouringTiles() const; - - 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 - - template - void serialize(Handler &h, const int version) - { - h & hex; - } - - using NeighbouringTiles = std::array; - using NeighbouringTilesCache = std::vector; - - static const NeighbouringTilesCache neighbouringTilesCache; -private: - //Constexpr defined array with all directions used in battle - static constexpr auto hexagonalDirections() { - return std::array{BattleHex::TOP_LEFT, BattleHex::TOP_RIGHT, BattleHex::RIGHT, BattleHex::BOTTOM_RIGHT, BattleHex::BOTTOM_LEFT, BattleHex::LEFT}; - } -}; - -DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleHex & hex); - -VCMI_LIB_NAMESPACE_END +/* + * BattleHex.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 + +//TODO: change to enum class + +namespace BattleSide +{ + enum Type + { + ATTACKER = 0, + DEFENDER = 1 + }; +} + +namespace GameConstants +{ + const int BFIELD_WIDTH = 17; + const int BFIELD_HEIGHT = 11; + 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 +{ + // helpers for siege + static constexpr si16 CASTLE_CENTRAL_TOWER = -2; + static constexpr si16 CASTLE_BOTTOM_TOWER = -3; + static constexpr si16 CASTLE_UPPER_TOWER = -4; + + // hexes for interaction with heroes + static constexpr si16 HERO_ATTACKER = 0; + static constexpr si16 HERO_DEFENDER = GameConstants::BFIELD_WIDTH - 1; + + // helpers for rendering + static constexpr si16 HEX_BEFORE_ALL = std::numeric_limits::min(); + static constexpr si16 HEX_AFTER_ALL = std::numeric_limits::max(); + + static constexpr si16 DESTRUCTIBLE_WALL_1 = 29; + static constexpr si16 DESTRUCTIBLE_WALL_2 = 78; + static constexpr si16 DESTRUCTIBLE_WALL_3 = 130; + static constexpr si16 DESTRUCTIBLE_WALL_4 = 182; + static constexpr si16 GATE_BRIDGE = 94; + static constexpr si16 GATE_OUTER = 95; + static constexpr si16 GATE_INNER = 96; + + si16 hex; + static constexpr si16 INVALID = -1; + enum EDir + { + NONE = -1, + + TOP_LEFT, + TOP_RIGHT, + RIGHT, + BOTTOM_RIGHT, + BOTTOM_LEFT, + LEFT, + + //Note: unused by BattleHex class, used by other code + TOP, + BOTTOM + }; + + BattleHex(); + BattleHex(si16 _hex); + BattleHex(si16 x, si16 y); + BattleHex(std::pair xy); + operator si16() const; + bool isValid() const; + bool isAvailable() const; //valid position not in first or last column + void setX(si16 x); + void setY(si16 y); + void setXY(si16 x, si16 y, bool hasToBeValid = true); + void setXY(std::pair xy); + si16 getX() const; + si16 getY() const; + std::pair getXY() const; + BattleHex& moveInDirection(EDir dir, bool hasToBeValid = true); + BattleHex& operator+=(EDir dir); + BattleHex cloneInDirection(EDir dir, bool hasToBeValid = true) const; + BattleHex operator+(EDir dir) const; + + /// returns all valid neighbouring tiles + std::vector neighbouringTiles() const; + + /// returns all tiles, unavailable tiles will be set as invalid + /// order of returned tiles matches EDir enim + std::vector allNeighbouringTiles() const; + + 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 + + template + void serialize(Handler &h, const int version) + { + h & hex; + } + + using NeighbouringTiles = std::array; + using NeighbouringTilesCache = std::vector; + + static const NeighbouringTilesCache neighbouringTilesCache; +private: + //Constexpr defined array with all directions used in battle + static constexpr auto hexagonalDirections() { + return std::array{BattleHex::TOP_LEFT, BattleHex::TOP_RIGHT, BattleHex::RIGHT, BattleHex::BOTTOM_RIGHT, BattleHex::BOTTOM_LEFT, BattleHex::LEFT}; + } +}; + +DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleHex & hex); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index b2d5cd3a8..6db67eab8 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -1,1056 +1,1054 @@ -/* - * BattleInfo.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 "BattleInfo.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "../CStack.h" -#include "../CHeroHandler.h" -#include "../NetPacks.h" -#include "../filesystem/Filesystem.h" -#include "../mapObjects/CGTownInstance.h" -#include "../CGeneralTextHandler.h" -#include "../BattleFieldHandler.h" -#include "../ObstacleHandler.h" - -//TODO: remove -#include "../IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -///BattleInfo -std::pair< std::vector, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) -{ - auto reachability = getReachability(stack); - - if(reachability.predecessors[dest] == -1) //cannot reach destination - { - return std::make_pair(std::vector(), 0); - } - - //making the Path - std::vector path; - BattleHex curElem = dest; - while(curElem != start) - { - path.push_back(curElem); - curElem = reachability.predecessors[curElem]; - } - - return std::make_pair(path, reachability.distances[dest]); -} - -void BattleInfo::calculateCasualties(std::map * casualties) const -{ - for(const auto & st : stacks) //setting casualties - { - si32 killed = st->getKilled(); - if(killed > 0) - casualties[st->unitSide()][st->creatureId()] += killed; - } -} - -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) -{ - PlayerColor owner = sides[side].color; - assert((owner >= PlayerColor::PLAYER_LIMIT) || - (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? - stacks.push_back(ret); - return ret; -} - -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position) -{ - PlayerColor owner = sides[side].color; - auto * ret = new CStack(&base, owner, id, side, slot); - ret->initialPosition = position; - stacks.push_back(ret); - return ret; -} - -void BattleInfo::localInit() -{ - for(int i = 0; i < 2; i++) - { - auto * armyObj = battleGetArmyObject(i); - armyObj->battle = this; - armyObj->attachTo(*this); - } - - for(CStack * s : stacks) - s->localInit(this); - - 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 -{ - ui32 seed; - - void srand(ui32 s) - { - seed = s; - } - void srand(const int3 & pos) - { - srand(110291 * static_cast(pos.x) + 167801 * static_cast(pos.y) + 81569); - } - int rand() - { - seed = 214013 * seed + 2531011; - return (seed >> 16) & 0x7FFF; - } - int rand(int min, int max) - { - if(min == max) - return min; - if(min > max) - return min; - return min + rand() % (max - min + 1); - } -}; - -struct RangeGenerator -{ - class ExhaustedPossibilities : public std::exception - { - }; - - RangeGenerator(int _min, int _max, std::function _myRand): - min(_min), - remainingCount(_max - _min + 1), - remaining(remainingCount, true), - myRand(std::move(_myRand)) - { - } - - int generateNumber() const - { - if(!remainingCount) - throw ExhaustedPossibilities(); - if(remainingCount == 1) - return 0; - return myRand() % remainingCount; - } - - //get number fulfilling predicate. Never gives the same number twice. - int getSuchNumber(const std::function & goodNumberPred = nullptr) - { - int ret = -1; - do - { - int n = generateNumber(); - int i = 0; - for(;;i++) - { - assert(i < (int)remaining.size()); - if(!remaining[i]) - continue; - if(!n) - break; - n--; - } - - remainingCount--; - remaining[i] = false; - ret = i + min; - } while(goodNumberPred && !goodNumberPred(ret)); - return ret; - } - - int min, remainingCount; - std::vector remaining; - 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) -{ - CMP_stack cmpst; - auto * curB = new BattleInfo(); - - for(auto i = 0u; i < curB->sides.size(); i++) - curB->sides[i].init(heroes[i], armies[i]); - - - 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; - } - - //setting up siege obstacles - if (town && town->hasFort()) - { - curB->si.gateState = EGateState::CLOSED; - - curB->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; - } - - if (town->hasBuilt(BuildingID::CITADEL)) - curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT; - - if (town->hasBuilt(BuildingID::CASTLE)) - { - curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT; - curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT; - } - } - - //randomize obstacles - if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank - { - RandGen r{}; - auto ourRand = [&](){ return r.rand(); }; - r.srand(tile); - r.rand(1,8); //battle sound ID to play... can't do anything with it here - int tilesToBlock = r.rand(5,12); - - std::vector blockedTiles; - - auto appropriateAbsoluteObstacle = [&](int id) - { - const auto * info = Obstacle(id).getInfo(); - return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); - }; - auto appropriateUsualObstacle = [&](int id) - { - const auto * info = Obstacle(id).getInfo(); - return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); - }; - - if(r.rand(1,100) <= 40) //put cliff-like obstacle - { - try - { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); - 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); - - for(BattleHex blocked : obstPtr->getBlockedTiles()) - blockedTiles.push_back(blocked); - tilesToBlock -= Obstacle(obstPtr->ID).getInfo()->blockedTiles.size() / 2; - } - catch(RangeGenerator::ExhaustedPossibilities &) - { - //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place absolute obstacle"); - } - } - - try - { - while(tilesToBlock > 0) - { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); - auto tileAccessibility = curB->getAccesibility(); - const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); - const ObstacleInfo &obi = *Obstacle(obid).getInfo(); - - auto validPosition = [&](BattleHex pos) -> bool - { - if(obi.height >= pos.getY()) - return false; - if(pos.getX() == 0) - return false; - if(pos.getX() + obi.width > 15) - return false; - if(vstd::contains(blockedTiles, pos)) - return false; - - for(BattleHex blocked : obi.getBlocked(pos)) - { - if(tileAccessibility[blocked] == EAccessibility::UNAVAILABLE) //for ship-to-ship battlefield - exclude hardcoded unavailable tiles - return false; - if(vstd::contains(blockedTiles, blocked)) - return false; - int x = blocked.getX(); - if(x <= 2 || x >= 14) - return false; - } - - return true; - }; - - RangeGenerator posgenerator(18, 168, ourRand); - - 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); - - for(BattleHex blocked : obstPtr->getBlockedTiles()) - blockedTiles.push_back(blocked); - tilesToBlock -= static_cast(obi.blockedTiles.size()); - } - } - catch(RangeGenerator::ExhaustedPossibilities &) - { - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place usual obstacle"); - } - } - - //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(ResourceID("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 = [&](int side, const ArtifactPosition & artslot, BattleHex hex) - { - const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); - - 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(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); - } - } - //war machines added - - //battleStartpos read - for(int side = 0; side < 2; side++) - { - int formationNo = armies[side]->stacksCount() - 1; - vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1); - - 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]; - - 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); - } - } - - //adding commanders - for (int i = 0; i < 2; ++i) - { - 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]); - } - - } - - if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL) - { - // keep tower - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, 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); - - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); - } - - //Moat generating is done on server - } - - std::stable_sort(stacks.begin(),stacks.end(),cmpst); - - auto neutral = std::make_shared(EAlignment::NEUTRAL); - auto good = std::make_shared(EAlignment::GOOD); - auto evil = std::make_shared(EAlignment::EVIL); - - const auto * bgInfo = VLC->battlefields()->getById(battlefieldType); - - for(const std::shared_ptr & bonus : bgInfo->bonuses) - { - curB->addNewBonus(bonus); - } - - //native terrain bonuses - static auto nativeTerrain = std::make_shared(); - - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, PrimarySkill::ATTACK)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, 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++) - { - if(heroes[i]) - { - battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)); - battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION_BLOCK)); - } - } - int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER]; - int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER]; - - /* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides - when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this - will not happen because tactics block opposite tactics on same value. - TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if - double tactics will be implemented. - */ - - if(isTacticsAllowed) - { - if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) - logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); - if(tacticsSkillDiffAttacker > 0) - { - curB->tacticsSide = BattleSide::ATTACKER; - //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffAttacker; - } - else if(tacticsSkillDiffDefender > 0) - { - curB->tacticsSide = BattleSide::DEFENDER; - //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffDefender; - } - else - curB->tacticDistance = 0; - } - - return curB; -} - -const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const -{ - for(const auto & side : sides) - if(side.color == player) - return side.hero; - - logGlobal->error("Player %s is not in battle!", player.getStr()); - return nullptr; -} - -ui8 BattleInfo::whatSide(const PlayerColor & player) const -{ - for(int i = 0; i < sides.size(); i++) - if(sides[i].color == player) - return i; - - logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.getStr()); - return -1; -} - -CStack * BattleInfo::getStack(int stackID, bool onlyAlive) -{ - return const_cast(battleGetStackByID(stackID, onlyAlive)); -} - -BattleInfo::BattleInfo(): - round(-1), - activeStack(-1), - town(nullptr), - tile(-1,-1,-1), - battlefieldType(BattleField::NONE), - tacticsSide(0), - tacticDistance(0) -{ - setBattle(this); - setNodeType(BATTLE); -} - -BattleInfo::~BattleInfo() -{ - for (auto & elem : stacks) - delete elem; - - for(int i = 0; i < 2; i++) - if(auto * _armyObj = battleGetArmyObject(i)) - _armyObj->battle = nullptr; -} - -int32_t BattleInfo::getActiveStackID() const -{ - return activeStack; -} - -TStacks BattleInfo::getStacksIf(TStackFilter predicate) const -{ - TStacks ret; - vstd::copy_if(stacks, std::back_inserter(ret), predicate); - return ret; -} - -battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const -{ - battle::Units ret; - vstd::copy_if(stacks, std::back_inserter(ret), predicate); - return ret; -} - - -BattleField BattleInfo::getBattlefieldType() const -{ - return battlefieldType; -} - -TerrainId BattleInfo::getTerrainType() const -{ - return terrainType; -} - -IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const -{ - ObstacleCList ret; - - for(const auto & obstacle : obstacles) - ret.push_back(obstacle); - - return ret; -} - -PlayerColor BattleInfo::getSidePlayer(ui8 side) const -{ - return sides.at(side).color; -} - -const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const -{ - return sides.at(side).armyObject; -} - -const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const -{ - return sides.at(side).hero; -} - -ui8 BattleInfo::getTacticDist() const -{ - return tacticDistance; -} - -ui8 BattleInfo::getTacticsSide() const -{ - return tacticsSide; -} - -const CGTownInstance * BattleInfo::getDefendedTown() const -{ - return town; -} - -EWallState BattleInfo::getWallState(EWallPart partOfWall) const -{ - return si.wallState.at(partOfWall); -} - -EGateState BattleInfo::getGateState() const -{ - return si.gateState; -} - -uint32_t BattleInfo::getCastSpells(ui8 side) const -{ - return sides.at(side).castSpellsCount; -} - -int32_t BattleInfo::getEnchanterCounter(ui8 side) const -{ - return sides.at(side).enchanterCounter; -} - -const IBonusBearer * BattleInfo::getBonusBearer() const -{ - return this; -} - -int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const -{ - if(damage.min != damage.max) - { - 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(); - - return sum / howManyToAv; - } - else - { - return damage.min; - } -} - -void BattleInfo::nextRound(int32_t roundNr) -{ - for(int i = 0; i < 2; ++i) - { - sides.at(i).castSpellsCount = 0; - vstd::amax(--sides.at(i).enchanterCounter, 0); - } - round = roundNr; - - for(CStack * s : stacks) - { - // new turn effects - s->reduceBonusDurations(Bonus::NTurns); - - s->afterNewRound(); - } - - for(auto & obst : obstacles) - obst->battleTurnPassed(); -} - -void BattleInfo::nextTurn(uint32_t unitId) -{ - activeStack = unitId; - - CStack * st = getStack(activeStack); - - //remove bonuses that last until when stack gets new turn - st->removeBonusesRecursive(Bonus::UntilGetsTurn); - - st->afterGetsTurn(); -} - -void BattleInfo::addUnit(uint32_t id, const JsonNode & data) -{ - battle::UnitInfo info; - info.load(id, data); - CStackBasicDescriptor base(info.type, info.count); - - PlayerColor owner = getSidePlayer(info.side); - - auto * ret = new CStack(&base, owner, info.id, info.side, SlotID::SUMMONED_SLOT_PLACEHOLDER); - ret->initialPosition = info.position; - stacks.push_back(ret); - ret->localInit(this); - ret->summoned = info.summoned; -} - -void BattleInfo::moveUnit(uint32_t id, BattleHex destination) -{ - auto * sta = getStack(id); - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - sta->position = destination; - //Bonuses can be limited by unit placement, so, change tree version - //to force updating a bonus. TODO: update version only when such bonuses are present - CBonusSystemNode::treeHasChanged(); -} - -void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) -{ - CStack * changedStack = getStack(id, false); - if(!changedStack) - throw std::runtime_error("Invalid unit id in BattleInfo update"); - - if(!changedStack->alive() && healthDelta > 0) - { - //checking if we resurrect a stack that is under a living stack - auto accessibility = getAccesibility(); - - if(!accessibility.accessible(changedStack->getPosition(), changedStack)) - { - logNetwork->error("Cannot resurrect %s because hex %d is occupied!", changedStack->nodeName(), changedStack->getPosition().hex); - return; //position is already occupied - } - } - - bool killed = (-healthDelta) >= changedStack->getAvailableHealth();//todo: check using alive state once rebirth will be handled separately - - bool resurrected = !changedStack->alive() && healthDelta > 0; - - //applying changes - changedStack->load(data); - - - if(healthDelta < 0) - { - changedStack->removeBonusesRecursive(Bonus::UntilBeingAttacked); - } - - resurrected = resurrected || (killed && changedStack->alive()); - - if(killed) - { - if(changedStack->cloneID >= 0) - { - //remove clone as well - CStack * clone = getStack(changedStack->cloneID); - if(clone) - clone->makeGhost(); - - changedStack->cloneID = -1; - } - } - - if(resurrected || killed) - { - //removing all spells effects - auto selector = [](const Bonus * b) - { - //Special case: DISRUPTING_RAY is absolutely permanent - return b->source == BonusSource::SPELL_EFFECT && b->sid != SpellID::DISRUPTING_RAY; - }; - changedStack->removeBonusesRecursive(selector); - } - - if(!changedStack->alive() && changedStack->isClone()) - { - for(CStack * s : stacks) - { - if(s->cloneID == changedStack->unitId()) - s->cloneID = -1; - } - } -} - -void BattleInfo::removeUnit(uint32_t id) -{ - std::set ids; - ids.insert(id); - - while(!ids.empty()) - { - auto toRemoveId = *ids.begin(); - auto * toRemove = getStack(toRemoveId, false); - - if(!toRemove) - { - logGlobal->error("Cannot find stack %d", toRemoveId); - return; - } - - if(!toRemove->ghost) - { - toRemove->onRemoved(); - toRemove->detachFromAll(); - - //stack may be removed instantly (not being killed first) - //handle clone remove also here - if(toRemove->cloneID >= 0) - { - ids.insert(toRemove->cloneID); - toRemove->cloneID = -1; - } - - //cleanup remaining clone links if any - for(auto * s : stacks) - { - if(s->cloneID == toRemoveId) - s->cloneID = -1; - } - } - - ids.erase(toRemoveId); - } -} - -void BattleInfo::updateUnit(uint32_t id, const JsonNode & data) -{ - //TODO -} - -void BattleInfo::addUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & b : bonus) - addOrUpdateUnitBonus(sta, b, true); -} - -void BattleInfo::updateUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & b : bonus) - addOrUpdateUnitBonus(sta, b, false); -} - -void BattleInfo::removeUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & one : bonus) - { - auto selector = [one](const Bonus * b) - { - //compare everything but turnsRemain, limiter and propagator - return one.duration == b->duration - && one.type == b->type - && one.subtype == b->subtype - && one.source == b->source - && one.val == b->val - && one.sid == b->sid - && one.valType == b->valType - && one.additionalInfo == b->additionalInfo - && one.effectRange == b->effectRange - && one.description == b->description; - }; - sta->removeBonusesRecursive(selector); - } -} - -uint32_t BattleInfo::nextUnitId() const -{ - return static_cast(stacks.size()); -} - -void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd) -{ - if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtype(value.type, value.subtype)))) - { - //no such effect or cumulative - add new - logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description()); - sta->addNewBonus(std::make_shared(value)); - } - else - { - logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description()); - - for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize - { - if(stackBonus->source == value.source && stackBonus->sid == value.sid && stackBonus->type == value.type && stackBonus->subtype == value.subtype) - { - stackBonus->turnsRemain = std::max(stackBonus->turnsRemain, value.turnsRemain); - } - } - CBonusSystemNode::treeHasChanged(); - } -} - -void BattleInfo::setWallState(EWallPart partOfWall, EWallState state) -{ - si.wallState[partOfWall] = state; -} - -void BattleInfo::addObstacle(const ObstacleChanges & changes) -{ - std::shared_ptr obstacle = std::make_shared(); - obstacle->fromInfo(changes); - obstacles.push_back(obstacle); -} - -void BattleInfo::updateObstacle(const ObstacleChanges& changes) -{ - std::shared_ptr changedObstacle = std::make_shared(); - changedObstacle->fromInfo(changes); - - for(auto & obstacle : obstacles) - { - if(obstacle->uniqueID == changes.id) // update this obstacle - { - auto * spellObstacle = dynamic_cast(obstacle.get()); - assert(spellObstacle); - - // Currently we only support to update the "revealed" property - spellObstacle->revealed = changedObstacle->revealed; - - break; - } - } -} - -void BattleInfo::removeObstacle(uint32_t id) -{ - for(int i=0; i < obstacles.size(); ++i) - { - if(obstacles[i]->uniqueID == id) //remove this obstacle - { - obstacles.erase(obstacles.begin() + i); - break; - } - } -} - -CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const -{ - return const_cast(CBattleInfoEssentials::battleGetArmyObject(side)); -} - -CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const -{ - return const_cast(CBattleInfoEssentials::battleGetFightingHero(side)); -} - -#if SCRIPTING_ENABLED -scripting::Pool * BattleInfo::getContextPool() const -{ - //this is real battle, use global scripting context pool - //TODO: make this line not ugly - return IObjectInterface::cb->getGlobalContextPool(); -} -#endif - -bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const -{ - switch(phase) - { - case 0: //catapult moves after turrets - return a->creatureIndex() > b->creatureIndex(); //catapult is 145 and turrets are 149 - case 1: - case 2: - case 3: - { - int as = a->getInitiative(turn); - int bs = b->getInitiative(turn); - - if(as != bs) - return as > bs; - - if(a->unitSide() == b->unitSide()) - return a->unitSlot() < b->unitSlot(); - - return (a->unitSide() == side || b->unitSide() == side) - ? a->unitSide() != side - : a->unitSide() < b->unitSide(); - } - default: - assert(false); - return false; - } - - assert(false); - return false; -} - -CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side): - phase(Phase), - turn(Turn), - side(Side) -{ -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleInfo.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 "BattleInfo.h" +#include "CObstacleInstance.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.h" +#include "../CStack.h" +#include "../CHeroHandler.h" +#include "../filesystem/Filesystem.h" +#include "../mapObjects/CGTownInstance.h" +#include "../CGeneralTextHandler.h" +#include "../BattleFieldHandler.h" +#include "../ObstacleHandler.h" + +//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) +{ + PlayerColor owner = sides[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? + stacks.push_back(ret); + return ret; +} + +CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position) +{ + PlayerColor owner = sides[side].color; + auto * ret = new CStack(&base, owner, id, side, slot); + ret->initialPosition = position; + stacks.push_back(ret); + return ret; +} + +void BattleInfo::localInit() +{ + for(int i = 0; i < 2; i++) + { + auto * armyObj = battleGetArmyObject(i); + armyObj->battle = this; + armyObj->attachTo(*this); + } + + for(CStack * s : stacks) + s->localInit(this); + + 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 +{ + ui32 seed; + + void srand(ui32 s) + { + seed = s; + } + void srand(const int3 & pos) + { + srand(110291 * static_cast(pos.x) + 167801 * static_cast(pos.y) + 81569); + } + int rand() + { + seed = 214013 * seed + 2531011; + return (seed >> 16) & 0x7FFF; + } + int rand(int min, int max) + { + if(min == max) + return min; + if(min > max) + return min; + return min + rand() % (max - min + 1); + } +}; + +struct RangeGenerator +{ + class ExhaustedPossibilities : public std::exception + { + }; + + RangeGenerator(int _min, int _max, std::function _myRand): + min(_min), + remainingCount(_max - _min + 1), + remaining(remainingCount, true), + myRand(std::move(_myRand)) + { + } + + int generateNumber() const + { + if(!remainingCount) + throw ExhaustedPossibilities(); + if(remainingCount == 1) + return 0; + return myRand() % remainingCount; + } + + //get number fulfilling predicate. Never gives the same number twice. + int getSuchNumber(const std::function & goodNumberPred = nullptr) + { + int ret = -1; + do + { + int n = generateNumber(); + int i = 0; + for(;;i++) + { + assert(i < (int)remaining.size()); + if(!remaining[i]) + continue; + if(!n) + break; + n--; + } + + remainingCount--; + remaining[i] = false; + ret = i + min; + } while(goodNumberPred && !goodNumberPred(ret)); + return ret; + } + + int min, remainingCount; + std::vector remaining; + 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) +{ + CMP_stack cmpst; + auto * curB = new BattleInfo(); + + for(auto i = 0u; i < curB->sides.size(); i++) + curB->sides[i].init(heroes[i], armies[i]); + + + 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; + } + + //setting up siege obstacles + if (town && town->hasFort()) + { + curB->si.gateState = EGateState::CLOSED; + + curB->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; + } + + if (town->hasBuilt(BuildingID::CITADEL)) + curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT; + + if (town->hasBuilt(BuildingID::CASTLE)) + { + curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT; + curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT; + } + } + + //randomize obstacles + if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank + { + RandGen r{}; + auto ourRand = [&](){ return r.rand(); }; + r.srand(tile); + r.rand(1,8); //battle sound ID to play... can't do anything with it here + int tilesToBlock = r.rand(5,12); + + std::vector blockedTiles; + + auto appropriateAbsoluteObstacle = [&](int id) + { + const auto * info = Obstacle(id).getInfo(); + return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + }; + auto appropriateUsualObstacle = [&](int id) + { + const auto * info = Obstacle(id).getInfo(); + return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + }; + + if(r.rand(1,100) <= 40) //put cliff-like obstacle + { + try + { + RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + 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); + + for(BattleHex blocked : obstPtr->getBlockedTiles()) + blockedTiles.push_back(blocked); + tilesToBlock -= Obstacle(obstPtr->ID).getInfo()->blockedTiles.size() / 2; + } + catch(RangeGenerator::ExhaustedPossibilities &) + { + //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place absolute obstacle"); + } + } + + try + { + while(tilesToBlock > 0) + { + RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + auto tileAccessibility = curB->getAccesibility(); + const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); + const ObstacleInfo &obi = *Obstacle(obid).getInfo(); + + auto validPosition = [&](BattleHex pos) -> bool + { + if(obi.height >= pos.getY()) + return false; + if(pos.getX() == 0) + return false; + if(pos.getX() + obi.width > 15) + return false; + if(vstd::contains(blockedTiles, pos)) + return false; + + for(BattleHex blocked : obi.getBlocked(pos)) + { + if(tileAccessibility[blocked] == EAccessibility::UNAVAILABLE) //for ship-to-ship battlefield - exclude hardcoded unavailable tiles + return false; + if(vstd::contains(blockedTiles, blocked)) + return false; + int x = blocked.getX(); + if(x <= 2 || x >= 14) + return false; + } + + return true; + }; + + RangeGenerator posgenerator(18, 168, ourRand); + + 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); + + for(BattleHex blocked : obstPtr->getBlockedTiles()) + blockedTiles.push_back(blocked); + tilesToBlock -= static_cast(obi.blockedTiles.size()); + } + } + catch(RangeGenerator::ExhaustedPossibilities &) + { + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place usual obstacle"); + } + } + + //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 = [&](int side, const ArtifactPosition & artslot, BattleHex hex) + { + const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + + 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(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); + } + } + //war machines added + + //battleStartpos read + for(int side = 0; side < 2; side++) + { + int formationNo = armies[side]->stacksCount() - 1; + vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1); + + 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]; + + 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); + } + } + + //adding commanders + for (int i = 0; i < 2; ++i) + { + 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]); + } + + } + + if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL) + { + // keep tower + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, 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); + + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); + } + + //Moat generating is done on server + } + + std::stable_sort(stacks.begin(),stacks.end(),cmpst); + + auto neutral = std::make_shared(EAlignment::NEUTRAL); + auto good = std::make_shared(EAlignment::GOOD); + auto evil = std::make_shared(EAlignment::EVIL); + + const auto * bgInfo = VLC->battlefields()->getById(battlefieldType); + + for(const std::shared_ptr & bonus : bgInfo->bonuses) + { + curB->addNewBonus(bonus); + } + + //native terrain bonuses + static 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)); + ////////////////////////////////////////////////////////////////////////// + + //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++) + { + if(heroes[i]) + { + battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)); + battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION_BLOCK)); + } + } + int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER]; + int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER]; + + /* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides + when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this + will not happen because tactics block opposite tactics on same value. + TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if + double tactics will be implemented. + */ + + if(isTacticsAllowed) + { + if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) + logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); + if(tacticsSkillDiffAttacker > 0) + { + curB->tacticsSide = BattleSide::ATTACKER; + //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics + curB->tacticDistance = 1 + tacticsSkillDiffAttacker; + } + else if(tacticsSkillDiffDefender > 0) + { + curB->tacticsSide = BattleSide::DEFENDER; + //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics + curB->tacticDistance = 1 + tacticsSkillDiffDefender; + } + else + curB->tacticDistance = 0; + } + + return curB; +} + +const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const +{ + for(const auto & side : sides) + if(side.color == player) + return side.hero; + + logGlobal->error("Player %s is not in battle!", player.toString()); + return nullptr; +} + +ui8 BattleInfo::whatSide(const PlayerColor & player) const +{ + for(int i = 0; i < sides.size(); i++) + if(sides[i].color == player) + return i; + + logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.toString()); + return -1; +} + +CStack * BattleInfo::getStack(int stackID, bool onlyAlive) +{ + return const_cast(battleGetStackByID(stackID, onlyAlive)); +} + +BattleInfo::BattleInfo(): + round(-1), + activeStack(-1), + town(nullptr), + tile(-1,-1,-1), + battlefieldType(BattleField::NONE), + tacticsSide(0), + tacticDistance(0) +{ + setNodeType(BATTLE); +} + +BattleID BattleInfo::getBattleID() const +{ + return battleID; +} + +const IBattleInfo * BattleInfo::getBattle() const +{ + return this; +} + +std::optional BattleInfo::getPlayerID() const +{ + return std::nullopt; +} + +BattleInfo::~BattleInfo() +{ + for (auto & elem : stacks) + delete elem; + + for(int i = 0; i < 2; i++) + if(auto * _armyObj = battleGetArmyObject(i)) + _armyObj->battle = nullptr; +} + +int32_t BattleInfo::getActiveStackID() const +{ + return activeStack; +} + +TStacks BattleInfo::getStacksIf(const TStackFilter & predicate) const +{ + TStacks ret; + vstd::copy_if(stacks, std::back_inserter(ret), predicate); + return ret; +} + +battle::Units BattleInfo::getUnitsIf(const battle::UnitFilter & predicate) const +{ + battle::Units ret; + vstd::copy_if(stacks, std::back_inserter(ret), predicate); + return ret; +} + + +BattleField BattleInfo::getBattlefieldType() const +{ + return battlefieldType; +} + +TerrainId BattleInfo::getTerrainType() const +{ + return terrainType; +} + +IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const +{ + ObstacleCList ret; + + for(const auto & obstacle : obstacles) + ret.push_back(obstacle); + + return ret; +} + +PlayerColor BattleInfo::getSidePlayer(ui8 side) const +{ + return sides.at(side).color; +} + +const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const +{ + return sides.at(side).armyObject; +} + +const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const +{ + return sides.at(side).hero; +} + +ui8 BattleInfo::getTacticDist() const +{ + return tacticDistance; +} + +ui8 BattleInfo::getTacticsSide() const +{ + return tacticsSide; +} + +const CGTownInstance * BattleInfo::getDefendedTown() const +{ + return town; +} + +EWallState BattleInfo::getWallState(EWallPart partOfWall) const +{ + return si.wallState.at(partOfWall); +} + +EGateState BattleInfo::getGateState() const +{ + return si.gateState; +} + +uint32_t BattleInfo::getCastSpells(ui8 side) const +{ + return sides.at(side).castSpellsCount; +} + +int32_t BattleInfo::getEnchanterCounter(ui8 side) const +{ + return sides.at(side).enchanterCounter; +} + +const IBonusBearer * BattleInfo::getBonusBearer() const +{ + return this; +} + +int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const +{ + if(damage.min != damage.max) + { + 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(); + + return sum / howManyToAv; + } + else + { + return damage.min; + } +} + +int3 BattleInfo::getLocation() const +{ + return tile; +} + +bool BattleInfo::isCreatureBank() const +{ + return creatureBank; +} + + +std::vector BattleInfo::getUsedSpells(ui8 side) const +{ + return sides.at(side).usedSpellsHistory; +} + +void BattleInfo::nextRound() +{ + for(int i = 0; i < 2; ++i) + { + sides.at(i).castSpellsCount = 0; + vstd::amax(--sides.at(i).enchanterCounter, 0); + } + round += 1; + + for(CStack * s : stacks) + { + // new turn effects + s->reduceBonusDurations(Bonus::NTurns); + + s->afterNewRound(); + } + + for(auto & obst : obstacles) + obst->battleTurnPassed(); +} + +void BattleInfo::nextTurn(uint32_t unitId) +{ + activeStack = unitId; + + CStack * st = getStack(activeStack); + + //remove bonuses that last until when stack gets new turn + st->removeBonusesRecursive(Bonus::UntilGetsTurn); + + st->afterGetsTurn(); +} + +void BattleInfo::addUnit(uint32_t id, const JsonNode & data) +{ + battle::UnitInfo info; + info.load(id, data); + CStackBasicDescriptor base(info.type, info.count); + + PlayerColor owner = getSidePlayer(info.side); + + auto * ret = new CStack(&base, owner, info.id, info.side, SlotID::SUMMONED_SLOT_PLACEHOLDER); + ret->initialPosition = info.position; + stacks.push_back(ret); + ret->localInit(this); + ret->summoned = info.summoned; +} + +void BattleInfo::moveUnit(uint32_t id, BattleHex destination) +{ + auto * sta = getStack(id); + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + sta->position = destination; + //Bonuses can be limited by unit placement, so, change tree version + //to force updating a bonus. TODO: update version only when such bonuses are present + CBonusSystemNode::treeHasChanged(); +} + +void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) +{ + CStack * changedStack = getStack(id, false); + if(!changedStack) + throw std::runtime_error("Invalid unit id in BattleInfo update"); + + if(!changedStack->alive() && healthDelta > 0) + { + //checking if we resurrect a stack that is under a living stack + auto accessibility = getAccesibility(); + + if(!accessibility.accessible(changedStack->getPosition(), changedStack)) + { + logNetwork->error("Cannot resurrect %s because hex %d is occupied!", changedStack->nodeName(), changedStack->getPosition().hex); + return; //position is already occupied + } + } + + bool killed = (-healthDelta) >= changedStack->getAvailableHealth();//todo: check using alive state once rebirth will be handled separately + + bool resurrected = !changedStack->alive() && healthDelta > 0; + + //applying changes + changedStack->load(data); + + + if(healthDelta < 0) + { + changedStack->removeBonusesRecursive(Bonus::UntilBeingAttacked); + } + + resurrected = resurrected || (killed && changedStack->alive()); + + if(killed) + { + if(changedStack->cloneID >= 0) + { + //remove clone as well + CStack * clone = getStack(changedStack->cloneID); + if(clone) + clone->makeGhost(); + + changedStack->cloneID = -1; + } + } + + if(resurrected || killed) + { + //removing all spells effects + auto selector = [](const Bonus * b) + { + //Special case: DISRUPTING_RAY is absolutely permanent + return b->source == BonusSource::SPELL_EFFECT && b->sid.as() != SpellID::DISRUPTING_RAY; + }; + changedStack->removeBonusesRecursive(selector); + } + + if(!changedStack->alive() && changedStack->isClone()) + { + for(CStack * s : stacks) + { + if(s->cloneID == changedStack->unitId()) + s->cloneID = -1; + } + } +} + +void BattleInfo::removeUnit(uint32_t id) +{ + std::set ids; + ids.insert(id); + + while(!ids.empty()) + { + auto toRemoveId = *ids.begin(); + auto * toRemove = getStack(toRemoveId, false); + + if(!toRemove) + { + logGlobal->error("Cannot find stack %d", toRemoveId); + return; + } + + if(!toRemove->ghost) + { + toRemove->onRemoved(); + toRemove->detachFromAll(); + + //stack may be removed instantly (not being killed first) + //handle clone remove also here + if(toRemove->cloneID >= 0) + { + ids.insert(toRemove->cloneID); + toRemove->cloneID = -1; + } + + //cleanup remaining clone links if any + for(auto * s : stacks) + { + if(s->cloneID == toRemoveId) + s->cloneID = -1; + } + } + + ids.erase(toRemoveId); + } +} + +void BattleInfo::updateUnit(uint32_t id, const JsonNode & data) +{ + //TODO +} + +void BattleInfo::addUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & b : bonus) + addOrUpdateUnitBonus(sta, b, true); +} + +void BattleInfo::updateUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & b : bonus) + addOrUpdateUnitBonus(sta, b, false); +} + +void BattleInfo::removeUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & one : bonus) + { + auto selector = [one](const Bonus * b) + { + //compare everything but turnsRemain, limiter and propagator + return one.duration == b->duration + && one.type == b->type + && one.subtype == b->subtype + && one.source == b->source + && one.val == b->val + && one.sid == b->sid + && one.valType == b->valType + && one.additionalInfo == b->additionalInfo + && one.effectRange == b->effectRange + && one.description == b->description; + }; + sta->removeBonusesRecursive(selector); + } +} + +uint32_t BattleInfo::nextUnitId() const +{ + return static_cast(stacks.size()); +} + +void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd) +{ + if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtype(value.type, value.subtype)))) + { + //no such effect or cumulative - add new + logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description()); + sta->addNewBonus(std::make_shared(value)); + } + else + { + logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description()); + + for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize + { + if(stackBonus->source == value.source && stackBonus->sid == value.sid && stackBonus->type == value.type && stackBonus->subtype == value.subtype) + { + stackBonus->turnsRemain = std::max(stackBonus->turnsRemain, value.turnsRemain); + } + } + CBonusSystemNode::treeHasChanged(); + } +} + +void BattleInfo::setWallState(EWallPart partOfWall, EWallState state) +{ + si.wallState[partOfWall] = state; +} + +void BattleInfo::addObstacle(const ObstacleChanges & changes) +{ + std::shared_ptr obstacle = std::make_shared(); + obstacle->fromInfo(changes); + obstacles.push_back(obstacle); +} + +void BattleInfo::updateObstacle(const ObstacleChanges& changes) +{ + std::shared_ptr changedObstacle = std::make_shared(); + changedObstacle->fromInfo(changes); + + for(auto & obstacle : obstacles) + { + if(obstacle->uniqueID == changes.id) // update this obstacle + { + auto * spellObstacle = dynamic_cast(obstacle.get()); + assert(spellObstacle); + + // Currently we only support to update the "revealed" property + spellObstacle->revealed = changedObstacle->revealed; + + break; + } + } +} + +void BattleInfo::removeObstacle(uint32_t id) +{ + for(int i=0; i < obstacles.size(); ++i) + { + if(obstacles[i]->uniqueID == id) //remove this obstacle + { + obstacles.erase(obstacles.begin() + i); + break; + } + } +} + +CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const +{ + return const_cast(CBattleInfoEssentials::battleGetArmyObject(side)); +} + +CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const +{ + return const_cast(CBattleInfoEssentials::battleGetFightingHero(side)); +} + +#if SCRIPTING_ENABLED +scripting::Pool * BattleInfo::getContextPool() const +{ + //this is real battle, use global scripting context pool + //TODO: make this line not ugly + return IObjectInterface::cb->getGlobalContextPool(); +} +#endif + +bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const +{ + switch(phase) + { + case 0: //catapult moves after turrets + return a->creatureIndex() > b->creatureIndex(); //catapult is 145 and turrets are 149 + case 1: + case 2: + case 3: + { + int as = a->getInitiative(turn); + int bs = b->getInitiative(turn); + + if(as != bs) + return as > bs; + + if(a->unitSide() == b->unitSide()) + return a->unitSlot() < b->unitSlot(); + + return (a->unitSide() == side || b->unitSide() == side) + ? a->unitSide() != side + : a->unitSide() < b->unitSide(); + } + default: + assert(false); + return false; + } + + assert(false); + return false; +} + +CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side): + phase(Phase), + turn(Turn), + side(Side) +{ +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index e875cadfa..3b892c680 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -1,171 +1,180 @@ -/* - * BattleInfo.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 "../int3.h" -#include "../bonuses/Bonus.h" -#include "../bonuses/CBonusSystemNode.h" -#include "CBattleInfoCallback.h" -#include "IBattleState.h" -#include "SiegeInfo.h" -#include "SideInBattle.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CStackInstance; -class CStackBasicDescriptor; -class BattleField; - -class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState -{ -public: - enum BattleSide - { - ATTACKER = 0, - DEFENDER - }; - std::array sides; //sides[0] - attacker, sides[1] - defender - si32 round, 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; - SiegeInfo si; - - 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 - ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) - - template void serialize(Handler &h, const int version) - { - h & sides; - h & round; - h & activeStack; - h & town; - h & tile; - h & stacks; - h & obstacles; - h & si; - h & battlefieldType; - h & terrainType; - h & tacticsSide; - h & tacticDistance; - h & static_cast(*this); - if (version > 824) - h & replayAllowed; - else - replayAllowed = false; - } - - ////////////////////////////////////////////////////////////////////////// - BattleInfo(); - virtual ~BattleInfo(); - - ////////////////////////////////////////////////////////////////////////// - // IBattleInfo - - int32_t getActiveStackID() const override; - - TStacks getStacksIf(TStackFilter predicate) const override; - - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; - - BattleField getBattlefieldType() const override; - TerrainId getTerrainType() const override; - - ObstacleCList getAllObstacles() const override; - - PlayerColor getSidePlayer(ui8 side) const override; - const CArmedInstance * getSideArmy(ui8 side) const override; - const CGHeroInstance * getSideHero(ui8 side) const override; - - ui8 getTacticDist() const override; - ui8 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; - - const IBonusBearer * getBonusBearer() const override; - - uint32_t nextUnitId() const override; - - int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; - - ////////////////////////////////////////////////////////////////////////// - // IBattleState - - void nextRound(int32_t roundNr) override; - void nextTurn(uint32_t unitId) override; - - void addUnit(uint32_t id, const JsonNode & data) override; - void moveUnit(uint32_t id, BattleHex destination) override; - void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; - void removeUnit(uint32_t id) override; - void updateUnit(uint32_t id, const JsonNode & data) override; - - void addUnitBonus(uint32_t id, const std::vector & bonus) override; - void updateUnitBonus(uint32_t id, const std::vector & bonus) override; - void removeUnitBonus(uint32_t id, const std::vector & bonus) override; - - void setWallState(EWallPart partOfWall, EWallState state) override; - - void addObstacle(const ObstacleChanges & changes) override; - void updateObstacle(const ObstacleChanges& changes) override; - void removeObstacle(uint32_t id) override; - - static void addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd); - - ////////////////////////////////////////////////////////////////////////// - CStack * getStack(int stackID, bool onlyAlive = true); - using CBattleInfoEssentials::battleGetArmyObject; - CArmedInstance * battleGetArmyObject(ui8 side) const; - using CBattleInfoEssentials::battleGetFightingHero; - CGHeroInstance * battleGetFightingHero(ui8 side) const; - - std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack); //returned value: pair; length may be different than number of elements in path since flying creatures jump between distant hexes - - void calculateCasualties(std::map * casualties) const; //casualties are array of maps size 2 (attacker, defeneder), maps are (crid => amount) - - 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); - - 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); - - ui8 whatSide(const PlayerColor & player) const; - -protected: -#if SCRIPTING_ENABLED - scripting::Pool * getContextPool() const override; -#endif -}; - - -class DLL_LINKAGE CMP_stack -{ - int phase; //rules of which phase will be used - int turn; - uint8_t 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); -}; - -VCMI_LIB_NAMESPACE_END +/* + * BattleInfo.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 "../int3.h" +#include "../bonuses/Bonus.h" +#include "../bonuses/CBonusSystemNode.h" +#include "CBattleInfoCallback.h" +#include "IBattleState.h" +#include "SiegeInfo.h" +#include "SideInBattle.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CStackInstance; +class CStackBasicDescriptor; +class BattleField; + +class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState +{ +public: + BattleID battleID = BattleID(0); + + enum BattleSide + { + ATTACKER = 0, + DEFENDER + }; + std::array sides; //sides[0] - attacker, sides[1] - defender + si32 round, 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; + SiegeInfo si; + + 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 + ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) + + template void serialize(Handler &h, const int version) + { + h & battleID; + h & sides; + h & round; + h & activeStack; + h & town; + h & tile; + h & stacks; + h & obstacles; + h & si; + h & battlefieldType; + h & terrainType; + h & tacticsSide; + h & tacticDistance; + h & static_cast(*this); + if (version > 824) + h & replayAllowed; + else + replayAllowed = false; + } + + ////////////////////////////////////////////////////////////////////////// + BattleInfo(); + virtual ~BattleInfo(); + + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + + ////////////////////////////////////////////////////////////////////////// + // IBattleInfo + + BattleID getBattleID() const override; + + int32_t getActiveStackID() const override; + + TStacks getStacksIf(const TStackFilter & predicate) const override; + + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; + + BattleField getBattlefieldType() const override; + TerrainId getTerrainType() const override; + + ObstacleCList getAllObstacles() const override; + + PlayerColor getSidePlayer(ui8 side) const override; + const CArmedInstance * getSideArmy(ui8 side) const override; + const CGHeroInstance * getSideHero(ui8 side) const override; + + ui8 getTacticDist() const override; + ui8 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; + + const IBonusBearer * getBonusBearer() const override; + + uint32_t nextUnitId() const override; + + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + + int3 getLocation() const override; + bool isCreatureBank() const override; + + std::vector getUsedSpells(ui8 side) const override; + + ////////////////////////////////////////////////////////////////////////// + // IBattleState + + void nextRound() override; + void nextTurn(uint32_t unitId) override; + + void addUnit(uint32_t id, const JsonNode & data) override; + void moveUnit(uint32_t id, BattleHex destination) override; + void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; + void removeUnit(uint32_t id) override; + void updateUnit(uint32_t id, const JsonNode & data) override; + + void addUnitBonus(uint32_t id, const std::vector & bonus) override; + void updateUnitBonus(uint32_t id, const std::vector & bonus) override; + void removeUnitBonus(uint32_t id, const std::vector & bonus) override; + + void setWallState(EWallPart partOfWall, EWallState state) override; + + void addObstacle(const ObstacleChanges & changes) override; + void updateObstacle(const ObstacleChanges& changes) override; + void removeObstacle(uint32_t id) override; + + static void addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd); + + ////////////////////////////////////////////////////////////////////////// + CStack * getStack(int stackID, bool onlyAlive = true); + using CBattleInfoEssentials::battleGetArmyObject; + CArmedInstance * battleGetArmyObject(ui8 side) const; + using CBattleInfoEssentials::battleGetFightingHero; + CGHeroInstance * battleGetFightingHero(ui8 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); + + 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); + + ui8 whatSide(const PlayerColor & player) const; + +protected: +#if SCRIPTING_ENABLED + scripting::Pool * getContextPool() const override; +#endif +}; + + +class DLL_LINKAGE CMP_stack +{ + int phase; //rules of which phase will be used + int turn; + uint8_t 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); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleProxy.cpp b/lib/battle/BattleProxy.cpp index f62a5a073..6bc34a77e 100644 --- a/lib/battle/BattleProxy.cpp +++ b/lib/battle/BattleProxy.cpp @@ -17,13 +17,20 @@ VCMI_LIB_NAMESPACE_BEGIN BattleProxy::BattleProxy(Subject subject_): subject(std::move(subject_)) -{ - setBattle(this); - player = subject->getPlayerID(); -} +{} BattleProxy::~BattleProxy() = default; +const IBattleInfo * BattleProxy::getBattle() const +{ + return this; +} + +std::optional BattleProxy::getPlayerID() const +{ + return subject->getPlayerID(); +} + int32_t BattleProxy::getActiveStackID() const { const auto * ret = subject->battleActiveUnit(); @@ -33,12 +40,12 @@ int32_t BattleProxy::getActiveStackID() const return -1; } -TStacks BattleProxy::getStacksIf(TStackFilter predicate) const +TStacks BattleProxy::getStacksIf(const TStackFilter & predicate) const { return subject->battleGetStacksIf(predicate); } -battle::Units BattleProxy::getUnitsIf(battle::UnitFilter predicate) const +battle::Units BattleProxy::getUnitsIf(const battle::UnitFilter & predicate) const { return subject->battleGetUnitsIf(predicate); } diff --git a/lib/battle/BattleProxy.h b/lib/battle/BattleProxy.h index da0880528..c43ede14d 100644 --- a/lib/battle/BattleProxy.h +++ b/lib/battle/BattleProxy.h @@ -24,12 +24,14 @@ public: ////////////////////////////////////////////////////////////////////////// // IBattleInfo + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; int32_t getActiveStackID() const override; - TStacks getStacksIf(TStackFilter predicate) const override; + TStacks getStacksIf(const TStackFilter & predicate) const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; BattleField getBattlefieldType() const override; TerrainId getTerrainType() const override; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index ff44c2ba9..523be0346 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1,1859 +1,1880 @@ -/* - * CBattleInfoCallback.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 "CBattleInfoCallback.h" - -#include - -#include "../CStack.h" -#include "BattleInfo.h" -#include "CObstacleInstance.h" -#include "DamageCalculator.h" -#include "PossiblePlayerBattleAction.h" -#include "../NetPacks.h" -#include "../spells/ObstacleCasterProxy.h" -#include "../spells/ISpellMechanics.h" -#include "../spells/Problem.h" -#include "../spells/CSpellHandler.h" -#include "../mapObjects/CGTownInstance.h" -#include "../BattleFieldHandler.h" -#include "../CModHandler.h" -#include "../Rect.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO -{ - -static BattleHex lineToWallHex(int line) //returns hex with wall in given line (y coordinate) -{ - static const BattleHex lineToHex[] = {12, 29, 45, 62, 78, 96, 112, 130, 147, 165, 182}; - - return lineToHex[line]; -} - -static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) -{ - const int wallInStackLine = lineToWallHex(pos1.getY()); - const int wallInDestLine = lineToWallHex(pos2.getY()); - - const bool stackLeft = pos1 < wallInStackLine; - const bool destLeft = pos2 < wallInDestLine; - - return stackLeft == destLeft; -} - -// parts of wall -static const std::pair wallParts[] = -{ - std::make_pair(50, EWallPart::KEEP), - std::make_pair(183, EWallPart::BOTTOM_TOWER), - std::make_pair(182, EWallPart::BOTTOM_WALL), - std::make_pair(130, EWallPart::BELOW_GATE), - std::make_pair(78, EWallPart::OVER_GATE), - std::make_pair(29, EWallPart::UPPER_WALL), - std::make_pair(12, EWallPart::UPPER_TOWER), - std::make_pair(95, EWallPart::INDESTRUCTIBLE_PART_OF_GATE), - std::make_pair(96, EWallPart::GATE), - std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(62, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(165, EWallPart::INDESTRUCTIBLE_PART) -}; - -static EWallPart hexToWallPart(BattleHex hex) -{ - for(const auto & elem : wallParts) - { - if(elem.first == hex) - return elem.second; - } - - return EWallPart::INVALID; //not found! -} - -static BattleHex WallPartToHex(EWallPart part) -{ - for(const auto & elem : wallParts) - { - if(elem.second == part) - return elem.first; - } - - return BattleHex::INVALID; //not found! -} -} - -using namespace SiegeStuffThatShouldBeMovedToHandlers; - -ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const -{ - RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); - if(caster == nullptr) - { - logGlobal->error("CBattleInfoCallback::battleCanCastSpell: no spellcaster."); - return ESpellCastProblem::INVALID; - } - const PlayerColor player = caster->getCasterOwner(); - const auto side = playerToSide(player); - if(!side) - return ESpellCastProblem::INVALID; - if(!battleDoWeKnowAbout(side.value())) - { - logGlobal->warn("You can't check if enemy can cast given spell!"); - return ESpellCastProblem::INVALID; - } - - if(battleTacticDist()) - return ESpellCastProblem::ONGOING_TACTIC_PHASE; - - switch(mode) - { - case spells::Mode::HERO: - { - if(battleCastSpells(side.value()) > 0) - return ESpellCastProblem::CASTS_PER_TURN_LIMIT; - - const auto * hero = dynamic_cast(caster); - - if(!hero) - return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; - if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)) - return ESpellCastProblem::MAGIC_IS_BLOCKED; - } - break; - default: - break; - } - - return ESpellCastProblem::OK; -} - -bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const -{ - auto isTileBlocked = [&](BattleHex tile) - { - EWallPart wallPart = battleHexToWallPart(tile); - if (wallPart == EWallPart::INVALID) - return false; // there is no wall here - if (wallPart == EWallPart::INDESTRUCTIBLE_PART_OF_GATE) - return false; // does not blocks ranged attacks - if (wallPart == EWallPart::INDESTRUCTIBLE_PART) - return true; // always blocks ranged attacks - - return isWallPartAttackable(wallPart); - }; - // Count wall penalty requirement by shortest path, not by arbitrary line, to avoid various OH3 bugs - auto getShortestPath = [](BattleHex from, BattleHex dest) -> std::vector - { - //Out early - if(from == dest) - return {}; - - std::vector ret; - auto next = from; - //Not a real direction, only to indicate to which side we should search closest tile - auto direction = from.getX() > dest.getX() ? BattleSide::DEFENDER : BattleSide::ATTACKER; - - while (next != dest) - { - auto tiles = next.neighbouringTiles(); - std::set possibilities = {tiles.begin(), tiles.end()}; - next = BattleHex::getClosestTile(direction, dest, possibilities); - ret.push_back(next); - } - assert(!ret.empty()); - ret.pop_back(); //Remove destination hex - return ret; - }; - - RETURN_IF_NOT_BATTLE(false); - auto checkNeeded = !sameSideOfWall(from, dest); - bool pathHasWall = false; - bool pathHasMoat = false; - - for(const auto & hex : getShortestPath(from, dest)) - { - pathHasWall |= isTileBlocked(hex); - if(!checkMoat) - continue; - - auto obstacles = battleGetAllObstaclesOnPos(hex, false); - - if(hex != ESiegeHex::GATE_BRIDGE || (battleIsGatePassable())) - for(const auto & obst : obstacles) - if(obst->obstacleType == CObstacleInstance::MOAT) - pathHasMoat |= true; - } - - return checkNeeded && ( (checkWall && pathHasWall) || (checkMoat && pathHasMoat) ); -} - -bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const -{ - RETURN_IF_NOT_BATTLE(false); - if(!battleGetSiegeLevel()) - return false; - - const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY"; - static const auto selectorNoWallPenalty = Selector::type()(BonusType::NO_WALL_PENALTY); - - if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty)) - return false; - - const auto shooterOutsideWalls = shooterPosition < lineToWallHex(shooterPosition.getY()); - - return shooterOutsideWalls && battleHasPenaltyOnLine(shooterPosition, destHex, true, false); -} - -std::vector CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data) -{ - RETURN_IF_NOT_BATTLE(std::vector()); - std::vector allowedActionList; - if(data.tacticsMode) //would "if(battleGetTacticDist() > 0)" work? - { - allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_TACTICS); - allowedActionList.push_back(PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK); - } - else - { - if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? - { - if(stack->hasBonusOfType(BonusType::SPELLCASTER)) - { - for(const auto & spellID : data.creatureSpellsToCast) - { - const CSpell *spell = spellID.toSpell(); - PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); - allowedActionList.push_back(act); - } - } - if(stack->hasBonusOfType(BonusType::RANDOM_SPELLCASTER)) - allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); - } - if(stack->canShoot()) - allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT); - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) - allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN); - - allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack - allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere - - if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks - allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); - - const auto * siegedTown = battleGetDefendedTown(); - if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots - allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT); - if(stack->hasBonusOfType(BonusType::HEALER)) - allowedActionList.push_back(PossiblePlayerBattleAction::HEAL); - } - - return allowedActionList; -} - -PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const -{ - RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID); - auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; - - const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); - - if(ti.massive || ti.type == spells::AimType::NO_TARGET) - spellSelMode = PossiblePlayerBattleAction::NO_LOCATION; - else if(ti.type == spells::AimType::LOCATION && ti.clearAffected) - spellSelMode = PossiblePlayerBattleAction::FREE_LOCATION; - else if(ti.type == spells::AimType::CREATURE) - spellSelMode = PossiblePlayerBattleAction::AIMED_SPELL_CREATURE; - else if(ti.type == spells::AimType::OBSTACLE) - spellSelMode = PossiblePlayerBattleAction::OBSTACLE; - - return PossiblePlayerBattleAction(spellSelMode, spell->id); -} - -std::set CBattleInfoCallback::battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const -{ - std::set attackedHexes; - RETURN_IF_NOT_BATTLE(attackedHexes); - - AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - for (BattleHex tile : at.hostileCreaturePositions) - { - const auto * st = battleGetUnitByPos(tile, true); - if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? - { - attackedHexes.insert(tile); - } - } - for (BattleHex tile : at.friendlyCreaturePositions) - { - if(battleGetUnitByPos(tile, true)) //friendly stacks can also be damaged by Dragon Breath - { - attackedHexes.insert(tile); - } - } - return attackedHexes; -} - -SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const -{ - switch (mode) - { - case RANDOM_GENIE: - return getRandomBeneficialSpell(rand, stack); //target - break; - case RANDOM_AIMED: - return getRandomCastedSpell(rand, stack); //caster - break; - default: - logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast(mode)); - return SpellID::NONE; - } -} - -const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - for(const auto * s : battleGetAllStacks(true)) - if(vstd::contains(s->getHexes(), pos) && (!onlyAlive || s->alive())) - return s; - - return nullptr; -} - -const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - auto ret = battleGetUnitsIf([=](const battle::Unit * unit) - { - return !unit->isGhost() - && vstd::contains(battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()), pos) - && (!onlyAlive || unit->alive()); - }); - - if(!ret.empty()) - return ret.front(); - else - return nullptr; -} - -battle::Units CBattleInfoCallback::battleAliveUnits() const -{ - return battleGetUnitsIf([](const battle::Unit * unit) - { - return unit->isValidTarget(false); - }); -} - -battle::Units CBattleInfoCallback::battleAliveUnits(ui8 side) const -{ - return battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->isValidTarget(false) && unit->unitSide() == side; - }); -} - -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 * returnedUnit = nullptr; - size_t currentUnitIndex = 0; - - for(size_t i = 0; i < allUnits.size(); i++) - { - int32_t currentUnitInitiative = -1; - int32_t returnedUnitInitiative = -1; - - if(returnedUnit) - returnedUnitInitiative = returnedUnit->getInitiative(turn); - - if(!allUnits[i]) - continue; - - auto currentUnit = allUnits[i]; - currentUnitInitiative = currentUnit->getInitiative(turn); - - switch(phase) - { - case BattlePhases::NORMAL: // Faster first, attacker priority, higher slot first - if(returnedUnit == nullptr || currentUnitInitiative > returnedUnitInitiative) - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - else if(currentUnitInitiative == returnedUnitInitiative) - { - if(sideThatLastMoved == -1 && 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 - && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - } - break; - case BattlePhases::WAIT_MORALE: // Slower first, higher slot first - case BattlePhases::WAIT: - if(returnedUnit == nullptr || currentUnitInitiative < returnedUnitInitiative) - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved - && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - break; - default: - break; - } - } - - if(!returnedUnit) - return nullptr; - - allUnits[currentUnitIndex] = nullptr; - - return returnedUnit; -} - -void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, int8_t sideThatLastMoved) const -{ - RETURN_IF_NOT_BATTLE(); - - if(maxUnits == 0 && maxTurns == 0) - { - logGlobal->error("Attempt to get infinite battle queue"); - return; - } - - auto actualTurn = turn > 0 ? turn : 0; - - auto turnsIsFull = [&]() -> bool - { - if(maxUnits == 0) - return false;//no limit - - size_t turnsSize = 0; - for(const auto & oneTurn : turns) - turnsSize += oneTurn.size(); - return turnsSize >= maxUnits; - }; - - turns.emplace_back(); - - // We'll split creatures with remaining movement to 4 buckets (SIEGE, NORMAL, WAIT_MORALE, WAIT) - std::array phases; // Access using BattlePhases enum - - const battle::Unit * activeUnit = battleActiveUnit(); - - 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()) - { - turns.back().push_back(activeUnit); - if(turnsIsFull()) - return; - } - - //its first or current turn, turn priority for active stack side - //TODO: what if active stack mind-controlled? - if(turn <= 0 && sideThatLastMoved < 0) - sideThatLastMoved = activeUnit->unitSide(); - } - - auto allUnits = battleGetUnitsIf([](const battle::Unit * unit) - { - return !unit->isGhost(); - }); - - // If no unit will be EVER! able to move, battle is over. - if(!vstd::contains_if(allUnits, [](const battle::Unit * unit) { return unit->willMove(100000); })) //little evil, but 100000 should be enough for all effects to disappear - { - turns.clear(); - return; - } - - for(const auto * unit : allUnits) - { - if((actualTurn == 0 && !unit->willMove()) //we are considering current round and unit won't move - || (actualTurn > 0 && !unit->canMove(turn)) //unit won't be able to move in later rounds - || (actualTurn == 0 && unit == activeUnit && !turns.at(0).empty() && unit == turns.front().front())) //it's active unit already added at the beginning of queue - { - continue; - } - - int unitPhase = unit->battleQueuePhase(turn); - - phases[unitPhase].push_back(unit); - } - - boost::sort(phases[BattlePhases::SIEGE], CMP_stack(BattlePhases::SIEGE, actualTurn, sideThatLastMoved)); - std::copy(phases[BattlePhases::SIEGE].begin(), phases[BattlePhases::SIEGE].end(), std::back_inserter(turns.back())); - - if(turnsIsFull()) - return; - - for(uint8_t phase = BattlePhases::NORMAL; phase < BattlePhases::NUMBER_OF_PHASES; phase++) - boost::sort(phases[phase], CMP_stack(phase, actualTurn, sideThatLastMoved)); - - uint8_t phase = BattlePhases::NORMAL; - while(!turnsIsFull() && phase < BattlePhases::NUMBER_OF_PHASES) - { - const battle::Unit * currentUnit = nullptr; - if(phases[phase].empty()) - phase++; - else - { - currentUnit = takeOneUnit(phases[phase], actualTurn, sideThatLastMoved, phase); - if(!currentUnit) - { - phase++; - } - else - { - turns.back().push_back(currentUnit); - sideThatLastMoved = currentUnit->unitSide(); - } - } - } - - if(sideThatLastMoved < 0) - sideThatLastMoved = BattleSide::ATTACKER; - - if(!turnsIsFull() && (maxTurns == 0 || turns.size() < maxTurns)) - battleGetTurnOrder(turns, maxUnits, maxTurns, actualTurn + 1, sideThatLastMoved); -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const -{ - - RETURN_IF_NOT_BATTLE(std::vector()); - if(!unit->getPosition().isValid()) //turrets - return std::vector(); - - auto reachability = getReachability(unit); - - return battleGetAvailableHexes(reachability, unit, obtainMovementRange); -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const -{ - std::vector ret; - - RETURN_IF_NOT_BATTLE(ret); - if(!unit->getPosition().isValid()) //turrets - return ret; - - auto unitSpeed = unit->speed(0, true); - - const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); - - for(int i = 0; i < GameConstants::BFIELD_SIZE; ++i) - { - // If obstacles or other stacks makes movement impossible, it can't be helped. - if(!cache.isReachable(i)) - continue; - - if(tacticsPhase && !obtainMovementRange) // if obtainMovementRange requested do not return tactics range - { - // Stack has to perform tactic-phase movement -> can enter any reachable tile within given range - if(!isInTacticRange(i)) - continue; - } - else - { - // Not tactics phase -> destination must be reachable and within unit range. - if(cache.distances[i] > static_cast(unitSpeed)) - continue; - } - - ret.emplace_back(i); - } - - return ret; -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const -{ - std::vector ret = battleGetAvailableHexes(unit, obtainMovementRange); - - if(ret.empty()) - return ret; - - if(addOccupiable && unit->doubleWide()) - { - std::vector occupiable; - - occupiable.reserve(ret.size()); - for(auto hex : ret) - occupiable.push_back(unit->occupiedHex(hex)); - - vstd::concatenate(ret, occupiable); - } - - - if(attackable) - { - auto meleeAttackable = [&](BattleHex hex) -> bool - { - // Return true if given hex has at least one available neighbour. - // Available hexes are already present in ret vector. - auto availableNeighbor = boost::find_if(ret, [=] (BattleHex availableHex) - { - return BattleHex::mutualPosition(hex, availableHex) >= 0; - }); - return availableNeighbor != ret.end(); - }; - for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide()))) - { - if(!otherSt->isValidTarget(false)) - continue; - - std::vector occupied = otherSt->getHexes(); - - if(battleCanShoot(unit, otherSt->getPosition())) - { - attackable->insert(attackable->end(), occupied.begin(), occupied.end()); - continue; - } - - for(BattleHex he : occupied) - { - if(meleeAttackable(he)) - attackable->push_back(he); - } - } - } - - //adding occupiable likely adds duplicates to ret -> clean it up - boost::sort(ret); - ret.erase(boost::unique(ret).end(), ret.end()); - return ret; -} - -bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(battleTacticDist()) - return false; - - if (!stack || !target) - return false; - - if(!battleMatchOwner(stack, target)) - return false; - - auto id = stack->unitType()->getId(); - if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) - return false; - - return target->alive(); -} - -bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(battleTacticDist()) //no shooting during tactics - return false; - - if (!attacker) - return false; - if (attacker->creatureIndex() == CreatureID::CATAPULT) //catapult cannot attack creatures - return false; - - //forgetfulness - TConstBonusListPtr forgetfulList = attacker->getBonuses(Selector::type()(BonusType::FORGETFULL)); - if(!forgetfulList->empty()) - { - int forgetful = forgetfulList->valOfBonuses(Selector::type()(BonusType::FORGETFULL)); - - //advanced+ level - if(forgetful > 1) - return false; - } - - return attacker->canShoot() && (!battleIsUnitBlocked(attacker) - || attacker->hasBonusOfType(BonusType::FREE_SHOOTING)); -} - -bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - - const battle::Unit * defender = battleGetUnitByPos(dest); - if(!attacker || !defender) - return false; - - if(battleMatchOwner(attacker, defender) && defender->alive()) - { - if(battleCanShoot(attacker)) - { - auto limitedRangeBonus = attacker->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); - if(limitedRangeBonus == nullptr) - { - return true; - } - - int shootingRange = limitedRangeBonus->val; - return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); - } - } - - return false; -} - -DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const -{ - DamageCalculator calculator(*this, info); - - return calculator.calculateDmgRange(); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int movementDistance = reachability[attackerPosition]; - return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); - return battleEstimateDamage(bai, retaliationDmg); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - - DamageEstimation ret = calculateDmgRange(bai); - - if(retaliationDmg) - { - if(bai.shooting) - { - //FIXME: handle RANGED_RETALIATION - *retaliationDmg = DamageEstimation(); - } - else - { - //TODO: rewrite using boost::numeric::interval - //TODO: rewire once more using interval-based fuzzy arithmetic - - const auto & estimateRetaliation = [&](int64_t damage) - { - auto retaliationAttack = bai.reverse(); - auto state = retaliationAttack.attacker->acquireState(); - state->damage(damage); - retaliationAttack.attacker = state.get(); - return calculateDmgRange(retaliationAttack); - }; - - DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); - DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); - - retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); - retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); - - retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); - retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); - } - } - - return ret; -} - -std::vector> CBattleInfoCallback::battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking) const -{ - std::vector> obstacles = std::vector>(); - RETURN_IF_NOT_BATTLE(obstacles); - for(auto & obs : battleGetAllObstacles()) - { - if(vstd::contains(obs->getBlockedTiles(), tile) - || (!onlyBlocking && vstd::contains(obs->getAffectedTiles(), tile))) - { - obstacles.push_back(obs); - } - } - return obstacles; -} - -std::vector> CBattleInfoCallback::getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const -{ - auto affectedObstacles = std::vector>(); - RETURN_IF_NOT_BATTLE(affectedObstacles); - if(unit->alive()) - { - if(!passed.count(unit->getPosition())) - affectedObstacles = battleGetAllObstaclesOnPos(unit->getPosition(), false); - if(unit->doubleWide()) - { - BattleHex otherHex = unit->occupiedHex(); - if(otherHex.isValid() && !passed.count(otherHex)) - for(auto & i : battleGetAllObstaclesOnPos(otherHex, false)) - if(!vstd::contains(affectedObstacles, i)) - affectedObstacles.push_back(i); - } - for(auto hex : unit->getHexes()) - if(hex == ESiegeHex::GATE_BRIDGE && battleIsGatePassable()) - for(int i=0; iobstacleType == CObstacleInstance::MOAT) - affectedObstacles.erase(affectedObstacles.begin()+i); - } - return affectedObstacles; -} - -bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed) const -{ - if(!unit.alive()) - return false; - bool movementStopped = false; - for(auto & obstacle : getAllAffectedObstaclesByStack(&unit, passed)) - { - //helper info - const SpellCreatedObstacle * spellObstacle = dynamic_cast(obstacle.get()); - - if(spellObstacle) - { - auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void - { - // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage - auto operation = ObstacleChanges::EOperation::UPDATE; - if (spellObstacle.removeOnTrigger) - operation = ObstacleChanges::EOperation::REMOVE; - - SpellCreatedObstacle changedObstacle; - changedObstacle.uniqueID = spellObstacle.uniqueID; - changedObstacle.revealed = true; - - BattleObstaclesChanged bocp; - bocp.changes.emplace_back(spellObstacle.uniqueID, operation); - changedObstacle.toInfo(bocp.changes.back(), operation); - spellEnv.apply(&bocp); - }; - const auto side = unit.unitSide(); - auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side); - const auto * hero = battleGetFightingHero(spellObstacle->casterSide); - auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); - const auto * sp = obstacle->getTrigger().toSpell(); - if(obstacle->triggersEffects() && sp) - { - auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp); - spells::detail::ProblemImpl ignored; - auto target = spells::Target(1, spells::Destination(&unit)); - if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures - { - if(shouldReveal) { //hidden obstacle triggers effects after revealed - revealObstacles(*spellObstacle); - cast.cast(&spellEnv, target); - } - } - } - else if(shouldReveal) - revealObstacles(*spellObstacle); - } - - if(!unit.alive()) - return false; - - if(obstacle->stopsMovement()) - movementStopped = true; - } - - return unit.alive() && !movementStopped; -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility() const -{ - AccessibilityInfo ret; - ret.fill(EAccessibility::ACCESSIBLE); - - //removing accessibility for side columns of hexes - for(int y = 0; y < GameConstants::BFIELD_HEIGHT; y++) - { - ret[BattleHex(GameConstants::BFIELD_WIDTH - 1, y)] = EAccessibility::SIDE_COLUMN; - ret[BattleHex(0, y)] = EAccessibility::SIDE_COLUMN; - } - - //special battlefields with logically unavailable tiles - auto bFieldType = battleGetBattlefieldType(); - - if(bFieldType != BattleField::NONE) - { - std::vector impassableHexes = bFieldType.getInfo()->impassableHexes; - - for(auto hex : impassableHexes) - ret[hex] = EAccessibility::UNAVAILABLE; - } - - //gate -> should be before stacks - if(battleGetSiegeLevel() > 0) - { - EAccessibility accessability = EAccessibility::ACCESSIBLE; - switch(battleGetGateState()) - { - case EGateState::CLOSED: - accessability = EAccessibility::GATE; - break; - - case EGateState::BLOCKED: - accessability = EAccessibility::UNAVAILABLE; - break; - } - ret[ESiegeHex::GATE_OUTER] = ret[ESiegeHex::GATE_INNER] = accessability; - } - - //tiles occupied by standing stacks - for(const auto * unit : battleAliveUnits()) - { - for(auto hex : unit->getHexes()) - if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns - ret[hex] = EAccessibility::ALIVE_STACK; - } - - //obstacles - for(const auto &obst : battleGetAllObstacles()) - { - for(auto hex : obst->getBlockedTiles()) - ret[hex] = EAccessibility::OBSTACLE; - } - - //walls - if(battleGetSiegeLevel() > 0) - { - static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165}; - for(auto hex : permanentlyLocked) - ret[hex] = EAccessibility::UNAVAILABLE; - - //TODO likely duplicated logic - static const std::pair lockedIfNotDestroyed[] = - { - //which part of wall, which hex is blocked if this part of wall is not destroyed - std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_4)), - std::make_pair(EWallPart::BELOW_GATE, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_3)), - std::make_pair(EWallPart::OVER_GATE, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_2)), - std::make_pair(EWallPart::UPPER_WALL, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_1)) - }; - - for(const auto & elem : lockedIfNotDestroyed) - { - if(battleGetWallState(elem.first) != EWallState::DESTROYED) - ret[elem.second] = EAccessibility::DESTRUCTIBLE_WALL; - } - } - - return ret; -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility(const battle::Unit * stack) const -{ - return getAccesibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector & accessibleHexes) const -{ - auto ret = getAccesibility(); - for(auto hex : accessibleHexes) - if(hex.isValid()) - ret[hex] = EAccessibility::ACCESSIBLE; - - return ret; -} - -ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters & params) const -{ - ReachabilityInfo ret; - ret.accessibility = accessibility; - ret.params = params; - - ret.predecessors.fill(BattleHex::INVALID); - ret.distances.fill(ReachabilityInfo::INFINITE_DIST); - - if(!params.startPosition.isValid()) //if got call for arrow turrets - return ret; - - const std::set obstacles = getStoppers(params.perspective); - auto checkParams = params; - checkParams.ignoreKnownAccessible = true; //Ignore starting hexes obstacles - - std::queue hexq; //bfs queue - - //first element - hexq.push(params.startPosition); - ret.distances[params.startPosition] = 0; - - std::array accessibleCache{}; - for(int hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) - accessibleCache[hex] = accessibility.accessible(hex, params.doubleWide, params.side); - - while(!hexq.empty()) //bfs loop - { - const BattleHex curHex = hexq.front(); - hexq.pop(); - - //walking stack can't step past the obstacles - if(isInObstacle(curHex, obstacles, checkParams)) - continue; - - const int costToNeighbour = ret.distances[curHex.hex] + 1; - for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) - { - if(neighbour.isValid()) - { - const int costFoundSoFar = ret.distances[neighbour.hex]; - - if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) - { - hexq.push(neighbour); - ret.distances[neighbour.hex] = costToNeighbour; - ret.predecessors[neighbour.hex] = curHex; - } - } - } - } - - return ret; -} - -bool CBattleInfoCallback::isInObstacle( - BattleHex hex, - const std::set & obstacles, - const ReachabilityInfo::Parameters & params) const -{ - auto occupiedHexes = battle::Unit::getHexes(hex, params.doubleWide, params.side); - - for(auto occupiedHex : occupiedHexes) - { - if(params.ignoreKnownAccessible && vstd::contains(params.knownAccessible, occupiedHex)) - continue; - - if(vstd::contains(obstacles, occupiedHex)) - { - if(occupiedHex == ESiegeHex::GATE_BRIDGE) - { - if(battleGetGateState() != EGateState::DESTROYED && params.side == BattleSide::ATTACKER) - return true; - } - else - return true; - } - } - - return false; -} - -std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const -{ - std::set ret; - RETURN_IF_NOT_BATTLE(ret); - - for(auto &oi : battleGetAllObstacles(whichSidePerspective)) - { - if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) - continue; - - for(const auto & hex : oi->getStoppingTile()) - { - if(hex == ESiegeHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) - { - if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) - continue; // this tile is disabled by drawbridge on top of it - } - ret.insert(hex); - } - } - return ret; -} - -std::pair CBattleInfoCallback::getNearestStack(const battle::Unit * closest) const -{ - auto reachability = getReachability(closest); - auto avHexes = battleGetAvailableHexes(reachability, closest, false); - - // I hate std::pairs with their undescriptive member names first / second - struct DistStack - { - uint32_t distanceToPred; - BattleHex destination; - const battle::Unit * stack; - }; - - std::vector stackPairs; - - std::vector possible = battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->isValidTarget(false) && unit != closest; - }); - - for(const battle::Unit * st : possible) - { - for(BattleHex hex : avHexes) - if(CStack::isMeleeAttackPossible(closest, st, hex)) - { - DistStack hlp = {reachability.distances[hex], hex, st}; - stackPairs.push_back(hlp); - } - } - - if(!stackPairs.empty()) - { - auto comparator = [](DistStack lhs, DistStack rhs) { return lhs.distanceToPred < rhs.distanceToPred; }; - auto minimal = boost::min_element(stackPairs, comparator); - return std::make_pair(minimal->stack, minimal->destination); - } - else - return std::make_pair(nullptr, BattleHex::INVALID); -} - -BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const -{ - bool twoHex = VLC->creh->objects[creID]->isDoubleWide(); - - int pos; - if (initialPos > -1) - pos = initialPos; - else //summon elementals depending on player side - { - if(side == BattleSide::ATTACKER) - pos = 0; //top left - else - pos = GameConstants::BFIELD_WIDTH - 1; //top right - } - - auto accessibility = getAccesibility(); - - std::set occupyable; - for(int i = 0; i < accessibility.size(); i++) - if(accessibility.accessible(i, twoHex, side)) - occupyable.insert(i); - - if(occupyable.empty()) - { - return BattleHex::INVALID; //all tiles are covered - } - - return BattleHex::getClosestTile(side, pos, occupyable); -} - -si8 CBattleInfoCallback::battleGetTacticDist() const -{ - RETURN_IF_NOT_BATTLE(0); - - //TODO get rid of this method - if(battleDoWeKnowAbout(battleGetTacticsSide())) - return battleTacticDist(); - - return 0; -} - -bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - 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)); -} - -ReachabilityInfo CBattleInfoCallback::getReachability(const battle::Unit * unit) const -{ - ReachabilityInfo::Parameters params(unit, unit->getPosition()); - - if(!battleDoWeKnowAbout(unit->unitSide())) - { - //Stack is held by enemy, we can't use his perspective to check for reachability. - // Happens ie. when hovering enemy stack for its range. The arg could be set properly, but it's easier to fix it here. - params.perspective = battleGetMySide(); - } - - return getReachability(params); -} - -ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Parameters ¶ms) const -{ - if(params.flying) - return getFlyingReachability(params); - else - return makeBFS(getAccesibility(params.knownAccessible), params); -} - -ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const -{ - ReachabilityInfo ret; - ret.accessibility = getAccesibility(params.knownAccessible); - - for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) - { - if(ret.accessibility.accessible(i, params.doubleWide, params.side)) - { - ret.predecessors[i] = params.startPosition; - ret.distances[i] = BattleHex::getDistance(params.startPosition, i); - } - } - - return ret; -} - -AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) 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); - if(reverse && attacker->doubleWide()) - { - attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on - } - if(attacker->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT)) - { - boost::copy(attacker->getSurroundingHexes(attackerPos), vstd::set_inserter(at.hostileCreaturePositions)); - } - if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK)) - { - std::vector hexes = attacker->getSurroundingHexes(attackerPos); - for(BattleHex tile : hexes) - { - if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, attackOriginHex) > -1)) //adjacent both to attacker's head and attacked tile - { - const auto * st = battleGetUnitByPos(tile, true); - if(st && battleMatchOwner(st, attacker)) //only hostile stacks - does it work well with Berserk? - at.hostileCreaturePositions.insert(tile); - } - } - } - if(attacker->hasBonusOfType(BonusType::WIDE_BREATH)) - { - std::vector hexes = destinationTile.neighbouringTiles(); - for(int i = 0; ihasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) - { - auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); - if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation - { - BattleHex nextHex = destinationTile.cloneInDirection(direction, false); - - if ( defender->doubleWide() ) - { - auto secondHex = destinationTile == defender->getPosition() ? - defender->occupiedHex(): - defender->getPosition(); - - // 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) - at.friendlyCreaturePositions.insert(nextHex); - } - } - } - return at; -} - -AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const -{ - //does not return hex attacked directly - AttackableTiles at; - RETURN_IF_NOT_BATTLE(at); - - if(attacker->hasBonusOfType(BonusType::SHOOTS_ALL_ADJACENT) && !vstd::contains(attackerPos.neighbouringTiles(), destinationTile)) - { - std::vector targetHexes = destinationTile.neighbouringTiles(); - targetHexes.push_back(destinationTile); - boost::copy(targetHexes, vstd::set_inserter(at.hostileCreaturePositions)); - } - - return at; -} - -std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const -{ - std::vector units; - RETURN_IF_NOT_BATTLE(units); - - AttackableTiles at; - - if (rangedAttack) - at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); - else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - units = battleGetUnitsIf([=](const battle::Unit * unit) - { - if (unit->isGhost() || !unit->alive()) - return false; - - for (BattleHex hex : battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide())) - { - if (vstd::contains(at.hostileCreaturePositions, hex)) - return true; - if (vstd::contains(at.friendlyCreaturePositions, hex)) - return true; - } - return false; - }); - - return units; -} - -std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const -{ - std::set attackedCres; - RETURN_IF_NOT_BATTLE(attackedCres); - - AttackableTiles at; - - if(rangedAttack) - at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); - else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - for (BattleHex tile : at.hostileCreaturePositions) //all around & three-headed attack - { - const CStack * st = battleGetStackByPos(tile, true); - if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? - { - attackedCres.insert(st); - } - } - for (BattleHex tile : at.friendlyCreaturePositions) - { - const CStack * st = battleGetStackByPos(tile, true); - if(st) //friendly stacks can also be damaged by Dragon Breath - { - attackedCres.insert(st); - } - } - return attackedCres; -} - -static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type 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 }; - - auto mutualPos = BattleHex::mutualPosition(hex, testHex); - - if (side == BattleSide::ATTACKER) - return rightDirs.count(mutualPos); - else - return leftDirs.count(mutualPos); -} - -//TODO: this should apply also to mechanics and cursor interface -bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const -{ - BattleHex attackerHex = attacker->getPosition(); - BattleHex defenderHex = defender->getPosition(); - - if (attackerHex < 0 ) //turret - return false; - - if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) - return false; - - if (defender->doubleWide()) - { - if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) - return false; - } - - if (attacker->doubleWide()) - { - if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) - return false; - } - - // a bit weird case since here defender is slightly behind attacker, so reversing seems preferable, - // 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()))) - return false; - } - return true; -} - -ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const -{ - ReachabilityInfo::TDistances ret; - ret.fill(-1); - RETURN_IF_NOT_BATTLE(ret); - - auto reachability = getReachability(unit); - - boost::copy(reachability.distances, ret.begin()); - - return ret; -} - -bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const -{ - RETURN_IF_NOT_BATTLE(false); - - const std::string cachingStrNoDistancePenalty = "type_NO_DISTANCE_PENALTY"; - static const auto selectorNoDistancePenalty = Selector::type()(BonusType::NO_DISTANCE_PENALTY); - - if(shooter->hasBonus(selectorNoDistancePenalty, cachingStrNoDistancePenalty)) - return false; - - if(const auto * target = battleGetUnitByPos(destHex, true)) - { - //If any hex of target creature is within range, there is no penalty - int range = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE; - - auto bonus = shooter->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); - if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE) - range = bonus->additionalInfo[0]; - - if(isEnemyUnitWithinSpecifiedRange(shooterPosition, target, range)) - return false; - } - else - { - if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE) - return false; - } - - return true; -} - -bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const -{ - for(auto hex : defenderUnit->getHexes()) - if(BattleHex::getDistance(attackerPosition, hex) <= range) - return true; - - return false; -} - -BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const -{ - RETURN_IF_NOT_BATTLE(BattleHex::INVALID); - return WallPartToHex(part); -} - -EWallPart CBattleInfoCallback::battleHexToWallPart(BattleHex hex) const -{ - RETURN_IF_NOT_BATTLE(EWallPart::INVALID); - return hexToWallPart(hex); -} - -bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) const -{ - RETURN_IF_NOT_BATTLE(false); - return wallPart != EWallPart::INDESTRUCTIBLE_PART && wallPart != EWallPart::INDESTRUCTIBLE_PART_OF_GATE && - wallPart != EWallPart::INVALID; -} - -bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(isWallPartPotentiallyAttackable(wallPart)) - { - auto wallState = battleGetWallState(wallPart); - return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED); - } - return false; -} - -std::vector CBattleInfoCallback::getAttackableBattleHexes() const -{ - std::vector attackableBattleHexes; - RETURN_IF_NOT_BATTLE(attackableBattleHexes); - - for(const auto & wallPartPair : wallParts) - { - if(isWallPartAttackable(wallPartPair.second)) - attackableBattleHexes.emplace_back(wallPartPair.first); - } - - return attackableBattleHexes; -} - -int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const -{ - RETURN_IF_NOT_BATTLE(-1); - //TODO should be replaced using bonus system facilities (propagation onto battle node) - - int32_t ret = caster->getSpellCost(sp); - - //checking for friendly stacks reducing cost of the spell and - //enemy stacks increasing it - int32_t manaReduction = 0; - int32_t manaIncrease = 0; - - for(const auto * unit : battleAliveUnits()) - { - if(unit->unitOwner() == caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ALLY)) - { - vstd::amax(manaReduction, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ALLY)); - } - if(unit->unitOwner() != caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)) - { - vstd::amax(manaIncrease, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)); - } - } - - return ret - manaReduction + manaIncrease; -} - -bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const -{ - return battleHasDistancePenalty(shooter, shooter->getPosition(), destHex) || battleHasWallPenalty(shooter, shooter->getPosition(), destHex); -} - -bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked - return false; - - for(const auto * adjacent : battleAdjacentUnits(unit)) - { - if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack - return true; - } - return false; -} - -std::set CBattleInfoCallback::battleAdjacentUnits(const battle::Unit * unit) const -{ - std::set ret; - RETURN_IF_NOT_BATTLE(ret); - - for(auto hex : unit->getSurroundingHexes()) - { - if(const auto * neighbour = battleGetUnitByPos(hex, true)) - ret.insert(neighbour); - } - - return ret; -} - -SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const -{ - RETURN_IF_NOT_BATTLE(SpellID::NONE); - //This is complete list. No spells from mods. - //todo: this should be Spellbook of caster Stack - static const std::set allPossibleSpells = - { - SpellID::AIR_SHIELD, - SpellID::ANTI_MAGIC, - SpellID::BLESS, - SpellID::BLOODLUST, - SpellID::COUNTERSTRIKE, - SpellID::CURE, - SpellID::FIRE_SHIELD, - SpellID::FORTUNE, - SpellID::HASTE, - SpellID::MAGIC_MIRROR, - SpellID::MIRTH, - SpellID::PRAYER, - SpellID::PRECISION, - SpellID::PROTECTION_FROM_AIR, - SpellID::PROTECTION_FROM_EARTH, - SpellID::PROTECTION_FROM_FIRE, - SpellID::PROTECTION_FROM_WATER, - SpellID::SHIELD, - SpellID::SLAYER, - SpellID::STONE_SKIN - }; - std::vector beneficialSpells; - - auto getAliveEnemy = [=](const std::function & pred) -> const CStack * - { - auto stacks = battleGetStacksIf([=](const CStack * stack) - { - return pred(stack) && stack->unitOwner() != subject->unitOwner() && stack->isValidTarget(false); - }); - - if(stacks.empty()) - return nullptr; - else - return stacks.front(); - }; - - for(const SpellID& spellID : allPossibleSpells) - { - std::stringstream cachingStr; - cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; - - if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, spellID), Selector::all, cachingStr.str()) - //TODO: this ability has special limitations - || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) - continue; - - switch (spellID) - { - case SpellID::SHIELD: - case SpellID::FIRE_SHIELD: // not if all enemy units are shooters - { - const auto * walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack - { - return !stack->canShoot(); - }); - - if(!walker) - continue; - } - break; - case SpellID::AIR_SHIELD: //only against active shooters - { - const auto * shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack - { - return stack->canShoot(); - }); - if(!shooter) - continue; - } - break; - case SpellID::ANTI_MAGIC: - case SpellID::MAGIC_MIRROR: - case SpellID::PROTECTION_FROM_AIR: - case SpellID::PROTECTION_FROM_EARTH: - case SpellID::PROTECTION_FROM_FIRE: - case SpellID::PROTECTION_FROM_WATER: - { - const ui8 enemySide = 1 - subject->unitSide(); - //todo: only if enemy has spellbook - if (!battleHasHero(enemySide)) //only if there is enemy hero - continue; - } - break; - case SpellID::CURE: //only damaged units - { - //do not cast on affected by debuffs - if(!subject->canBeHealed()) - continue; - } - break; - case SpellID::BLOODLUST: - { - if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent - continue; - } - break; - case SpellID::PRECISION: - { - if(!subject->canShoot()) - continue; - } - break; - case SpellID::SLAYER://only if monsters are present - { - const auto * kingMonster = getAliveEnemy([&](const CStack * stack) -> bool //look for enemy, non-shooting stack - { - const auto isKing = Selector::type()(BonusType::KING); - - return stack->hasBonus(isKing); - }); - - if (!kingMonster) - continue; - } - break; - } - beneficialSpells.push_back(spellID); - } - - if(!beneficialSpells.empty()) - { - return *RandomGeneratorUtil::nextItem(beneficialSpells, rand); - } - else - { - return SpellID::NONE; - } -} - -SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const CStack * caster) const -{ - RETURN_IF_NOT_BATTLE(SpellID::NONE); - - TConstBonusListPtr bl = caster->getBonuses(Selector::type()(BonusType::SPELLCASTER)); - if (!bl->size()) - return SpellID::NONE; - - if(bl->size() == 1) - return SpellID(bl->front()->subtype); - - int totalWeight = 0; - for(const auto & b : *bl) - { - totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them - } - - if (totalWeight == 0) - return SpellID::NONE; - - int randomPos = rand.nextInt(totalWeight - 1); - for(const auto & b : *bl) - { - randomPos -= std::max(b->additionalInfo[0], 0); - if(randomPos < 0) - { - return SpellID(b->subtype); - } - } - - return SpellID::NONE; -} - -int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) const -{ - RETURN_IF_NOT_BATTLE(-3); - if(!battleCanSurrender(Player)) - return -1; - - const auto sideOpt = playerToSide(Player); - if(!sideOpt) - return -1; - const auto side = sideOpt.value(); - - int ret = 0; - double discount = 0; - - for(const auto * unit : battleAliveUnits(side)) - ret += unit->getRawSurrenderCost(); - - if(const CGHeroInstance * h = battleGetFightingHero(side)) - discount += h->valOfBonuses(BonusType::SURRENDER_DISCOUNT); - - ret = static_cast(ret * (100.0 - discount) / 100.0); - vstd::amax(ret, 0); //no negative costs for >100% discounts (impossible in original H3 mechanics, but some day...) - return ret; -} - -si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const -{ - const IBonusBearer * node = nullptr; - if(const CGHeroInstance * h = battleGetFightingHero(side)) - node = h; - else - node = getBonusBearer(); - - if(!node) - return 0; - - auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_BELOW)); - if(b->size()) - return b->totalValue(); - - return 0; -} - -si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const -{ - const IBonusBearer *node = nullptr; - if(const CGHeroInstance * h = battleGetFightingHero(side)) - node = h; - else - node = getBonusBearer(); - - if(!node) - return GameConstants::SPELL_LEVELS; - - //We can't "just get value" - it'd be 0 if there are bonuses (and all would be blocked) - auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); - if(b->size()) - return b->totalValue(); - - return GameConstants::SPELL_LEVELS; -} - -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 - - for(auto & unit : units) - { - //todo: move SIEGE_WEAPON check to Unit state - hasUnit.at(unit->unitSide()) = true; - - if(hasUnit[0] && hasUnit[1]) - return std::nullopt; - } - - hasUnit = {false, false}; - - for(auto & unit : units) - { - if(!unit->isClone() && !unit->acquireState()->summoned && !dynamic_cast (unit)) - { - hasUnit.at(unit->unitSide()) = true; - } - } - - if(!hasUnit[0] && !hasUnit[1]) - return 2; - if(!hasUnit[1]) - return 0; - else - return 1; -} - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoCallback.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 "CBattleInfoCallback.h" + +#include + +#include "../CStack.h" +#include "BattleInfo.h" +#include "CObstacleInstance.h" +#include "DamageCalculator.h" +#include "PossiblePlayerBattleAction.h" +#include "../spells/ObstacleCasterProxy.h" +#include "../spells/ISpellMechanics.h" +#include "../spells/Problem.h" +#include "../spells/CSpellHandler.h" +#include "../mapObjects/CGTownInstance.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../BattleFieldHandler.h" +#include "../Rect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO +{ + +static BattleHex lineToWallHex(int line) //returns hex with wall in given line (y coordinate) +{ + static const BattleHex lineToHex[] = {12, 29, 45, 62, 78, 96, 112, 130, 147, 165, 182}; + + return lineToHex[line]; +} + +static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) +{ + const int wallInStackLine = lineToWallHex(pos1.getY()); + const int wallInDestLine = lineToWallHex(pos2.getY()); + + const bool stackLeft = pos1 < wallInStackLine; + const bool destLeft = pos2 < wallInDestLine; + + return stackLeft == destLeft; +} + +// parts of wall +static const std::pair wallParts[] = +{ + std::make_pair(50, EWallPart::KEEP), + std::make_pair(183, EWallPart::BOTTOM_TOWER), + std::make_pair(182, EWallPart::BOTTOM_WALL), + std::make_pair(130, EWallPart::BELOW_GATE), + std::make_pair(78, EWallPart::OVER_GATE), + std::make_pair(29, EWallPart::UPPER_WALL), + std::make_pair(12, EWallPart::UPPER_TOWER), + std::make_pair(95, EWallPart::INDESTRUCTIBLE_PART_OF_GATE), + std::make_pair(96, EWallPart::GATE), + std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(62, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(165, EWallPart::INDESTRUCTIBLE_PART) +}; + +static EWallPart hexToWallPart(BattleHex hex) +{ + for(const auto & elem : wallParts) + { + if(elem.first == hex) + return elem.second; + } + + return EWallPart::INVALID; //not found! +} + +static BattleHex WallPartToHex(EWallPart part) +{ + for(const auto & elem : wallParts) + { + if(elem.second == part) + return elem.first; + } + + return BattleHex::INVALID; //not found! +} +} + +using namespace SiegeStuffThatShouldBeMovedToHandlers; + +ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const +{ + RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); + if(caster == nullptr) + { + logGlobal->error("CBattleInfoCallback::battleCanCastSpell: no spellcaster."); + return ESpellCastProblem::INVALID; + } + const PlayerColor player = caster->getCasterOwner(); + const auto side = playerToSide(player); + if(!side) + return ESpellCastProblem::INVALID; + if(!battleDoWeKnowAbout(side.value())) + { + logGlobal->warn("You can't check if enemy can cast given spell!"); + return ESpellCastProblem::INVALID; + } + + if(battleTacticDist()) + return ESpellCastProblem::ONGOING_TACTIC_PHASE; + + switch(mode) + { + case spells::Mode::HERO: + { + if(battleCastSpells(side.value()) > 0) + return ESpellCastProblem::CASTS_PER_TURN_LIMIT; + + const auto * hero = dynamic_cast(caster); + + if(!hero) + return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; + if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)) + return ESpellCastProblem::MAGIC_IS_BLOCKED; + } + break; + default: + break; + } + + return ESpellCastProblem::OK; +} + +std::pair< std::vector, int > CBattleInfoCallback::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const +{ + auto reachability = getReachability(stack); + + if(reachability.predecessors[dest] == -1) //cannot reach destination + { + return std::make_pair(std::vector(), 0); + } + + //making the Path + std::vector path; + BattleHex curElem = dest; + while(curElem != start) + { + path.push_back(curElem); + curElem = reachability.predecessors[curElem]; + } + + return std::make_pair(path, reachability.distances[dest]); +} + +bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const +{ + auto isTileBlocked = [&](BattleHex tile) + { + EWallPart wallPart = battleHexToWallPart(tile); + if (wallPart == EWallPart::INVALID) + return false; // there is no wall here + if (wallPart == EWallPart::INDESTRUCTIBLE_PART_OF_GATE) + return false; // does not blocks ranged attacks + if (wallPart == EWallPart::INDESTRUCTIBLE_PART) + return true; // always blocks ranged attacks + + return isWallPartAttackable(wallPart); + }; + // Count wall penalty requirement by shortest path, not by arbitrary line, to avoid various OH3 bugs + auto getShortestPath = [](BattleHex from, BattleHex dest) -> std::vector + { + //Out early + if(from == dest) + return {}; + + std::vector ret; + auto next = from; + //Not a real direction, only to indicate to which side we should search closest tile + auto direction = from.getX() > dest.getX() ? BattleSide::DEFENDER : BattleSide::ATTACKER; + + while (next != dest) + { + auto tiles = next.neighbouringTiles(); + std::set possibilities = {tiles.begin(), tiles.end()}; + next = BattleHex::getClosestTile(direction, dest, possibilities); + ret.push_back(next); + } + assert(!ret.empty()); + ret.pop_back(); //Remove destination hex + return ret; + }; + + RETURN_IF_NOT_BATTLE(false); + auto checkNeeded = !sameSideOfWall(from, dest); + bool pathHasWall = false; + bool pathHasMoat = false; + + for(const auto & hex : getShortestPath(from, dest)) + { + pathHasWall |= isTileBlocked(hex); + if(!checkMoat) + continue; + + auto obstacles = battleGetAllObstaclesOnPos(hex, false); + + if(hex != BattleHex::GATE_BRIDGE || (battleIsGatePassable())) + for(const auto & obst : obstacles) + if(obst->obstacleType == CObstacleInstance::MOAT) + pathHasMoat |= true; + } + + return checkNeeded && ( (checkWall && pathHasWall) || (checkMoat && pathHasMoat) ); +} + +bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const +{ + RETURN_IF_NOT_BATTLE(false); + if(!battleGetSiegeLevel()) + return false; + + const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY"; + static const auto selectorNoWallPenalty = Selector::type()(BonusType::NO_WALL_PENALTY); + + if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty)) + return false; + + const auto shooterOutsideWalls = shooterPosition < lineToWallHex(shooterPosition.getY()); + + return shooterOutsideWalls && battleHasPenaltyOnLine(shooterPosition, destHex, true, false); +} + +std::vector CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data) +{ + RETURN_IF_NOT_BATTLE(std::vector()); + std::vector allowedActionList; + if(data.tacticsMode) //would "if(battleGetTacticDist() > 0)" work? + { + allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_TACTICS); + allowedActionList.push_back(PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK); + } + else + { + if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? + { + if(stack->hasBonusOfType(BonusType::SPELLCASTER)) + { + for(const auto & spellID : data.creatureSpellsToCast) + { + const CSpell *spell = spellID.toSpell(); + PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); + allowedActionList.push_back(act); + } + } + if(stack->hasBonusOfType(BonusType::RANDOM_SPELLCASTER)) + allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); + } + if(stack->canShoot()) + allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT); + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) + allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN); + + allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack + allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere + + if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks + allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); + + const auto * siegedTown = battleGetDefendedTown(); + if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots + allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT); + if(stack->hasBonusOfType(BonusType::HEALER)) + allowedActionList.push_back(PossiblePlayerBattleAction::HEAL); + } + + return allowedActionList; +} + +PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const +{ + RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID); + auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; + + const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); + + if(ti.massive || ti.type == spells::AimType::NO_TARGET) + spellSelMode = PossiblePlayerBattleAction::NO_LOCATION; + else if(ti.type == spells::AimType::LOCATION && ti.clearAffected) + spellSelMode = PossiblePlayerBattleAction::FREE_LOCATION; + else if(ti.type == spells::AimType::CREATURE) + spellSelMode = PossiblePlayerBattleAction::AIMED_SPELL_CREATURE; + else if(ti.type == spells::AimType::OBSTACLE) + spellSelMode = PossiblePlayerBattleAction::OBSTACLE; + + return PossiblePlayerBattleAction(spellSelMode, spell->id); +} + +std::set CBattleInfoCallback::battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const +{ + std::set attackedHexes; + RETURN_IF_NOT_BATTLE(attackedHexes); + + AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + for (BattleHex tile : at.hostileCreaturePositions) + { + const auto * st = battleGetUnitByPos(tile, true); + if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? + { + attackedHexes.insert(tile); + } + } + for (BattleHex tile : at.friendlyCreaturePositions) + { + if(battleGetUnitByPos(tile, true)) //friendly stacks can also be damaged by Dragon Breath + { + attackedHexes.insert(tile); + } + } + return attackedHexes; +} + +SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const +{ + switch (mode) + { + case RANDOM_GENIE: + return getRandomBeneficialSpell(rand, stack); //target + break; + case RANDOM_AIMED: + return getRandomCastedSpell(rand, stack); //caster + break; + default: + logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast(mode)); + return SpellID::NONE; + } +} + +const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + for(const auto * s : battleGetAllStacks(true)) + if(vstd::contains(s->getHexes(), pos) && (!onlyAlive || s->alive())) + return s; + + return nullptr; +} + +const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + auto ret = battleGetUnitsIf([=](const battle::Unit * unit) + { + return !unit->isGhost() + && unit->coversPos(pos) + && (!onlyAlive || unit->alive()); + }); + + if(!ret.empty()) + return ret.front(); + else + return nullptr; +} + +battle::Units CBattleInfoCallback::battleAliveUnits() const +{ + return battleGetUnitsIf([](const battle::Unit * unit) + { + return unit->isValidTarget(false); + }); +} + +battle::Units CBattleInfoCallback::battleAliveUnits(ui8 side) const +{ + return battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->isValidTarget(false) && unit->unitSide() == side; + }); +} + +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 * returnedUnit = nullptr; + size_t currentUnitIndex = 0; + + for(size_t i = 0; i < allUnits.size(); i++) + { + int32_t currentUnitInitiative = -1; + int32_t returnedUnitInitiative = -1; + + if(returnedUnit) + returnedUnitInitiative = returnedUnit->getInitiative(turn); + + if(!allUnits[i]) + continue; + + auto currentUnit = allUnits[i]; + currentUnitInitiative = currentUnit->getInitiative(turn); + + switch(phase) + { + case BattlePhases::NORMAL: // Faster first, attacker priority, higher slot first + if(returnedUnit == nullptr || currentUnitInitiative > returnedUnitInitiative) + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + else if(currentUnitInitiative == returnedUnitInitiative) + { + if(sideThatLastMoved == -1 && 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 + && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + } + break; + case BattlePhases::WAIT_MORALE: // Slower first, higher slot first + case BattlePhases::WAIT: + if(returnedUnit == nullptr || currentUnitInitiative < returnedUnitInitiative) + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved + && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + break; + default: + break; + } + } + + if(!returnedUnit) + return nullptr; + + allUnits[currentUnitIndex] = nullptr; + + return returnedUnit; +} + +void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, int8_t sideThatLastMoved) const +{ + RETURN_IF_NOT_BATTLE(); + + if(maxUnits == 0 && maxTurns == 0) + { + logGlobal->error("Attempt to get infinite battle queue"); + return; + } + + auto actualTurn = turn > 0 ? turn : 0; + + auto turnsIsFull = [&]() -> bool + { + if(maxUnits == 0) + return false;//no limit + + size_t turnsSize = 0; + for(const auto & oneTurn : turns) + turnsSize += oneTurn.size(); + return turnsSize >= maxUnits; + }; + + turns.emplace_back(); + + // We'll split creatures with remaining movement to 4 buckets (SIEGE, NORMAL, WAIT_MORALE, WAIT) + std::array phases; // Access using BattlePhases enum + + const battle::Unit * activeUnit = battleActiveUnit(); + + 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()) + { + turns.back().push_back(activeUnit); + if(turnsIsFull()) + return; + } + + //its first or current turn, turn priority for active stack side + //TODO: what if active stack mind-controlled? + if(turn <= 0 && sideThatLastMoved < 0) + sideThatLastMoved = activeUnit->unitSide(); + } + + auto allUnits = battleGetUnitsIf([](const battle::Unit * unit) + { + return !unit->isGhost(); + }); + + // If no unit will be EVER! able to move, battle is over. + if(!vstd::contains_if(allUnits, [](const battle::Unit * unit) { return unit->willMove(100000); })) //little evil, but 100000 should be enough for all effects to disappear + { + turns.clear(); + return; + } + + for(const auto * unit : allUnits) + { + if((actualTurn == 0 && !unit->willMove()) //we are considering current round and unit won't move + || (actualTurn > 0 && !unit->canMove(turn)) //unit won't be able to move in later rounds + || (actualTurn == 0 && unit == activeUnit && !turns.at(0).empty() && unit == turns.front().front())) //it's active unit already added at the beginning of queue + { + continue; + } + + int unitPhase = unit->battleQueuePhase(turn); + + phases[unitPhase].push_back(unit); + } + + boost::sort(phases[BattlePhases::SIEGE], CMP_stack(BattlePhases::SIEGE, actualTurn, sideThatLastMoved)); + std::copy(phases[BattlePhases::SIEGE].begin(), phases[BattlePhases::SIEGE].end(), std::back_inserter(turns.back())); + + if(turnsIsFull()) + return; + + for(uint8_t phase = BattlePhases::NORMAL; phase < BattlePhases::NUMBER_OF_PHASES; phase++) + boost::sort(phases[phase], CMP_stack(phase, actualTurn, sideThatLastMoved)); + + uint8_t phase = BattlePhases::NORMAL; + while(!turnsIsFull() && phase < BattlePhases::NUMBER_OF_PHASES) + { + const battle::Unit * currentUnit = nullptr; + if(phases[phase].empty()) + phase++; + else + { + currentUnit = takeOneUnit(phases[phase], actualTurn, sideThatLastMoved, phase); + if(!currentUnit) + { + phase++; + } + else + { + turns.back().push_back(currentUnit); + sideThatLastMoved = currentUnit->unitSide(); + } + } + } + + if(sideThatLastMoved < 0) + sideThatLastMoved = BattleSide::ATTACKER; + + if(!turnsIsFull() && (maxTurns == 0 || turns.size() < maxTurns)) + battleGetTurnOrder(turns, maxUnits, maxTurns, actualTurn + 1, sideThatLastMoved); +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const +{ + + RETURN_IF_NOT_BATTLE(std::vector()); + if(!unit->getPosition().isValid()) //turrets + return std::vector(); + + auto reachability = getReachability(unit); + + return battleGetAvailableHexes(reachability, unit, obtainMovementRange); +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const +{ + std::vector ret; + + RETURN_IF_NOT_BATTLE(ret); + if(!unit->getPosition().isValid()) //turrets + return ret; + + auto unitSpeed = unit->speed(0, true); + + const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); + + for(int i = 0; i < GameConstants::BFIELD_SIZE; ++i) + { + // If obstacles or other stacks makes movement impossible, it can't be helped. + if(!cache.isReachable(i)) + continue; + + if(tacticsPhase && !obtainMovementRange) // if obtainMovementRange requested do not return tactics range + { + // Stack has to perform tactic-phase movement -> can enter any reachable tile within given range + if(!isInTacticRange(i)) + continue; + } + else + { + // Not tactics phase -> destination must be reachable and within unit range. + if(cache.distances[i] > static_cast(unitSpeed)) + continue; + } + + ret.emplace_back(i); + } + + return ret; +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const +{ + std::vector ret = battleGetAvailableHexes(unit, obtainMovementRange); + + if(ret.empty()) + return ret; + + if(addOccupiable && unit->doubleWide()) + { + std::vector occupiable; + + occupiable.reserve(ret.size()); + for(auto hex : ret) + occupiable.push_back(unit->occupiedHex(hex)); + + vstd::concatenate(ret, occupiable); + } + + + if(attackable) + { + auto meleeAttackable = [&](BattleHex hex) -> bool + { + // Return true if given hex has at least one available neighbour. + // Available hexes are already present in ret vector. + auto availableNeighbor = boost::find_if(ret, [=] (BattleHex availableHex) + { + return BattleHex::mutualPosition(hex, availableHex) >= 0; + }); + return availableNeighbor != ret.end(); + }; + for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide()))) + { + if(!otherSt->isValidTarget(false)) + continue; + + std::vector occupied = otherSt->getHexes(); + + if(battleCanShoot(unit, otherSt->getPosition())) + { + attackable->insert(attackable->end(), occupied.begin(), occupied.end()); + continue; + } + + for(BattleHex he : occupied) + { + if(meleeAttackable(he)) + attackable->push_back(he); + } + } + } + + //adding occupiable likely adds duplicates to ret -> clean it up + boost::sort(ret); + ret.erase(boost::unique(ret).end(), ret.end()); + return ret; +} + +bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(battleTacticDist()) + return false; + + if (!stack || !target) + return false; + + if(!battleMatchOwner(stack, target)) + return false; + + auto id = stack->unitType()->getId(); + if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) + return false; + + return target->alive(); +} + +bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(battleTacticDist()) //no shooting during tactics + return false; + + if (!attacker) + return false; + if (attacker->creatureIndex() == CreatureID::CATAPULT) //catapult cannot attack creatures + return false; + + //forgetfulness + TConstBonusListPtr forgetfulList = attacker->getBonuses(Selector::type()(BonusType::FORGETFULL)); + if(!forgetfulList->empty()) + { + int forgetful = forgetfulList->valOfBonuses(Selector::type()(BonusType::FORGETFULL)); + + //advanced+ level + if(forgetful > 1) + return false; + } + + return attacker->canShoot() && (!battleIsUnitBlocked(attacker) + || attacker->hasBonusOfType(BonusType::FREE_SHOOTING)); +} + +bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + + const battle::Unit * defender = battleGetUnitByPos(dest); + if(!attacker || !defender) + return false; + + if(battleMatchOwner(attacker, defender) && defender->alive()) + { + if(battleCanShoot(attacker)) + { + auto limitedRangeBonus = attacker->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); + if(limitedRangeBonus == nullptr) + { + return true; + } + + int shootingRange = limitedRangeBonus->val; + return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); + } + } + + return false; +} + +DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const +{ + DamageCalculator calculator(*this, info); + + return calculator.calculateDmgRange(); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + auto reachability = battleGetDistances(attacker, attacker->getPosition()); + int movementDistance = reachability[attackerPosition]; + return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + const bool shooting = battleCanShoot(attacker, defender->getPosition()); + const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); + return battleEstimateDamage(bai, retaliationDmg); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + + DamageEstimation ret = calculateDmgRange(bai); + + if(retaliationDmg) + { + if(bai.shooting) + { + //FIXME: handle RANGED_RETALIATION + *retaliationDmg = DamageEstimation(); + } + else + { + //TODO: rewrite using boost::numeric::interval + //TODO: rewire once more using interval-based fuzzy arithmetic + + const auto & estimateRetaliation = [&](int64_t damage) + { + auto retaliationAttack = bai.reverse(); + auto state = retaliationAttack.attacker->acquireState(); + state->damage(damage); + retaliationAttack.attacker = state.get(); + return calculateDmgRange(retaliationAttack); + }; + + DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); + DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); + + retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); + retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); + + retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); + retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); + } + } + + return ret; +} + +std::vector> CBattleInfoCallback::battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking) const +{ + std::vector> obstacles = std::vector>(); + RETURN_IF_NOT_BATTLE(obstacles); + for(auto & obs : battleGetAllObstacles()) + { + if(vstd::contains(obs->getBlockedTiles(), tile) + || (!onlyBlocking && vstd::contains(obs->getAffectedTiles(), tile))) + { + obstacles.push_back(obs); + } + } + return obstacles; +} + +std::vector> CBattleInfoCallback::getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const +{ + auto affectedObstacles = std::vector>(); + RETURN_IF_NOT_BATTLE(affectedObstacles); + if(unit->alive()) + { + if(!passed.count(unit->getPosition())) + affectedObstacles = battleGetAllObstaclesOnPos(unit->getPosition(), false); + if(unit->doubleWide()) + { + BattleHex otherHex = unit->occupiedHex(); + if(otherHex.isValid() && !passed.count(otherHex)) + for(auto & i : battleGetAllObstaclesOnPos(otherHex, false)) + if(!vstd::contains(affectedObstacles, i)) + affectedObstacles.push_back(i); + } + for(auto hex : unit->getHexes()) + if(hex == BattleHex::GATE_BRIDGE && battleIsGatePassable()) + for(int i=0; iobstacleType == CObstacleInstance::MOAT) + affectedObstacles.erase(affectedObstacles.begin()+i); + } + return affectedObstacles; +} + +bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed) const +{ + if(!unit.alive()) + return false; + bool movementStopped = false; + for(auto & obstacle : getAllAffectedObstaclesByStack(&unit, passed)) + { + //helper info + const SpellCreatedObstacle * spellObstacle = dynamic_cast(obstacle.get()); + + if(spellObstacle) + { + auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void + { + // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage + auto operation = ObstacleChanges::EOperation::UPDATE; + if (spellObstacle.removeOnTrigger) + operation = ObstacleChanges::EOperation::REMOVE; + + SpellCreatedObstacle changedObstacle; + changedObstacle.uniqueID = spellObstacle.uniqueID; + changedObstacle.revealed = true; + + BattleObstaclesChanged bocp; + bocp.battleID = getBattle()->getBattleID(); + bocp.changes.emplace_back(spellObstacle.uniqueID, operation); + changedObstacle.toInfo(bocp.changes.back(), operation); + spellEnv.apply(&bocp); + }; + const auto side = unit.unitSide(); + auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side); + const auto * hero = battleGetFightingHero(spellObstacle->casterSide); + auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); + const auto * sp = obstacle->getTrigger().toSpell(); + if(obstacle->triggersEffects() && sp) + { + auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp); + spells::detail::ProblemImpl ignored; + auto target = spells::Target(1, spells::Destination(&unit)); + if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures + { + if(shouldReveal) { //hidden obstacle triggers effects after revealed + revealObstacles(*spellObstacle); + cast.cast(&spellEnv, target); + } + } + } + else if(shouldReveal) + revealObstacles(*spellObstacle); + } + + if(!unit.alive()) + return false; + + if(obstacle->stopsMovement()) + movementStopped = true; + } + + return unit.alive() && !movementStopped; +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility() const +{ + AccessibilityInfo ret; + ret.fill(EAccessibility::ACCESSIBLE); + + //removing accessibility for side columns of hexes + for(int y = 0; y < GameConstants::BFIELD_HEIGHT; y++) + { + ret[BattleHex(GameConstants::BFIELD_WIDTH - 1, y)] = EAccessibility::SIDE_COLUMN; + ret[BattleHex(0, y)] = EAccessibility::SIDE_COLUMN; + } + + //special battlefields with logically unavailable tiles + auto bFieldType = battleGetBattlefieldType(); + + if(bFieldType != BattleField::NONE) + { + std::vector impassableHexes = bFieldType.getInfo()->impassableHexes; + + for(auto hex : impassableHexes) + ret[hex] = EAccessibility::UNAVAILABLE; + } + + //gate -> should be before stacks + if(battleGetSiegeLevel() > 0) + { + EAccessibility accessability = EAccessibility::ACCESSIBLE; + switch(battleGetGateState()) + { + case EGateState::CLOSED: + accessability = EAccessibility::GATE; + break; + + case EGateState::BLOCKED: + accessability = EAccessibility::UNAVAILABLE; + break; + } + ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessability; + } + + //tiles occupied by standing stacks + for(const auto * unit : battleAliveUnits()) + { + for(auto hex : unit->getHexes()) + if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns + ret[hex] = EAccessibility::ALIVE_STACK; + } + + //obstacles + for(const auto &obst : battleGetAllObstacles()) + { + for(auto hex : obst->getBlockedTiles()) + ret[hex] = EAccessibility::OBSTACLE; + } + + //walls + if(battleGetSiegeLevel() > 0) + { + static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165}; + for(auto hex : permanentlyLocked) + ret[hex] = EAccessibility::UNAVAILABLE; + + //TODO likely duplicated logic + static const std::pair lockedIfNotDestroyed[] = + { + //which part of wall, which hex is blocked if this part of wall is not destroyed + std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_4)), + std::make_pair(EWallPart::BELOW_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_3)), + std::make_pair(EWallPart::OVER_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_2)), + std::make_pair(EWallPart::UPPER_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_1)) + }; + + for(const auto & elem : lockedIfNotDestroyed) + { + if(battleGetWallState(elem.first) != EWallState::DESTROYED) + ret[elem.second] = EAccessibility::DESTRUCTIBLE_WALL; + } + } + + return ret; +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility(const battle::Unit * stack) const +{ + return getAccesibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector & accessibleHexes) const +{ + auto ret = getAccesibility(); + for(auto hex : accessibleHexes) + if(hex.isValid()) + ret[hex] = EAccessibility::ACCESSIBLE; + + return ret; +} + +ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters & params) const +{ + ReachabilityInfo ret; + ret.accessibility = accessibility; + ret.params = params; + + ret.predecessors.fill(BattleHex::INVALID); + ret.distances.fill(ReachabilityInfo::INFINITE_DIST); + + if(!params.startPosition.isValid()) //if got call for arrow turrets + return ret; + + const std::set obstacles = getStoppers(params.perspective); + auto checkParams = params; + checkParams.ignoreKnownAccessible = true; //Ignore starting hexes obstacles + + std::queue hexq; //bfs queue + + //first element + hexq.push(params.startPosition); + ret.distances[params.startPosition] = 0; + + std::array accessibleCache{}; + for(int hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) + accessibleCache[hex] = accessibility.accessible(hex, params.doubleWide, params.side); + + while(!hexq.empty()) //bfs loop + { + const BattleHex curHex = hexq.front(); + hexq.pop(); + + //walking stack can't step past the obstacles + if(isInObstacle(curHex, obstacles, checkParams)) + continue; + + const int costToNeighbour = ret.distances[curHex.hex] + 1; + for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) + { + if(neighbour.isValid()) + { + const int costFoundSoFar = ret.distances[neighbour.hex]; + + if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) + { + hexq.push(neighbour); + ret.distances[neighbour.hex] = costToNeighbour; + ret.predecessors[neighbour.hex] = curHex; + } + } + } + } + + return ret; +} + +bool CBattleInfoCallback::isInObstacle( + BattleHex hex, + const std::set & obstacles, + const ReachabilityInfo::Parameters & params) const +{ + auto occupiedHexes = battle::Unit::getHexes(hex, params.doubleWide, params.side); + + for(auto occupiedHex : occupiedHexes) + { + if(params.ignoreKnownAccessible && vstd::contains(params.knownAccessible, occupiedHex)) + continue; + + if(vstd::contains(obstacles, occupiedHex)) + { + if(occupiedHex == BattleHex::GATE_BRIDGE) + { + if(battleGetGateState() != EGateState::DESTROYED && params.side == BattleSide::ATTACKER) + return true; + } + else + return true; + } + } + + return false; +} + +std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const +{ + std::set ret; + RETURN_IF_NOT_BATTLE(ret); + + for(auto &oi : battleGetAllObstacles(whichSidePerspective)) + { + if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) + continue; + + for(const auto & hex : oi->getStoppingTile()) + { + if(hex == BattleHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) + { + if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) + continue; // this tile is disabled by drawbridge on top of it + } + ret.insert(hex); + } + } + return ret; +} + +std::pair CBattleInfoCallback::getNearestStack(const battle::Unit * closest) const +{ + auto reachability = getReachability(closest); + auto avHexes = battleGetAvailableHexes(reachability, closest, false); + + // I hate std::pairs with their undescriptive member names first / second + struct DistStack + { + uint32_t distanceToPred; + BattleHex destination; + const battle::Unit * stack; + }; + + std::vector stackPairs; + + std::vector possible = battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->isValidTarget(false) && unit != closest; + }); + + for(const battle::Unit * st : possible) + { + for(BattleHex hex : avHexes) + if(CStack::isMeleeAttackPossible(closest, st, hex)) + { + DistStack hlp = {reachability.distances[hex], hex, st}; + stackPairs.push_back(hlp); + } + } + + if(!stackPairs.empty()) + { + auto comparator = [](DistStack lhs, DistStack rhs) { return lhs.distanceToPred < rhs.distanceToPred; }; + auto minimal = boost::min_element(stackPairs, comparator); + return std::make_pair(minimal->stack, minimal->destination); + } + else + return std::make_pair(nullptr, BattleHex::INVALID); +} + +BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const +{ + bool twoHex = VLC->creatures()->getById(creID)->isDoubleWide(); + + int pos; + if (initialPos > -1) + pos = initialPos; + else //summon elementals depending on player side + { + if(side == BattleSide::ATTACKER) + pos = 0; //top left + else + pos = GameConstants::BFIELD_WIDTH - 1; //top right + } + + auto accessibility = getAccesibility(); + + std::set occupyable; + for(int i = 0; i < accessibility.size(); i++) + if(accessibility.accessible(i, twoHex, side)) + occupyable.insert(i); + + if(occupyable.empty()) + { + return BattleHex::INVALID; //all tiles are covered + } + + return BattleHex::getClosestTile(side, pos, occupyable); +} + +si8 CBattleInfoCallback::battleGetTacticDist() const +{ + RETURN_IF_NOT_BATTLE(0); + + //TODO get rid of this method + if(battleDoWeKnowAbout(battleGetTacticsSide())) + return battleTacticDist(); + + return 0; +} + +bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + 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)); +} + +ReachabilityInfo CBattleInfoCallback::getReachability(const battle::Unit * unit) const +{ + ReachabilityInfo::Parameters params(unit, unit->getPosition()); + + if(!battleDoWeKnowAbout(unit->unitSide())) + { + //Stack is held by enemy, we can't use his perspective to check for reachability. + // Happens ie. when hovering enemy stack for its range. The arg could be set properly, but it's easier to fix it here. + params.perspective = battleGetMySide(); + } + + return getReachability(params); +} + +ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Parameters ¶ms) const +{ + if(params.flying) + return getFlyingReachability(params); + else + return makeBFS(getAccesibility(params.knownAccessible), params); +} + +ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const +{ + ReachabilityInfo ret; + ret.accessibility = getAccesibility(params.knownAccessible); + + for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + if(ret.accessibility.accessible(i, params.doubleWide, params.side)) + { + ret.predecessors[i] = params.startPosition; + ret.distances[i] = BattleHex::getDistance(params.startPosition, i); + } + } + + return ret; +} + +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) 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); + if(reverse && attacker->doubleWide()) + { + attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on + } + if(attacker->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT)) + { + boost::copy(attacker->getSurroundingHexes(attackerPos), vstd::set_inserter(at.hostileCreaturePositions)); + } + if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK)) + { + std::vector hexes = attacker->getSurroundingHexes(attackerPos); + for(BattleHex tile : hexes) + { + if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, attackOriginHex) > -1)) //adjacent both to attacker's head and attacked tile + { + const auto * st = battleGetUnitByPos(tile, true); + if(st && battleMatchOwner(st, attacker)) //only hostile stacks - does it work well with Berserk? + at.hostileCreaturePositions.insert(tile); + } + } + } + if(attacker->hasBonusOfType(BonusType::WIDE_BREATH)) + { + std::vector hexes = destinationTile.neighbouringTiles(); + for(int i = 0; ihasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) + { + auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation + { + BattleHex nextHex = destinationTile.cloneInDirection(direction, false); + + if ( defender->doubleWide() ) + { + auto secondHex = destinationTile == defender->getPosition() ? + defender->occupiedHex(): + defender->getPosition(); + + // 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) + at.friendlyCreaturePositions.insert(nextHex); + } + } + } + return at; +} + +AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const +{ + //does not return hex attacked directly + AttackableTiles at; + RETURN_IF_NOT_BATTLE(at); + + if(attacker->hasBonusOfType(BonusType::SHOOTS_ALL_ADJACENT) && !vstd::contains(attackerPos.neighbouringTiles(), destinationTile)) + { + std::vector targetHexes = destinationTile.neighbouringTiles(); + targetHexes.push_back(destinationTile); + boost::copy(targetHexes, vstd::set_inserter(at.hostileCreaturePositions)); + } + + return at; +} + +std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +{ + std::vector units; + RETURN_IF_NOT_BATTLE(units); + + AttackableTiles at; + + if (rangedAttack) + at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); + else + at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + units = battleGetUnitsIf([=](const battle::Unit * unit) + { + if (unit->isGhost() || !unit->alive()) + return false; + + for (BattleHex hex : battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide())) + { + if (vstd::contains(at.hostileCreaturePositions, hex)) + return true; + if (vstd::contains(at.friendlyCreaturePositions, hex)) + return true; + } + return false; + }); + + return units; +} + +std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +{ + std::set attackedCres; + RETURN_IF_NOT_BATTLE(attackedCres); + + AttackableTiles at; + + if(rangedAttack) + at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); + else + at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + for (BattleHex tile : at.hostileCreaturePositions) //all around & three-headed attack + { + const CStack * st = battleGetStackByPos(tile, true); + if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? + { + attackedCres.insert(st); + } + } + for (BattleHex tile : at.friendlyCreaturePositions) + { + const CStack * st = battleGetStackByPos(tile, true); + if(st) //friendly stacks can also be damaged by Dragon Breath + { + attackedCres.insert(st); + } + } + return attackedCres; +} + +static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type 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 }; + + auto mutualPos = BattleHex::mutualPosition(hex, testHex); + + if (side == BattleSide::ATTACKER) + return rightDirs.count(mutualPos); + else + return leftDirs.count(mutualPos); +} + +//TODO: this should apply also to mechanics and cursor interface +bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const +{ + BattleHex attackerHex = attacker->getPosition(); + BattleHex defenderHex = defender->getPosition(); + + if (attackerHex < 0 ) //turret + return false; + + if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) + return false; + + if (defender->doubleWide()) + { + if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) + return false; + } + + if (attacker->doubleWide()) + { + if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) + return false; + } + + // a bit weird case since here defender is slightly behind attacker, so reversing seems preferable, + // 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()))) + return false; + } + return true; +} + +ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const +{ + ReachabilityInfo::TDistances ret; + ret.fill(-1); + RETURN_IF_NOT_BATTLE(ret); + + auto reachability = getReachability(unit); + + boost::copy(reachability.distances, ret.begin()); + + return ret; +} + +bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const +{ + RETURN_IF_NOT_BATTLE(false); + + const std::string cachingStrNoDistancePenalty = "type_NO_DISTANCE_PENALTY"; + static const auto selectorNoDistancePenalty = Selector::type()(BonusType::NO_DISTANCE_PENALTY); + + if(shooter->hasBonus(selectorNoDistancePenalty, cachingStrNoDistancePenalty)) + return false; + + if(const auto * target = battleGetUnitByPos(destHex, true)) + { + //If any hex of target creature is within range, there is no penalty + int range = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE; + + auto bonus = shooter->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); + if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE) + range = bonus->additionalInfo[0]; + + if(isEnemyUnitWithinSpecifiedRange(shooterPosition, target, range)) + return false; + } + else + { + if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE) + return false; + } + + return true; +} + +bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const +{ + for(auto hex : defenderUnit->getHexes()) + if(BattleHex::getDistance(attackerPosition, hex) <= range) + return true; + + return false; +} + +BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const +{ + RETURN_IF_NOT_BATTLE(BattleHex::INVALID); + return WallPartToHex(part); +} + +EWallPart CBattleInfoCallback::battleHexToWallPart(BattleHex hex) const +{ + RETURN_IF_NOT_BATTLE(EWallPart::INVALID); + return hexToWallPart(hex); +} + +bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) const +{ + RETURN_IF_NOT_BATTLE(false); + return wallPart != EWallPart::INDESTRUCTIBLE_PART && wallPart != EWallPart::INDESTRUCTIBLE_PART_OF_GATE && + wallPart != EWallPart::INVALID; +} + +bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(isWallPartPotentiallyAttackable(wallPart)) + { + auto wallState = battleGetWallState(wallPart); + return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED); + } + return false; +} + +std::vector CBattleInfoCallback::getAttackableBattleHexes() const +{ + std::vector attackableBattleHexes; + RETURN_IF_NOT_BATTLE(attackableBattleHexes); + + for(const auto & wallPartPair : wallParts) + { + if(isWallPartAttackable(wallPartPair.second)) + attackableBattleHexes.emplace_back(wallPartPair.first); + } + + return attackableBattleHexes; +} + +int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const +{ + RETURN_IF_NOT_BATTLE(-1); + //TODO should be replaced using bonus system facilities (propagation onto battle node) + + int32_t ret = caster->getSpellCost(sp); + + //checking for friendly stacks reducing cost of the spell and + //enemy stacks increasing it + int32_t manaReduction = 0; + int32_t manaIncrease = 0; + + for(const auto * unit : battleAliveUnits()) + { + if(unit->unitOwner() == caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ALLY)) + { + vstd::amax(manaReduction, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ALLY)); + } + if(unit->unitOwner() != caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)) + { + vstd::amax(manaIncrease, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)); + } + } + + return std::max(0, ret - manaReduction + manaIncrease); +} + +bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const +{ + return battleHasDistancePenalty(shooter, shooter->getPosition(), destHex) || battleHasWallPenalty(shooter, shooter->getPosition(), destHex); +} + +bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked + return false; + + for(const auto * adjacent : battleAdjacentUnits(unit)) + { + if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack + return true; + } + return false; +} + +std::set CBattleInfoCallback::battleAdjacentUnits(const battle::Unit * unit) const +{ + std::set ret; + RETURN_IF_NOT_BATTLE(ret); + + for(auto hex : unit->getSurroundingHexes()) + { + if(const auto * neighbour = battleGetUnitByPos(hex, true)) + ret.insert(neighbour); + } + + return ret; +} + +SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const +{ + RETURN_IF_NOT_BATTLE(SpellID::NONE); + //This is complete list. No spells from mods. + //todo: this should be Spellbook of caster Stack + static const std::set allPossibleSpells = + { + SpellID::AIR_SHIELD, + SpellID::ANTI_MAGIC, + SpellID::BLESS, + SpellID::BLOODLUST, + SpellID::COUNTERSTRIKE, + SpellID::CURE, + SpellID::FIRE_SHIELD, + SpellID::FORTUNE, + SpellID::HASTE, + SpellID::MAGIC_MIRROR, + SpellID::MIRTH, + SpellID::PRAYER, + SpellID::PRECISION, + SpellID::PROTECTION_FROM_AIR, + SpellID::PROTECTION_FROM_EARTH, + SpellID::PROTECTION_FROM_FIRE, + SpellID::PROTECTION_FROM_WATER, + SpellID::SHIELD, + SpellID::SLAYER, + SpellID::STONE_SKIN + }; + std::vector beneficialSpells; + + auto getAliveEnemy = [=](const std::function & pred) -> const CStack * + { + auto stacks = battleGetStacksIf([=](const CStack * stack) + { + return pred(stack) && stack->unitOwner() != subject->unitOwner() && stack->isValidTarget(false); + }); + + if(stacks.empty()) + return nullptr; + else + return stacks.front(); + }; + + for(const SpellID& spellID : allPossibleSpells) + { + std::stringstream cachingStr; + cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; + + if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str()) + //TODO: this ability has special limitations + || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) + continue; + + switch (spellID.toEnum()) + { + case SpellID::SHIELD: + case SpellID::FIRE_SHIELD: // not if all enemy units are shooters + { + const auto * walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack + { + return !stack->canShoot(); + }); + + if(!walker) + continue; + } + break; + case SpellID::AIR_SHIELD: //only against active shooters + { + const auto * shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack + { + return stack->canShoot(); + }); + if(!shooter) + continue; + } + break; + case SpellID::ANTI_MAGIC: + case SpellID::MAGIC_MIRROR: + case SpellID::PROTECTION_FROM_AIR: + case SpellID::PROTECTION_FROM_EARTH: + case SpellID::PROTECTION_FROM_FIRE: + case SpellID::PROTECTION_FROM_WATER: + { + const ui8 enemySide = 1 - subject->unitSide(); + //todo: only if enemy has spellbook + if (!battleHasHero(enemySide)) //only if there is enemy hero + continue; + } + break; + case SpellID::CURE: //only damaged units + { + //do not cast on affected by debuffs + if(!subject->canBeHealed()) + continue; + } + break; + case SpellID::BLOODLUST: + { + if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent + continue; + } + break; + case SpellID::PRECISION: + { + if(!subject->canShoot()) + continue; + } + break; + case SpellID::SLAYER://only if monsters are present + { + const auto * kingMonster = getAliveEnemy([&](const CStack * stack) -> bool //look for enemy, non-shooting stack + { + const auto isKing = Selector::type()(BonusType::KING); + + return stack->hasBonus(isKing); + }); + + if (!kingMonster) + continue; + } + break; + } + beneficialSpells.push_back(spellID); + } + + if(!beneficialSpells.empty()) + { + return *RandomGeneratorUtil::nextItem(beneficialSpells, rand); + } + else + { + return SpellID::NONE; + } +} + +SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const CStack * caster) const +{ + RETURN_IF_NOT_BATTLE(SpellID::NONE); + + TConstBonusListPtr bl = caster->getBonuses(Selector::type()(BonusType::SPELLCASTER)); + if (!bl->size()) + return SpellID::NONE; + + if(bl->size() == 1) + return bl->front()->subtype.as(); + + int totalWeight = 0; + for(const auto & b : *bl) + { + totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them + } + + if (totalWeight == 0) + return SpellID::NONE; + + int randomPos = rand.nextInt(totalWeight - 1); + for(const auto & b : *bl) + { + randomPos -= std::max(b->additionalInfo[0], 0); + if(randomPos < 0) + { + return b->subtype.as(); + } + } + + return SpellID::NONE; +} + +int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) const +{ + RETURN_IF_NOT_BATTLE(-3); + if(!battleCanSurrender(Player)) + return -1; + + const auto sideOpt = playerToSide(Player); + if(!sideOpt) + return -1; + const auto side = sideOpt.value(); + + int ret = 0; + double discount = 0; + + for(const auto * unit : battleAliveUnits(side)) + ret += unit->getRawSurrenderCost(); + + if(const CGHeroInstance * h = battleGetFightingHero(side)) + discount += h->valOfBonuses(BonusType::SURRENDER_DISCOUNT); + + ret = static_cast(ret * (100.0 - discount) / 100.0); + vstd::amax(ret, 0); //no negative costs for >100% discounts (impossible in original H3 mechanics, but some day...) + return ret; +} + +si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const +{ + const IBonusBearer * node = nullptr; + if(const CGHeroInstance * h = battleGetFightingHero(side)) + node = h; + else + node = getBonusBearer(); + + if(!node) + return 0; + + auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_BELOW)); + if(b->size()) + return b->totalValue(); + + return 0; +} + +si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const +{ + const IBonusBearer *node = nullptr; + if(const CGHeroInstance * h = battleGetFightingHero(side)) + node = h; + else + node = getBonusBearer(); + + if(!node) + return GameConstants::SPELL_LEVELS; + + //We can't "just get value" - it'd be 0 if there are bonuses (and all would be blocked) + auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); + if(b->size()) + return b->totalValue(); + + return GameConstants::SPELL_LEVELS; +} + +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 + + for(auto & unit : units) + { + //todo: move SIEGE_WEAPON check to Unit state + hasUnit.at(unit->unitSide()) = true; + + if(hasUnit[0] && hasUnit[1]) + return std::nullopt; + } + + hasUnit = {false, false}; + + for(auto & unit : units) + { + if(!unit->isClone() && !unit->acquireState()->summoned && !dynamic_cast (unit)) + { + hasUnit.at(unit->unitSide()) = true; + } + } + + if(!hasUnit[0] && !hasUnit[1]) + return 2; + if(!hasUnit[1]) + return 0; + else + return 1; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index 00decc929..a05027a5d 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -1,155 +1,156 @@ -/* - * CBattleInfoCallback.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 "CCallbackBase.h" -#include "ReachabilityInfo.h" -#include "BattleAttackInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CStack; -class ISpellCaster; -class SpellCastEnvironment; -class CSpell; -struct CObstacleInstance; -class IBonusBearer; -class CRandomGenerator; -class PossiblePlayerBattleAction; - -namespace spells -{ - class Caster; - class Spell; -} - -struct DLL_LINKAGE AttackableTiles -{ - std::set hostileCreaturePositions; - std::set friendlyCreaturePositions; //for Dragon Breath - template void serialize(Handler &h, const int version) - { - h & hostileCreaturePositions; - h & friendlyCreaturePositions; - } -}; - -struct DLL_LINKAGE BattleClientInterfaceData -{ - std::vector creatureSpellsToCast; - ui8 tacticsMode; -}; - -class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials -{ -public: - enum ERandomSpell - { - RANDOM_GENIE, RANDOM_AIMED - }; - - 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; - //Handle obstacle damage here, requires SpellCastEnvironment - bool handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed = {}) const; - - const CStack * battleGetStackByPos(BattleHex pos, bool onlyAlive = true) const; - - const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const override; - - ///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; - - void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, int8_t lastMoved = -1) 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; - - ///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version) - std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const; - - std::vector battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const; - - int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible - 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 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 - bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack - std::set battleAdjacentUnits(const battle::Unit * unit) const; - - DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; - - /// estimates damage dealt by attacker to defender; - /// only non-random bonuses are considered in estimation - /// returns pair - DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; - 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 movementDistance, DamageEstimation * retaliationDmg = nullptr) 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; - bool battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const; - - BattleHex wallPartToBattleHex(EWallPart part) const; - EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found - bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not - 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 - int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell - ESpellCastProblem::ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell - - SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; - SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; - SpellID getRandomCastedSpell(CRandomGenerator & 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; - - //convenience methods using the ones above - 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 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::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 - - 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 - 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 -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) -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoCallback.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 "ReachabilityInfo.h" +#include "BattleAttackInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CStack; +class ISpellCaster; +class SpellCastEnvironment; +class CSpell; +struct CObstacleInstance; +class IBonusBearer; +class CRandomGenerator; +class PossiblePlayerBattleAction; + +namespace spells +{ + class Caster; + class Spell; +} + +struct DLL_LINKAGE AttackableTiles +{ + std::set hostileCreaturePositions; + std::set friendlyCreaturePositions; //for Dragon Breath + template void serialize(Handler &h, const int version) + { + h & hostileCreaturePositions; + h & friendlyCreaturePositions; + } +}; + +struct DLL_LINKAGE BattleClientInterfaceData +{ + std::vector creatureSpellsToCast; + ui8 tacticsMode; +}; + +class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials +{ +public: + enum ERandomSpell + { + RANDOM_GENIE, RANDOM_AIMED + }; + + 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; + //Handle obstacle damage here, requires SpellCastEnvironment + bool handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed = {}) const; + + const CStack * battleGetStackByPos(BattleHex pos, bool onlyAlive = true) const; + + const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const override; + + ///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; + + void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, int8_t lastMoved = -1) 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; + + ///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version) + std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const; + + std::vector battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const; + + int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible + 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; + + std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; + + 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 + bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack + std::set battleAdjacentUnits(const battle::Unit * unit) const; + + DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; + + /// estimates damage dealt by attacker to defender; + /// only non-random bonuses are considered in estimation + /// returns pair + DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; + 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 movementDistance, DamageEstimation * retaliationDmg = nullptr) 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; + bool battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const; + + BattleHex wallPartToBattleHex(EWallPart part) const; + EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found + bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not + 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 + 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 battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; + SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; + SpellID getRandomCastedSpell(CRandomGenerator & 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; + + //convenience methods using the ones above + 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 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::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 + + 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 + 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 +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) +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index df60ba5f5..b7761a6f8 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -1,436 +1,442 @@ -/* - * CBattleInfoEssentials.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 "CBattleInfoEssentials.h" -#include "../CStack.h" -#include "BattleInfo.h" -#include "../NetPacks.h" -#include "../mapObjects/CGTownInstance.h" -#include "../gameState/InfoAboutArmy.h" - -VCMI_LIB_NAMESPACE_BEGIN - -TerrainId CBattleInfoEssentials::battleTerrainType() const -{ - RETURN_IF_NOT_BATTLE(TerrainId()); - return getBattle()->getTerrainType(); -} - -BattleField CBattleInfoEssentials::battleGetBattlefieldType() const -{ - RETURN_IF_NOT_BATTLE(BattleField::NONE); - return getBattle()->getBattlefieldType(); -} - -int32_t CBattleInfoEssentials::battleGetEnchanterCounter(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(0); - return getBattle()->getEnchanterCounter(side); -} - -std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const -{ - std::vector > ret; - RETURN_IF_NOT_BATTLE(ret); - - if(!perspective) - { - //if no particular perspective request, use default one - perspective = std::make_optional(battleGetMySide()); - } - else - { - if(!!player && *perspective != battleGetMySide()) - logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell"); - } - - for(const auto & obstacle : getBattle()->getAllObstacles()) - { - if(battleIsObstacleVisibleForSide(*(obstacle), *perspective)) - ret.push_back(obstacle); - } - - return ret; -} - -std::shared_ptr CBattleInfoEssentials::battleGetObstacleByID(uint32_t ID) const -{ - std::shared_ptr ret; - - RETURN_IF_NOT_BATTLE(std::shared_ptr()); - - for(auto obstacle : getBattle()->getAllObstacles()) - { - if(obstacle->uniqueID == ID) - return obstacle; - } - - logGlobal->error("Invalid obstacle ID %d", ID); - return std::shared_ptr(); -} - -bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const -{ - RETURN_IF_NOT_BATTLE(false); - return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); -} - -bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - - for(const auto * s : battleGetAllStacks()) - { - if(s->unitSide() == side && s->isNativeTerrain(getBattle()->getTerrainType())) - return true; - } - - return false; -} - -TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const -{ - return battleGetStacksIf([=](const CStack * s) - { - return !s->isGhost() && (includeTurrets || !s->isTurret()); - }); -} - -TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate) const -{ - RETURN_IF_NOT_BATTLE(TStacks()); - return getBattle()->getStacksIf(std::move(predicate)); -} - -battle::Units CBattleInfoEssentials::battleGetUnitsIf(battle::UnitFilter predicate) const -{ - RETURN_IF_NOT_BATTLE(battle::Units()); - return getBattle()->getUnitsIf(predicate); -} - -const battle::Unit * CBattleInfoEssentials::battleGetUnitByID(uint32_t ID) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - //TODO: consider using map ID -> Unit - - auto ret = battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->unitId() == ID; - }); - - if(ret.empty()) - return nullptr; - else - return ret[0]; -} - -const battle::Unit * CBattleInfoEssentials::battleActiveUnit() const -{ - RETURN_IF_NOT_BATTLE(nullptr); - auto id = getBattle()->getActiveStackID(); - if(id >= 0) - return battleGetUnitByID(static_cast(id)); - else - return nullptr; -} - -uint32_t CBattleInfoEssentials::battleNextUnitId() const -{ - return getBattle()->nextUnitId(); -} - -const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - return getBattle()->getDefendedTown(); -} - -BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const -{ - RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); - if(!player || player->isSpectator()) - return BattlePerspective::ALL_KNOWING; - if(*player == getBattle()->getSidePlayer(BattleSide::ATTACKER)) - return BattlePerspective::LEFT_SIDE; - if(*player == getBattle()->getSidePlayer(BattleSide::DEFENDER)) - return BattlePerspective::RIGHT_SIDE; - - logGlobal->error("Cannot find player %s in battle!", player->getStr()); - return BattlePerspective::INVALID; -} - -const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - auto stacks = battleGetStacksIf([=](const CStack * s) - { - return s->unitId() == ID && (!onlyAlive || s->alive()); - }); - - if(stacks.empty()) - return nullptr; - else - return stacks[0]; -} - -bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - auto p = battleGetMySide(); - return p == BattlePerspective::ALL_KNOWING || p == side; -} - -si8 CBattleInfoEssentials::battleTacticDist() const -{ - RETURN_IF_NOT_BATTLE(0); - return getBattle()->getTacticDist(); -} - -si8 CBattleInfoEssentials::battleGetTacticsSide() const -{ - RETURN_IF_NOT_BATTLE(-1); - return getBattle()->getTacticsSide(); -} - -const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) - { - logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); - return nullptr; - } - - if(!battleDoWeKnowAbout(side)) - { - logGlobal->error("FIXME: %s access check ", __FUNCTION__); - return nullptr; - } - - return getBattle()->getSideHero(side); -} - -const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) - { - logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); - return nullptr; - } - if(!battleDoWeKnowAbout(side)) - { - logGlobal->error("FIXME: %s access check!", __FUNCTION__); - return nullptr; - } - return getBattle()->getSideArmy(side); -} - -InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const -{ - const auto * hero = getBattle()->getSideHero(side); - if(!hero) - { - return InfoAboutHero(); - } - InfoAboutHero::EInfoLevel infoLevel = battleDoWeKnowAbout(side) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC; - return InfoAboutHero(hero, infoLevel); -} - -uint32_t CBattleInfoEssentials::battleCastSpells(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(-1); - return getBattle()->getCastSpells(side); -} - -const IBonusBearer * CBattleInfoEssentials::getBonusBearer() const -{ - return getBattle()->getBonusBearer(); -} - -bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(!side) - return false; - - const CGHeroInstance * myHero = battleGetFightingHero(side.value()); - - //current player have no hero - if(!myHero) - return false; - - //eg. one of heroes is wearing shakles of war - if(myHero->hasBonusOfType(BonusType::BATTLE_NO_FLEEING)) - return false; - - //we are besieged defender - if(side == BattleSide::DEFENDER && battleGetSiegeLevel()) - { - const auto * town = battleGetDefendedTown(); - if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) - return false; - } - - return true; -} - -BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(std::nullopt); - - if(getBattle()->getSidePlayer(BattleSide::ATTACKER) == player) - return BattleSideOpt(BattleSide::ATTACKER); - - if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) - return BattleSideOpt(BattleSide::DEFENDER); - - logGlobal->warn("Cannot find side for player %s", player.getStr()); - - return std::nullopt; -} - -PlayerColor CBattleInfoEssentials::sideToPlayer(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - return getBattle()->getSidePlayer(side); -} - -ui8 CBattleInfoEssentials::otherSide(ui8 side) const -{ - if(side == BattleSide::ATTACKER) - return BattleSide::DEFENDER; - else - return BattleSide::ATTACKER; -} - -PlayerColor CBattleInfoEssentials::otherPlayer(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - - auto side = playerToSide(player); - if(!side) - return PlayerColor::CANNOT_DETERMINE; - - return getBattle()->getSidePlayer(otherSide(side.value())); -} - -bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(side) - { - auto opponentSide = otherSide(side.value()); - if(getBattle()->getSideHero(opponentSide) == h) - return true; - } - return false; -} - -ui8 CBattleInfoEssentials::battleGetSiegeLevel() const -{ - RETURN_IF_NOT_BATTLE(CGTownInstance::NONE); - return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE; -} - -bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(!side) - return false; - bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && battleGetSiegeLevel()); - //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero - return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); -} - -bool CBattleInfoEssentials::battleHasHero(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - return getBattle()->getSideHero(side) != nullptr; -} - -EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const -{ - RETURN_IF_NOT_BATTLE(EWallState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return EWallState::NONE; - - return getBattle()->getWallState(partOfWall); -} - -EGateState CBattleInfoEssentials::battleGetGateState() const -{ - RETURN_IF_NOT_BATTLE(EGateState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return EGateState::NONE; - - return getBattle()->getGateState(); -} - -bool CBattleInfoEssentials::battleIsGatePassable() const -{ - RETURN_IF_NOT_BATTLE(true); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return true; - - return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; -} - -PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - - PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); - - static CSelector selector = Selector::type()(BonusType::HYPNOTIZED); - static std::string cachingString = "type_103s-1"; - - if(unit->hasBonus(selector, cachingString)) - return otherPlayer(initialOwner); - else - return initialOwner; -} - -const CGHeroInstance * CBattleInfoEssentials::battleGetOwnerHero(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - const auto side = playerToSide(battleGetOwner(unit)); - if(!side) - return nullptr; - return getBattle()->getSideHero(side.value()); -} - -bool CBattleInfoEssentials::battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const -{ - RETURN_IF_NOT_BATTLE(false); - if(boost::logic::indeterminate(positivness)) - return true; - else if(attacker->unitId() == defender->unitId()) - return (bool)positivness; - else - return battleMatchOwner(battleGetOwner(attacker), defender, positivness); -} - -bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const -{ - RETURN_IF_NOT_BATTLE(false); - - PlayerColor initialOwner = getBattle()->getSidePlayer(defender->unitSide()); - - return boost::logic::indeterminate(positivness) || (attacker == initialOwner) == (bool)positivness; -} - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoEssentials.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 "CBattleInfoEssentials.h" +#include "../CStack.h" +#include "BattleInfo.h" +#include "CObstacleInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../gameState/InfoAboutArmy.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool CBattleInfoEssentials::duringBattle() const +{ + return getBattle() != nullptr; +} + +TerrainId CBattleInfoEssentials::battleTerrainType() const +{ + RETURN_IF_NOT_BATTLE(TerrainId()); + return getBattle()->getTerrainType(); +} + +BattleField CBattleInfoEssentials::battleGetBattlefieldType() const +{ + RETURN_IF_NOT_BATTLE(BattleField::NONE); + return getBattle()->getBattlefieldType(); +} + +int32_t CBattleInfoEssentials::battleGetEnchanterCounter(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(0); + return getBattle()->getEnchanterCounter(side); +} + +std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const +{ + std::vector > ret; + RETURN_IF_NOT_BATTLE(ret); + + if(!perspective) + { + //if no particular perspective request, use default one + perspective = std::make_optional(battleGetMySide()); + } + else + { + if(!!getPlayerID() && *perspective != battleGetMySide()) + logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell"); + } + + for(const auto & obstacle : getBattle()->getAllObstacles()) + { + if(battleIsObstacleVisibleForSide(*(obstacle), *perspective)) + ret.push_back(obstacle); + } + + return ret; +} + +std::shared_ptr CBattleInfoEssentials::battleGetObstacleByID(uint32_t ID) const +{ + std::shared_ptr ret; + + RETURN_IF_NOT_BATTLE(std::shared_ptr()); + + for(auto obstacle : getBattle()->getAllObstacles()) + { + if(obstacle->uniqueID == ID) + return obstacle; + } + + logGlobal->error("Invalid obstacle ID %d", ID); + return std::shared_ptr(); +} + +bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const +{ + RETURN_IF_NOT_BATTLE(false); + return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); +} + +bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + + for(const auto * s : battleGetAllStacks()) + { + if(s->unitSide() == side && s->isNativeTerrain(getBattle()->getTerrainType())) + return true; + } + + return false; +} + +TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const +{ + return battleGetStacksIf([=](const CStack * s) + { + return !s->isGhost() && (includeTurrets || !s->isTurret()); + }); +} + +TStacks CBattleInfoEssentials::battleGetStacksIf(const TStackFilter & predicate) const +{ + RETURN_IF_NOT_BATTLE(TStacks()); + return getBattle()->getStacksIf(std::move(predicate)); +} + +battle::Units CBattleInfoEssentials::battleGetUnitsIf(const battle::UnitFilter & predicate) const +{ + RETURN_IF_NOT_BATTLE(battle::Units()); + return getBattle()->getUnitsIf(predicate); +} + +const battle::Unit * CBattleInfoEssentials::battleGetUnitByID(uint32_t ID) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + //TODO: consider using map ID -> Unit + + auto ret = battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->unitId() == ID; + }); + + if(ret.empty()) + return nullptr; + else + return ret[0]; +} + +const battle::Unit * CBattleInfoEssentials::battleActiveUnit() const +{ + RETURN_IF_NOT_BATTLE(nullptr); + auto id = getBattle()->getActiveStackID(); + if(id >= 0) + return battleGetUnitByID(static_cast(id)); + else + return nullptr; +} + +uint32_t CBattleInfoEssentials::battleNextUnitId() const +{ + return getBattle()->nextUnitId(); +} + +const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + return getBattle()->getDefendedTown(); +} + +BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const +{ + RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); + if(!getPlayerID() || getPlayerID()->isSpectator()) + return BattlePerspective::ALL_KNOWING; + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER)) + return BattlePerspective::LEFT_SIDE; + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER)) + return BattlePerspective::RIGHT_SIDE; + + logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString()); + return BattlePerspective::INVALID; +} + +const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + auto stacks = battleGetStacksIf([=](const CStack * s) + { + return s->unitId() == ID && (!onlyAlive || s->alive()); + }); + + if(stacks.empty()) + return nullptr; + else + return stacks[0]; +} + +bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + auto p = battleGetMySide(); + return p == BattlePerspective::ALL_KNOWING || p == side; +} + +si8 CBattleInfoEssentials::battleTacticDist() const +{ + RETURN_IF_NOT_BATTLE(0); + return getBattle()->getTacticDist(); +} + +si8 CBattleInfoEssentials::battleGetTacticsSide() const +{ + RETURN_IF_NOT_BATTLE(-1); + return getBattle()->getTacticsSide(); +} + +const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + if(side > 1) + { + logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); + return nullptr; + } + + if(!battleDoWeKnowAbout(side)) + { + logGlobal->error("FIXME: %s access check ", __FUNCTION__); + return nullptr; + } + + return getBattle()->getSideHero(side); +} + +const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + if(side > 1) + { + logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); + return nullptr; + } + if(!battleDoWeKnowAbout(side)) + { + logGlobal->error("FIXME: %s access check!", __FUNCTION__); + return nullptr; + } + return getBattle()->getSideArmy(side); +} + +InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const +{ + const auto * hero = getBattle()->getSideHero(side); + if(!hero) + { + return InfoAboutHero(); + } + InfoAboutHero::EInfoLevel infoLevel = battleDoWeKnowAbout(side) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC; + return InfoAboutHero(hero, infoLevel); +} + +uint32_t CBattleInfoEssentials::battleCastSpells(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(-1); + return getBattle()->getCastSpells(side); +} + +const IBonusBearer * CBattleInfoEssentials::getBonusBearer() const +{ + return getBattle()->getBonusBearer(); +} + +bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(!side) + return false; + + const CGHeroInstance * myHero = battleGetFightingHero(side.value()); + + //current player have no hero + if(!myHero) + return false; + + //eg. one of heroes is wearing shakles of war + if(myHero->hasBonusOfType(BonusType::BATTLE_NO_FLEEING)) + return false; + + //we are besieged defender + if(side == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr) + { + const auto * town = battleGetDefendedTown(); + if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) + return false; + } + + return true; +} + +BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(std::nullopt); + + if(getBattle()->getSidePlayer(BattleSide::ATTACKER) == player) + return BattleSideOpt(BattleSide::ATTACKER); + + if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) + return BattleSideOpt(BattleSide::DEFENDER); + + logGlobal->warn("Cannot find side for player %s", player.toString()); + + return std::nullopt; +} + +PlayerColor CBattleInfoEssentials::sideToPlayer(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + return getBattle()->getSidePlayer(side); +} + +ui8 CBattleInfoEssentials::otherSide(ui8 side) const +{ + if(side == BattleSide::ATTACKER) + return BattleSide::DEFENDER; + else + return BattleSide::ATTACKER; +} + +PlayerColor CBattleInfoEssentials::otherPlayer(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + + auto side = playerToSide(player); + if(!side) + return PlayerColor::CANNOT_DETERMINE; + + return getBattle()->getSidePlayer(otherSide(side.value())); +} + +bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(side) + { + auto opponentSide = otherSide(side.value()); + if(getBattle()->getSideHero(opponentSide) == h) + return true; + } + return false; +} + +ui8 CBattleInfoEssentials::battleGetSiegeLevel() const +{ + RETURN_IF_NOT_BATTLE(CGTownInstance::NONE); + return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE; +} + +bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(!side) + return false; + bool iAmSiegeDefender = (side.value() == 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())); +} + +bool CBattleInfoEssentials::battleHasHero(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + return getBattle()->getSideHero(side) != nullptr; +} + +EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const +{ + RETURN_IF_NOT_BATTLE(EWallState::NONE); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return EWallState::NONE; + + return getBattle()->getWallState(partOfWall); +} + +EGateState CBattleInfoEssentials::battleGetGateState() const +{ + RETURN_IF_NOT_BATTLE(EGateState::NONE); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return EGateState::NONE; + + return getBattle()->getGateState(); +} + +bool CBattleInfoEssentials::battleIsGatePassable() const +{ + RETURN_IF_NOT_BATTLE(true); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return true; + + return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; +} + +PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + + PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); + + static CSelector selector = Selector::type()(BonusType::HYPNOTIZED); + static std::string cachingString = "type_103s-1"; + + if(unit->hasBonus(selector, cachingString)) + return otherPlayer(initialOwner); + else + return initialOwner; +} + +const CGHeroInstance * CBattleInfoEssentials::battleGetOwnerHero(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + const auto side = playerToSide(battleGetOwner(unit)); + if(!side) + return nullptr; + return getBattle()->getSideHero(side.value()); +} + +bool CBattleInfoEssentials::battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const +{ + RETURN_IF_NOT_BATTLE(false); + if(boost::logic::indeterminate(positivness)) + return true; + else if(attacker->unitId() == defender->unitId()) + return (bool)positivness; + else + return battleMatchOwner(battleGetOwner(attacker), defender, positivness); +} + +bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const +{ + RETURN_IF_NOT_BATTLE(false); + + PlayerColor initialOwner = getBattle()->getSidePlayer(defender->unitSide()); + + return boost::logic::indeterminate(positivness) || (attacker == initialOwner) == (bool)positivness; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index d5d198150..f19563a5d 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -1,120 +1,120 @@ -/* - * CBattleInfoEssentials.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 "CCallbackBase.h" -#include "IBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; -class CGHeroInstance; -class CStack; -class IBonusBearer; -struct InfoAboutHero; -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 virtual CCallbackBase, public IBattleInfoCallback -{ -protected: - bool battleDoWeKnowAbout(ui8 side) const; - -public: - enum EStackOwnership - { - ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY - }; - - BattlePerspective::BattlePerspective battleGetMySide() const; - const IBonusBearer * getBonusBearer() const override; - - TerrainId battleTerrainType() const override; - BattleField battleGetBattlefieldType() const override; - int32_t battleGetEnchanterCounter(ui8 side) const; - - std::vector > battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield - - std::shared_ptr battleGetObstacleByID(uint32_t ID) const; - - /** @brief Main method for getting battle stacks - * returns also turrets and removed stacks - * @param predicate Functor that shall return true for desired stack - * @return filtered stacks - * - */ - TStacks battleGetStacksIf(TStackFilter predicate) const; //deprecated - battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const override; - - const battle::Unit * battleGetUnitByID(uint32_t ID) const override; - const battle::Unit * battleActiveUnit() const override; - - uint32_t battleNextUnitId() const override; - - bool battleHasNativeStack(ui8 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 (?) - - bool battleCanFlee(const PlayerColor & player) const; - bool battleCanSurrender(const PlayerColor & player) const; - - ui8 otherSide(ui8 side) const; - PlayerColor otherPlayer(const PlayerColor & player) const; - - BattleSideOpt playerToSide(const PlayerColor & player) const; - PlayerColor sideToPlayer(ui8 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; - - // 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 - EWallState battleGetWallState(EWallPart partOfWall) const; - EGateState battleGetGateState() const; - bool battleIsGatePassable() const; - - //helpers - ///returns all stacks, alive or dead or undead or mechanical :) - 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; - - ///returns player that controls given stack; mind control included - PlayerColor battleGetOwner(const battle::Unit * unit) const; - - ///returns hero that controls given stack; nullptr if none; mind control included - const CGHeroInstance * battleGetOwnerHero(const battle::Unit * unit) const; - - ///check that stacks are controlled by same|other player(s) depending on positiveness - ///mind control included - bool battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; - bool battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoEssentials.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 "IBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; +class CGHeroInstance; +class CStack; +class IBonusBearer; +struct InfoAboutHero; +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; + +public: + enum EStackOwnership + { + ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY + }; + + bool duringBattle() const; + BattlePerspective::BattlePerspective battleGetMySide() const; + const IBonusBearer * getBonusBearer() const override; + + TerrainId battleTerrainType() const override; + BattleField battleGetBattlefieldType() const override; + int32_t battleGetEnchanterCounter(ui8 side) const; + + std::vector > battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield + + std::shared_ptr battleGetObstacleByID(uint32_t ID) const; + + /** @brief Main method for getting battle stacks + * returns also turrets and removed stacks + * @param predicate Functor that shall return true for desired stack + * @return filtered stacks + * + */ + TStacks battleGetStacksIf(const TStackFilter & predicate) const; //deprecated + battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const override; + + const battle::Unit * battleGetUnitByID(uint32_t ID) const override; + const battle::Unit * battleActiveUnit() const override; + + uint32_t battleNextUnitId() const override; + + bool battleHasNativeStack(ui8 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 (?) + + bool battleCanFlee(const PlayerColor & player) const; + bool battleCanSurrender(const PlayerColor & player) const; + + ui8 otherSide(ui8 side) const; + PlayerColor otherPlayer(const PlayerColor & player) const; + + BattleSideOpt playerToSide(const PlayerColor & player) const; + PlayerColor sideToPlayer(ui8 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; + + // 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 + EWallState battleGetWallState(EWallPart partOfWall) const; + EGateState battleGetGateState() const; + bool battleIsGatePassable() const; + + //helpers + ///returns all stacks, alive or dead or undead or mechanical :) + 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; + + ///returns player that controls given stack; mind control included + PlayerColor battleGetOwner(const battle::Unit * unit) const; + + ///returns hero that controls given stack; nullptr if none; mind control included + const CGHeroInstance * battleGetOwnerHero(const battle::Unit * unit) const; + + ///check that stacks are controlled by same|other player(s) depending on positiveness + ///mind control included + bool battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; + bool battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CCallbackBase.cpp b/lib/battle/CCallbackBase.cpp deleted file mode 100644 index fb638793c..000000000 --- a/lib/battle/CCallbackBase.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* - * CCallbackBase.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 "CCallbackBase.h" -#include "IBattleState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool CCallbackBase::duringBattle() const -{ - return getBattle() != nullptr; -} - -const IBattleInfo * CCallbackBase::getBattle() const -{ - return battle; -} - -CCallbackBase::CCallbackBase(std::optional Player): - player(std::move(Player)) -{ -} - -void CCallbackBase::setBattle(const IBattleInfo * B) -{ - battle = B; -} - -std::optional CCallbackBase::getPlayerID() const -{ - return player; -} - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CCallbackBase.h b/lib/battle/CCallbackBase.h deleted file mode 100644 index dc5ac95eb..000000000 --- a/lib/battle/CCallbackBase.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * CCallbackBase.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" - -#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } -#define ASSERT_IF_CALLED_WITH_PLAYER if(!player) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} - -VCMI_LIB_NAMESPACE_BEGIN - -class IBattleInfo; -class BattleInfo; -class CBattleInfoEssentials; - - -//Basic class for various callbacks (interfaces called by players to get info about game and so forth) -class DLL_LINKAGE CCallbackBase -{ - const IBattleInfo * battle = nullptr; //battle to which the player is engaged, nullptr if none or not applicable - -protected: - std::optional player; // not set gives access to all information, otherwise callback provides only information "visible" for player - - CCallbackBase(std::optional Player); - CCallbackBase() = default; - - const IBattleInfo * getBattle() const; - void setBattle(const IBattleInfo * B); - bool duringBattle() const; - -public: - std::optional getPlayerID() const; - - friend class CBattleInfoEssentials; -}; - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index c69db1117..ccf01f07c 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -1,269 +1,268 @@ -/* - * CObstacleInstance.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 "CObstacleInstance.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../ObstacleHandler.h" -#include "../VCMI_Lib.h" -#include "../NetPacksBase.h" - -#include "../serializer/JsonDeserializer.h" -#include "../serializer/JsonSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const ObstacleInfo & CObstacleInstance::getInfo() const -{ - assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE); - - return *Obstacle(ID).getInfo(); -} - -std::vector CObstacleInstance::getBlockedTiles() const -{ - if(blocksTiles()) - return getAffectedTiles(); - return std::vector(); -} - -std::vector CObstacleInstance::getStoppingTile() const -{ - if(stopsMovement()) - return getAffectedTiles(); - return std::vector(); -} - -std::vector CObstacleInstance::getAffectedTiles() const -{ - switch(obstacleType) - { - case ABSOLUTE_OBSTACLE: - case USUAL: - return getInfo().getBlocked(pos); - default: - assert(0); - return std::vector(); - } -} - -bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const -{ - //by default obstacle is visible for everyone - return true; -} - -const std::string & CObstacleInstance::getAnimation() const -{ - return getInfo().animation; -} - -const std::string & CObstacleInstance::getAppearAnimation() const -{ - return getInfo().appearAnimation; -} - -const std::string & CObstacleInstance::getAppearSound() const -{ - return getInfo().appearSound; -} - -int CObstacleInstance::getAnimationYOffset(int imageHeight) const -{ - int offset = imageHeight % 42; - if(obstacleType == CObstacleInstance::USUAL) - { - if(getInfo().blockedTiles.front() < 0 || offset > 37) //second or part is for holy ground ID=62,65,63 - offset -= 42; - } - return offset; -} - -bool CObstacleInstance::stopsMovement() const -{ - return obstacleType == MOAT; -} - -bool CObstacleInstance::blocksTiles() const -{ - return obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE ; -} - -bool CObstacleInstance::triggersEffects() const -{ - return getTrigger() != SpellID::NONE; -} - -SpellID CObstacleInstance::getTrigger() const -{ - return SpellID::NONE; -} - -void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) -{ - auto obstacleInfo = getInfo(); - auto hidden = false; - auto needAnimationOffsetFix = obstacleType == CObstacleInstance::USUAL; - int animationYOffset = 0; - - if(getInfo().blockedTiles.front() < 0) //TODO: holy ground ID=62,65,63 - animationYOffset -= 42; - - //We need only a subset of obstacle info for correct render - handler.serializeInt("position", pos); - handler.serializeString("appearSound", obstacleInfo.appearSound); - handler.serializeString("appearAnimation", obstacleInfo.appearAnimation); - handler.serializeString("animation", obstacleInfo.animation); - handler.serializeInt("animationYOffset", animationYOffset); - - handler.serializeBool("hidden", hidden); - handler.serializeBool("needAnimationOffsetFix", needAnimationOffsetFix); -} - -void CObstacleInstance::toInfo(ObstacleChanges & info, BattleChanges::EOperation operation) -{ - info.id = uniqueID; - info.operation = operation; - - info.data.clear(); - JsonSerializer ser(nullptr, info.data); - ser.serializeStruct("obstacle", *this); -} - -SpellCreatedObstacle::SpellCreatedObstacle() - : turnsRemaining(-1), - casterSpellPower(0), - spellLevel(0), - casterSide(0), - hidden(false), - passable(false), - trigger(false), - trap(false), - removeOnTrigger(false), - revealed(false), - animationYOffset(0), - nativeVisible(true), - minimalDamage(0) -{ - obstacleType = SPELL_CREATED; -} - -bool SpellCreatedObstacle::visibleForSide(ui8 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 - //additionally if side has a native unit, mines/quicksands will be visible - //but it is not a case for a moat, so, hasNativeStack should not work for moats - - auto nativeVis = hasNativeStack && nativeVisible; - - return casterSide == side || !hidden || revealed || nativeVis; -} - -bool SpellCreatedObstacle::blocksTiles() const -{ - return !passable; -} - -bool SpellCreatedObstacle::stopsMovement() const -{ - return trap; -} - -SpellID SpellCreatedObstacle::getTrigger() const -{ - return trigger; -} - -void SpellCreatedObstacle::fromInfo(const ObstacleChanges & info) -{ - uniqueID = info.id; - - if(info.operation != ObstacleChanges::EOperation::ADD && info.operation != ObstacleChanges::EOperation::UPDATE) - logGlobal->error("ADD or UPDATE operation expected"); - - JsonDeserializer deser(nullptr, info.data); - deser.serializeStruct("obstacle", *this); -} - -void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("spell", ID); - handler.serializeInt("position", pos); - - handler.serializeInt("turnsRemaining", turnsRemaining); - handler.serializeInt("casterSpellPower", casterSpellPower); - handler.serializeInt("spellLevel", spellLevel); - handler.serializeInt("casterSide", casterSide); - handler.serializeInt("minimalDamage", minimalDamage); - handler.serializeInt("type", obstacleType); - - handler.serializeBool("hidden", hidden); - handler.serializeBool("revealed", revealed); - handler.serializeBool("passable", passable); - handler.serializeId("trigger", trigger, SpellID::NONE); - handler.serializeBool("trap", trap); - handler.serializeBool("removeOnTrigger", removeOnTrigger); - handler.serializeBool("nativeVisible", nativeVisible); - - handler.serializeString("appearSound", appearSound); - handler.serializeString("appearAnimation", appearAnimation); - handler.serializeString("animation", animation); - - handler.serializeInt("animationYOffset", animationYOffset); - - { - JsonArraySerializer customSizeJson = handler.enterArray("customSize"); - customSizeJson.syncSize(customSize, JsonNode::JsonType::DATA_INTEGER); - - for(size_t index = 0; index < customSizeJson.size(); index++) - customSizeJson.serializeInt(index, customSize.at(index)); - } -} - -std::vector SpellCreatedObstacle::getAffectedTiles() const -{ - return customSize; -} - -void SpellCreatedObstacle::battleTurnPassed() -{ - if(turnsRemaining > 0) - turnsRemaining--; -} - -const std::string & SpellCreatedObstacle::getAnimation() const -{ - return animation; -} - -const std::string & SpellCreatedObstacle::getAppearAnimation() const -{ - return appearAnimation; -} - -const std::string & SpellCreatedObstacle::getAppearSound() const -{ - return appearSound; -} - -int SpellCreatedObstacle::getAnimationYOffset(int imageHeight) const -{ - int offset = imageHeight % 42; - - if(obstacleType == CObstacleInstance::SPELL_CREATED || obstacleType == CObstacleInstance::MOAT) - { - offset += animationYOffset; - } - - return offset; -} - -VCMI_LIB_NAMESPACE_END +/* + * CObstacleInstance.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 "CObstacleInstance.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../ObstacleHandler.h" +#include "../VCMI_Lib.h" + +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const ObstacleInfo & CObstacleInstance::getInfo() const +{ + assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE); + + return *Obstacle(ID).getInfo(); +} + +std::vector CObstacleInstance::getBlockedTiles() const +{ + if(blocksTiles()) + return getAffectedTiles(); + return std::vector(); +} + +std::vector CObstacleInstance::getStoppingTile() const +{ + if(stopsMovement()) + return getAffectedTiles(); + return std::vector(); +} + +std::vector CObstacleInstance::getAffectedTiles() const +{ + switch(obstacleType) + { + case ABSOLUTE_OBSTACLE: + case USUAL: + return getInfo().getBlocked(pos); + default: + assert(0); + return std::vector(); + } +} + +bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const +{ + //by default obstacle is visible for everyone + return true; +} + +const AnimationPath & CObstacleInstance::getAnimation() const +{ + return getInfo().animation; +} + +const AnimationPath & CObstacleInstance::getAppearAnimation() const +{ + return getInfo().appearAnimation; +} + +const AudioPath & CObstacleInstance::getAppearSound() const +{ + return getInfo().appearSound; +} + +int CObstacleInstance::getAnimationYOffset(int imageHeight) const +{ + int offset = imageHeight % 42; + if(obstacleType == CObstacleInstance::USUAL) + { + if(getInfo().blockedTiles.front() < 0 || offset > 37) //second or part is for holy ground ID=62,65,63 + offset -= 42; + } + return offset; +} + +bool CObstacleInstance::stopsMovement() const +{ + return obstacleType == MOAT; +} + +bool CObstacleInstance::blocksTiles() const +{ + return obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE ; +} + +bool CObstacleInstance::triggersEffects() const +{ + return getTrigger() != SpellID::NONE; +} + +SpellID CObstacleInstance::getTrigger() const +{ + return SpellID::NONE; +} + +void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) +{ + auto obstacleInfo = getInfo(); + auto hidden = false; + auto needAnimationOffsetFix = obstacleType == CObstacleInstance::USUAL; + int animationYOffset = 0; + + if(getInfo().blockedTiles.front() < 0) //TODO: holy ground ID=62,65,63 + animationYOffset -= 42; + + //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); +} + +void CObstacleInstance::toInfo(ObstacleChanges & info, BattleChanges::EOperation operation) +{ + info.id = uniqueID; + info.operation = operation; + + info.data.clear(); + JsonSerializer ser(nullptr, info.data); + ser.serializeStruct("obstacle", *this); +} + +SpellCreatedObstacle::SpellCreatedObstacle() + : turnsRemaining(-1), + casterSpellPower(0), + spellLevel(0), + casterSide(0), + hidden(false), + passable(false), + trigger(false), + trap(false), + removeOnTrigger(false), + revealed(false), + animationYOffset(0), + nativeVisible(true), + minimalDamage(0) +{ + obstacleType = SPELL_CREATED; +} + +bool SpellCreatedObstacle::visibleForSide(ui8 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 + //additionally if side has a native unit, mines/quicksands will be visible + //but it is not a case for a moat, so, hasNativeStack should not work for moats + + auto nativeVis = hasNativeStack && nativeVisible; + + return casterSide == side || !hidden || revealed || nativeVis; +} + +bool SpellCreatedObstacle::blocksTiles() const +{ + return !passable; +} + +bool SpellCreatedObstacle::stopsMovement() const +{ + return trap; +} + +SpellID SpellCreatedObstacle::getTrigger() const +{ + return trigger; +} + +void SpellCreatedObstacle::fromInfo(const ObstacleChanges & info) +{ + uniqueID = info.id; + + if(info.operation != ObstacleChanges::EOperation::ADD && info.operation != ObstacleChanges::EOperation::UPDATE) + logGlobal->error("ADD or UPDATE operation expected"); + + JsonDeserializer deser(nullptr, info.data); + deser.serializeStruct("obstacle", *this); +} + +void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("spell", ID); + handler.serializeInt("position", pos); + + handler.serializeInt("turnsRemaining", turnsRemaining); + handler.serializeInt("casterSpellPower", casterSpellPower); + handler.serializeInt("spellLevel", spellLevel); + handler.serializeInt("casterSide", casterSide); + handler.serializeInt("minimalDamage", minimalDamage); + handler.serializeInt("type", obstacleType); + + handler.serializeBool("hidden", hidden); + handler.serializeBool("revealed", revealed); + handler.serializeBool("passable", passable); + handler.serializeId("trigger", trigger, SpellID::NONE); + handler.serializeBool("trap", trap); + handler.serializeBool("removeOnTrigger", removeOnTrigger); + handler.serializeBool("nativeVisible", nativeVisible); + + handler.serializeStruct("appearSound", appearSound); + handler.serializeStruct("appearAnimation", appearAnimation); + handler.serializeStruct("animation", animation); + + handler.serializeInt("animationYOffset", animationYOffset); + + { + JsonArraySerializer customSizeJson = handler.enterArray("customSize"); + customSizeJson.syncSize(customSize, JsonNode::JsonType::DATA_INTEGER); + + for(size_t index = 0; index < customSizeJson.size(); index++) + customSizeJson.serializeInt(index, customSize.at(index)); + } +} + +std::vector SpellCreatedObstacle::getAffectedTiles() const +{ + return customSize; +} + +void SpellCreatedObstacle::battleTurnPassed() +{ + if(turnsRemaining > 0) + turnsRemaining--; +} + +const AnimationPath & SpellCreatedObstacle::getAnimation() const +{ + return animation; +} + +const AnimationPath & SpellCreatedObstacle::getAppearAnimation() const +{ + return appearAnimation; +} + +const AudioPath & SpellCreatedObstacle::getAppearSound() const +{ + return appearSound; +} + +int SpellCreatedObstacle::getAnimationYOffset(int imageHeight) const +{ + int offset = imageHeight % 42; + + if(obstacleType == CObstacleInstance::SPELL_CREATED || obstacleType == CObstacleInstance::MOAT) + { + offset += animationYOffset; + } + + return offset; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index 9cf304d97..cf8bc157f 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -1,138 +1,140 @@ -/* - * CObstacleInstance.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 "NetPacksBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class ObstacleInfo; -class ObstacleChanges; -class JsonSerializeFormat; -class SpellID; - -struct DLL_LINKAGE CObstacleInstance -{ - enum EObstacleType : ui8 - { - //ABSOLUTE needs an underscore because it's a Win - USUAL, ABSOLUTE_OBSTACLE, SPELL_CREATED, MOAT - }; - - BattleHex pos; //position on battlefield, typically left bottom corner - EObstacleType obstacleType = USUAL; - si32 uniqueID = -1; - si32 ID = -1; //ID of obstacle (defines type of it) - - virtual ~CObstacleInstance() = default; - - const ObstacleInfo &getInfo() const; //allowed only when not generated by spell (usual or absolute) - - std::vector getBlockedTiles() const; - std::vector getStoppingTile() const; //hexes that will stop stack move - - //The two functions below describe how the obstacle affects affected tiles - //additional effects (like hurting stack or disappearing) are hardcoded for appropriate obstacleTypes - virtual bool blocksTiles() const; - virtual bool stopsMovement() const; //if unit stepping onto obstacle, can't continue movement (in general, doesn't checks for the side) - virtual bool triggersEffects() const; - virtual SpellID getTrigger() const; - - virtual std::vector getAffectedTiles() const; - virtual bool visibleForSide(ui8 side, bool hasNativeStack) const; //0 attacker - - virtual void battleTurnPassed(){}; - - //Client helper functions, make it easier to render animations - virtual const std::string & getAnimation() const; - virtual const std::string & getAppearAnimation() const; - virtual const std::string & getAppearSound() const; - - virtual int getAnimationYOffset(int imageHeight) const; - - void toInfo(ObstacleChanges & info, BattleChanges::EOperation operation = BattleChanges::EOperation::ADD); - - virtual void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & pos; - h & obstacleType; - h & uniqueID; - } -}; - -struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance -{ - int32_t turnsRemaining; - 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 - - SpellID trigger; - - bool hidden; - bool passable; - bool trap; - bool removeOnTrigger; - bool revealed; - bool nativeVisible; //Should native terrain creatures reveal obstacle - - std::string appearSound; - std::string appearAnimation; - std::string animation; - - int animationYOffset; - - std::vector customSize; - - SpellCreatedObstacle(); - - std::vector getAffectedTiles() const override; - bool visibleForSide(ui8 side, bool hasNativeStack) const override; - - bool blocksTiles() const override; - bool stopsMovement() const override; - SpellID getTrigger() const override; - - void battleTurnPassed() override; - - //Client helper functions, make it easier to render animations - const std::string & getAnimation() const override; - const std::string & getAppearAnimation() const override; - const std::string & getAppearSound() const override; - int getAnimationYOffset(int imageHeight) const override; - - void fromInfo(const ObstacleChanges & info); - - void serializeJson(JsonSerializeFormat & handler) override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & turnsRemaining; - h & casterSpellPower; - h & spellLevel; - h & casterSide; - - h & hidden; - h & nativeVisible; - h & passable; - h & trigger; - h & minimalDamage; - h & trap; - - h & customSize; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CObstacleInstance.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 "../filesystem/ResourcePath.h" +#include "../networkPacks/BattleChanges.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ObstacleInfo; +class ObstacleChanges; +class JsonSerializeFormat; +class SpellID; + +struct DLL_LINKAGE CObstacleInstance +{ + enum EObstacleType : ui8 + { + //ABSOLUTE needs an underscore because it's a Win + USUAL, ABSOLUTE_OBSTACLE, SPELL_CREATED, MOAT + }; + + BattleHex pos; //position on battlefield, typically left bottom corner + EObstacleType obstacleType = USUAL; + si32 uniqueID = -1; + si32 ID = -1; //ID of obstacle (defines type of it) + + virtual ~CObstacleInstance() = default; + + const ObstacleInfo &getInfo() const; //allowed only when not generated by spell (usual or absolute) + + std::vector getBlockedTiles() const; + std::vector getStoppingTile() const; //hexes that will stop stack move + + //The two functions below describe how the obstacle affects affected tiles + //additional effects (like hurting stack or disappearing) are hardcoded for appropriate obstacleTypes + virtual bool blocksTiles() const; + virtual bool stopsMovement() const; //if unit stepping onto obstacle, can't continue movement (in general, doesn't checks for the side) + virtual bool triggersEffects() const; + virtual SpellID getTrigger() const; + + virtual std::vector getAffectedTiles() const; + virtual bool visibleForSide(ui8 side, bool hasNativeStack) const; //0 attacker + + virtual void battleTurnPassed(){}; + + //Client helper functions, make it easier to render animations + virtual const AnimationPath & getAnimation() const; + virtual const AnimationPath & getAppearAnimation() const; + virtual const AudioPath & getAppearSound() const; + + virtual int getAnimationYOffset(int imageHeight) const; + + void toInfo(ObstacleChanges & info, BattleChanges::EOperation operation = BattleChanges::EOperation::ADD); + + virtual void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & ID; + h & pos; + h & obstacleType; + h & uniqueID; + } +}; + +struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance +{ + int32_t turnsRemaining; + 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 + + SpellID trigger; + + bool hidden; + bool passable; + bool trap; + bool removeOnTrigger; + bool revealed; + bool nativeVisible; //Should native terrain creatures reveal obstacle + + AudioPath appearSound; + AnimationPath appearAnimation; + AnimationPath animation; + + int animationYOffset; + + std::vector customSize; + + SpellCreatedObstacle(); + + std::vector getAffectedTiles() const override; + bool visibleForSide(ui8 side, bool hasNativeStack) const override; + + bool blocksTiles() const override; + bool stopsMovement() const override; + SpellID getTrigger() const override; + + void battleTurnPassed() override; + + //Client helper functions, make it easier to render animations + const AnimationPath & getAnimation() const override; + const AnimationPath & getAppearAnimation() const override; + const AudioPath & getAppearSound() const override; + int getAnimationYOffset(int imageHeight) const override; + + void fromInfo(const ObstacleChanges & info); + + void serializeJson(JsonSerializeFormat & handler) override; + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & turnsRemaining; + h & casterSpellPower; + h & spellLevel; + h & casterSide; + + h & hidden; + h & nativeVisible; + h & passable; + h & trigger; + h & minimalDamage; + h & trap; + + h & customSize; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index 6aaafcc12..c1256e531 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -1,58 +1,83 @@ -/* - * CPlayerBattleCallback.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 "CPlayerBattleCallback.h" -#include "../CStack.h" -#include "../gameState/InfoAboutArmy.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool CPlayerBattleCallback::battleCanFlee() const -{ - RETURN_IF_NOT_BATTLE(false); - ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoEssentials::battleCanFlee(*player); -} - -TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const -{ - if(whose != MINE_AND_ENEMY) - { - ASSERT_IF_CALLED_WITH_PLAYER - } - - return battleGetStacksIf([=](const CStack * s){ - const bool ownerMatches = (whose == MINE_AND_ENEMY) - || (whose == ONLY_MINE && s->unitOwner() == player) - || (whose == ONLY_ENEMY && s->unitOwner() != player); - - return ownerMatches && s->isValidTarget(!onlyAlive); - }); -} - -int CPlayerBattleCallback::battleGetSurrenderCost() const -{ - RETURN_IF_NOT_BATTLE(-3) - ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoCallback::battleGetSurrenderCost(*player); -} - -const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const -{ - return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide()); -} - -InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const -{ - return battleGetHeroInfo(!battleGetMySide()); -} - - -VCMI_LIB_NAMESPACE_END +/* + * CPlayerBattleCallback.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 "CPlayerBattleCallback.h" +#include "../CStack.h" +#include "../gameState/InfoAboutArmy.h" +#include "../CGameInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player): + battle(battle), + player(player) +{ + +} + +#if SCRIPTING_ENABLED +scripting::Pool * CPlayerBattleCallback::getContextPool() const +{ + return nullptr; //TODO cl->getGlobalContextPool(); +} +#endif + +const IBattleInfo * CPlayerBattleCallback::getBattle() const +{ + return battle; +} + +std::optional CPlayerBattleCallback::getPlayerID() const +{ + return player; +} + +bool CPlayerBattleCallback::battleCanFlee() const +{ + RETURN_IF_NOT_BATTLE(false); + ASSERT_IF_CALLED_WITH_PLAYER + return CBattleInfoEssentials::battleCanFlee(*getPlayerID()); +} + +TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const +{ + if(whose != MINE_AND_ENEMY) + { + ASSERT_IF_CALLED_WITH_PLAYER + } + + return battleGetStacksIf([=](const CStack * s){ + const bool ownerMatches = (whose == MINE_AND_ENEMY) + || (whose == ONLY_MINE && s->unitOwner() == getPlayerID()) + || (whose == ONLY_ENEMY && s->unitOwner() != getPlayerID()); + + return ownerMatches && s->isValidTarget(!onlyAlive); + }); +} + +int CPlayerBattleCallback::battleGetSurrenderCost() const +{ + RETURN_IF_NOT_BATTLE(-3) + ASSERT_IF_CALLED_WITH_PLAYER + return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID()); +} + +const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const +{ + return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide()); +} + +InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const +{ + return battleGetHeroInfo(!battleGetMySide()); +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CPlayerBattleCallback.h b/lib/battle/CPlayerBattleCallback.h index f903a37af..864fb7002 100644 --- a/lib/battle/CPlayerBattleCallback.h +++ b/lib/battle/CPlayerBattleCallback.h @@ -1,30 +1,42 @@ -/* - * CPlayerBattleCallback.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 "CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; - -class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback -{ -public: - bool battleCanFlee() const; //returns true if caller can flee from the battle - TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield - - int battleGetSurrenderCost() const; //returns cost of surrendering battle, -1 if surrendering is not possible - - const CGHeroInstance * battleGetMyHero() const; - InfoAboutHero battleGetEnemyHero() const; -}; - - -VCMI_LIB_NAMESPACE_END +/* + * CPlayerBattleCallback.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 "CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; + +class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback +{ + const IBattleInfo * battle; + PlayerColor player; + +public: + CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player); + +#if SCRIPTING_ENABLED + scripting::Pool * getContextPool() const override; +#endif + + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + + bool battleCanFlee() const; //returns true if caller can flee from the battle + TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield + + int battleGetSurrenderCost() const; //returns cost of surrendering battle, -1 if surrendering is not possible + + const CGHeroInstance * battleGetMyHero() const; + InfoAboutHero battleGetEnemyHero() const; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index f317d429d..eb026acc4 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -13,8 +13,8 @@ #include -#include "../NetPacks.h" #include "../CCreatureHandler.h" +#include "../MetaString.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -46,7 +46,7 @@ int32_t CAmmo::available() const bool CAmmo::canUse(int32_t amount) const { - return !isLimited() || (available() - amount >= 0); + return (available() - amount >= 0) || !isLimited(); } bool CAmmo::isLimited() const @@ -99,7 +99,7 @@ CShots & CShots::operator=(const CShots & other) bool CShots::isLimited() const { - return !env->unitHasAmmoCart(owner) || !shooter.getHasBonus(); + return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner); } void CShots::setEnv(const IUnitEnvironment * env_) @@ -340,12 +340,12 @@ CUnitState::CUnitState(): health(this), shots(this), totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), - minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1)), 0), - maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2)), 0), - attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK), 0), - defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE), 0), + minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)), 0), + maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)), 0), + attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)), 0), + defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), - cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, SpellID::CLONE))), + cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE))))), cloneID(-1) { @@ -428,9 +428,9 @@ const CGHeroInstance * CUnitState::getHeroCaster() const return nullptr; } -int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { - int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, spell->getIndex())); + int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spell->getId()))); vstd::abetween(skill, 0, 3); return skill; } @@ -466,7 +466,7 @@ int32_t CUnitState::getEnchantPower(const spells::Spell * spell) const int64_t CUnitState::getEffectValue(const spells::Spell * spell) const { - return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, spell->getIndex()); + return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, BonusSubtypeID(spell->getId())); } PlayerColor CUnitState::getCasterOwner() const @@ -485,7 +485,7 @@ void CUnitState::getCastDescription(const spells::Spell * spell, const std::vect text.appendLocalString(EMetaText::GENERAL_TXT, 565);//The %s casts %s //todo: use text 566 for single creature getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); } int32_t CUnitState::manaLimit() const @@ -511,7 +511,7 @@ bool CUnitState::isGhost() const bool CUnitState::isFrozen() const { - return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, SpellID::STONE_GAZE), Selector::all); + return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::STONE_GAZE))), Selector::all); } bool CUnitState::isValidTarget(bool allowDead) const diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index e4597b4ad..9e0fb41c9 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -182,7 +182,7 @@ public: int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const spells::Spell * spell) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const Unit * affectedStack) const override; diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 5ef8c7bed..dfed47953 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -50,9 +50,9 @@ DamageRange DamageCalculator::getBaseDamageSingle() const if(info.attacker->hasBonus(selectorSiedgeWeapon, cachingStrSiedgeWeapon) && info.attacker->creatureIndex() != CreatureID::ARROW_TOWERS) { - auto retrieveHeroPrimSkill = [&](int skill) -> int + auto retrieveHeroPrimSkill = [&](PrimarySkill skill) -> int { - std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, skill))); + std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(skill)))); return b ? b->val : 0; }; @@ -142,8 +142,9 @@ int DamageCalculator::getActorAttackSlayer() const if(isAffected) { - int attackBonus = SpellID(SpellID::SLAYER).toSpell()->getLevelPower(spLevel); - if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, SpellID::SLAYER)) + SpellID spell(SpellID::SLAYER); + int attackBonus = spell.toSpell()->getLevelPower(spLevel); + if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(spell))) { ui8 attackerTier = info.attacker->unitType()->getLevel(); ui8 specialtyBonus = std::max(5 - attackerTier, 0); @@ -205,11 +206,11 @@ double DamageCalculator::getAttackOffenseArcheryFactor() const if(info.shooting) { const std::string cachingStrArchery = "type_PERCENTAGE_DAMAGE_BOOSTs_1"; - static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, 1); + static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusCustomSubtype::damageTypeRanged); return info.attacker->valOfBonuses(selectorArchery, cachingStrArchery) / 100.0; } const std::string cachingStrOffence = "type_PERCENTAGE_DAMAGE_BOOSTs_0"; - static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, 0); + static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusCustomSubtype::damageTypeMelee); return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0; } @@ -231,7 +232,7 @@ double DamageCalculator::getAttackDoubleDamageFactor() const { if(info.doubleDamage) { const auto cachingStr = "type_BONUS_DAMAGE_PERCENTAGEs_" + std::to_string(info.attacker->creatureIndex()); - const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, info.attacker->creatureIndex()); + const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, BonusSubtypeID(info.attacker->creatureId())); return info.attacker->valOfBonuses(selector, cachingStr) / 100.0; } return 0.0; @@ -259,7 +260,7 @@ double DamageCalculator::getAttackHateFactor() const auto allHateEffects = info.attacker->getBonuses(selectorHate, cachingStrHate); - return allHateEffects->valOfBonuses(Selector::subtype()(info.defender->creatureIndex())) / 100.0; + return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0; } double DamageCalculator::getDefenseSkillFactor() const @@ -281,7 +282,7 @@ double DamageCalculator::getDefenseSkillFactor() const double DamageCalculator::getDefenseArmorerFactor() const { const std::string cachingStrArmorer = "type_GENERAL_DAMAGE_REDUCTIONs_N1_NsrcSPELL_EFFECT"; - static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); + static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); return info.defender->valOfBonuses(selectorArmorer, cachingStrArmorer) / 100.0; } @@ -289,10 +290,10 @@ double DamageCalculator::getDefenseArmorerFactor() const double DamageCalculator::getDefenseMagicShieldFactor() const { const std::string cachingStrMeleeReduction = "type_GENERAL_DAMAGE_REDUCTIONs_0"; - static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, 0); + static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeMelee); const std::string cachingStrRangedReduction = "type_GENERAL_DAMAGE_REDUCTIONs_1"; - static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, 1); + static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeRanged); //handling spell effects - shield and air shield if(info.shooting) @@ -312,8 +313,8 @@ double DamageCalculator::getDefenseRangePenaltiesFactor() const auto isAdvancedAirShield = [](const Bonus* bonus) { return bonus->source == BonusSource::SPELL_EFFECT - && bonus->sid == SpellID::AIR_SHIELD - && bonus->val >= SecSkillLevel::ADVANCED; + && bonus->sid == BonusSourceID(SpellID(SpellID::AIR_SHIELD)) + && bonus->val >= MasteryLevel::ADVANCED; }; const bool distPenalty = callback.battleHasDistancePenalty(info.attacker, attackerPos, defenderPos); @@ -386,7 +387,7 @@ double DamageCalculator::getDefensePetrificationFactor() const { // Creatures that are petrified by a Basilisk's Petrifying attack or a Medusa's Stone gaze take 50% damage (R8 = 0.50) from ranged and melee attacks. Taking damage also deactivates the effect. const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1_srcSPELL_EFFECT"; - static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); + static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); return info.defender->valOfBonuses(selectorAllReduction, cachingStrAllReduction) / 100.0; } diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 984bf9093..6ee05b2f6 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -15,10 +15,13 @@ #include +#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } + VCMI_LIB_NAMESPACE_BEGIN struct CObstacleInstance; class BattleField; +class IBattleInfo; namespace battle { @@ -53,6 +56,10 @@ public: #if SCRIPTING_ENABLED virtual scripting::Pool * getContextPool() const = 0; #endif + virtual ~IBattleInfoCallback() = default; + + virtual const IBattleInfo * getBattle() const = 0; + virtual std::optional getPlayerID() const = 0; virtual TerrainId battleTerrainType() const = 0; virtual BattleField battleGetBattlefieldType() const = 0; @@ -65,7 +72,7 @@ public: virtual uint32_t battleNextUnitId() const = 0; - virtual battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual const battle::Unit * battleGetUnitByID(uint32_t ID) const = 0; virtual const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const = 0; diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 3f43eb0fc..fd643c56c 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -19,6 +19,7 @@ struct Bonus; class JsonNode; class JsonSerializeFormat; class BattleField; +class int3; namespace vstd { @@ -37,11 +38,13 @@ public: virtual ~IBattleInfo() = default; + virtual BattleID getBattleID() const = 0; + virtual int32_t getActiveStackID() const = 0; - virtual TStacks getStacksIf(TStackFilter predicate) const = 0; + virtual TStacks getStacksIf(const TStackFilter & predicate) const = 0; - virtual battle::Units getUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units getUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual BattleField getBattlefieldType() const = 0; virtual TerrainId getTerrainType() const = 0; @@ -55,6 +58,8 @@ public: virtual PlayerColor getSidePlayer(ui8 side) const = 0; virtual const CArmedInstance * getSideArmy(ui8 side) const = 0; virtual const CGHeroInstance * getSideHero(ui8 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 uint32_t getCastSpells(ui8 side) const = 0; virtual int32_t getEnchanterCounter(ui8 side) const = 0; @@ -65,14 +70,15 @@ public: 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; }; class DLL_LINKAGE IBattleState : public IBattleInfo { public: - //TODO: add non-const API - - virtual void nextRound(int32_t roundNr) = 0; + virtual void nextRound() = 0; virtual void nextTurn(uint32_t unitId) = 0; virtual void addUnit(uint32_t id, const JsonNode & data) = 0; diff --git a/lib/battle/ReachabilityInfo.cpp b/lib/battle/ReachabilityInfo.cpp index 078fe44c8..ecb6a32bf 100644 --- a/lib/battle/ReachabilityInfo.cpp +++ b/lib/battle/ReachabilityInfo.cpp @@ -1,75 +1,75 @@ -/* - * ReachabilityInfo.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 "ReachabilityInfo.h" -#include "Unit.h" - -VCMI_LIB_NAMESPACE_BEGIN - -ReachabilityInfo::Parameters::Parameters(const battle::Unit * Stack, BattleHex StartPosition): - perspective(static_cast(Stack->unitSide())), - startPosition(StartPosition), - doubleWide(Stack->doubleWide()), - side(Stack->unitSide()), - flying(Stack->hasBonusOfType(BonusType::FLYING)) -{ - knownAccessible = battle::Unit::getHexes(startPosition, doubleWide, side); -} - -ReachabilityInfo::ReachabilityInfo() -{ - distances.fill(INFINITE_DIST); - predecessors.fill(BattleHex::INVALID); -} - -bool ReachabilityInfo::isReachable(BattleHex hex) const -{ - return distances[hex] < INFINITE_DIST; -} - -uint32_t ReachabilityInfo::distToNearestNeighbour( - const std::vector & targetHexes, - BattleHex * chosenHex) const -{ - uint32_t ret = 1000000; - - for(auto targetHex : targetHexes) - { - for(auto & n : targetHex.neighbouringTiles()) - { - if(distances[n] < ret) - { - ret = distances[n]; - if(chosenHex) - *chosenHex = n; - } - } - } - - return ret; -} - -uint32_t ReachabilityInfo::distToNearestNeighbour( - const battle::Unit * attacker, - const battle::Unit * defender, - BattleHex * chosenHex) const -{ - auto attackableHexes = defender->getHexes(); - - if(attacker->doubleWide()) - { - vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide())); - } - - return distToNearestNeighbour(attackableHexes, chosenHex); -} - -VCMI_LIB_NAMESPACE_END +/* + * ReachabilityInfo.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 "ReachabilityInfo.h" +#include "Unit.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ReachabilityInfo::Parameters::Parameters(const battle::Unit * Stack, BattleHex StartPosition): + perspective(static_cast(Stack->unitSide())), + startPosition(StartPosition), + doubleWide(Stack->doubleWide()), + side(Stack->unitSide()), + flying(Stack->hasBonusOfType(BonusType::FLYING)) +{ + knownAccessible = battle::Unit::getHexes(startPosition, doubleWide, side); +} + +ReachabilityInfo::ReachabilityInfo() +{ + distances.fill(INFINITE_DIST); + predecessors.fill(BattleHex::INVALID); +} + +bool ReachabilityInfo::isReachable(BattleHex hex) const +{ + return distances[hex] < INFINITE_DIST; +} + +uint32_t ReachabilityInfo::distToNearestNeighbour( + const std::vector & targetHexes, + BattleHex * chosenHex) const +{ + uint32_t ret = 1000000; + + for(auto targetHex : targetHexes) + { + for(auto & n : targetHex.neighbouringTiles()) + { + if(distances[n] < ret) + { + ret = distances[n]; + if(chosenHex) + *chosenHex = n; + } + } + } + + return ret; +} + +uint32_t ReachabilityInfo::distToNearestNeighbour( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex * chosenHex) const +{ + auto attackableHexes = defender->getHexes(); + + if(attacker->doubleWide()) + { + vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide())); + } + + return distToNearestNeighbour(attackableHexes, chosenHex); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/ReachabilityInfo.h b/lib/battle/ReachabilityInfo.h index 6931e1662..8f6bbb663 100644 --- a/lib/battle/ReachabilityInfo.h +++ b/lib/battle/ReachabilityInfo.h @@ -1,60 +1,60 @@ -/* - * ReachabilityInfo.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 "CBattleInfoEssentials.h" -#include "AccessibilityInfo.h" - -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. -struct DLL_LINKAGE ReachabilityInfo -{ - using TDistances = std::array; - using TPredecessors = std::array; - - enum { INFINITE_DIST = 1000000 }; - - struct DLL_LINKAGE Parameters - { - ui8 side = 0; - bool doubleWide = false; - bool flying = false; - bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes - 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) - - BattleHex startPosition; //assumed position of stack - BattlePerspective::BattlePerspective perspective = BattlePerspective::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side - - Parameters() = default; - Parameters(const battle::Unit * Stack, BattleHex StartPosition); - }; - - Parameters params; - AccessibilityInfo accessibility; - TDistances distances; - TPredecessors predecessors; - - ReachabilityInfo(); - - bool isReachable(BattleHex hex) const; - - uint32_t distToNearestNeighbour( - const std::vector & targetHexes, - BattleHex * chosenHex = nullptr) const; - - uint32_t distToNearestNeighbour( - const battle::Unit * attacker, - const battle::Unit * defender, - BattleHex * chosenHex = nullptr) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * ReachabilityInfo.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 "CBattleInfoEssentials.h" +#include "AccessibilityInfo.h" + +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. +struct DLL_LINKAGE ReachabilityInfo +{ + using TDistances = std::array; + using TPredecessors = std::array; + + enum { INFINITE_DIST = 1000000 }; + + struct DLL_LINKAGE Parameters + { + ui8 side = 0; + bool doubleWide = false; + bool flying = false; + bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes + 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) + + BattleHex startPosition; //assumed position of stack + BattlePerspective::BattlePerspective perspective = BattlePerspective::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side + + Parameters() = default; + Parameters(const battle::Unit * Stack, BattleHex StartPosition); + }; + + Parameters params; + AccessibilityInfo accessibility; + TDistances distances; + TPredecessors predecessors; + + ReachabilityInfo(); + + bool isReachable(BattleHex hex) const; + + uint32_t distToNearestNeighbour( + const std::vector & targetHexes, + BattleHex * chosenHex = nullptr) const; + + uint32_t distToNearestNeighbour( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex * chosenHex = nullptr) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index aeafb243c..bfdcafb95 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -1,37 +1,37 @@ -/* - * SideInBattle.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 "SideInBattle.h" -#include "../mapObjects/CArmedInstance.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army) -{ - hero = Hero; - armyObject = Army; - - switch(armyObject->ID) - { - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR2: - case Obj::CREATURE_GENERATOR3: - case Obj::CREATURE_GENERATOR4: - color = PlayerColor::NEUTRAL; - break; - default: - color = armyObject->getOwner(); - } - - if(color == PlayerColor::UNFLAGGABLE) - color = PlayerColor::NEUTRAL; -} - -VCMI_LIB_NAMESPACE_END +/* + * SideInBattle.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 "SideInBattle.h" +#include "../mapObjects/CArmedInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army) +{ + hero = Hero; + armyObject = Army; + + switch(armyObject->ID.toEnum()) + { + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + color = PlayerColor::NEUTRAL; + break; + default: + color = armyObject->getOwner(); + } + + if(color == PlayerColor::UNFLAGGABLE) + color = PlayerColor::NEUTRAL; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SideInBattle.h b/lib/battle/SideInBattle.h index d7ca30dc2..152b79361 100644 --- a/lib/battle/SideInBattle.h +++ b/lib/battle/SideInBattle.h @@ -1,42 +1,42 @@ -/* - * SideInBattle.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 CGHeroInstance; -class CArmedInstance; - -struct DLL_LINKAGE SideInBattle -{ - PlayerColor color = PlayerColor::CANNOT_DETERMINE; - const CGHeroInstance * hero = nullptr; //may be NULL if army is not commanded by hero - const CArmedInstance * armyObject = nullptr; //adv. map object with army that participates in battle; may be same as hero - - uint32_t castSpellsCount = 0; //how many spells each side has been cast this turn - std::vector usedSpellsHistory; //every time hero casts spell, it's inserted here -> eagle eye skill - int32_t enchanterCounter = 0; //tends to pass through 0, so sign is needed - - void init(const CGHeroInstance * Hero, const CArmedInstance * Army); - - - template void serialize(Handler &h, const int version) - { - h & color; - h & hero; - h & armyObject; - h & castSpellsCount; - h & usedSpellsHistory; - h & enchanterCounter; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * SideInBattle.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 CGHeroInstance; +class CArmedInstance; + +struct DLL_LINKAGE SideInBattle +{ + PlayerColor color = PlayerColor::CANNOT_DETERMINE; + const CGHeroInstance * hero = nullptr; //may be NULL if army is not commanded by hero + const CArmedInstance * armyObject = nullptr; //adv. map object with army that participates in battle; may be same as hero + + uint32_t castSpellsCount = 0; //how many spells each side has been cast this turn + std::vector usedSpellsHistory; //every time hero casts spell, it's inserted here -> eagle eye skill + int32_t enchanterCounter = 0; //tends to pass through 0, so sign is needed + + void init(const CGHeroInstance * Hero, const CArmedInstance * Army); + + + template void serialize(Handler &h, const int version) + { + h & color; + h & hero; + h & armyObject; + h & castSpellsCount; + h & usedSpellsHistory; + h & enchanterCounter; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SiegeInfo.cpp b/lib/battle/SiegeInfo.cpp index 10877860f..be7a9d4b8 100644 --- a/lib/battle/SiegeInfo.cpp +++ b/lib/battle/SiegeInfo.cpp @@ -1,45 +1,45 @@ -/* - * SiegeInfo.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 "SiegeInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -SiegeInfo::SiegeInfo() -{ - for(int i = 0; i < static_cast(EWallPart::PARTS_COUNT); ++i) - { - wallState[static_cast(i)] = EWallState::NONE; - } - gateState = EGateState::NONE; -} - -EWallState SiegeInfo::applyDamage(EWallState state, unsigned int value) -{ - if(value == 0) - return state; - - switch(applyDamage(state, value - 1)) - { - case EWallState::REINFORCED: - return EWallState::INTACT; - case EWallState::INTACT: - return EWallState::DAMAGED; - case EWallState::DAMAGED: - return EWallState::DESTROYED; - case EWallState::DESTROYED: - return EWallState::DESTROYED; - default: - return EWallState::NONE; - } -} - -VCMI_LIB_NAMESPACE_END +/* + * SiegeInfo.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 "SiegeInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +SiegeInfo::SiegeInfo() +{ + for(int i = 0; i < static_cast(EWallPart::PARTS_COUNT); ++i) + { + wallState[static_cast(i)] = EWallState::NONE; + } + gateState = EGateState::NONE; +} + +EWallState SiegeInfo::applyDamage(EWallState state, unsigned int value) +{ + if(value == 0) + return state; + + switch(applyDamage(state, value - 1)) + { + case EWallState::REINFORCED: + return EWallState::INTACT; + case EWallState::INTACT: + return EWallState::DAMAGED; + case EWallState::DAMAGED: + return EWallState::DESTROYED; + case EWallState::DESTROYED: + return EWallState::DESTROYED; + default: + return EWallState::NONE; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SiegeInfo.h b/lib/battle/SiegeInfo.h index 680888201..6f69d8041 100644 --- a/lib/battle/SiegeInfo.h +++ b/lib/battle/SiegeInfo.h @@ -1,33 +1,33 @@ -/* - * SiegeInfo.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 - -//only for use in BattleInfo -struct DLL_LINKAGE SiegeInfo -{ - std::map wallState; - EGateState gateState; - - SiegeInfo(); - - // return EWallState decreased by value of damage points - static EWallState applyDamage(EWallState state, unsigned int value); - - template void serialize(Handler &h, const int version) - { - h & wallState; - h & gateState; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * SiegeInfo.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 + +//only for use in BattleInfo +struct DLL_LINKAGE SiegeInfo +{ + std::map wallState; + EGateState gateState; + + SiegeInfo(); + + // return EWallState decreased by value of damage points + static EWallState applyDamage(EWallState state, unsigned int value); + + template void serialize(Handler &h, const int version) + { + h & wallState; + h & gateState; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 77905eafe..c08713f43 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -14,7 +14,6 @@ #include "../VCMI_Lib.h" #include "../CGeneralTextHandler.h" #include "../MetaString.h" -#include "../NetPacksBase.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -188,11 +187,11 @@ void Unit::addText(MetaString & text, EMetaText type, int32_t serial, const boos void Unit::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const { if(boost::logic::indeterminate(plural)) - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); else if(plural) - text.replaceLocalString(EMetaText::CRE_PL_NAMES, creatureIndex()); + text.replaceNamePlural(creatureIndex()); else - text.replaceLocalString(EMetaText::CRE_SING_NAMES, creatureIndex()); + text.replaceNameSingular(creatureIndex()); } std::string Unit::formatGeneralMessage(const int32_t baseTextId) const @@ -201,7 +200,7 @@ std::string Unit::formatGeneralMessage(const int32_t baseTextId) const MetaString text; text.appendLocalString(EMetaText::GENERAL_TXT, textId); - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); return text.toString(); } @@ -219,7 +218,7 @@ int Unit::getRawSurrenderCost() const void UnitInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("count", count); - handler.serializeId("type", type, CreatureID::NONE); + handler.serializeId("type", type, CreatureID(CreatureID::NONE)); handler.serializeInt("side", side); handler.serializeInt("position", position); handler.serializeBool("summoned", summoned); diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 8bdd67cc3..bcf782b78 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -1,304 +1,295 @@ -/* - * Bonus.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 "Bonus.h" -#include "CBonusSystemNode.h" -#include "Limiters.h" -#include "Updaters.h" -#include "Propagators.h" - -#include "../VCMI_Lib.h" -#include "../spells/CSpellHandler.h" -#include "../CCreatureHandler.h" -#include "../CCreatureSet.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CSkillHandler.h" -#include "../CArtHandler.h" -#include "../CModHandler.h" -#include "../TerrainHandler.h" -#include "../StringConstants.h" -#include "../battle/BattleInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -//This constructor should be placed here to avoid side effects -CAddInfo::CAddInfo() = default; - -CAddInfo::CAddInfo(si32 value) -{ - if(value != CAddInfo::NONE) - push_back(value); -} - -bool CAddInfo::operator==(si32 value) const -{ - switch(size()) - { - case 0: - return value == CAddInfo::NONE; - case 1: - return operator[](0) == value; - default: - return false; - } -} - -bool CAddInfo::operator!=(si32 value) const -{ - return !operator==(value); -} - -si32 & CAddInfo::operator[](size_type pos) -{ - if(pos >= size()) - resize(pos + 1, CAddInfo::NONE); - return vector::operator[](pos); -} - -si32 CAddInfo::operator[](size_type pos) const -{ - return pos < size() ? vector::operator[](pos) : CAddInfo::NONE; -} - -std::string CAddInfo::toString() const -{ - return toJsonNode().toJson(true); -} - -JsonNode CAddInfo::toJsonNode() const -{ - if(size() < 2) - { - return JsonUtils::intNode(operator[](0)); - } - else - { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); - for(si32 value : *this) - node.Vector().push_back(JsonUtils::intNode(value)); - return node; - } -} -std::string Bonus::Description(std::optional customValue) const -{ - std::ostringstream str; - - if(description.empty()) - { - if(stacking.empty() || stacking == "ALWAYS") - { - switch(source) - { - case BonusSource::ARTIFACT: - str << ArtifactID(sid).toArtifact(VLC->artifacts())->getNameTranslated(); - break; - case BonusSource::SPELL_EFFECT: - str << SpellID(sid).toSpell(VLC->spells())->getNameTranslated(); - break; - case BonusSource::CREATURE_ABILITY: - str << CreatureID(sid).toCreature(VLC->creatures())->getNamePluralTranslated(); - break; - case BonusSource::SECONDARY_SKILL: - str << VLC->skills()->getByIndex(sid)->getNameTranslated(); - break; - case BonusSource::HERO_SPECIAL: - str << VLC->heroTypes()->getByIndex(sid)->getNameTranslated(); - break; - default: - //todo: handle all possible sources - str << "Unknown"; - break; - } - } - else - str << stacking; - } - else - { - str << description; - } - - if(auto value = customValue.value_or(val)) - str << " " << std::showpos << value; - - return str.str(); -} - -static JsonNode subtypeToJson(BonusType type, int subtype) -{ - switch(type) - { - case BonusType::PRIMARY_SKILL: - return JsonUtils::stringNode("primSkill." + PrimarySkill::names[subtype]); - case BonusType::SPECIAL_SPELL_LEV: - case BonusType::SPECIFIC_SPELL_DAMAGE: - case BonusType::SPELL: - case BonusType::SPECIAL_PECULIAR_ENCHANT: - case BonusType::SPECIAL_ADD_VALUE_ENCHANT: - case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "spell", SpellID::encode(subtype))); - case BonusType::IMPROVED_NECROMANCY: - case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); - case BonusType::GENERATE_RESOURCE: - return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]); - default: - return JsonUtils::intNode(subtype); - } -} - -static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) -{ - switch(type) - { - case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); - default: - return addInfo.toJsonNode(); - } -} - -JsonNode Bonus::toJsonNode() const -{ - JsonNode root(JsonNode::JsonType::DATA_STRUCT); - // only add values that might reasonably be found in config files - root["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtype != -1) - root["subtype"] = subtypeToJson(type, subtype); - if(additionalInfo != CAddInfo::NONE) - root["addInfo"] = additionalInfoToJson(type, additionalInfo); - if(turnsRemain != 0) - root["turns"].Integer() = turnsRemain; - if(source != BonusSource::OTHER) - root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); - if(targetSourceType != BonusSource::OTHER) - root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); - if(sid != 0) - root["sourceID"].Integer() = sid; - if(val != 0) - root["val"].Integer() = val; - if(valType != BonusValueType::ADDITIVE_VALUE) - root["valueType"].String() = vstd::findKey(bonusValueMap, valType); - if(!stacking.empty()) - root["stacking"].String() = stacking; - if(!description.empty()) - root["description"].String() = description; - if(effectRange != BonusLimitEffect::NO_LIMIT) - root["effectRange"].String() = vstd::findKey(bonusLimitEffect, effectRange); - if(duration != BonusDuration::PERMANENT) - root["duration"] = BonusDuration::toJson(duration); - if(turnsRemain) - root["turns"].Integer() = turnsRemain; - if(limiter) - root["limiters"] = limiter->toJsonNode(); - if(updater) - root["updater"] = updater->toJsonNode(); - if(propagator) - root["propagator"].String() = vstd::findKey(bonusPropagatorMap, propagator); - return root; -} - -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype): - duration(Duration), - type(Type), - subtype(Subtype), - source(Src), - val(Val), - sid(ID), - description(std::move(Desc)) -{ - boost::algorithm::trim(description); - targetSourceType = BonusSource::OTHER; -} - -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype, BonusValueType ValType): - duration(Duration), - type(Type), - subtype(Subtype), - source(Src), - val(Val), - sid(ID), - valType(ValType) -{ - turnsRemain = 0; - effectRange = BonusLimitEffect::NO_LIMIT; - targetSourceType = BonusSource::OTHER; -} - -std::shared_ptr Bonus::addPropagator(const TPropagatorPtr & Propagator) -{ - propagator = Propagator; - return this->shared_from_this(); -} - -DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) -{ - for(const auto & i : bonusNameMap) - if(i.second == bonus.type) - out << "\tType: " << i.first << " \t"; - -#define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" - printField(val); - printField(subtype); - printField(duration.to_ulong()); - printField(source); - printField(sid); - if(bonus.additionalInfo != CAddInfo::NONE) - out << "\taddInfo: " << bonus.additionalInfo.toString() << "\n"; - printField(turnsRemain); - printField(valType); - if(!bonus.stacking.empty()) - out << "\tstacking: \"" << bonus.stacking << "\"\n"; - printField(effectRange); -#undef printField - - if(bonus.limiter) - out << "\tLimiter: " << bonus.limiter->toString() << "\n"; - if(bonus.updater) - out << "\tUpdater: " << bonus.updater->toString() << "\n"; - - return out; -} - -std::shared_ptr Bonus::addLimiter(const TLimiterPtr & Limiter) -{ - if (limiter) - { - //If we already have limiter list, retrieve it - auto limiterList = std::dynamic_pointer_cast(limiter); - if(!limiterList) - { - //Create a new limiter list with old limiter and the new one will be pushed later - limiterList = std::make_shared(); - limiterList->add(limiter); - limiter = limiterList; - } - - limiterList->add(Limiter); - } - else - { - limiter = Limiter; - } - return this->shared_from_this(); -} - -// Updaters - -std::shared_ptr Bonus::addUpdater(const TUpdaterPtr & Updater) -{ - updater = Updater; - return this->shared_from_this(); -} - -VCMI_LIB_NAMESPACE_END +/* + * Bonus.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 "Bonus.h" +#include "CBonusSystemNode.h" +#include "Limiters.h" +#include "Updaters.h" +#include "Propagators.h" + +#include "../VCMI_Lib.h" +#include "../spells/CSpellHandler.h" +#include "../CCreatureHandler.h" +#include "../CCreatureSet.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CSkillHandler.h" +#include "../CArtHandler.h" +#include "../TerrainHandler.h" +#include "../constants/StringConstants.h" +#include "../battle/BattleInfo.h" +#include "../modding/ModUtility.h" + +VCMI_LIB_NAMESPACE_BEGIN + +//This constructor should be placed here to avoid side effects +CAddInfo::CAddInfo() = default; + +CAddInfo::CAddInfo(si32 value) +{ + if(value != CAddInfo::NONE) + push_back(value); +} + +bool CAddInfo::operator==(si32 value) const +{ + switch(size()) + { + case 0: + return value == CAddInfo::NONE; + case 1: + return operator[](0) == value; + default: + return false; + } +} + +bool CAddInfo::operator!=(si32 value) const +{ + return !operator==(value); +} + +si32 & CAddInfo::operator[](size_type pos) +{ + if(pos >= size()) + resize(pos + 1, CAddInfo::NONE); + return vector::operator[](pos); +} + +si32 CAddInfo::operator[](size_type pos) const +{ + return pos < size() ? vector::operator[](pos) : CAddInfo::NONE; +} + +std::string CAddInfo::toString() const +{ + return toJsonNode().toJson(true); +} + +JsonNode CAddInfo::toJsonNode() const +{ + if(size() < 2) + { + return JsonUtils::intNode(operator[](0)); + } + else + { + JsonNode node(JsonNode::JsonType::DATA_VECTOR); + for(si32 value : *this) + node.Vector().push_back(JsonUtils::intNode(value)); + return node; + } +} +std::string Bonus::Description(std::optional customValue) const +{ + std::string str; + + if(description.empty()) + { + if(stacking.empty() || stacking == "ALWAYS") + { + switch(source) + { + case BonusSource::ARTIFACT: + str = sid.as().toEntity(VLC)->getNameTranslated(); + break; + case BonusSource::SPELL_EFFECT: + str = sid.as().toEntity(VLC)->getNameTranslated(); + break; + case BonusSource::CREATURE_ABILITY: + str = sid.as().toEntity(VLC)->getNamePluralTranslated(); + break; + case BonusSource::SECONDARY_SKILL: + str = VLC->skills()->getById(sid.as())->getNameTranslated(); + break; + case BonusSource::HERO_SPECIAL: + str = VLC->heroTypes()->getById(sid.as())->getNameTranslated(); + break; + default: + //todo: handle all possible sources + str = "Unknown"; + break; + } + } + else + str = stacking; + } + else + { + str = description; + } + + if(auto value = customValue.value_or(val)) { + //arraytxt already contains +-value + std::string valueString = boost::str(boost::format(" %+d") % value); + if(!boost::algorithm::ends_with(str, valueString)) + str += valueString; + } + + return str; +} + +static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) +{ + switch(type) + { + case BonusType::SPECIAL_UPGRADE: + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); + default: + return addInfo.toJsonNode(); + } +} + +JsonNode Bonus::toJsonNode() const +{ + JsonNode root(JsonNode::JsonType::DATA_STRUCT); + // only add values that might reasonably be found in config files + root["type"].String() = vstd::findKey(bonusNameMap, type); + if(subtype != BonusSubtypeID()) + root["subtype"].String() = subtype.toString(); + if(additionalInfo != CAddInfo::NONE) + root["addInfo"] = additionalInfoToJson(type, additionalInfo); + if(source != BonusSource::OTHER) + root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); + if(targetSourceType != BonusSource::OTHER) + root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); + if(sid != BonusSourceID()) + root["sourceID"].String() = sid.toString(); + if(val != 0) + root["val"].Integer() = val; + if(valType != BonusValueType::ADDITIVE_VALUE) + root["valueType"].String() = vstd::findKey(bonusValueMap, valType); + if(!stacking.empty()) + root["stacking"].String() = stacking; + if(!description.empty()) + root["description"].String() = description; + if(effectRange != BonusLimitEffect::NO_LIMIT) + root["effectRange"].String() = vstd::findKey(bonusLimitEffect, effectRange); + if(duration != BonusDuration::PERMANENT) + root["duration"] = BonusDuration::toJson(duration); + if(turnsRemain) + root["turns"].Integer() = turnsRemain; + if(limiter) + root["limiters"] = limiter->toJsonNode(); + if(updater) + root["updater"] = updater->toJsonNode(); + if(propagator) + root["propagator"].String() = vstd::findKey(bonusPropagatorMap, propagator); + return root; +} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID) + : Bonus(Duration, Type, Src, Val, ID, BonusSubtypeID(), std::string()) +{} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, std::string Desc) + : Bonus(Duration, Type, Src, Val, ID, BonusSubtypeID(), Desc) +{} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype) + : Bonus(Duration, Type, Src, Val, ID, Subtype, std::string()) +{} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype, std::string Desc): + duration(Duration), + type(Type), + subtype(Subtype), + source(Src), + val(Val), + sid(ID), + description(std::move(Desc)) +{ + boost::algorithm::trim(description); + targetSourceType = BonusSource::OTHER; +} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype, BonusValueType ValType): + duration(Duration), + type(Type), + subtype(Subtype), + source(Src), + val(Val), + sid(ID), + valType(ValType) +{ + turnsRemain = 0; + effectRange = BonusLimitEffect::NO_LIMIT; + targetSourceType = BonusSource::OTHER; +} + +std::shared_ptr Bonus::addPropagator(const TPropagatorPtr & Propagator) +{ + propagator = Propagator; + return this->shared_from_this(); +} + +DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) +{ + for(const auto & i : bonusNameMap) + if(i.second == bonus.type) + out << "\tType: " << i.first << " \t"; + +#define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" + printField(val); + out << "\tSubtype: " << bonus.subtype.toString() << "\n"; + printField(duration.to_ulong()); + printField(source); + out << "\tSource ID: " << bonus.sid.toString() << "\n"; + if(bonus.additionalInfo != CAddInfo::NONE) + out << "\taddInfo: " << bonus.additionalInfo.toString() << "\n"; + printField(turnsRemain); + printField(valType); + if(!bonus.stacking.empty()) + out << "\tstacking: \"" << bonus.stacking << "\"\n"; + printField(effectRange); +#undef printField + + if(bonus.limiter) + out << "\tLimiter: " << bonus.limiter->toString() << "\n"; + if(bonus.updater) + out << "\tUpdater: " << bonus.updater->toString() << "\n"; + + return out; +} + +std::shared_ptr Bonus::addLimiter(const TLimiterPtr & Limiter) +{ + if (limiter) + { + //If we already have limiter list, retrieve it + auto limiterList = std::dynamic_pointer_cast(limiter); + if(!limiterList) + { + //Create a new limiter list with old limiter and the new one will be pushed later + limiterList = std::make_shared(); + limiterList->add(limiter); + limiter = limiterList; + } + + limiterList->add(Limiter); + } + else + { + limiter = Limiter; + } + return this->shared_from_this(); +} + +// Updaters + +std::shared_ptr Bonus::addUpdater(const TUpdaterPtr & Updater) +{ + updater = Updater; + return this->shared_from_this(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 20bc16f3d..5f4a63608 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -1,191 +1,184 @@ -/* - * Bonus.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 "BonusEnum.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct Bonus; -class IBonusBearer; -class CBonusSystemNode; -class ILimiter; -class IPropagator; -class IUpdater; -class BonusList; -class CSelector; - -using TBonusSubtype = int32_t; -using TBonusListPtr = std::shared_ptr; -using TConstBonusListPtr = std::shared_ptr; -using TLimiterPtr = std::shared_ptr; -using TPropagatorPtr = std::shared_ptr; -using TUpdaterPtr = std::shared_ptr; - -class DLL_LINKAGE CAddInfo : public std::vector -{ -public: - enum { NONE = -1 }; - - CAddInfo(); - CAddInfo(si32 value); - - bool operator==(si32 value) const; - bool operator!=(si32 value) const; - - si32 & operator[](size_type pos); - si32 operator[](size_type pos) const; - - std::string toString() const; - JsonNode toJsonNode() const; -}; - -#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); - -/// Struct for handling bonuses of several types. Can be transferred to any hero -struct DLL_LINKAGE Bonus : public std::enable_shared_from_this -{ - BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values - 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 - TBonusSubtype subtype = -1; //-1 if not applicable - 4 bytes - - BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus - BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. - si32 val = 0; - ui32 sid = 0; //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; //if not NO_LIMIT, bonus will be omitted by default - - TLimiterPtr limiter; - TPropagatorPtr propagator; - TUpdaterPtr updater; - TUpdaterPtr propagationUpdater; - - std::string description; - - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype=-1); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype=-1, BonusValueType ValType = BonusValueType::ADDITIVE_VALUE); - Bonus() = default; - - template void serialize(Handler &h, const int version) - { - h & duration; - h & type; - h & subtype; - h & source; - h & val; - h & sid; - h & description; - h & additionalInfo; - h & turnsRemain; - h & valType; - h & stacking; - h & effectRange; - h & limiter; - h & propagator; - h & updater; - h & propagationUpdater; - h & targetSourceType; - } - - template - static bool compareByAdditionalInfo(const Ptr& a, const Ptr& b) - { - return a->additionalInfo < b->additionalInfo; - } - static bool NDays(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::N_DAYS; - return set.any(); - } - static bool NTurns(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::N_TURNS; - return set.any(); - } - static bool OneDay(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_DAY; - return set.any(); - } - static bool OneWeek(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_WEEK; - return set.any(); - } - static bool OneBattle(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_BATTLE; - return set.any(); - } - static bool Permanent(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::PERMANENT; - return set.any(); - } - static bool UntilGetsTurn(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::STACK_GETS_TURN; - return set.any(); - } - static bool UntilAttack(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::UNTIL_ATTACK; - return set.any(); - } - static bool UntilBeingAttacked(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::UNTIL_BEING_ATTACKED; - return set.any(); - } - static bool UntilCommanderKilled(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::COMMANDER_KILLED; - return set.any(); - } - inline bool operator == (const BonusType & cf) const - { - return type == cf; - } - inline void operator += (const ui32 Val) //no return - { - val += Val; - } - STRONG_INLINE static ui32 getSid32(ui32 high, ui32 low) - { - return (high << 16) + low; - } - - STRONG_INLINE static ui32 getHighFromSid32(ui32 sid) - { - return sid >> 16; - } - - STRONG_INLINE static ui32 getLowFromSid32(ui32 sid) - { - return sid & 0x0000FFFF; - } - - std::string Description(std::optional customValue = {}) const; - JsonNode toJsonNode() const; - - std::shared_ptr addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls - std::shared_ptr addPropagator(const TPropagatorPtr & Propagator); //returns this for convenient chain-calls - std::shared_ptr addUpdater(const TUpdaterPtr & Updater); //returns this for convenient chain-calls -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus); - -VCMI_LIB_NAMESPACE_END +/* + * Bonus.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 "BonusEnum.h" +#include "BonusCustomTypes.h" +#include "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct Bonus; +class IBonusBearer; +class CBonusSystemNode; +class ILimiter; +class IPropagator; +class IUpdater; +class BonusList; +class CSelector; + +using BonusSubtypeID = VariantIdentifier; +using BonusSourceID = VariantIdentifier; +using TBonusListPtr = std::shared_ptr; +using TConstBonusListPtr = std::shared_ptr; +using TLimiterPtr = std::shared_ptr; +using TPropagatorPtr = std::shared_ptr; +using TUpdaterPtr = std::shared_ptr; + +class DLL_LINKAGE CAddInfo : public std::vector +{ +public: + enum { NONE = -1 }; + + CAddInfo(); + CAddInfo(si32 value); + + bool operator==(si32 value) const; + bool operator!=(si32 value) const; + + si32 & operator[](size_type pos); + si32 operator[](size_type pos) const; + + std::string toString() const; + JsonNode toJsonNode() const; +}; + +#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); + +/// Struct for handling bonuses of several types. Can be transferred to any hero +struct DLL_LINKAGE Bonus : public std::enable_shared_from_this +{ + BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values + 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;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. + si32 val = 0; + 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; //if not NO_LIMIT, bonus will be omitted by default + + TLimiterPtr limiter; + TPropagatorPtr propagator; + TUpdaterPtr updater; + TUpdaterPtr propagationUpdater; + + std::string description; + + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, BonusValueType ValType); + Bonus() = default; + + template void serialize(Handler &h, const int version) + { + h & duration; + h & type; + h & subtype; + h & source; + h & val; + h & sid; + h & description; + h & additionalInfo; + h & turnsRemain; + h & valType; + h & stacking; + h & effectRange; + h & limiter; + h & propagator; + h & updater; + h & propagationUpdater; + h & targetSourceType; + } + + template + static bool compareByAdditionalInfo(const Ptr& a, const Ptr& b) + { + return a->additionalInfo < b->additionalInfo; + } + static bool NDays(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::N_DAYS; + return set.any(); + } + static bool NTurns(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::N_TURNS; + return set.any(); + } + static bool OneDay(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_DAY; + return set.any(); + } + static bool OneWeek(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_WEEK; + return set.any(); + } + static bool OneBattle(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_BATTLE; + return set.any(); + } + static bool Permanent(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::PERMANENT; + return set.any(); + } + static bool UntilGetsTurn(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::STACK_GETS_TURN; + return set.any(); + } + static bool UntilAttack(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::UNTIL_ATTACK; + return set.any(); + } + static bool UntilBeingAttacked(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::UNTIL_BEING_ATTACKED; + return set.any(); + } + static bool UntilCommanderKilled(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::COMMANDER_KILLED; + return set.any(); + } + inline bool operator == (const BonusType & cf) const + { + return type == cf; + } + inline void operator += (const ui32 Val) //no return + { + val += Val; + } + + std::string Description(std::optional customValue = {}) const; + JsonNode toJsonNode() const; + + std::shared_ptr addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls + std::shared_ptr addPropagator(const TPropagatorPtr & Propagator); //returns this for convenient chain-calls + std::shared_ptr addUpdater(const TUpdaterPtr & Updater); //returns this for convenient chain-calls +}; + +DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusCustomTypes.cpp b/lib/bonuses/BonusCustomTypes.cpp new file mode 100644 index 000000000..4e793ac21 --- /dev/null +++ b/lib/bonuses/BonusCustomTypes.cpp @@ -0,0 +1,74 @@ +/* + * BonusCustomTypes.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 "BonusCustomTypes.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const BonusCustomSubtype BonusCustomSubtype::creatureDamageBoth(0); +const BonusCustomSubtype BonusCustomSubtype::creatureDamageMin(1); +const BonusCustomSubtype BonusCustomSubtype::creatureDamageMax(2); +const BonusCustomSubtype BonusCustomSubtype::damageTypeAll(-1); +const BonusCustomSubtype BonusCustomSubtype::damageTypeMelee(0); +const BonusCustomSubtype BonusCustomSubtype::damageTypeRanged(1); +const BonusCustomSubtype BonusCustomSubtype::heroMovementLand(1); +const BonusCustomSubtype BonusCustomSubtype::heroMovementSea(0); +const BonusCustomSubtype BonusCustomSubtype::deathStareGorgon(0); +const BonusCustomSubtype BonusCustomSubtype::deathStareCommander(1); +const BonusCustomSubtype BonusCustomSubtype::rebirthRegular(0); +const BonusCustomSubtype BonusCustomSubtype::rebirthSpecial(1); +const BonusCustomSubtype BonusCustomSubtype::visionsMonsters(0); +const BonusCustomSubtype BonusCustomSubtype::visionsHeroes(1); +const BonusCustomSubtype BonusCustomSubtype::visionsTowns(2); +const BonusCustomSubtype BonusCustomSubtype::immunityBattleWide(0); +const BonusCustomSubtype BonusCustomSubtype::immunityEnemyHero(1); +const BonusCustomSubtype BonusCustomSubtype::transmutationPerHealth(0); +const BonusCustomSubtype BonusCustomSubtype::transmutationPerUnit(1); +const BonusCustomSubtype BonusCustomSubtype::destructionKillPercentage(0); +const BonusCustomSubtype BonusCustomSubtype::destructionKillAmount(1); +const BonusCustomSubtype BonusCustomSubtype::soulStealPermanent(0); +const BonusCustomSubtype BonusCustomSubtype::soulStealBattle(1); +const BonusCustomSubtype BonusCustomSubtype::movementFlying(0); +const BonusCustomSubtype BonusCustomSubtype::movementTeleporting(1); + +const BonusCustomSource BonusCustomSource::undeadMoraleDebuff(-2); + +BonusCustomSubtype BonusCustomSubtype::spellLevel(int level) +{ + return BonusCustomSubtype(level); +} + +BonusCustomSubtype BonusCustomSubtype::creatureLevel(int level) +{ + return BonusCustomSubtype(level); +} + +si32 BonusCustomSubtype::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusCustomSubtype::encode(const si32 index) +{ + return std::to_string(index); +} + +si32 BonusCustomSource::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusCustomSource::encode(const si32 index) +{ + return std::to_string(index); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h new file mode 100644 index 000000000..a6a2c96e2 --- /dev/null +++ b/lib/bonuses/BonusCustomTypes.h @@ -0,0 +1,75 @@ +/* + * BonusCustomTypes.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 + +class DLL_LINKAGE BonusCustomSource : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusCustomSource undeadMoraleDebuff; // -2 +}; + +class DLL_LINKAGE BonusCustomSubtype : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusCustomSubtype creatureDamageBoth; // 0 + static const BonusCustomSubtype creatureDamageMin; // 1 + static const BonusCustomSubtype creatureDamageMax; // 2 + + static const BonusCustomSubtype damageTypeAll; // -1 + static const BonusCustomSubtype damageTypeMelee; // 0 + static const BonusCustomSubtype damageTypeRanged; // 1 + + static const BonusCustomSubtype heroMovementLand; // 1 + static const BonusCustomSubtype heroMovementSea; // 0 + + static const BonusCustomSubtype deathStareGorgon; // 0 + static const BonusCustomSubtype deathStareCommander; + + static const BonusCustomSubtype rebirthRegular; // 0 + static const BonusCustomSubtype rebirthSpecial; // 1 + + static const BonusCustomSubtype visionsMonsters; // 0 + static const BonusCustomSubtype visionsHeroes; // 1 + static const BonusCustomSubtype visionsTowns; // 2 + + static const BonusCustomSubtype immunityBattleWide; // 0 + static const BonusCustomSubtype immunityEnemyHero; // 1 + + static const BonusCustomSubtype transmutationPerHealth; // 0 + static const BonusCustomSubtype transmutationPerUnit; // 1 + + static const BonusCustomSubtype destructionKillPercentage; // 0 + static const BonusCustomSubtype destructionKillAmount; // 1 + + static const BonusCustomSubtype soulStealPermanent; // 0 + static const BonusCustomSubtype soulStealBattle; // 1 + + static const BonusCustomSubtype movementFlying; // 0 + static const BonusCustomSubtype movementTeleporting; // 1 + + static BonusCustomSubtype spellLevel(int level); + static BonusCustomSubtype creatureLevel(int level); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 8d43a48ac..6902508a9 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -74,10 +74,6 @@ class JsonNode; BONUS_NAME(SPELL_LIKE_ATTACK) /*subtype - spell, value - spell level; range is taken from spell, but damage from creature; eg. magog*/ \ BONUS_NAME(THREE_HEADED_ATTACK) /*eg. cerberus*/ \ BONUS_NAME(GENERAL_DAMAGE_PREMY) \ - BONUS_NAME(FIRE_IMMUNITY) /*subtype 0 - all, 1 - all except positive*/ \ - BONUS_NAME(WATER_IMMUNITY) \ - BONUS_NAME(EARTH_IMMUNITY) \ - BONUS_NAME(AIR_IMMUNITY) \ BONUS_NAME(MIND_IMMUNITY) \ BONUS_NAME(FIRE_SHIELD) \ BONUS_NAME(UNDEAD) \ @@ -121,7 +117,6 @@ class JsonNode; BONUS_NAME(SPECIAL_UPGRADE) /*subtype = base, additionalInfo = target */\ BONUS_NAME(DRAGON_NATURE) \ BONUS_NAME(CREATURE_DAMAGE)/*subtype 0 = both, 1 = min, 2 = max*/\ - BONUS_NAME(EXP_MULTIPLIER)/* val - percent of additional exp gained by stack/commander (base value 100)*/\ BONUS_NAME(SHOTS)\ BONUS_NAME(DEATH_STARE) /*subtype 0 - gorgon, 1 - commander*/\ BONUS_NAME(POISON) /*val - max health penalty from poison possible*/\ @@ -134,9 +129,6 @@ class JsonNode; BONUS_NAME(CREATURE_ENCHANT_POWER) /* total duration of spells cast by creature */ \ BONUS_NAME(ENCHANTED) /* permanently enchanted with spell subID of level = val, if val > 3 then spell is mass and has level of val-3*/ \ BONUS_NAME(REBIRTH) /* val - percent of life restored, subtype = 0 - regular, 1 - at least one unit (sacred Phoenix) */\ - BONUS_NAME(ADDITIONAL_UNITS) /*val of units with id = subtype will be added to hero's army at the beginning of battle */\ - BONUS_NAME(SPOILS_OF_WAR) /*val * 10^-6 * gained exp resources of subtype will be given to hero after battle*/\ - BONUS_NAME(BLOCK)\ BONUS_NAME(DISGUISED) /* subtype - spell level */\ BONUS_NAME(VISIONS) /* subtype - spell level */\ BONUS_NAME(NO_TERRAIN_PENALTY) /* subtype - terrain type */\ @@ -175,13 +167,17 @@ class JsonNode; BONUS_NAME(BONUS_DAMAGE_PERCENTAGE) /*If hero can grant conditional damage to creature, value is percentage, subtype is creatureID*/\ BONUS_NAME(BONUS_DAMAGE_CHANCE) /*If hero can grant additional damage to creature, value is chance, subtype is creatureID*/\ BONUS_NAME(MAX_LEARNABLE_SPELL_LEVEL) /*This can work as wisdom before. val = max learnable spell level*/\ + BONUS_NAME(SPELL_SCHOOL_IMMUNITY) /*This bonus will work as spell school immunity for all spells, subtype - spell school: 0 - air, 1 - fire, 2 - water, 3 - earth. Any is not handled for reducing overlap from LEVEL_SPELL_IMMUNITY*/\ + BONUS_NAME(NEGATIVE_EFFECTS_IMMUNITY) /*This bonus will work as spell school immunity for negative effects from spells of school, subtype - spell school: -1 - any, 0 - air, 1 - fire, 2 - water, 3 - earth*/\ + BONUS_NAME(TERRAIN_NATIVE) /* end of list */ #define BONUS_SOURCE_LIST \ BONUS_SOURCE(ARTIFACT)\ BONUS_SOURCE(ARTIFACT_INSTANCE)\ - BONUS_SOURCE(OBJECT)\ + BONUS_SOURCE(OBJECT_TYPE)\ + BONUS_SOURCE(OBJECT_INSTANCE)\ BONUS_SOURCE(CREATURE_ABILITY)\ BONUS_SOURCE(TERRAIN_NATIVE)\ BONUS_SOURCE(TERRAIN_OVERLAY)\ @@ -192,7 +188,6 @@ class JsonNode; BONUS_SOURCE(HERO_SPECIAL)\ BONUS_SOURCE(ARMY)\ BONUS_SOURCE(CAMPAIGN_BONUS)\ - BONUS_SOURCE(SPECIAL_WEEK)\ BONUS_SOURCE(STACK_EXPERIENCE)\ BONUS_SOURCE(COMMANDER) /*TODO: consider using simply STACK_INSTANCE */\ BONUS_SOURCE(GLOBAL) /*used for base bonuses which all heroes or all stacks should have*/\ @@ -257,4 +252,4 @@ extern DLL_LINKAGE const std::map bonusSourceMap; extern DLL_LINKAGE const std::map bonusDurationMap; extern DLL_LINKAGE const std::map bonusLimitEffect; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusList.cpp b/lib/bonuses/BonusList.cpp index f3e0b12bf..4920881b3 100644 --- a/lib/bonuses/BonusList.cpp +++ b/lib/bonuses/BonusList.cpp @@ -275,4 +275,4 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const BonusList &bonusL return out; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusList.h b/lib/bonuses/BonusList.h index d219e8a97..eef5a8777 100644 --- a/lib/bonuses/BonusList.h +++ b/lib/bonuses/BonusList.h @@ -111,4 +111,4 @@ public: DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const BonusList &bonusList); -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index 64c5a2eeb..7274c0d9e 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -10,10 +10,14 @@ #include "StdInc.h" +#include "BonusEnum.h" #include "BonusParams.h" #include "BonusSelector.h" #include "../ResourceSet.h" +#include "../VCMI_Lib.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,7 +46,11 @@ const std::set deprecatedBonusSet = { "FIRE_SPELLS", "AIR_SPELLS", "WATER_SPELLS", - "EARTH_SPELLS" + "EARTH_SPELLS", + "FIRE_IMMUNITY", + "AIR_IMMUNITY", + "WATER_IMMUNITY", + "EARTH_IMMUNITY" }; BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSubtypeStr, int deprecatedSubtype): @@ -76,76 +84,76 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::ANY); + subtype = BonusSubtypeID(SpellSchool::ANY); } else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar") type = BonusType::LEARN_MEETING_SPELL_LIMIT; else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery") { - subtype = 1; + subtype = BonusCustomSubtype::damageTypeRanged; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence") { - subtype = 0; + subtype = BonusCustomSubtype::damageTypeMelee; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer") { - subtype = -1; + subtype = BonusCustomSubtype::damageTypeAll; type = BonusType::GENERAL_DAMAGE_REDUCTION; } else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation") { - subtype = 0; + subtype = BonusCustomSubtype::heroMovementSea; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics") { - subtype = 1; + subtype = BonusCustomSubtype::heroMovementLand; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::ESTATES || deprecatedSubtypeStr == "skill.estates") { type = BonusType::GENERATE_RESOURCE; - subtype = GameResID(EGameResID::GOLD); + subtype = BonusSubtypeID(GameResID(EGameResID::GOLD)); } else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); } else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::BONUS_DAMAGE_CHANCE; - subtypeStr = "core:creature.ballista"; + subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA)); } else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid") { type = BonusType::SPECIFIC_SPELL_POWER; - subtypeStr = "core:spell.firstAid"; + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "firstAid")); } else if (deprecatedSubtype == SecondarySkill::BALLISTICS || deprecatedSubtypeStr == "skill.ballistics") { type = BonusType::CATAPULT_EXTRA_SHOTS; - subtypeStr = "core:spell.catapultShot"; + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "catapultShot")); } else isConverted = false; @@ -157,27 +165,27 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::HERO_GRANTS_ATTACKS; - subtypeStr = "core:creature.ballista"; + subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA)); } else isConverted = false; } else if (deprecatedTypeStr == "SEA_MOVEMENT") { - subtype = 0; + subtype = BonusCustomSubtype::heroMovementSea; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "LAND_MOVEMENT") { - subtype = 1; + subtype = BonusCustomSubtype::heroMovementLand; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "MAXED_SPELL") { type = BonusType::SPELL; - subtypeStr = deprecatedSubtypeStr; + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", deprecatedSubtypeStr)); valueType = BonusValueType::INDEPENDENT_MAX; val = 3; } @@ -218,48 +226,112 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedTypeStr == "DIRECT_DAMAGE_IMMUNITY") { type = BonusType::SPELL_DAMAGE_REDUCTION; - subtype = SpellSchool(ESpellSchool::ANY); + subtype = BonusSubtypeID(SpellSchool::ANY); val = 100; } else if (deprecatedTypeStr == "AIR_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); + } + else if (deprecatedTypeStr == "AIR_IMMUNITY") + { + subtype = BonusSubtypeID(SpellSchool::AIR); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "FIRE_IMMUNITY") + { + subtype = BonusSubtypeID(SpellSchool::FIRE); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "WATER_IMMUNITY") + { + subtype = BonusSubtypeID(SpellSchool::WATER); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "EARTH_IMMUNITY") + { + subtype = BonusSubtypeID(SpellSchool::EARTH); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } } else isConverted = false; @@ -271,10 +343,8 @@ const JsonNode & BonusParams::toJson() if(ret.isNull()) { ret["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtypeStr) - ret["subtype"].String() = *subtypeStr; - else if(subtype) - ret["subtype"].Integer() = *subtype; + if(subtype) + ret["subtype"].String() = subtype->toString(); if(valueType) ret["valueType"].String() = vstd::findKey(bonusValueMap, *valueType); if(val) @@ -283,17 +353,13 @@ const JsonNode & BonusParams::toJson() ret["targetSourceType"].String() = vstd::findKey(bonusSourceMap, *targetType); jsonCreated = true; } + ret.setMeta(ModScope::scopeGame()); return ret; }; CSelector BonusParams::toSelector() { assert(isConverted); - if(subtypeStr) - { - subtype = -1; - JsonUtils::resolveIdentifier(*subtype, toJson(), "subtype"); - } auto ret = Selector::type()(type); if(subtype) @@ -305,4 +371,4 @@ CSelector BonusParams::toSelector() return ret; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusParams.h b/lib/bonuses/BonusParams.h index 31e86b38d..b0b2d3ef2 100644 --- a/lib/bonuses/BonusParams.h +++ b/lib/bonuses/BonusParams.h @@ -19,8 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE BonusParams { bool isConverted; BonusType type = BonusType::NONE; - std::optional subtype = std::nullopt; - std::optional subtypeStr = std::nullopt; + std::optional subtype = std::nullopt; std::optional valueType = std::nullopt; std::optional val = std::nullopt; std::optional targetType = std::nullopt; @@ -36,4 +35,4 @@ private: extern DLL_LINKAGE const std::set deprecatedBonusSet; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSelector.cpp b/lib/bonuses/BonusSelector.cpp index bfc4dab86..1d57e8e13 100644 --- a/lib/bonuses/BonusSelector.cpp +++ b/lib/bonuses/BonusSelector.cpp @@ -21,9 +21,9 @@ namespace Selector return stype; } - DLL_LINKAGE CSelectFieldEqual & subtype() + DLL_LINKAGE CSelectFieldEqual & subtype() { - static CSelectFieldEqual ssubtype(&Bonus::subtype); + static CSelectFieldEqual ssubtype(&Bonus::subtype); return ssubtype; } @@ -54,22 +54,22 @@ namespace Selector DLL_LINKAGE CWillLastTurns turns; DLL_LINKAGE CWillLastDays days; - CSelector DLL_LINKAGE typeSubtype(BonusType Type, TBonusSubtype Subtype) + CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype) { return type()(Type).And(subtype()(Subtype)); } - CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, TBonusSubtype subtype, const CAddInfo & info) + CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, BonusSubtypeID subtype, const CAddInfo & info) { return CSelectFieldEqual(&Bonus::type)(type) - .And(CSelectFieldEqual(&Bonus::subtype)(subtype)) + .And(CSelectFieldEqual(&Bonus::subtype)(subtype)) .And(CSelectFieldEqual(&Bonus::additionalInfo)(info)); } - CSelector DLL_LINKAGE source(BonusSource source, ui32 sourceID) + CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID) { return CSelectFieldEqual(&Bonus::source)(source) - .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); + .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); } CSelector DLL_LINKAGE sourceTypeSel(BonusSource source) @@ -86,4 +86,4 @@ namespace Selector DLL_LINKAGE CSelector none([](const Bonus * b){return false;}); } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSelector.h b/lib/bonuses/BonusSelector.h index b74710539..4e2ebbc74 100644 --- a/lib/bonuses/BonusSelector.h +++ b/lib/bonuses/BonusSelector.h @@ -22,7 +22,7 @@ public: template CSelector(const T &t, //SFINAE trick -> include this c-tor in overload resolution only if parameter is class //(includes functors, lambdas) or function. Without that VC is going mad about ambiguities. - typename std::enable_if < boost::mpl::or_ < std::is_class, std::is_function> ::value>::type *dummy = nullptr) + typename std::enable_if_t < std::is_class_v || std::is_function_v > *dummy = nullptr) : TBase(t) {} @@ -126,7 +126,7 @@ public: namespace Selector { extern DLL_LINKAGE CSelectFieldEqual & type(); - extern DLL_LINKAGE CSelectFieldEqual & subtype(); + extern DLL_LINKAGE CSelectFieldEqual & subtype(); extern DLL_LINKAGE CSelectFieldEqual & info(); extern DLL_LINKAGE CSelectFieldEqual & sourceType(); extern DLL_LINKAGE CSelectFieldEqual & targetSourceType(); @@ -134,9 +134,9 @@ namespace Selector extern DLL_LINKAGE CWillLastTurns turns; extern DLL_LINKAGE CWillLastDays days; - CSelector DLL_LINKAGE typeSubtype(BonusType Type, TBonusSubtype Subtype); - CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, TBonusSubtype subtype, const CAddInfo & info); - CSelector DLL_LINKAGE source(BonusSource source, ui32 sourceID); + CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype); + CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, BonusSubtypeID subtype, const CAddInfo & info); + CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID); CSelector DLL_LINKAGE sourceTypeSel(BonusSource source); CSelector DLL_LINKAGE valueType(BonusValueType valType); @@ -153,4 +153,4 @@ namespace Selector extern DLL_LINKAGE CSelector none; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 219d730bd..63ab0b631 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -62,30 +62,30 @@ bool IBonusBearer::hasBonusOfType(BonusType type) const return hasBonus(s, cachingStr); } -int IBonusBearer::valOfBonuses(BonusType type, int subtype) const +int IBonusBearer::valOfBonuses(BonusType type, BonusSubtypeID subtype) const { //This part is performance-critical - std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + std::to_string(subtype); + std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); CSelector s = Selector::typeSubtype(type, subtype); return valOfBonuses(s, cachingStr); } -bool IBonusBearer::hasBonusOfType(BonusType type, int subtype) const +bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const { //This part is performance-critical - std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + std::to_string(subtype); + std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); CSelector s = Selector::typeSubtype(type, subtype); return hasBonus(s, cachingStr); } -bool IBonusBearer::hasBonusFrom(BonusSource source, ui32 sourceID) const +bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const { - boost::format fmt("source_%did_%d"); - fmt % static_cast(source) % sourceID; + boost::format fmt("source_%did_%s"); + fmt % static_cast(source) % sourceID.toString(); return hasBonus(Selector::source(source,sourceID), fmt.str()); } @@ -95,4 +95,4 @@ std::shared_ptr IBonusBearer::getBonus(const CSelector &selector) c return bonuses->getFirst(Selector::all); } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index 242c66f83..cae96f06a 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -35,11 +35,11 @@ public: //Optimized interface (with auto-caching) int valOfBonuses(BonusType type) const; //subtype -> subtype of bonus; bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype) - int valOfBonuses(BonusType type, int subtype) const; //subtype -> subtype of bonus; - bool hasBonusOfType(BonusType type, int subtype) const;//determines if hero has a bonus of given type (and optionally subtype) - bool hasBonusFrom(BonusSource source, ui32 sourceID) const; + int valOfBonuses(BonusType type, BonusSubtypeID subtype) const; //subtype -> subtype of bonus; + bool hasBonusOfType(BonusType type, BonusSubtypeID subtype) const;//determines if hero has a bonus of given type (and optionally subtype) + bool hasBonusFrom(BonusSource source, BonusSourceID sourceID) const; virtual int64_t getTreeVersion() const = 0; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index eace34379..b0e031368 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -21,9 +21,8 @@ #include "../CSkillHandler.h" #include "../CStack.h" #include "../CArtHandler.h" -#include "../CModHandler.h" #include "../TerrainHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" VCMI_LIB_NAMESPACE_BEGIN @@ -115,7 +114,7 @@ CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool Inc void CCreatureTypeLimiter::setCreature(const CreatureID & id) { - creature = VLC->creh->objects[id]; + creature = id.toCreature(); } std::string CCreatureTypeLimiter::toString() const @@ -137,11 +136,11 @@ JsonNode CCreatureTypeLimiter::toJsonNode() const } HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus ) - : type(bonus), subtype(0), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false) + : type(bonus), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false) { } -HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, TBonusSubtype _subtype ) +HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, BonusSubtypeID _subtype ) : type(bonus), subtype(_subtype), isSubtypeRelevant(true), isSourceRelevant(false), isSourceIDRelevant(false) { } @@ -151,7 +150,7 @@ HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSource src) { } -HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype, BonusSource src) +HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src) : type(bonus), subtype(_subtype), isSubtypeRelevant(true), source(src), isSourceRelevant(true), isSourceIDRelevant(false) { } @@ -185,8 +184,8 @@ std::string HasAnotherBonusLimiter::toString() const std::string typeName = vstd::findKey(bonusNameMap, type); if(isSubtypeRelevant) { - boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%d)"); - fmt % typeName % subtype; + boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%s)"); + fmt % typeName % subtype.toString(); return fmt.str(); } else @@ -206,7 +205,7 @@ JsonNode HasAnotherBonusLimiter::toJsonNode() const root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER"; root["parameters"].Vector().push_back(JsonUtils::stringNode(typeName)); if(isSubtypeRelevant) - root["parameters"].Vector().push_back(JsonUtils::intNode(subtype)); + root["parameters"].Vector().push_back(JsonUtils::stringNode(subtype.toString())); if(isSourceRelevant) root["parameters"].Vector().push_back(JsonUtils::stringNode(sourceTypeName)); @@ -304,10 +303,10 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) switch(context.b.source) { case BonusSource::CREATURE_ABILITY: - return bearer->getFaction() == CreatureID(context.b.sid).toCreature()->getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFaction() == context.b.sid.as().toCreature()->getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; case BonusSource::TOWN_STRUCTURE: - return bearer->getFaction() == FactionID(Bonus::getHighFromSid32(context.b.sid)) ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFaction() == context.b.sid.as().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; //TODO: other sources of bonuses } @@ -318,7 +317,7 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) std::string FactionLimiter::toString() const { boost::format fmt("FactionLimiter(faction=%s)"); - fmt % VLC->factions()->getByIndex(faction)->getJsonKey(); + fmt % VLC->factions()->getById(faction)->getJsonKey(); return fmt.str(); } @@ -327,7 +326,7 @@ JsonNode FactionLimiter::toJsonNode() const JsonNode root(JsonNode::JsonType::DATA_STRUCT); root["type"].String() = "FACTION_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getByIndex(faction)->getJsonKey())); + root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getById(faction)->getJsonKey())); return root; } diff --git a/lib/bonuses/Limiters.h b/lib/bonuses/Limiters.h index b4a27c7b8..2e0b60126 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -116,17 +116,17 @@ class DLL_LINKAGE HasAnotherBonusLimiter : public ILimiter //applies only to nod { public: BonusType type; - TBonusSubtype subtype; + BonusSubtypeID subtype; BonusSource source; - si32 sid; + BonusSourceID sid; bool isSubtypeRelevant; //check for subtype only if this is true bool isSourceRelevant; //check for bonus source only if this is true bool isSourceIDRelevant; //check for bonus source only if this is true HasAnotherBonusLimiter(BonusType bonus = BonusType::NONE); - HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype); + HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype); HasAnotherBonusLimiter(BonusType bonus, BonusSource src); - HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype, BonusSource src); + HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src); EDecision limit(const BonusLimitationContext &context) const override; std::string toString() const override; diff --git a/lib/campaign/CampaignConstants.h b/lib/campaign/CampaignConstants.h index cc31d1f78..e4a615d47 100644 --- a/lib/campaign/CampaignConstants.h +++ b/lib/campaign/CampaignConstants.h @@ -48,10 +48,4 @@ enum class CampaignBonusType : int8_t HERO }; -enum class CampaignScenarioID : int8_t -{ - NONE = -1, - // no members - fake enum to create integer type that is not implicitly convertible to int -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index f4452ab7d..ad59559df 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -1,615 +1,622 @@ -/* - * CCampaignHandler.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 "CampaignHandler.h" - -#include "CampaignState.h" - -#include "../filesystem/Filesystem.h" -#include "../filesystem/CCompressedStream.h" -#include "../filesystem/CMemoryStream.h" -#include "../filesystem/CBinaryReader.h" -#include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" -#include "../TextOperations.h" -#include "../CModHandler.h" -#include "../Languages.h" -#include "../StringConstants.h" -#include "../mapping/CMapHeader.h" -#include "../mapping/CMapService.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void CampaignHandler::readCampaign(Campaign * ret, const std::vector & input, std::string filename, std::string modName, std::string encoding) -{ - if (input.front() < uint8_t(' ')) // binary format - { - CMemoryStream stream(input.data(), input.size()); - CBinaryReader reader(&stream); - - readHeaderFromMemory(*ret, reader, filename, modName, encoding); - - for(int g = 0; g < ret->numberOfScenarios; ++g) - { - auto scenarioID = static_cast(ret->scenarios.size()); - ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); - } - } - else // text format (json) - { - JsonNode jsonCampaign((const char*)input.data(), input.size()); - readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); - - for(auto & scenario : jsonCampaign["scenarios"].Vector()) - { - auto scenarioID = static_cast(ret->scenarios.size()); - ret->scenarios[scenarioID] = readScenarioFromJson(scenario); - } - } -} - -std::unique_ptr CampaignHandler::getHeader( const std::string & name) -{ - ResourceID 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; - - auto ret = std::make_unique(); - auto fileStream = CResourceHandler::get(modName)->load(resourceID); - std::vector cmpgn = getFile(std::move(fileStream), true)[0]; - - readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding); - - return ret; -} - -std::shared_ptr CampaignHandler::getCampaign( const std::string & name ) -{ - ResourceID 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; - - auto ret = std::make_unique(); - - auto fileStream = CResourceHandler::get(modName)->load(resourceID); - - std::vector> files = getFile(std::move(fileStream), false); - - readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding); - - //first entry is campaign header. start loop from 1 - for(int scenarioIndex = 0, fileIndex = 1; fileIndex < files.size() && scenarioIndex < ret->numberOfScenarios; scenarioIndex++) - { - auto scenarioID = static_cast(scenarioIndex); - - if(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios - continue; - - std::string scenarioName = resourceID.getName(); - boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(fileIndex - 1); - - //set map piece appropriately, convert vector to string - ret->mapPieces[scenarioID] = files[fileIndex]; - - auto hdr = ret->getMapHeader(scenarioID); - ret->scenarios[scenarioID].scenarioName = hdr->name; - fileIndex++; - } - - return ret; -} - -static std::string convertMapName(std::string input) -{ - boost::algorithm::to_lower(input); - boost::algorithm::trim(input); - - size_t slashPos = input.find_last_of("/"); - - if (slashPos != std::string::npos) - return input.substr(slashPos + 1); - - return input; -} - -std::string CampaignHandler::readLocalizedString(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 ""; - - VLC->generaltexth->registerString(modName, stringID, input); - return VLC->generaltexth->translate(stringID.get()); -} - -void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding) -{ - ret.version = static_cast(reader["version"].Integer()); - if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX) - { - logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, static_cast(ret.version)); - return; - } - - ret.version = CampaignVersion::VCMI; - ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); - ret.numberOfScenarios = reader["scenarios"].Vector().size(); - ret.name = reader["name"].String(); - ret.description = reader["description"].String(); - ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); - ret.music = reader["music"].String(); - ret.filename = filename; - ret.modName = modName; - ret.encoding = encoding; -} - -CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) -{ - auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog - { - CampaignScenarioPrologEpilog ret; - ret.hasPrologEpilog = !identifier.isNull(); - if(ret.hasPrologEpilog) - { - ret.prologVideo = identifier["video"].String(); - ret.prologMusic = identifier["music"].String(); - ret.prologText = identifier["text"].String(); - } - return ret; - }; - - CampaignScenario ret; - ret.mapName = reader["map"].String(); - for(auto & g : reader["preconditions"].Vector()) - ret.preconditionRegions.insert(static_cast(g.Integer())); - - ret.regionColor = reader["color"].Integer(); - ret.difficulty = reader["difficulty"].Integer(); - ret.regionText = reader["regionText"].String(); - ret.prolog = prologEpilogReader(reader["prolog"]); - ret.epilog = prologEpilogReader(reader["epilog"]); - - ret.travelOptions = readScenarioTravelFromJson(reader); - - return ret; -} - -CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) -{ - CampaignTravel ret; - - std::map startOptionsMap = { - {"none", CampaignStartOptions::NONE}, - {"bonus", CampaignStartOptions::START_BONUS}, - {"crossover", CampaignStartOptions::HERO_CROSSOVER}, - {"hero", CampaignStartOptions::HERO_OPTIONS} - }; - - std::map bonusTypeMap = { - {"spell", CampaignBonusType::SPELL}, - {"creature", CampaignBonusType::MONSTER}, - {"building", CampaignBonusType::BUILDING}, - {"artifact", CampaignBonusType::ARTIFACT}, - {"scroll", CampaignBonusType::SPELL_SCROLL}, - {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, - {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, - {"resource", CampaignBonusType::RESOURCE}, - //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, - //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, - }; - - std::map primarySkillsMap = { - {"attack", 0}, - {"defence", 8}, - {"spellpower", 16}, - {"knowledge", 24}, - }; - - std::map heroSpecialMap = { - {"strongest", 0xFFFD}, - {"generated", 0xFFFE}, - {"random", 0xFFFF} - }; - - std::map resourceTypeMap = { - //FD - wood+ore - //FE - mercury+sulfur+crystal+gem - {"wood", 0}, - {"mercury", 1}, - {"ore", 2}, - {"sulfur", 3}, - {"crystal", 4}, - {"gems", 5}, - {"gold", 6}, - {"common", 0xFD}, - {"rare", 0xFE} - }; - - for(auto & k : reader["heroKeeps"].Vector()) - { - if(k.String() == "experience") ret.whatHeroKeeps.experience = true; - if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true; - if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true; - if(k.String() == "spells") ret.whatHeroKeeps.spells = true; - if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true; - } - - for(auto & k : reader["keepCreatures"].Vector()) - { - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "creature", k.String())) - ret.monstersKeptByHero.insert(CreatureID(identifier.value())); - else - logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); - } - for(auto & k : reader["keepArtifacts"].Vector()) - { - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "artifact", k.String())) - ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); - else - logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); - } - - ret.startOptions = startOptionsMap[reader["startOptions"].String()]; - switch(ret.startOptions) - { - case CampaignStartOptions::NONE: - //no bonuses. Seems to be OK - break; - case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose - { - ret.playerColor = reader["playerColor"].Integer(); - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = bonusTypeMap[bjson["what"].String()]; - switch (bonus.type) - { - case CampaignBonusType::RESOURCE: - bonus.info1 = resourceTypeMap[bjson["type"].String()]; - bonus.info2 = bjson["amount"].Integer(); - break; - - case CampaignBonusType::BUILDING: - bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String()); - if(bonus.info1 == -1) - logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String()); - break; - - default: - if(int heroId = heroSpecialMap[bjson["hero"].String()]) - bonus.info1 = heroId; - else - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) - bonus.info1 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); - - bonus.info3 = bjson["amount"].Integer(); - - switch(bonus.type) - { - case CampaignBonusType::SPELL: - case CampaignBonusType::MONSTER: - case CampaignBonusType::SECONDARY_SKILL: - case CampaignBonusType::ARTIFACT: - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), bjson["what"].String(), bjson["type"].String())) - bonus.info2 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); - break; - - case CampaignBonusType::SPELL_SCROLL: - if(auto Identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "spell", bjson["type"].String())) - bonus.info2 = Identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); - break; - - case CampaignBonusType::PRIMARY_SKILL: - for(auto & ps : primarySkillsMap) - bonus.info2 |= bjson[ps.first].Integer() << ps.second; - break; - - default: - bonus.info2 = bjson["type"].Integer(); - } - break; - } - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose - { - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO; - bonus.info1 = bjson["playerColor"].Integer(); //player color - bonus.info2 = bjson["scenario"].Integer(); //from what scenario - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_OPTIONS: //heroes player can choose between - { - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = CampaignBonusType::HERO; - bonus.info1 = bjson["playerColor"].Integer(); //player color - - if(int heroId = heroSpecialMap[bjson["hero"].String()]) - bonus.info2 = heroId; - else - if (auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) - bonus.info2 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); - - ret.bonusesToChoose.push_back(bonus); - } - break; - } - default: - { - logGlobal->warn("VCMP Loading: Unsupported start options value"); - break; - } - } - - return ret; -} - - -void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) -{ - ret.version = static_cast(reader.readUInt32()); - ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] - ret.loadLegacyData(campId); - ret.name = readLocalizedString(reader, filename, modName, encoding, "name"); - ret.description = readLocalizedString(reader, filename, modName, encoding, "description"); - if (ret.version > CampaignVersion::RoE) - ret.difficultyChoosenByPlayer = reader.readInt8(); - else - ret.difficultyChoosenByPlayer = false; - - ret.music = prologMusicName(reader.readInt8()); - ret.filename = filename; - ret.modName = modName; - ret.encoding = encoding; -} - -CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) -{ - auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog - { - CampaignScenarioPrologEpilog ret; - ret.hasPrologEpilog = reader.readUInt8(); - if(ret.hasPrologEpilog) - { - ret.prologVideo = CampaignHandler::prologVideoName(reader.readUInt8()); - ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); - ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier); - } - return ret; - }; - - CampaignScenario ret; - ret.mapName = reader.readBaseString(); - reader.readUInt32(); //packedMapSize - not used - if(header.numberOfScenarios > 8) //unholy alliance - { - ret.loadPreconditionRegions(reader.readUInt16()); - } - else - { - ret.loadPreconditionRegions(reader.readUInt8()); - } - ret.regionColor = reader.readUInt8(); - ret.difficulty = reader.readUInt8(); - ret.regionText = readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"); - ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); - ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); - - ret.travelOptions = readScenarioTravelFromMemory(reader, header.version); - - return ret; -} - -template -static void readContainer(std::set & container, CBinaryReader & reader, int sizeBytes) -{ - for(int iId = 0, byte = 0; iId < sizeBytes * 8; ++iId) - { - if(iId % 8 == 0) - byte = reader.readUInt8(); - if(byte & (1 << iId % 8)) - container.insert(Identifier(iId)); - } -} - -CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version ) -{ - CampaignTravel ret; - - ui8 whatHeroKeeps = reader.readUInt8(); - ret.whatHeroKeeps.experience = whatHeroKeeps & 1; - ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2; - ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4; - ret.whatHeroKeeps.spells = whatHeroKeeps & 8; - ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16; - - readContainer(ret.monstersKeptByHero, reader, 19); - readContainer(ret.artifactsKeptByHero, reader, version < CampaignVersion::SoD ? 17 : 18); - - ret.startOptions = static_cast(reader.readUInt8()); - - switch(ret.startOptions) - { - case CampaignStartOptions::NONE: - //no bonuses. Seems to be OK - break; - case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose - { - ret.playerColor = reader.readUInt8(); - ui8 numOfBonuses = reader.readUInt8(); - for (int g=0; g(reader.readUInt8()); - //hero: FFFD means 'most powerful' and FFFE means 'generated' - switch(bonus.type) - { - case CampaignBonusType::SPELL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //spell ID - break; - } - case CampaignBonusType::MONSTER: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt16(); //monster type - bonus.info3 = reader.readUInt16(); //monster count - break; - } - case CampaignBonusType::BUILDING: - { - bonus.info1 = reader.readUInt8(); //building ID (0 - town hall, 1 - city hall, 2 - capitol, etc) - break; - } - case CampaignBonusType::ARTIFACT: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt16(); //artifact ID - break; - } - case CampaignBonusType::SPELL_SCROLL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //spell ID - break; - } - case CampaignBonusType::PRIMARY_SKILL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt32(); //bonuses (4 bytes for 4 skills) - break; - } - case CampaignBonusType::SECONDARY_SKILL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //skill ID - bonus.info3 = reader.readUInt8(); //skill level - break; - } - case CampaignBonusType::RESOURCE: - { - bonus.info1 = reader.readUInt8(); //type - //FD - wood+ore - //FE - mercury+sulfur+crystal+gem - bonus.info2 = reader.readUInt32(); //count - break; - } - default: - logGlobal->warn("Corrupted h3c file"); - break; - } - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose - { - ui8 numOfBonuses = reader.readUInt8(); - for (int g=0; gwarn("Corrupted h3c file"); - break; - } - } - - return ret; -} - -std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, bool headerOnly) -{ - CCompressedStream stream(std::move(file), true); - - std::vector< std::vector > ret; - 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()); - - return ret; -} - -std::string CampaignHandler::prologVideoName(ui8 index) -{ - JsonNode config(ResourceID(std::string("CONFIG/campaignMedia"), EResType::TEXT)); - auto vids = config["videos"].Vector(); - if(index < vids.size()) - return vids[index].String(); - return ""; -} - -std::string CampaignHandler::prologMusicName(ui8 index) -{ - std::vector music; - return VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index))); -} - -std::string CampaignHandler::prologVoiceName(ui8 index) -{ - JsonNode config(ResourceID(std::string("CONFIG/campaignMedia"), EResType::TEXT)); - auto audio = config["voice"].Vector(); - if(index < audio.size()) - return audio[index].String(); - return ""; -} - -VCMI_LIB_NAMESPACE_END +/* + * CCampaignHandler.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 "CampaignHandler.h" + +#include "CampaignState.h" + +#include "../filesystem/Filesystem.h" +#include "../filesystem/CCompressedStream.h" +#include "../filesystem/CMemoryStream.h" +#include "../filesystem/CBinaryReader.h" +#include "../modding/IdentifierStorage.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/ModScope.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CampaignHandler::readCampaign(Campaign * ret, const std::vector & input, std::string filename, std::string modName, std::string encoding) +{ + if (input.front() < uint8_t(' ')) // binary format + { + CMemoryStream stream(input.data(), input.size()); + CBinaryReader reader(&stream); + + readHeaderFromMemory(*ret, reader, filename, modName, encoding); + + for(int g = 0; g < ret->numberOfScenarios; ++g) + { + auto scenarioID = static_cast(ret->scenarios.size()); + ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); + } + } + else // text format (json) + { + JsonNode jsonCampaign((const char*)input.data(), input.size()); + readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); + + for(auto & scenario : jsonCampaign["scenarios"].Vector()) + { + auto scenarioID = static_cast(ret->scenarios.size()); + ret->scenarios[scenarioID] = readScenarioFromJson(scenario); + } + } +} + +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; + + auto ret = std::make_unique(); + auto fileStream = CResourceHandler::get(modName)->load(resourceID); + std::vector cmpgn = getFile(std::move(fileStream), true)[0]; + + readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding); + + return ret; +} + +std::shared_ptr CampaignHandler::getCampaign( 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; + + auto ret = std::make_unique(); + + auto fileStream = CResourceHandler::get(modName)->load(resourceID); + + std::vector> files = getFile(std::move(fileStream), false); + + readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding); + + //first entry is campaign header. start loop from 1 + for(int scenarioIndex = 0, fileIndex = 1; fileIndex < files.size() && scenarioIndex < ret->numberOfScenarios; scenarioIndex++) + { + auto scenarioID = static_cast(scenarioIndex); + + if(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios + continue; + + std::string scenarioName = resourceID.getName(); + boost::to_lower(scenarioName); + scenarioName += ':' + std::to_string(fileIndex - 1); + + //set map piece appropriately, convert vector to string + ret->mapPieces[scenarioID] = files[fileIndex]; + + auto hdr = ret->getMapHeader(scenarioID); + ret->scenarios[scenarioID].scenarioName = hdr->name; + fileIndex++; + } + + return ret; +} + +static std::string convertMapName(std::string input) +{ + boost::algorithm::to_lower(input); + boost::algorithm::trim(input); + + size_t slashPos = input.find_last_of("/"); + + if (slashPos != std::string::npos) + return input.substr(slashPos + 1); + + return input; +} + +std::string CampaignHandler::readLocalizedString(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 ""; + + VLC->generaltexth->registerString(modName, stringID, input); + return stringID.get(); +} + +void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding) +{ + ret.version = static_cast(reader["version"].Integer()); + if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX) + { + logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, static_cast(ret.version)); + return; + } + + 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.music = AudioPath::fromJson(reader["music"]); + ret.filename = filename; + ret.modName = modName; + ret.encoding = encoding; +} + +CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) +{ + auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog + { + CampaignScenarioPrologEpilog ret; + ret.hasPrologEpilog = !identifier.isNull(); + if(ret.hasPrologEpilog) + { + ret.prologVideo = VideoPath::fromJson(identifier["video"]); + ret.prologMusic = AudioPath::fromJson(identifier["music"]); + ret.prologVoice = AudioPath::fromJson(identifier["voice"]); + ret.prologText.jsonDeserialize(identifier["text"]); + } + return ret; + }; + + CampaignScenario ret; + ret.mapName = reader["map"].String(); + for(auto & g : reader["preconditions"].Vector()) + ret.preconditionRegions.insert(static_cast(g.Integer())); + + ret.regionColor = reader["color"].Integer(); + ret.difficulty = reader["difficulty"].Integer(); + ret.regionText.jsonDeserialize(reader["regionText"]); + ret.prolog = prologEpilogReader(reader["prolog"]); + ret.epilog = prologEpilogReader(reader["epilog"]); + + ret.travelOptions = readScenarioTravelFromJson(reader); + + return ret; +} + +CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) +{ + CampaignTravel ret; + + std::map startOptionsMap = { + {"none", CampaignStartOptions::NONE}, + {"bonus", CampaignStartOptions::START_BONUS}, + {"crossover", CampaignStartOptions::HERO_CROSSOVER}, + {"hero", CampaignStartOptions::HERO_OPTIONS} + }; + + std::map bonusTypeMap = { + {"spell", CampaignBonusType::SPELL}, + {"creature", CampaignBonusType::MONSTER}, + {"building", CampaignBonusType::BUILDING}, + {"artifact", CampaignBonusType::ARTIFACT}, + {"scroll", CampaignBonusType::SPELL_SCROLL}, + {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, + {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, + {"resource", CampaignBonusType::RESOURCE}, + //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, + //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, + }; + + std::map primarySkillsMap = { + {"attack", 0}, + {"defence", 8}, + {"spellpower", 16}, + {"knowledge", 24}, + }; + + std::map heroSpecialMap = { + {"strongest", 0xFFFD}, + {"generated", 0xFFFE}, + {"random", 0xFFFF} + }; + + std::map resourceTypeMap = { + //FD - wood+ore + //FE - mercury+sulfur+crystal+gem + {"wood", 0}, + {"mercury", 1}, + {"ore", 2}, + {"sulfur", 3}, + {"crystal", 4}, + {"gems", 5}, + {"gold", 6}, + {"common", 0xFD}, + {"rare", 0xFE} + }; + + for(auto & k : reader["heroKeeps"].Vector()) + { + if(k.String() == "experience") ret.whatHeroKeeps.experience = true; + if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true; + if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true; + if(k.String() == "spells") ret.whatHeroKeeps.spells = true; + if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true; + } + + for(auto & k : reader["keepCreatures"].Vector()) + { + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "creature", k.String())) + ret.monstersKeptByHero.insert(CreatureID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); + } + for(auto & k : reader["keepArtifacts"].Vector()) + { + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "artifact", k.String())) + ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); + } + + ret.startOptions = startOptionsMap[reader["startOptions"].String()]; + switch(ret.startOptions) + { + case CampaignStartOptions::NONE: + //no bonuses. Seems to be OK + break; + case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose + { + ret.playerColor = PlayerColor(PlayerColor::decode(reader["playerColor"].String())); + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = bonusTypeMap[bjson["what"].String()]; + switch (bonus.type) + { + case CampaignBonusType::RESOURCE: + bonus.info1 = resourceTypeMap[bjson["type"].String()]; + bonus.info2 = bjson["amount"].Integer(); + break; + + case CampaignBonusType::BUILDING: + bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String()); + if(bonus.info1 == -1) + logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String()); + break; + + default: + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info1 = heroId; + else + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) + bonus.info1 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + bonus.info3 = bjson["amount"].Integer(); + + switch(bonus.type) + { + case CampaignBonusType::SPELL: + case CampaignBonusType::MONSTER: + case CampaignBonusType::SECONDARY_SKILL: + case CampaignBonusType::ARTIFACT: + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), bjson["what"].String(), bjson["type"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); + break; + + case CampaignBonusType::SPELL_SCROLL: + if(auto Identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "spell", bjson["type"].String())) + bonus.info2 = Identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); + break; + + case CampaignBonusType::PRIMARY_SKILL: + for(auto & ps : primarySkillsMap) + bonus.info2 |= bjson[ps.first].Integer() << ps.second; + break; + + default: + bonus.info2 = bjson["type"].Integer(); + } + break; + } + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + bonus.info2 = bjson["scenario"].Integer(); //from what scenario + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_OPTIONS: //heroes player can choose between + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = CampaignBonusType::HERO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info2 = heroId; + else + if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + ret.bonusesToChoose.push_back(bonus); + } + break; + } + default: + { + logGlobal->warn("VCMP Loading: Unsupported start options value"); + break; + } + } + + return ret; +} + + +void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) +{ + ret.version = static_cast(reader.readUInt32()); + ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] + ret.loadLegacyData(campId); + ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); + ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); + if (ret.version > CampaignVersion::RoE) + ret.difficultyChoosenByPlayer = reader.readInt8(); + else + ret.difficultyChoosenByPlayer = false; + + ret.music = prologMusicName(reader.readInt8()); + ret.filename = filename; + ret.modName = modName; + ret.encoding = encoding; +} + +CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) +{ + auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog + { + CampaignScenarioPrologEpilog ret; + ret.hasPrologEpilog = reader.readUInt8(); + if(ret.hasPrologEpilog) + { + bool isOriginalCampaign = boost::starts_with(header.getFilename(), "DATA/"); + + ui8 index = reader.readUInt8(); + ret.prologVideo = CampaignHandler::prologVideoName(index); + ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); + ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); + ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier)); + } + return ret; + }; + + CampaignScenario ret; + ret.mapName = reader.readBaseString(); + reader.readUInt32(); //packedMapSize - not used + if(header.numberOfScenarios > 8) //unholy alliance + { + ret.loadPreconditionRegions(reader.readUInt16()); + } + else + { + ret.loadPreconditionRegions(reader.readUInt8()); + } + ret.regionColor = reader.readUInt8(); + ret.difficulty = reader.readUInt8(); + ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); + ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); + ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); + + ret.travelOptions = readScenarioTravelFromMemory(reader, header.version); + + return ret; +} + +template +static void readContainer(std::set & container, CBinaryReader & reader, int sizeBytes) +{ + for(int iId = 0, byte = 0; iId < sizeBytes * 8; ++iId) + { + if(iId % 8 == 0) + byte = reader.readUInt8(); + if(byte & (1 << iId % 8)) + container.insert(Identifier(iId)); + } +} + +CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version ) +{ + CampaignTravel ret; + + ui8 whatHeroKeeps = reader.readUInt8(); + ret.whatHeroKeeps.experience = whatHeroKeeps & 1; + ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2; + ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4; + ret.whatHeroKeeps.spells = whatHeroKeeps & 8; + ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16; + + readContainer(ret.monstersKeptByHero, reader, 19); + readContainer(ret.artifactsKeptByHero, reader, version < CampaignVersion::SoD ? 17 : 18); + + ret.startOptions = static_cast(reader.readUInt8()); + + switch(ret.startOptions) + { + case CampaignStartOptions::NONE: + //no bonuses. Seems to be OK + break; + case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose + { + ret.playerColor.setNum(reader.readUInt8()); + ui8 numOfBonuses = reader.readUInt8(); + for (int g=0; g(reader.readUInt8()); + //hero: FFFD means 'most powerful' and FFFE means 'generated' + switch(bonus.type) + { + case CampaignBonusType::SPELL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //spell ID + break; + } + case CampaignBonusType::MONSTER: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt16(); //monster type + bonus.info3 = reader.readUInt16(); //monster count + break; + } + case CampaignBonusType::BUILDING: + { + bonus.info1 = reader.readUInt8(); //building ID (0 - town hall, 1 - city hall, 2 - capitol, etc) + break; + } + case CampaignBonusType::ARTIFACT: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt16(); //artifact ID + break; + } + case CampaignBonusType::SPELL_SCROLL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //spell ID + break; + } + case CampaignBonusType::PRIMARY_SKILL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt32(); //bonuses (4 bytes for 4 skills) + break; + } + case CampaignBonusType::SECONDARY_SKILL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //skill ID + bonus.info3 = reader.readUInt8(); //skill level + break; + } + case CampaignBonusType::RESOURCE: + { + bonus.info1 = reader.readUInt8(); //type + //FD - wood+ore + //FE - mercury+sulfur+crystal+gem + bonus.info2 = reader.readUInt32(); //count + break; + } + default: + logGlobal->warn("Corrupted h3c file"); + break; + } + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose + { + ui8 numOfBonuses = reader.readUInt8(); + for (int g=0; gwarn("Corrupted h3c file"); + break; + } + } + + return ret; +} + +std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, bool headerOnly) +{ + CCompressedStream stream(std::move(file), true); + + std::vector< std::vector > ret; + 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()); + + return ret; +} + +VideoPath CampaignHandler::prologVideoName(ui8 index) +{ + JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); + auto vids = config["videos"].Vector(); + if(index < vids.size()) + return VideoPath::fromJson(vids[index]); + return VideoPath(); +} + +AudioPath CampaignHandler::prologMusicName(ui8 index) +{ + std::vector music; + return AudioPath::builtinTODO(VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index)))); +} + +AudioPath CampaignHandler::prologVoiceName(ui8 index) +{ + JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); + auto audio = config["voice"].Vector(); + if(index < audio.size()) + return AudioPath::fromJson(audio[index]); + return AudioPath(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index 4e0dd544d..d6e2d0579 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -1,45 +1,46 @@ -/* - * CampaignHandler.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 "CampaignState.h" // Convenience include - not required for build, but required for any user of CampaignHandler - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CampaignHandler -{ - static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); - - static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); - - //parsers for VCMI campaigns (*.vcmp) - static void readHeaderFromJson(CampaignHeader & target, JsonNode & reader, std::string filename, std::string modName, std::string encoding); - static CampaignScenario readScenarioFromJson(JsonNode & reader); - static CampaignTravel readScenarioTravelFromJson(JsonNode & reader); - - //parsers for original H3C campaigns - static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); - static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header); - static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version); - /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) - /// headerOnly - only header will be decompressed, returned vector wont have any maps - static std::vector> getFile(std::unique_ptr file, bool headerOnly); - - static std::string prologVideoName(ui8 index); - static std::string prologMusicName(ui8 index); - static std::string prologVoiceName(ui8 index); - -public: - static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file - - static std::shared_ptr getCampaign(const std::string & name); //name - name of appropriate file -}; - -VCMI_LIB_NAMESPACE_END +/* + * CampaignHandler.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 "CampaignState.h" // Convenience include - not required for build, but required for any user of CampaignHandler +#include "../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CampaignHandler +{ + static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); + + static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); + + //parsers for VCMI campaigns (*.vcmp) + static void readHeaderFromJson(CampaignHeader & target, JsonNode & reader, std::string filename, std::string modName, std::string encoding); + static CampaignScenario readScenarioFromJson(JsonNode & reader); + static CampaignTravel readScenarioTravelFromJson(JsonNode & reader); + + //parsers for original H3C campaigns + static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); + static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header); + static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version); + /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) + /// headerOnly - only header will be decompressed, returned vector wont have any maps + static std::vector> getFile(std::unique_ptr file, bool headerOnly); + + static VideoPath prologVideoName(ui8 index); + static AudioPath prologMusicName(ui8 index); + static AudioPath prologVoiceName(ui8 index); + +public: + static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file + + static std::shared_ptr getCampaign(const std::string & name); //name - name of appropriate file +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 5ab80f680..64611be70 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -9,20 +9,25 @@ */ #pragma once +#include "../filesystem/ResourcePath.h" +#include "../MetaString.h" + VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE CampaignScenarioPrologEpilog { bool hasPrologEpilog = false; - std::string prologVideo; // from CmpMovie.txt - std::string prologMusic; // from CmpMusic.txt - std::string prologText; + VideoPath prologVideo; + AudioPath prologMusic; // from CmpMusic.txt + AudioPath prologVoice; + MetaString prologText; template void serialize(Handler &h, const int formatVersion) { h & hasPrologEpilog; h & prologVideo; h & prologMusic; + h & prologVoice; h & prologText; } }; diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 554f89d44..d23ffd314 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -11,7 +11,7 @@ #include "CampaignState.h" #include "../JsonNode.h" -#include "../filesystem/ResourceID.h" +#include "../filesystem/ResourcePath.h" #include "../VCMI_Lib.h" #include "../CGeneralTextHandler.h" #include "../mapping/CMapService.h" @@ -58,7 +58,7 @@ CampaignRegions CampaignRegions::getLegacy(int campId) static std::vector campDescriptions; if(campDescriptions.empty()) //read once { - const JsonNode config(ResourceID("config/campaign_regions.json")); + const JsonNode config(JsonPath::builtin("config/campaign_regions.json")); for(const JsonNode & campaign : config["campaign_regions"].Vector()) campDescriptions.push_back(CampaignRegions::fromJson(campaign)); } @@ -66,20 +66,20 @@ CampaignRegions CampaignRegions::getLegacy(int campId) return campDescriptions.at(campId); } -std::string CampaignRegions::getBackgroundName() const +ImagePath CampaignRegions::getBackgroundName() const { - return campPrefix + "_BG.BMP"; + return ImagePath::builtin(campPrefix + "_BG.BMP"); } Point CampaignRegions::getPosition(CampaignScenarioID which) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; return Point(region.xpos, region.ypos); } -std::string CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const +ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; static const std::string colors[2][8] = { @@ -89,20 +89,20 @@ std::string CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex std::string color = colors[colorSuffixLength - 1][colorIndex]; - return campPrefix + region.infix + "_" + type + color + ".BMP"; + return ImagePath::builtin(campPrefix + region.infix + "_" + type + color + ".BMP"); } -std::string CampaignRegions::getAvailableName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getAvailableName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "En"); } -std::string CampaignRegions::getSelectedName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getSelectedName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "Se"); } -std::string CampaignRegions::getConqueredName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getConqueredName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "Co"); } @@ -134,14 +134,14 @@ bool CampaignHeader::formatVCMI() const return version == CampaignVersion::VCMI; } -std::string CampaignHeader::getDescription() const +std::string CampaignHeader::getDescriptionTranslated() const { - return description; + return description.toString(); } -std::string CampaignHeader::getName() const +std::string CampaignHeader::getNameTranslated() const { - return name; + return name.toString(); } std::string CampaignHeader::getFilename() const @@ -159,7 +159,7 @@ std::string CampaignHeader::getEncoding() const return encoding; } -std::string CampaignHeader::getMusic() const +AudioPath CampaignHeader::getMusic() const { return music; } @@ -267,24 +267,23 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe return a->getHeroStrength() > b->getHeroStrength(); }); - logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast(*currentMap), getFilename(), getName()); + logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); mapsConquered.push_back(*currentMap); auto reservedHeroes = getReservedHeroes(); for (auto * hero : heroes) { - HeroTypeID heroType(hero->subID); JsonNode node = CampaignState::crossoverSerialize(hero); - if (reservedHeroes.count(heroType)) + if (reservedHeroes.count(hero->getHeroType())) { - logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->subID, hero->getNameTranslated()); - globalHeroPool[heroType] = node; + logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroType(), hero->getNameTranslated()); + globalHeroPool[hero->getHeroType()] = node; } else { - logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroType(), hero->getNameTranslated()); scenarioHeroPool[*currentMap].push_back(node); } } @@ -321,7 +320,7 @@ std::unique_ptr CampaignState::getMap(CampaignScenarioID scenarioId) const CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; return mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); } @@ -334,7 +333,7 @@ std::unique_ptr CampaignState::getMapHeader(CampaignScenarioID scena CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; return mapService.loadMapHeader(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); } diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 51fb5c034..cbe133467 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -9,7 +9,9 @@ */ #pragma once -#include "../../lib/GameConstants.h" +#include "../lib/GameConstants.h" +#include "../lib/MetaString.h" +#include "../lib/filesystem/ResourcePath.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" @@ -47,14 +49,14 @@ class DLL_LINKAGE CampaignRegions std::vector regions; - std::string getNameFor(CampaignScenarioID which, int color, std::string type) const; + ImagePath getNameFor(CampaignScenarioID which, int color, std::string type) const; public: - std::string getBackgroundName() const; + ImagePath getBackgroundName() const; Point getPosition(CampaignScenarioID which) const; - std::string getAvailableName(CampaignScenarioID which, int color) const; - std::string getSelectedName(CampaignScenarioID which, int color) const; - std::string getConqueredName(CampaignScenarioID which, int color) const; + ImagePath getAvailableName(CampaignScenarioID which, int color) const; + ImagePath getSelectedName(CampaignScenarioID which, int color) const; + ImagePath getConqueredName(CampaignScenarioID which, int color) const; template void serialize(Handler &h, const int formatVersion) { @@ -73,9 +75,9 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable CampaignVersion version = CampaignVersion::NONE; CampaignRegions campaignRegions; - std::string name; - std::string description; - std::string music; + MetaString name; + MetaString description; + AudioPath music; std::string filename; std::string modName; std::string encoding; @@ -89,12 +91,12 @@ public: bool playerSelectedDifficulty() const; bool formatVCMI() const; - std::string getDescription() const; - std::string getName() const; + std::string getDescriptionTranslated() const; + std::string getNameTranslated() const; std::string getFilename() const; std::string getModName() const; std::string getEncoding() const; - std::string getMusic() const; + AudioPath getMusic() const; const CampaignRegions & getRegions() const; @@ -175,12 +177,12 @@ struct DLL_LINKAGE CampaignTravel struct DLL_LINKAGE CampaignScenario { std::string mapName; //*.h3m - std::string scenarioName; //from header. human-readble + MetaString scenarioName; //from header std::set preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c) ui8 regionColor = 0; ui8 difficulty = 0; - std::string regionText; + MetaString regionText; CampaignScenarioPrologEpilog prolog; CampaignScenarioPrologEpilog epilog; @@ -292,6 +294,8 @@ public: static JsonNode crossoverSerialize(CGHeroInstance * hero); static CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map); + std::string campaignSet; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -301,6 +305,7 @@ public: h & mapsConquered; h & currentMap; h & chosenCampaignBonuses; + h & campaignSet; } }; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp new file mode 100644 index 000000000..60cf811c2 --- /dev/null +++ b/lib/constants/EntityIdentifiers.cpp @@ -0,0 +1,622 @@ +/* + * EntityIdentifiers.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" + +#ifndef VCMI_NO_EXTRA_VERSION +#include "../Version.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#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 "mapObjectConstructors/AObjectTypeHandler.h" +#include "constants/StringConstants.h" +#include "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 + +const CampaignScenarioID CampaignScenarioID::NONE(-1); +const BattleID BattleID::NONE(-1); +const QueryID QueryID::NONE(-1); +const QueryID QueryID::CLIENT(-2); +const HeroTypeID HeroTypeID::NONE(-1); +const HeroTypeID HeroTypeID::RANDOM(-2); +const ObjectInstanceID ObjectInstanceID::NONE(-1); + +const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER(-2); +const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER(-3); +const SlotID SlotID::WAR_MACHINES_SLOT(-4); +const SlotID SlotID::ARROW_TOWERS_SLOT(-5); + +const PlayerColor PlayerColor::SPECTATOR(-4); +const PlayerColor PlayerColor::CANNOT_DETERMINE(-3); +const PlayerColor PlayerColor::UNFLAGGABLE(-2); +const PlayerColor PlayerColor::NEUTRAL(-1); +const PlayerColor PlayerColor::PLAYER_LIMIT(PLAYER_LIMIT_I); +const TeamID TeamID::NO_TEAM(-1); + +const SpellSchool SpellSchool::ANY(-1); +const SpellSchool SpellSchool::AIR(0); +const SpellSchool SpellSchool::FIRE(1); +const SpellSchool SpellSchool::WATER(2); +const SpellSchool SpellSchool::EARTH(3); + +const FactionID FactionID::NONE(-2); +const FactionID FactionID::DEFAULT(-1); +const FactionID FactionID::RANDOM(-1); +const FactionID FactionID::ANY(-1); +const FactionID FactionID::CASTLE(0); +const FactionID FactionID::RAMPART(1); +const FactionID FactionID::TOWER(2); +const FactionID FactionID::INFERNO(3); +const FactionID FactionID::NECROPOLIS(4); +const FactionID FactionID::DUNGEON(5); +const FactionID FactionID::STRONGHOLD(6); +const FactionID FactionID::FORTRESS(7); +const FactionID FactionID::CONFLUX(8); +const FactionID FactionID::NEUTRAL(9); + +const PrimarySkill PrimarySkill::NONE(-1); +const PrimarySkill PrimarySkill::ATTACK(0); +const PrimarySkill PrimarySkill::DEFENSE(1); +const PrimarySkill PrimarySkill::SPELL_POWER(2); +const PrimarySkill PrimarySkill::KNOWLEDGE(3); +const PrimarySkill PrimarySkill::BEGIN(0); +const PrimarySkill PrimarySkill::END(4); +const PrimarySkill PrimarySkill::EXPERIENCE(4); + +const BoatId BoatId::NONE(-1); +const BoatId BoatId::NECROPOLIS(0); +const BoatId BoatId::CASTLE(1); +const BoatId BoatId::FORTRESS(2); + +const RiverId RiverId::NO_RIVER(0); +const RiverId RiverId::WATER_RIVER(1); +const RiverId RiverId::ICY_RIVER(2); +const RiverId RiverId::MUD_RIVER(3); +const RiverId RiverId::LAVA_RIVER(4); + +const RoadId RoadId::NO_ROAD(0); +const RoadId RoadId::DIRT_ROAD(1); +const RoadId RoadId::GRAVEL_ROAD(2); +const RoadId RoadId::COBBLESTONE_ROAD(3); + +namespace GameConstants +{ +#ifdef VCMI_NO_EXTRA_VERSION + const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING; +#else + const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING "." + std::string{GIT_SHA1}; +#endif +} + +int32_t IdentifierBase::resolveIdentifier(const std::string & entityType, const std::string identifier) +{ + if (identifier.empty()) + return -1; + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType, identifier); + + if (rawId) + return rawId.value(); + throw IdentifierResolutionException(identifier); +} + +si32 HeroClassID::decode(const std::string & identifier) +{ + return resolveIdentifier("heroClass", identifier); +} + +std::string HeroClassID::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->heroClasses()->getByIndex(index)->getJsonKey(); +} + +std::string HeroClassID::entityType() +{ + return "heroClass"; +} + +si32 ObjectInstanceID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string ObjectInstanceID::encode(const si32 index) +{ + return std::to_string(index); +} + +si32 CampaignScenarioID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string CampaignScenarioID::encode(const si32 index) +{ + return std::to_string(index); +} + +std::string MapObjectID::encode(int32_t index) +{ + if (index == -1) + return ""; + return VLC->objtypeh->getJsonKey(MapObjectID(index)); +} + +si32 MapObjectID::decode(const std::string & identifier) +{ + return resolveIdentifier("object", identifier); +} + +std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) +{ + if (index == -1) + return ""; + + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::encode(index); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::encode(index); + + return VLC->objtypeh->getHandlerFor(primaryID, index)->getJsonKey(); +} + +si32 MapObjectSubID::decode(MapObjectID primaryID, const std::string & identifier) +{ + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::decode(identifier); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::decode(identifier); + + return resolveIdentifier(VLC->objtypeh->getJsonKey(primaryID), identifier); +} + +std::string BoatId::encode(int32_t index) +{ + if (index == -1) + return ""; + return VLC->objtypeh->getHandlerFor(MapObjectID::BOAT, index)->getJsonKey(); +} + +si32 BoatId::decode(const std::string & identifier) +{ + return resolveIdentifier("core:boat", identifier); +} + +si32 HeroTypeID::decode(const std::string & identifier) +{ + if (identifier == "random") + return -2; + return resolveIdentifier("hero", identifier); +} + +std::string HeroTypeID::encode(const si32 index) +{ + if (index == -1) + return ""; + if (index == -2) + return "random"; + return VLC->heroTypes()->getByIndex(index)->getJsonKey(); +} + +std::string HeroTypeID::entityType() +{ + return "hero"; +} + +const CArtifact * ArtifactIDBase::toArtifact() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const Artifact * ArtifactIDBase::toEntity(const Services * services) const +{ + return services->artifacts()->getByIndex(num); +} + +si32 ArtifactID::decode(const std::string & identifier) +{ + return resolveIdentifier("artifact", identifier); +} + +std::string ArtifactID::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->artifacts()->getByIndex(index)->getJsonKey(); +} + +std::string ArtifactID::entityType() +{ + return "artifact"; +} + +si32 SecondarySkill::decode(const std::string& identifier) +{ + return resolveIdentifier("secondarySkill", identifier); +} + +std::string SecondarySkill::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->skills()->getById(SecondarySkill(index))->getJsonKey(); +} + +const CSkill * SecondarySkill::toSkill() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const Skill * SecondarySkill::toEntity(const Services * services) const +{ + return services->skills()->getByIndex(num); +} + +const CCreature * CreatureIDBase::toCreature() const +{ + return VLC->creh->objects.at(num); +} + +const Creature * CreatureIDBase::toEntity(const Services * services) const +{ + return toEntity(services->creatures()); +} + +const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) const +{ + return creatures->getByIndex(num); +} + +si32 CreatureID::decode(const std::string & identifier) +{ + return resolveIdentifier("creature", identifier); +} + +std::string CreatureID::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); +} + +std::string CreatureID::entityType() +{ + return "creature"; +} + +const CSpell * SpellIDBase::toSpell() const +{ + if(num < 0 || num >= VLC->spellh->objects.size()) + { + logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); + return nullptr; + } + return VLC->spellh->objects[num]; +} + +const spells::Spell * SpellIDBase::toEntity(const Services * services) const +{ + return toEntity(services->spells()); +} + +const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) const +{ + return service->getByIndex(num); +} + +const CHero * HeroTypeID::toHeroType() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const HeroType * HeroTypeID::toEntity(const Services * services) const +{ + return services->heroTypes()->getByIndex(num); +} + +si32 SpellID::decode(const std::string & identifier) +{ + return resolveIdentifier("spell", identifier); +} + +std::string SpellID::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->spells()->getByIndex(index)->getJsonKey(); +} + +si32 BattleField::decode(const std::string & identifier) +{ + return resolveIdentifier("battlefield", identifier); +} + +std::string BattleField::encode(const si32 index) +{ + return VLC->battlefields()->getByIndex(index)->getJsonKey(); +} + +std::string SpellID::entityType() +{ + return "spell"; +} + +bool PlayerColor::isValidPlayer() const +{ + return num >= 0 && num < PLAYER_LIMIT_I; +} + +bool PlayerColor::isSpectator() const +{ + return num == SPECTATOR.num; +} + +std::string PlayerColor::toString() const +{ + return encode(num); +} + +si32 PlayerColor::decode(const std::string & identifier) +{ + return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); +} + +std::string PlayerColor::encode(const si32 index) +{ + if (index == -1) + return "neutral"; + + if (index < 0 || index >= std::size(GameConstants::PLAYER_COLOR_NAMES)) + { + assert(0); + return "invalid"; + } + + return GameConstants::PLAYER_COLOR_NAMES[index]; +} + +std::string PlayerColor::entityType() +{ + return "playerColor"; +} + +si32 PrimarySkill::decode(const std::string& identifier) +{ + return resolveIdentifier(entityType(), identifier); +} + +std::string PrimarySkill::encode(const si32 index) +{ + return NPrimarySkill::names[index]; +} + +std::string PrimarySkill::entityType() +{ + return "primarySkill"; +} + +si32 FactionID::decode(const std::string & identifier) +{ + return resolveIdentifier(entityType(), identifier); +} + +std::string FactionID::encode(const si32 index) +{ + if (index == -1) + return ""; + return VLC->factions()->getByIndex(index)->getJsonKey(); +} + +std::string FactionID::entityType() +{ + return "faction"; +} + +const Faction * FactionID::toEntity(const Services * service) const +{ + return service->factions()->getByIndex(num); +} + +si32 TerrainId::decode(const std::string & identifier) +{ + if (identifier == "native") + return TerrainId::NATIVE_TERRAIN; + + return resolveIdentifier(entityType(), identifier); +} + +std::string TerrainId::encode(const si32 index) +{ + if (index == TerrainId::NONE) + return ""; + if (index == TerrainId::NATIVE_TERRAIN) + return "native"; + return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string TerrainId::entityType() +{ + return "terrain"; +} + +si32 RoadId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RoadId::NO_ROAD.getNum(); + + return resolveIdentifier(entityType(), identifier); +} + +std::string RoadId::encode(const si32 index) +{ + if (index == RoadId::NO_ROAD.getNum()) + return ""; + return VLC->roadTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RoadId::entityType() +{ + return "road"; +} + +si32 RiverId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RiverId::NO_RIVER.getNum(); + + return resolveIdentifier(entityType(), identifier); +} + +std::string RiverId::encode(const si32 index) +{ + if (index == RiverId::NO_RIVER.getNum()) + return ""; + return VLC->riverTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RiverId::entityType() +{ + return "river"; +} + +const TerrainType * TerrainId::toEntity(const Services * service) const +{ + return VLC->terrainTypeHandler->getByIndex(num); +} + +const RoadType * RoadId::toEntity(const Services * service) const +{ + return VLC->roadTypeHandler->getByIndex(num); +} + +const RiverType * RiverId::toEntity(const Services * service) const +{ + return VLC->riverTypeHandler->getByIndex(num); +} + +const BattleField BattleField::NONE; + +const BattleFieldInfo * BattleField::getInfo() const +{ + return VLC->battlefields()->getById(*this); +} + +const ObstacleInfo * Obstacle::getInfo() const +{ + return VLC->obstacles()->getById(*this); +} + +si32 SpellSchool::decode(const std::string & identifier) +{ + return resolveIdentifier(entityType(), identifier); +} + +std::string SpellSchool::encode(const si32 index) +{ + if (index == ANY.getNum()) + return "any"; + + return SpellConfig::SCHOOL[index].jsonName; +} + +std::string SpellSchool::entityType() +{ + return "spellSchool"; +} + +si32 GameResID::decode(const std::string & identifier) +{ + return resolveIdentifier(entityType(), identifier); +} + +std::string GameResID::encode(const si32 index) +{ + return GameConstants::RESOURCE_NAMES[index]; +} + +si32 BuildingTypeUniqueID::decode(const std::string & identifier) +{ + assert(0); //TODO + return -1; +} + +std::string BuildingTypeUniqueID::encode(const si32 index) +{ + assert(0); // TODO + return ""; +} + +std::string GameResID::entityType() +{ + return "resource"; +} + +const std::array & GameResID::ALL_RESOURCES() +{ + static const std::array allResources = { + GameResID(WOOD), + GameResID(MERCURY), + GameResID(ORE), + GameResID(SULFUR), + GameResID(CRYSTAL), + GameResID(GEMS), + GameResID(GOLD) + }; + + return allResources; +} + +std::string SecondarySkill::entityType() +{ + return "secondarySkill"; +} + +std::string BuildingID::encode(int32_t index) +{ + return std::to_string(index); +} + +si32 BuildingID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h new file mode 100644 index 000000000..98d5ddf3c --- /dev/null +++ b/lib/constants/EntityIdentifiers.h @@ -0,0 +1,990 @@ +/* + * EntityIdentifiers.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 "NumericConstants.h" +#include "IdentifierBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Services; +class Artifact; +class ArtifactService; +class Creature; +class CreatureService; +class HeroType; +class CHero; +class HeroTypeService; +class Faction; +class Skill; +class RoadType; +class RiverType; +class TerrainType; + +namespace spells +{ + class Spell; + class Service; +} + +class CArtifact; +class CArtifactInstance; +class CCreature; +class CHero; +class CSpell; +class CSkill; +class CGameInfoCallback; +class CNonConstInfoCallback; + +class ArtifactInstanceID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; +}; + +class QueryID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + DLL_LINKAGE static const QueryID NONE; + DLL_LINKAGE static const QueryID CLIENT; +}; + +class BattleID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + DLL_LINKAGE static const BattleID NONE; +}; +class DLL_LINKAGE ObjectInstanceID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + static const ObjectInstanceID NONE; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class HeroClassID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class DLL_LINKAGE HeroTypeID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); + + const CHero * toHeroType() const; + const HeroType * toEntity(const Services * services) const; + + static const HeroTypeID NONE; + static const HeroTypeID RANDOM; + + bool isValid() const + { + return getNum() >= 0; + } +}; + +class SlotID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; + DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; + } +}; + +class DLL_LINKAGE PlayerColor : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + enum EPlayerColor + { + PLAYER_LIMIT_I = 8, + }; + + static const PlayerColor SPECTATOR; //252 + static const PlayerColor CANNOT_DETERMINE; //253 + static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) + static const PlayerColor NEUTRAL; //255 + static const PlayerColor PLAYER_LIMIT; //player limit per map + + bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + bool isSpectator() const; + + std::string toString() const; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class TeamID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + DLL_LINKAGE static const TeamID NO_TEAM; +}; + +class TeleportChannelID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; +}; + +class SecondarySkillBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NONE = -1, + PATHFINDING = 0, + ARCHERY, + LOGISTICS, + SCOUTING, + DIPLOMACY, + NAVIGATION, + LEADERSHIP, + WISDOM, + MYSTICISM, + LUCK, + BALLISTICS, + EAGLE_EYE, + NECROMANCY, + ESTATES, + FIRE_MAGIC, + AIR_MAGIC, + WATER_MAGIC, + EARTH_MAGIC, + SCHOLAR, + TACTICS, + ARTILLERY, + LEARNING, + OFFENCE, + ARMORER, + INTELLIGENCE, + SORCERY, + RESISTANCE, + FIRST_AID, + SKILL_SIZE + }; + static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); +}; + +class DLL_LINKAGE SecondarySkill : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + static std::string entityType(); + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + + const CSkill * toSkill() const; + const Skill * toEntity(const Services * services) const; +}; + +class DLL_LINKAGE PrimarySkill : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static const PrimarySkill NONE; + static const PrimarySkill ATTACK; + static const PrimarySkill DEFENSE; + static const PrimarySkill SPELL_POWER; + static const PrimarySkill KNOWLEDGE; + + static const PrimarySkill BEGIN; + static const PrimarySkill END; + + static const PrimarySkill EXPERIENCE; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class DLL_LINKAGE FactionID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + + static const FactionID NONE; + static const FactionID DEFAULT; + static const FactionID RANDOM; + static const FactionID ANY; + static const FactionID CASTLE; + static const FactionID RAMPART; + static const FactionID TOWER; + static const FactionID INFERNO; + static const FactionID NECROPOLIS; + static const FactionID DUNGEON; + static const FactionID STRONGHOLD; + static const FactionID FORTRESS; + static const FactionID CONFLUX; + static const FactionID NEUTRAL; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + const Faction * toEntity(const Services * service) const; + static std::string entityType(); + + bool isValid() const + { + return getNum() >= 0; + } +}; + +class BuildingIDBase : public IdentifierBase +{ +public: + //Quite useful as long as most of building mechanics hardcoded + // NOTE: all building with completely configurable mechanics will be removed from list + enum Type + { + DEFAULT = -50, + HORDE_PLACEHOLDER7 = -36, + HORDE_PLACEHOLDER6 = -35, + HORDE_PLACEHOLDER5 = -34, + HORDE_PLACEHOLDER4 = -33, + HORDE_PLACEHOLDER3 = -32, + HORDE_PLACEHOLDER2 = -31, + HORDE_PLACEHOLDER1 = -30, + NONE = -1, + FIRST_REGULAR_ID = 0, + MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, + TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, + VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, + RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, + SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, + 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_1 = DWELL_FIRST, + DWELL_LVL_7 = DWELL_LAST, + DWELL_LVL_1_UP = DWELL_UP_FIRST, + DWELL_LVL_7_UP = DWELL_UP_LAST, + + DWELL_UP2_FIRST = DWELL_LVL_7_UP + 1, + +// //Special buildings for towns. + CASTLE_GATE = SPECIAL_3, //Inferno + FREELANCERS_GUILD = SPECIAL_2, //Stronghold + ARTIFACT_MERCHANT = SPECIAL_1, + + }; + + bool IsSpecialOrGrail() const + { + return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; + } +}; + +class DLL_LINKAGE BuildingID : public StaticIdentifierWithEnum +{ +public: + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; + + static BuildingID HALL_LEVEL(unsigned int level) + { + assert(level < 4); + return BuildingID(Type::VILLAGE_HALL + level); + } + static BuildingID FORT_LEVEL(unsigned int level) + { + assert(level < 3); + return BuildingID(Type::TOWN_HALL + level); + } + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); +}; + +class MapObjectBaseID : public IdentifierBase +{ +public: + enum Type + { + NO_OBJ = -1, + ALTAR_OF_SACRIFICE [[deprecated]] = 2, + ANCHOR_POINT = 3, + ARENA = 4, + ARTIFACT = 5, + PANDORAS_BOX = 6, + BLACK_MARKET [[deprecated]] = 7, + BOAT = 8, + BORDERGUARD = 9, + KEYMASTER = 10, + BUOY = 11, + CAMPFIRE = 12, + CARTOGRAPHER = 13, + SWAN_POND = 14, + COVER_OF_DARKNESS = 15, + CREATURE_BANK = 16, + CREATURE_GENERATOR1 = 17, + CREATURE_GENERATOR2 = 18, + CREATURE_GENERATOR3 = 19, + CREATURE_GENERATOR4 = 20, + CURSED_GROUND1 = 21, + CORPSE = 22, + MARLETTO_TOWER = 23, + DERELICT_SHIP = 24, + DRAGON_UTOPIA = 25, + EVENT = 26, + EYE_OF_MAGI = 27, + FAERIE_RING = 28, + FLOTSAM = 29, + FOUNTAIN_OF_FORTUNE = 30, + FOUNTAIN_OF_YOUTH = 31, + GARDEN_OF_REVELATION = 32, + GARRISON = 33, + HERO = 34, + HILL_FORT = 35, + GRAIL = 36, + HUT_OF_MAGI = 37, + IDOL_OF_FORTUNE = 38, + LEAN_TO = 39, + LIBRARY_OF_ENLIGHTENMENT = 41, + LIGHTHOUSE = 42, + MONOLITH_ONE_WAY_ENTRANCE = 43, + MONOLITH_ONE_WAY_EXIT = 44, + MONOLITH_TWO_WAY = 45, + MAGIC_PLAINS1 = 46, + SCHOOL_OF_MAGIC = 47, + MAGIC_SPRING = 48, + MAGIC_WELL = 49, + MARKET_OF_TIME = 50, + MERCENARY_CAMP = 51, + MERMAID = 52, + MINE = 53, + MONSTER = 54, + MYSTICAL_GARDEN = 55, + OASIS = 56, + OBELISK = 57, + REDWOOD_OBSERVATORY = 58, + OCEAN_BOTTLE = 59, + PILLAR_OF_FIRE = 60, + STAR_AXIS = 61, + PRISON = 62, + PYRAMID = 63,//subtype 0 + WOG_OBJECT = 63,//subtype > 0 + RALLY_FLAG = 64, + RANDOM_ART = 65, + RANDOM_TREASURE_ART = 66, + RANDOM_MINOR_ART = 67, + RANDOM_MAJOR_ART = 68, + RANDOM_RELIC_ART = 69, + RANDOM_HERO = 70, + RANDOM_MONSTER = 71, + RANDOM_MONSTER_L1 = 72, + RANDOM_MONSTER_L2 = 73, + RANDOM_MONSTER_L3 = 74, + RANDOM_MONSTER_L4 = 75, + RANDOM_RESOURCE = 76, + RANDOM_TOWN = 77, + REFUGEE_CAMP = 78, + RESOURCE = 79, + SANCTUARY = 80, + SCHOLAR = 81, + SEA_CHEST = 82, + SEER_HUT = 83, + CRYPT = 84, + SHIPWRECK = 85, + SHIPWRECK_SURVIVOR = 86, + SHIPYARD = 87, + SHRINE_OF_MAGIC_INCANTATION = 88, + SHRINE_OF_MAGIC_GESTURE = 89, + SHRINE_OF_MAGIC_THOUGHT = 90, + SIGN = 91, + SIRENS = 92, + SPELL_SCROLL = 93, + STABLES = 94, + TAVERN = 95, + TEMPLE = 96, + DEN_OF_THIEVES = 97, + TOWN = 98, + TRADING_POST [[deprecated]] = 99, + LEARNING_STONE = 100, + TREASURE_CHEST = 101, + TREE_OF_KNOWLEDGE = 102, + SUBTERRANEAN_GATE = 103, + UNIVERSITY [[deprecated]] = 104, + WAGON = 105, + WAR_MACHINE_FACTORY = 106, + SCHOOL_OF_WAR = 107, + WARRIORS_TOMB = 108, + WATER_WHEEL = 109, + WATERING_HOLE = 110, + WHIRLPOOL = 111, + WINDMILL = 112, + WITCH_HUT = 113, + HOLE = 124, + RANDOM_MONSTER_L5 = 162, + RANDOM_MONSTER_L6 = 163, + RANDOM_MONSTER_L7 = 164, + BORDER_GATE = 212, + FREELANCERS_GUILD [[deprecated]] = 213, + HERO_PLACEHOLDER = 214, + QUEST_GUARD = 215, + RANDOM_DWELLING = 216, + RANDOM_DWELLING_LVL = 217, //subtype = creature level + RANDOM_DWELLING_FACTION = 218, //subtype = faction + GARRISON2 = 219, + ABANDONED_MINE = 220, + TRADING_POST_SNOW [[deprecated]] = 221, + CLOVER_FIELD = 222, + CURSED_GROUND2 = 223, + EVIL_FOG = 224, + FAVORABLE_WINDS = 225, + FIERY_FIELDS = 226, + HOLY_GROUNDS = 227, + LUCID_POOLS = 228, + MAGIC_CLOUDS = 229, + MAGIC_PLAINS2 = 230, + ROCKLANDS = 231, + }; +}; + +class DLL_LINKAGE MapObjectID : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } +}; + +class DLL_LINKAGE MapObjectSubID : public Identifier +{ +public: + constexpr MapObjectSubID(const IdentifierBase & value): + Identifier(value.getNum()) + {} + constexpr MapObjectSubID(int32_t value = -1): + Identifier(value) + {} + + MapObjectSubID & operator =(int32_t value) + { + this->num = value; + return *this; + } + + MapObjectSubID & operator =(const IdentifierBase & value) + { + this->num = value.getNum(); + return *this; + } + + static si32 decode(MapObjectID primaryID, const std::string & identifier); + static std::string encode(MapObjectID primaryID, si32 index); + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } + + template + void serializeIdentifier(Handler &h, const MapObjectID & primaryID, const int version) + { + std::string secondaryStringID; + + if (h.saving) + secondaryStringID = encode(primaryID, num); + + h & secondaryStringID; + + if (!h.saving) + num = decode(primaryID, secondaryStringID); + } +}; + +class DLL_LINKAGE RoadId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); + + static const RoadId NO_ROAD; + static const RoadId DIRT_ROAD; + static const RoadId GRAVEL_ROAD; + static const RoadId COBBLESTONE_ROAD; + + const RoadType * toEntity(const Services * service) const; +}; + +class DLL_LINKAGE RiverId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); + + static const RiverId NO_RIVER; + static const RiverId WATER_RIVER; + static const RiverId ICY_RIVER; + static const RiverId MUD_RIVER; + static const RiverId LAVA_RIVER; + + const RiverType * toEntity(const Services * service) const; +}; + +class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO + }; +}; + +class EPathfindingLayer : public StaticIdentifierWithEnum +{ +public: + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; +}; + +class ArtifactPositionBase : public IdentifierBase +{ +public: + enum Type + { + TRANSITION_POS = -3, + FIRST_AVAILABLE = -2, + PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack + + // Hero + HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 + RIGHT_RING, LEFT_RING, FEET, //8 + MISC1, MISC2, MISC3, MISC4, //12 + MACH1, MACH2, MACH3, MACH4, //16 + SPELLBOOK, MISC5, //18 + BACKPACK_START = 19, + + // Creatures + CREATURE_SLOT = 0, + + // Commander + COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6 + }; + + static_assert(MISC5 < BACKPACK_START, "incorrect number of artifact slots"); + + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); +}; + +class ArtifactPosition : public StaticIdentifierWithEnum +{ +public: + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } +}; + +class ArtifactIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + SPELLBOOK = 0, + SPELL_SCROLL = 1, + GRAIL = 2, + CATAPULT = 3, + BALLISTA = 4, + AMMO_CART = 5, + FIRST_AID_TENT = 6, + VIAL_OF_DRAGON_BLOOD = 127, + ARMAGEDDONS_BLADE = 128, + TITANS_THUNDER = 135, + ART_SELECTION = 144, + ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 + }; + + DLL_LINKAGE const CArtifact * toArtifact() const; + DLL_LINKAGE const Artifact * toEntity(const Services * service) const; +}; + +class ArtifactID : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class CreatureIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + ARCHER = 2, // for debug / fallback + IMP = 42, // for Deity of Fire + FAMILIAR = 43, // for Deity of Fire + SKELETON = 56, // for Skeleton Transformer + BONE_DRAGON = 68, // for Skeleton Transformer + TROGLODYTES = 70, // for Abandoned Mine + MEDUSA = 76, // for Siege UI workaround + HYDRA = 110, // for Skeleton Transformer + CHAOS_HYDRA = 111, // for Skeleton Transformer + AIR_ELEMENTAL = 112, // for tests + FIRE_ELEMENTAL = 114, // for tests + PSYCHIC_ELEMENTAL = 120, // for hardcoded ability + MAGIC_ELEMENTAL = 121, // for hardcoded ability + AZURE_DRAGON = 132, + CATAPULT = 145, + BALLISTA = 146, + FIRST_AID_TENT = 147, + AMMO_CART = 148, + ARROW_TOWERS = 149 + }; + + DLL_LINKAGE const CCreature * toCreature() const; + DLL_LINKAGE const Creature * toEntity(const Services * services) const; + DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const; +}; + +class DLL_LINKAGE CreatureID : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class DLL_LINKAGE SpellIDBase : public IdentifierBase +{ +public: + enum Type + { + // Special ID's + SPELLBOOK_PRESET = -3, + PRESET = -2, + NONE = -1, + + // Adventure map spells + SUMMON_BOAT = 0, + SCUTTLE_BOAT = 1, + VISIONS = 2, + VIEW_EARTH = 3, + DISGUISE = 4, + VIEW_AIR = 5, + FLY = 6, + WATER_WALK = 7, + DIMENSION_DOOR = 8, + TOWN_PORTAL = 9, + + // Combat spells + QUICKSAND = 10, + LAND_MINE = 11, + FORCE_FIELD = 12, + FIRE_WALL = 13, + EARTHQUAKE = 14, + MAGIC_ARROW = 15, + ICE_BOLT = 16, + LIGHTNING_BOLT = 17, + IMPLOSION = 18, + CHAIN_LIGHTNING = 19, + FROST_RING = 20, + FIREBALL = 21, + INFERNO = 22, + METEOR_SHOWER = 23, + DEATH_RIPPLE = 24, + DESTROY_UNDEAD = 25, + ARMAGEDDON = 26, + SHIELD = 27, + AIR_SHIELD = 28, + FIRE_SHIELD = 29, + PROTECTION_FROM_AIR = 30, + PROTECTION_FROM_FIRE = 31, + PROTECTION_FROM_WATER = 32, + PROTECTION_FROM_EARTH = 33, + ANTI_MAGIC = 34, + DISPEL = 35, + MAGIC_MIRROR = 36, + CURE = 37, + RESURRECTION = 38, + ANIMATE_DEAD = 39, + SACRIFICE = 40, + BLESS = 41, + CURSE = 42, + BLOODLUST = 43, + PRECISION = 44, + WEAKNESS = 45, + STONE_SKIN = 46, + DISRUPTING_RAY = 47, + PRAYER = 48, + MIRTH = 49, + SORROW = 50, + FORTUNE = 51, + MISFORTUNE = 52, + HASTE = 53, + SLOW = 54, + SLAYER = 55, + FRENZY = 56, + TITANS_LIGHTNING_BOLT = 57, + COUNTERSTRIKE = 58, + BERSERK = 59, + HYPNOTIZE = 60, + FORGETFULNESS = 61, + BLIND = 62, + TELEPORT = 63, + REMOVE_OBSTACLE = 64, + CLONE = 65, + SUMMON_FIRE_ELEMENTAL = 66, + SUMMON_EARTH_ELEMENTAL = 67, + SUMMON_WATER_ELEMENTAL = 68, + SUMMON_AIR_ELEMENTAL = 69, + + // Creature abilities + STONE_GAZE = 70, + POISON = 71, + BIND = 72, + DISEASE = 73, + PARALYZE = 74, + AGE = 75, + DEATH_CLOUD = 76, + THUNDERBOLT = 77, + DISPEL_HELPFUL_SPELLS = 78, + DEATH_STARE = 79, + ACID_BREATH_DEFENSE = 80, + ACID_BREATH_DAMAGE = 81, + + // Special ID's + FIRST_NON_SPELL = 70, + AFTER_LAST = 82 + }; + + const CSpell * toSpell() const; //deprecated + const spells::Spell * toEntity(const Services * service) const; + const spells::Spell * toEntity(const spells::Service * service) const; +}; + +class DLL_LINKAGE SpellID : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class BattleFieldInfo; +class DLL_LINKAGE BattleField : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + + static const BattleField NONE; + const BattleFieldInfo * getInfo() const; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class DLL_LINKAGE BoatId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + + static const BoatId NONE; + static const BoatId NECROPOLIS; + static const BoatId CASTLE; + static const BoatId FORTRESS; +}; + +class TerrainIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NATIVE_TERRAIN = -4, + ANY_TERRAIN = -3, + NONE = -1, + FIRST_REGULAR_TERRAIN = 0, + DIRT = 0, + SAND, + GRASS, + SNOW, + SWAMP, + ROUGH, + SUBTERRANEAN, + LAVA, + WATER, + ROCK, + ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK + }; +}; + +class DLL_LINKAGE TerrainId : public EntityIdentifierWithEnum +{ +public: + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); + const TerrainType * toEntity(const Services * service) const; +}; + +class ObstacleInfo; +class Obstacle : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; + DLL_LINKAGE const ObstacleInfo * getInfo() const; +}; + +class DLL_LINKAGE SpellSchool : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static const SpellSchool ANY; + static const SpellSchool AIR; + static const SpellSchool FIRE; + static const SpellSchool WATER; + static const SpellSchool EARTH; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class GameResIDBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + WOOD = 0, + MERCURY, + ORE, + SULFUR, + CRYSTAL, + GEMS, + GOLD, + MITHRIL, + COUNT, + + WOOD_AND_ORE = 127, // special case for town bonus resource + NONE = -1 + }; +}; + +class DLL_LINKAGE GameResID : public StaticIdentifierWithEnum +{ +public: + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); + + static const std::array & ALL_RESOURCES(); +}; + +class DLL_LINKAGE BuildingTypeUniqueID : public Identifier +{ +public: + BuildingTypeUniqueID(FactionID faction, BuildingID building ); + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + + BuildingID getBuilding() const; + FactionID getFaction() const; + + using Identifier::Identifier; + + template + void serialize(Handler & h, const int version) + { + FactionID faction = getFaction(); + BuildingID building = getBuilding(); + + h & faction; + h & building; + + if (!h.saving) + *this = BuildingTypeUniqueID(faction, building); + } +}; + +class DLL_LINKAGE CampaignScenarioID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static si32 decode(const std::string & identifier); + static std::string encode(int32_t index); + + static const CampaignScenarioID NONE; +}; + +// Deprecated +// TODO: remove +using Obj = MapObjectID; +using ETownType = FactionID; +using EGameResID = GameResID; +using River = RiverId; +using Road = RoadId; +using ETerrainId = TerrainId; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h new file mode 100644 index 000000000..67ddc1b4a --- /dev/null +++ b/lib/constants/Enumerations.h @@ -0,0 +1,255 @@ +/* + * Enumerations.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 EAlignment : int8_t +{ + GOOD, + EVIL, + NEUTRAL +}; + +namespace BuildingSubID +{ + enum EBuildingSubID + { + 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, + CUSTOM_VISITING_BONUS, + CUSTOM_VISITING_REWARD + }; +} + +enum class EMarketMode : int8_t +{ + RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, + ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, + MARTKET_AFTER_LAST_PLACEHOLDER +}; + +enum class EAiTactic : int8_t +{ + NONE = -1, + RANDOM, + WARRIOR, + BUILDER, + EXPLORER +}; + +enum class EBuildingState : int8_t +{ + HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, + NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED +}; + +enum class ESpellCastProblem : int8_t +{ + OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, + HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, + SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, + NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, + MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all + INVALID +}; + +namespace ECommander +{ + enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; + const int MAX_SKILL_LEVEL = 5; +} + +enum class EWallPart : int8_t +{ + INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, + KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, + PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ +}; + +enum class EWallState : int8_t +{ + NONE = -1, //no wall + DESTROYED, + DAMAGED, + INTACT, + REINFORCED, // walls in towns with castle +}; + +enum class EGateState : int8_t +{ + NONE, + CLOSED, + BLOCKED, // gate is blocked in closed state, e.g. by creature + OPENED, + DESTROYED +}; + +enum class ETileType : int8_t +{ + FREE, + POSSIBLE, + BLOCKED, + USED +}; + +enum class ETeleportChannelType : int8_t +{ + IMPASSABLE, + BIDIRECTIONAL, + UNIDIRECTIONAL, + MIXED +}; + +namespace MasteryLevel +{ + enum Type + { + NONE, + BASIC, + ADVANCED, + EXPERT, + LEVELS_SIZE + }; +} + +enum class Date : int8_t +{ + DAY = 0, + DAY_OF_WEEK = 1, + WEEK = 2, + MONTH = 3, + DAY_OF_MONTH +}; + +enum class EActionType : int8_t +{ + NO_ACTION, + + END_TACTIC_PHASE, + RETREAT, + SURRENDER, + + HERO_SPELL, + + WALK, + WAIT, + DEFEND, + WALK_AND_ATTACK, + SHOOT, + CATAPULT, + MONSTER_SPELL, + BAD_MORALE, + STACK_HEAL, +}; + +enum class EDiggingStatus : int8_t +{ + UNKNOWN = -1, + CAN_DIG = 0, + LACK_OF_MOVEMENT, + WRONG_TERRAIN, + TILE_OCCUPIED, + BACKPACK_IS_FULL +}; + +enum class EPlayerStatus : int8_t +{ + WRONG = -1, + INGAME, + LOSER, + WINNER +}; + +enum class PlayerRelations : int8_t +{ + ENEMIES, + ALLIES, + SAME_PLAYER +}; + +enum class EMetaclass : int8_t +{ + INVALID = 0, + ARTIFACT, + CREATURE, + FACTION, + EXPERIENCE, + HERO, + HEROCLASS, + LUCK, + MANA, + MORALE, + MOVEMENT, + OBJECT, + PRIMARY_SKILL, + SECONDARY_SKILL, + SPELL, + RESOURCE +}; + +enum class EHealLevel: int8_t +{ + HEAL, + RESURRECT, + OVERHEAL +}; + +enum class EHealPower : int8_t +{ + ONE_BATTLE, + PERMANENT +}; + +enum class EBattleResult : int8_t +{ + NORMAL = 0, + ESCAPE = 1, + SURRENDER = 2, +}; + +enum class ETileVisibility : int8_t // Fog of war change +{ + HIDDEN, + REVEALED +}; + +enum class EArmyFormation : int8_t +{ + LOOSE, + TIGHT +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h new file mode 100644 index 000000000..62c2e671b --- /dev/null +++ b/lib/constants/IdentifierBase.h @@ -0,0 +1,254 @@ +/* + * IdentifierBase.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 IdentifierResolutionException : public std::runtime_error +{ +public: + const std::string identifierName; + + IdentifierResolutionException(const std::string & identifierName ) + : std::runtime_error("Failed to resolve identifier " + identifierName) + , identifierName(identifierName) + {} +}; + +class IdentifierBase +{ +protected: + constexpr IdentifierBase(): + num(-1) + {} + + explicit constexpr IdentifierBase(int32_t value): + num(value) + {} + + ~IdentifierBase() = default; + + /// Attempts to resolve identifier using provided entity type + /// Returns resolved numeric identifier + /// Throws IdentifierResolutionException on failure + static int32_t resolveIdentifier(const std::string & entityType, const std::string identifier); +public: + int32_t num; + + constexpr int32_t getNum() const + { + return num; + } + + constexpr void setNum(int32_t value) + { + num = value; + } + + struct hash + { + size_t operator()(const IdentifierBase & id) const + { + return std::hash()(id.num); + } + }; + + constexpr void advance(int change) + { + num += change; + } + + constexpr operator int32_t () const + { + return num; + } + + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) + { + return os << dt.num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons +template +class Identifier : public IdentifierBase +{ + using BaseClass = IdentifierBase; +public: + constexpr Identifier() + {} + + explicit constexpr Identifier(int32_t value): + IdentifierBase(value) + {} + + constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass & operator--() + { + --BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator--(int) + { + FinalClass ret(num); + --BaseClass::num; + return ret; + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class IdentifierWithEnum : public BaseClass +{ + using EnumType = typename BaseClass::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr EnumType toEnum() const + { + return static_cast(BaseClass::num); + } + + constexpr IdentifierWithEnum(const EnumType & enumValue) + { + BaseClass::num = static_cast(enumValue); + } + + constexpr IdentifierWithEnum(int32_t _num = -1) + { + BaseClass::num = _num; + } + + constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(BaseClass::num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class EntityIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class EntityIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class StaticIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +template +class StaticIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h new file mode 100644 index 000000000..fcc3da876 --- /dev/null +++ b/lib/constants/NumericConstants.h @@ -0,0 +1,56 @@ +/* + * NumericConstants.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 + +namespace GameConstants +{ + DLL_LINKAGE extern const std::string VCMI_VERSION; + + constexpr int PUZZLE_MAP_PIECES = 48; + + constexpr int MAX_HEROES_PER_PLAYER = 8; + constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; + + constexpr int ALL_PLAYERS = 255; //bitfield + + constexpr int CREATURES_PER_TOWN = 7; //without upgrades + constexpr int SPELL_LEVELS = 5; + constexpr int SPELL_SCHOOL_LEVELS = 4; + constexpr int DEFAULT_SCHOOLS = 4; + constexpr int CRE_LEVELS = 10; // number of creature experience levels + + constexpr int HERO_GOLD_COST = 2500; + constexpr int SPELLBOOK_GOLD_COST = 500; + constexpr int SKILL_GOLD_COST = 2000; + constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty + constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit + constexpr int ARMY_SIZE = 7; + constexpr int SKILL_PER_HERO = 8; + constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order + + constexpr int SKILL_QUANTITY=28; + constexpr int PRIMARY_SKILLS=4; + constexpr int RESOURCE_QUANTITY=8; + constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type + + // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase + constexpr int F_NUMBER = 9; + constexpr int ARTIFACTS_QUANTITY=171; + constexpr int HEROES_QUANTITY=156; + constexpr int SPELLS_QUANTITY=70; + constexpr int CREATURES_COUNT = 197; + + constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement + constexpr int64_t PLAYER_RESOURCES_CAP = 1000 * 1000 * 1000; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h new file mode 100644 index 000000000..d7b40fd92 --- /dev/null +++ b/lib/constants/StringConstants.h @@ -0,0 +1,218 @@ +/* + * constants/StringConstants.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 + +/// +/// String ID which are pointless to move to config file - these types are mostly hardcoded +/// +namespace GameConstants +{ + const std::string RESOURCE_NAMES [RESOURCE_QUANTITY] = { + "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" + }; + + const std::string PLAYER_COLOR_NAMES [PlayerColor::PLAYER_LIMIT_I] = { + "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" + }; + + const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; + + const std::string DIFFICULTY_NAMES [5] = {"pawn", "knight", "rook", "queen", "king"}; +} + +namespace NPrimarySkill +{ + const std::string names [GameConstants::PRIMARY_SKILLS] = { "attack", "defence", "spellpower", "knowledge" }; +} + +namespace NSecondarySkill +{ + const std::string names [GameConstants::SKILL_QUANTITY] = + { + "pathfinding", "archery", "logistics", "scouting", "diplomacy", // 5 + "navigation", "leadership", "wisdom", "mysticism", "luck", // 10 + "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", // 15 + "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", // 20 + "artillery", "learning", "offence", "armorer", "intelligence", // 25 + "sorcery", "resistance", "firstAid" + }; + + const std::vector levels = + { + "none", "basic", "advanced", "expert" + }; +} + +namespace EBuildingType +{ + const std::string names [44] = + { + "mageGuild1", "mageGuild2", "mageGuild3", "mageGuild4", "mageGuild5", // 5 + "tavern", "shipyard", "fort", "citadel", "castle", // 10 + "villageHall", "townHall", "cityHall", "capitol", "marketplace", // 15 + "resourceSilo", "blacksmith", "special1", "horde1", "horde1Upgr", // 20 + "ship", "special2", "special3", "special4", "horde2", // 25 + "horde2Upgr", "grail", "extraTownHall", "extraCityHall", "extraCapitol", // 30 + "dwellingLvl1", "dwellingLvl2", "dwellingLvl3", "dwellingLvl4", "dwellingLvl5", // 35 + "dwellingLvl6", "dwellingLvl7", "dwellingUpLvl1", "dwellingUpLvl2", "dwellingUpLvl3", // 40 + "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7" + }; +} + +namespace NFaction +{ + const std::string names [GameConstants::F_NUMBER] = + { + "castle", "rampart", "tower", + "inferno", "necropolis", "dungeon", + "stronghold", "fortress", "conflux" + }; +} + +namespace NArtifactPosition +{ + const std::string namesHero [19] = + { + "head", "shoulders", "neck", "rightHand", "leftHand", "torso", //5 + "rightRing", "leftRing", "feet", //8 + "misc1", "misc2", "misc3", "misc4", //12 + "mach1", "mach2", "mach3", "mach4", //16 + "spellbook", "misc5" //18 + }; + + const std::string namesCreature[1] = + { + "creature1" + }; + + const std::string namesCommander[6] = + { + "commander1", "commander2", "commander3", "commander4", "commander5", "commander6", + }; + + + const std::string backpack = "backpack"; +} + +namespace NMetaclass +{ + const std::string names [16] = + { + "", + "artifact", "creature", "faction", "experience", "hero", + "heroClass", "luck", "mana", "morale", "movement", + "object", "primarySkill", "secondarySkill", "spell", "resource" + }; +} + +namespace NPathfindingLayer +{ + const std::string names[EPathfindingLayer::NUM_LAYERS] = + { + "land", "sail", "water", "air" + }; +} + +namespace MappedKeys +{ + + static const std::map BUILDING_NAMES_TO_TYPES = + { + { "special1", BuildingID::SPECIAL_1 }, + { "special2", BuildingID::SPECIAL_2 }, + { "special3", BuildingID::SPECIAL_3 }, + { "special4", BuildingID::SPECIAL_4 }, + { "grail", BuildingID::GRAIL }, + { "mageGuild1", BuildingID::MAGES_GUILD_1 }, + { "mageGuild2", BuildingID::MAGES_GUILD_2 }, + { "mageGuild3", BuildingID::MAGES_GUILD_3 }, + { "mageGuild4", BuildingID::MAGES_GUILD_4 }, + { "mageGuild5", BuildingID::MAGES_GUILD_5 }, + { "tavern", BuildingID::TAVERN }, + { "shipyard", BuildingID::SHIPYARD }, + { "fort", BuildingID::FORT }, + { "citadel", BuildingID::CITADEL }, + { "castle", BuildingID::CASTLE }, + { "villageHall", BuildingID::VILLAGE_HALL }, + { "townHall", BuildingID::TOWN_HALL }, + { "cityHall", BuildingID::CITY_HALL }, + { "capitol", BuildingID::CAPITOL }, + { "marketplace", BuildingID::MARKETPLACE }, + { "resourceSilo", BuildingID::RESOURCE_SILO }, + { "blacksmith", BuildingID::BLACKSMITH }, + { "horde1", BuildingID::HORDE_1 }, + { "horde1Upgr", BuildingID::HORDE_1_UPGR }, + { "horde2", BuildingID::HORDE_2 }, + { "horde2Upgr", BuildingID::HORDE_2_UPGR }, + { "ship", BuildingID::SHIP }, + { "dwellingLvl1", BuildingID::DWELL_LVL_1 }, + { "dwellingLvl2", BuildingID::DWELL_LVL_2 }, + { "dwellingLvl3", BuildingID::DWELL_LVL_3 }, + { "dwellingLvl4", BuildingID::DWELL_LVL_4 }, + { "dwellingLvl5", BuildingID::DWELL_LVL_5 }, + { "dwellingLvl6", BuildingID::DWELL_LVL_6 }, + { "dwellingLvl7", BuildingID::DWELL_LVL_7 }, + { "dwellingUpLvl1", BuildingID::DWELL_LVL_1_UP }, + { "dwellingUpLvl2", BuildingID::DWELL_LVL_2_UP }, + { "dwellingUpLvl3", BuildingID::DWELL_LVL_3_UP }, + { "dwellingUpLvl4", BuildingID::DWELL_LVL_4_UP }, + { "dwellingUpLvl5", BuildingID::DWELL_LVL_5_UP }, + { "dwellingUpLvl6", BuildingID::DWELL_LVL_6_UP }, + { "dwellingUpLvl7", BuildingID::DWELL_LVL_7_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 } + }; + + static const std::map MARKET_NAMES_TO_TYPES = + { + { "resource-resource", EMarketMode::RESOURCE_RESOURCE }, + { "resource-player", EMarketMode::RESOURCE_PLAYER }, + { "creature-resource", EMarketMode::CREATURE_RESOURCE }, + { "resource-artifact", EMarketMode::RESOURCE_ARTIFACT }, + { "artifact-resource", EMarketMode::ARTIFACT_RESOURCE }, + { "artifact-experience", EMarketMode::ARTIFACT_EXP }, + { "creature-experience", EMarketMode::CREATURE_EXP }, + { "creature-undead", EMarketMode::CREATURE_UNDEAD }, + { "resource-skill", EMarketMode::RESOURCE_SKILL }, + }; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h new file mode 100644 index 000000000..175411f67 --- /dev/null +++ b/lib/constants/VariantIdentifier.h @@ -0,0 +1,81 @@ +/* + * VariantIdentifier.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 "IdentifierBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// This class represents field that may contain value of multiple different identifier types +template +class VariantIdentifier +{ + using Type = std::variant; + Type value; + +public: + VariantIdentifier() + {} + + template + VariantIdentifier(const IdentifierType & identifier) + : value(identifier) + {} + + int32_t getNum() const + { + int32_t result; + + std::visit([&result] (const auto& v) { result = v.getNum(); }, value); + + return result; + } + + std::string toString() const + { + std::string result; + + std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value); + + return result; + } + + template + IdentifierType as() const + { + auto * result = std::get_if(&value); + assert(result); + + if (result) + return *result; + else + return IdentifierType(); + } + + template void serialize(Handler &h, const int version) + { + h & value; + } + + bool operator == (const VariantIdentifier & other) const + { + return value == other.value; + } + bool operator != (const VariantIdentifier & other) const + { + return value != other.value; + } + bool operator < (const VariantIdentifier & other) const + { + return value < other.value; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/events/ApplyDamage.cpp b/lib/events/ApplyDamage.cpp index 305149edf..2056f0c2d 100644 --- a/lib/events/ApplyDamage.cpp +++ b/lib/events/ApplyDamage.cpp @@ -12,8 +12,7 @@ #include #include "ApplyDamage.h" - -#include "../../lib/NetPacks.h" +#include "../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/events/PlayerGotTurn.cpp b/lib/events/PlayerGotTurn.cpp index 123853d31..ccbffecc5 100644 --- a/lib/events/PlayerGotTurn.cpp +++ b/lib/events/PlayerGotTurn.cpp @@ -24,12 +24,11 @@ SubscriptionRegistry * PlayerGotTurn::getRegistry() return Instance.get(); } -void PlayerGotTurn::defaultExecute(const EventBus * bus, const ExecHandler & execHandler, PlayerColor & player) +void PlayerGotTurn::defaultExecute(const EventBus * bus, const PlayerColor & player) { CPlayerGotTurn event; event.setPlayer(player); - bus->executeEvent(event, execHandler); - player = event.getPlayer(); + bus->executeEvent(event); } CPlayerGotTurn::CPlayerGotTurn() = default; diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index fddd889fb..8bdf8abab 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -19,17 +19,17 @@ CMappedFileLoader::CMappedFileLoader(const std::string & mountPoint, const JsonN { for(auto entry : config.Struct()) { - //fileList[ResourceID(mountPoint + entry.first)] = ResourceID(mountPoint + entry.second.String()); - fileList.emplace(ResourceID(mountPoint + entry.first), ResourceID(mountPoint + entry.second.String())); + //fileList[ResourcePath(mountPoint + entry.first)] = ResourcePath(mountPoint + entry.second.String()); + fileList.emplace(ResourcePath(mountPoint + entry.first), ResourcePath(mountPoint + entry.second.String())); } } -std::unique_ptr CMappedFileLoader::load(const ResourceID & resourceName) const +std::unique_ptr CMappedFileLoader::load(const ResourcePath & resourceName) const { return CResourceHandler::get()->load(fileList.at(resourceName)); } -bool CMappedFileLoader::existsResource(const ResourceID & resourceName) const +bool CMappedFileLoader::existsResource(const ResourcePath & resourceName) const { return fileList.count(resourceName) != 0; } @@ -39,14 +39,14 @@ std::string CMappedFileLoader::getMountPoint() const return ""; // does not have any meaning with this type of data source } -std::optional CMappedFileLoader::getResourceName(const ResourceID & resourceName) const +std::optional CMappedFileLoader::getResourceName(const ResourcePath & resourceName) const { return CResourceHandler::get()->getResourceName(fileList.at(resourceName)); } -std::unordered_set CMappedFileLoader::getFilteredFiles(std::function filter) const +std::unordered_set CMappedFileLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : fileList) { @@ -64,7 +64,7 @@ CFilesystemList::~CFilesystemList() { } -std::unique_ptr CFilesystemList::load(const ResourceID & resourceName) const +std::unique_ptr CFilesystemList::load(const ResourcePath & resourceName) const { // load resource from last loader that have it (last overridden version) for(const auto & loader : boost::adaptors::reverse(loaders)) @@ -77,7 +77,7 @@ std::unique_ptr CFilesystemList::load(const ResourceID & resourceN + EResTypeHelper::getEResTypeAsString(resourceName.getType()) + " wasn't found."); } -bool CFilesystemList::existsResource(const ResourceID & resourceName) const +bool CFilesystemList::existsResource(const ResourcePath & resourceName) const { for(const auto & loader : loaders) if (loader->existsResource(resourceName)) @@ -90,14 +90,14 @@ std::string CFilesystemList::getMountPoint() const return ""; } -std::optional CFilesystemList::getResourceName(const ResourceID & resourceName) const +std::optional CFilesystemList::getResourceName(const ResourcePath & resourceName) const { if (existsResource(resourceName)) return getResourcesWithName(resourceName).back()->getResourceName(resourceName); return std::optional(); } -std::set CFilesystemList::getResourceNames(const ResourceID & resourceName) const +std::set CFilesystemList::getResourceNames(const ResourcePath & resourceName) const { std::set paths; for(auto& loader : getResourcesWithName(resourceName)) @@ -117,9 +117,9 @@ void CFilesystemList::updateFilteredFiles(std::functionupdateFilteredFiles(filter); } -std::unordered_set CFilesystemList::getFilteredFiles(std::function filter) const +std::unordered_set CFilesystemList::getFilteredFiles(std::function filter) const { - std::unordered_set ret; + std::unordered_set ret; for(const auto & loader : loaders) for(const auto & entry : loader->getFilteredFiles(filter)) @@ -128,7 +128,7 @@ std::unordered_set CFilesystemList::getFilteredFiles(std::functiontrace("Creating %s", filename); for (auto & loader : boost::adaptors::reverse(loaders)) @@ -139,7 +139,7 @@ bool CFilesystemList::createResource(std::string filename, bool update) // Check if resource was created successfully. Possible reasons for this to fail // a) loader failed to create resource (e.g. read-only FS) // b) in update mode, call with filename that does not exists - assert(load(ResourceID(filename))); + assert(load(ResourcePath(filename))); logGlobal->trace("Resource created successfully"); return true; @@ -149,7 +149,7 @@ bool CFilesystemList::createResource(std::string filename, bool update) return false; } -std::vector CFilesystemList::getResourcesWithName(const ResourceID & resourceName) const +std::vector CFilesystemList::getResourcesWithName(const ResourcePath & resourceName) const { std::vector ret; diff --git a/lib/filesystem/AdapterLoaders.h b/lib/filesystem/AdapterLoaders.h index 3e0386ed5..6fea768d7 100644 --- a/lib/filesystem/AdapterLoaders.h +++ b/lib/filesystem/AdapterLoaders.h @@ -10,7 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -38,19 +38,19 @@ public: /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - std::optional getResourceName(const ResourceID & resourceName) const override; + std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; private: /** A list of files in this map - * key = ResourceID for resource loader - * value = ResourceID to which file this request will be redirected + * key = ResourcePath for resource loader + * value = ResourcePath to which file this request will be redirected */ - std::unordered_map fileList; + std::unordered_map fileList; }; class DLL_LINKAGE CFilesystemList : public ISimpleResourceLoader @@ -68,15 +68,15 @@ public: ~CFilesystemList(); /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - std::optional getResourceName(const ResourceID & resourceName) const override; - std::set getResourceNames(const ResourceID & resourceName) const override; + std::optional getResourceName(const ResourcePath & resourceName) const override; + std::set getResourceNames(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; - std::unordered_set getFilteredFiles(std::function filter) const override; - bool createResource(std::string filename, bool update = false) override; - std::vector getResourcesWithName(const ResourceID & resourceName) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; + bool createResource(const std::string & filename, bool update = false) override; + std::vector getResourcesWithName(const ResourcePath & resourceName) const override; /** * Adds a resource loader to the loaders list diff --git a/lib/filesystem/CArchiveLoader.cpp b/lib/filesystem/CArchiveLoader.cpp index bca104a45..c7b47f59c 100644 --- a/lib/filesystem/CArchiveLoader.cpp +++ b/lib/filesystem/CArchiveLoader.cpp @@ -24,7 +24,7 @@ ArchiveEntry::ArchiveEntry() } -CArchiveLoader::CArchiveLoader(std::string _mountPoint, bfs::path _archive, bool _extractArchives) : +CArchiveLoader::CArchiveLoader(std::string _mountPoint, boost::filesystem::path _archive, bool _extractArchives) : archive(std::move(_archive)), mountPoint(std::move(_mountPoint)), extractArchives(_extractArchives) @@ -78,7 +78,7 @@ void CArchiveLoader::initLODArchive(const std::string &mountPoint, CFileInputStr entry.compressedSize = reader.readUInt32(); // Add lod entry to local entries map - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; if(extractArchives) { @@ -123,7 +123,7 @@ void CArchiveLoader::initVIDArchive(const std::string &mountPoint, CFileInputStr entry.compressedSize = 0; offsets.insert(entry.offset); - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; } offsets.insert(static_cast(fileStream.getSize())); @@ -162,14 +162,14 @@ void CArchiveLoader::initSNDArchive(const std::string &mountPoint, CFileInputStr entry.offset = reader.readInt32(); entry.fullSize = reader.readInt32(); entry.compressedSize = 0; - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; if(extractArchives) extractToFolder("SOUND", fileStream, entry); } } -std::unique_ptr CArchiveLoader::load(const ResourceID & resourceName) const +std::unique_ptr CArchiveLoader::load(const ResourcePath & resourceName) const { assert(existsResource(resourceName)); @@ -187,7 +187,7 @@ std::unique_ptr CArchiveLoader::load(const ResourceID & resourceNa } } -bool CArchiveLoader::existsResource(const ResourceID & resourceName) const +bool CArchiveLoader::existsResource(const ResourcePath & resourceName) const { return entries.count(resourceName) != 0; } @@ -197,9 +197,9 @@ std::string CArchiveLoader::getMountPoint() const return mountPoint; } -std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const +std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : entries) { @@ -217,7 +217,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput fileStream.seek(entry.offset); fileStream.read(data.data(), entry.fullSize); - bfs::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); + boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); // writeToOutputFile std::ofstream out(extractedFilePath.string(), std::ofstream::binary); @@ -229,18 +229,18 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const { - std::unique_ptr inputStream = load(ResourceID(mountPoint + entry.name)); + std::unique_ptr inputStream = load(ResourcePath(mountPoint + entry.name)); entry.offset = 0; extractToFolder(outputSubFolder, *inputStream, entry); } -bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) { - bfs::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; - bfs::path extractedFilePath = extractionFolderPath / entryName; + boost::filesystem::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; + boost::filesystem::path extractedFilePath = extractionFolderPath / entryName; - bfs::create_directories(extractionFolderPath); + boost::filesystem::create_directories(extractionFolderPath); return extractedFilePath; } diff --git a/lib/filesystem/CArchiveLoader.h b/lib/filesystem/CArchiveLoader.h index e263e55c5..33b410062 100644 --- a/lib/filesystem/CArchiveLoader.h +++ b/lib/filesystem/CArchiveLoader.h @@ -10,9 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" - -namespace bfs = boost::filesystem; +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -58,15 +56,15 @@ public: * * @throws std::runtime_error if the archive wasn't found or if the archive isn't supported */ - CArchiveLoader(std::string mountPoint, bfs::path archive, bool extractArchives = false); + CArchiveLoader(std::string mountPoint, boost::filesystem::path archive, bool extractArchives = false); /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(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; /** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */ @@ -95,18 +93,18 @@ private: void initSNDArchive(const std::string &mountPoint, CFileInputStream & fileStream); /** The file path to the archive which is scanned and indexed. */ - bfs::path archive; + boost::filesystem::path archive; std::string mountPoint; /** Holds all entries of the archive file. An entry can be accessed via the entry name. **/ - std::unordered_map entries; + std::unordered_map entries; /** Specifies if Original H3 archives should be extracted to a separate folder **/ bool extractArchives; }; /** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/ -bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CFileInputStream.cpp b/lib/filesystem/CFileInputStream.cpp index eb14e3eda..8f10d0d36 100644 --- a/lib/filesystem/CFileInputStream.cpp +++ b/lib/filesystem/CFileInputStream.cpp @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN CFileInputStream::CFileInputStream(const boost::filesystem::path & file, si64 start, si64 size) : dataStart{start}, dataSize{size}, - fileStream{file, std::ios::in | std::ios::binary} + fileStream{file.c_str(), std::ios::in | std::ios::binary} { if (fileStream.fail()) throw std::runtime_error("File " + file.string() + " isn't available."); diff --git a/lib/filesystem/CFileInputStream.h b/lib/filesystem/CFileInputStream.h index 4dd499188..077ec4cef 100644 --- a/lib/filesystem/CFileInputStream.h +++ b/lib/filesystem/CFileInputStream.h @@ -10,7 +10,6 @@ #pragma once #include "CInputStream.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -75,7 +74,7 @@ private: si64 dataSize; /** Native c++ input file stream object. */ - FileStream fileStream; + std::fstream fileStream; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 1290406a6..09073660c 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -11,13 +11,10 @@ #include "CFilesystemLoader.h" #include "CFileInputStream.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN -namespace bfs = boost::filesystem; - -CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, bfs::path baseDirectory, size_t depth, bool initial): +CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, boost::filesystem::path baseDirectory, size_t depth, bool initial): baseDirectory(std::move(baseDirectory)), mountPoint(std::move(_mountPoint)), fileList(listFiles(mountPoint, depth, initial)), @@ -26,15 +23,15 @@ CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, bfs::path baseDire logGlobal->trace("File system loaded, %d files found", fileList.size()); } -std::unique_ptr CFilesystemLoader::load(const ResourceID & resourceName) const +std::unique_ptr CFilesystemLoader::load(const ResourcePath & resourceName) const { assert(fileList.count(resourceName)); - bfs::path file = baseDirectory / fileList.at(resourceName); + boost::filesystem::path file = baseDirectory / fileList.at(resourceName); logGlobal->trace("loading %s", file.string()); return std::make_unique(file); } -bool CFilesystemLoader::existsResource(const ResourceID & resourceName) const +bool CFilesystemLoader::existsResource(const ResourcePath & resourceName) const { return fileList.count(resourceName); } @@ -44,7 +41,7 @@ std::string CFilesystemLoader::getMountPoint() const return mountPoint; } -std::optional CFilesystemLoader::getResourceName(const ResourceID & resourceName) const +std::optional CFilesystemLoader::getResourceName(const ResourcePath & resourceName) const { assert(existsResource(resourceName)); @@ -59,9 +56,9 @@ void CFilesystemLoader::updateFilteredFiles(std::function CFilesystemLoader::getFilteredFiles(std::function filter) const +std::unordered_set CFilesystemLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for (auto & file : fileList) { @@ -71,9 +68,10 @@ std::unordered_set CFilesystemLoader::getFilteredFiles(std::function return foundID; } -bool CFilesystemLoader::createResource(std::string filename, bool update) +bool CFilesystemLoader::createResource(const std::string & requestedFilename, bool update) { - ResourceID resID(filename); + std::string filename = requestedFilename; + ResourcePath resID(filename); if (fileList.find(resID) != fileList.end()) return true; @@ -88,46 +86,53 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) if (!update) { - if (!FileStream::createFile(baseDirectory / filename)) + // create folders if not exists + boost::filesystem::path p((baseDirectory / filename).c_str()); + boost::filesystem::create_directories(p.parent_path()); + + // create file, if not exists + std::ofstream file((baseDirectory / filename).c_str(), std::ofstream::binary); + + if (!file.is_open()) return false; } fileList[resID] = filename; return true; } -std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const +std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const { - static const EResType::Type initArray[] = { + static const EResType initArray[] = { EResType::DIRECTORY, - EResType::TEXT, + EResType::JSON, EResType::ARCHIVE_LOD, EResType::ARCHIVE_VID, EResType::ARCHIVE_SND, EResType::ARCHIVE_ZIP }; - static const std::set initialTypes(initArray, initArray + std::size(initArray)); + static const std::set initialTypes(initArray, initArray + std::size(initArray)); - assert(bfs::is_directory(baseDirectory)); - std::unordered_map fileList; + assert(boost::filesystem::is_directory(baseDirectory)); + std::unordered_map fileList; - std::vector path; //vector holding relative path to our file + std::vector path; //vector holding relative path to our file - bfs::recursive_directory_iterator enddir; + boost::filesystem::recursive_directory_iterator enddir; #if BOOST_VERSION >= 107200 // 1.72 - bfs::recursive_directory_iterator it(baseDirectory, bfs::directory_options::follow_directory_symlink); + boost::filesystem::recursive_directory_iterator it(baseDirectory, boost::filesystem::directory_options::follow_directory_symlink); #else - bfs::recursive_directory_iterator it(baseDirectory, bfs::symlink_option::recurse); + boost::filesystem::recursive_directory_iterator it(baseDirectory, boost::filesystem::symlink_option::recurse); #endif for(; it != enddir; ++it) { - EResType::Type type; + EResType type; #if BOOST_VERSION >= 107200 const auto currentDepth = it.depth(); #else const auto currentDepth = it.level(); #endif - if (bfs::is_directory(it->status())) + if (boost::filesystem::is_directory(it->status())) { path.resize(currentDepth + 1); path.back() = it->path().filename(); @@ -146,7 +151,7 @@ std::unordered_map CFilesystemLoader::listFiles(const std if (!initial || vstd::contains(initialTypes, type)) { //reconstruct relative filename (not possible via boost AFAIK) - bfs::path filename; + boost::filesystem::path filename; const size_t iterations = std::min(static_cast(currentDepth), path.size()); if (iterations) { @@ -159,13 +164,13 @@ std::unordered_map CFilesystemLoader::listFiles(const std filename = it->path().filename(); std::string resName; - if (bfs::path::preferred_separator != '/') + if (boost::filesystem::path::preferred_separator != '/') { // resource names are using UNIX slashes (/) resName.reserve(resName.size() + filename.native().size()); resName = mountPoint; for (const char c : filename.string()) - if (c != bfs::path::preferred_separator) + if (c != boost::filesystem::path::preferred_separator) resName.push_back(c); else resName.push_back('/'); @@ -173,7 +178,7 @@ std::unordered_map CFilesystemLoader::listFiles(const std else resName = mountPoint + filename.string(); - fileList[ResourceID(resName, type)] = std::move(filename); + fileList[ResourcePath(resName, type)] = std::move(filename); } } diff --git a/lib/filesystem/CFilesystemLoader.h b/lib/filesystem/CFilesystemLoader.h index 8aeb23581..ec75c62a0 100644 --- a/lib/filesystem/CFilesystemLoader.h +++ b/lib/filesystem/CFilesystemLoader.h @@ -10,7 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -34,13 +34,13 @@ public: /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - bool createResource(std::string filename, bool update = false) override; - std::optional getResourceName(const ResourceID & resourceName) const override; + bool createResource(const std::string & filename, bool update = false) override; + std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; private: /** The base directory which is scanned and indexed. */ @@ -51,10 +51,10 @@ private: size_t recursiveDepth; /** A list of files in the directory - * key = ResourceID for resource loader + * key = ResourcePath for resource loader * value = name that can be used to access file */ - mutable std::unordered_map fileList; + mutable std::unordered_map fileList; /** * Returns a list of pathnames denoting the files in the directory denoted by this pathname. @@ -65,7 +65,7 @@ private: * @return a list of pathnames denoting the files and directories in the directory denoted by this pathname * The array will be empty if the directory is empty. Ptr is null if the directory doesn't exist or if it isn't a directory. */ - std::unordered_map listFiles(const std::string &mountPoint, size_t depth, bool initial) const; + std::unordered_map listFiles(const std::string &mountPoint, size_t depth, bool initial) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CZipLoader.cpp b/lib/filesystem/CZipLoader.cpp index b743e92da..e6592f7bd 100644 --- a/lib/filesystem/CZipLoader.cpp +++ b/lib/filesystem/CZipLoader.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "CZipLoader.h" -#include "FileStream.h" #include "../ScopeGuard.h" @@ -62,9 +61,9 @@ CZipLoader::CZipLoader(const std::string & mountPoint, const boost::filesystem:: logGlobal->trace("Zip archive loaded, %d files found", files.size()); } -std::unordered_map CZipLoader::listFiles(const std::string & mountPoint, const boost::filesystem::path & archive) +std::unordered_map CZipLoader::listFiles(const std::string & mountPoint, const boost::filesystem::path & archive) { - std::unordered_map ret; + std::unordered_map ret; unzFile file = unzOpen2_64(archive.c_str(), &zlibApi); @@ -85,7 +84,7 @@ std::unordered_map CZipLoader::listFiles(const std:: unzGetCurrentFileInfo64(file, &info, filename.data(), static_cast(filename.size()), nullptr, 0, nullptr, 0); std::string filenameString(filename.data(), filename.size()); - unzGetFilePos64(file, &ret[ResourceID(mountPoint + filenameString)]); + unzGetFilePos64(file, &ret[ResourcePath(mountPoint + filenameString)]); } while (unzGoToNextFile(file) == UNZ_OK); } @@ -94,12 +93,12 @@ std::unordered_map CZipLoader::listFiles(const std:: return ret; } -std::unique_ptr CZipLoader::load(const ResourceID & resourceName) const +std::unique_ptr CZipLoader::load(const ResourcePath & resourceName) const { return std::unique_ptr(new CZipStream(ioApi, archiveName, files.at(resourceName))); } -bool CZipLoader::existsResource(const ResourceID & resourceName) const +bool CZipLoader::existsResource(const ResourcePath & resourceName) const { return files.count(resourceName) != 0; } @@ -109,9 +108,9 @@ std::string CZipLoader::getMountPoint() const return mountPoint; } -std::unordered_set CZipLoader::getFilteredFiles(std::function filter) const +std::unordered_set CZipLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : files) { @@ -155,7 +154,10 @@ std::vector ZipArchive::listFiles(const boost::filesystem::path & f { std::vector ret; - unzFile file = unzOpen2_64(filename.c_str(), FileStream::GetMinizipFilefunc()); + CDefaultIOApi zipAPI; + auto zipStructure = zipAPI.getApiStructure(); + + unzFile file = unzOpen2_64(filename.c_str(), &zipStructure); if (file == nullptr) { @@ -208,7 +210,10 @@ bool ZipArchive::extract(const boost::filesystem::path & from, const boost::file bool ZipArchive::extract(const boost::filesystem::path & from, const boost::filesystem::path & where, const std::vector & what) { - unzFile archive = unzOpen2_64(from.c_str(), FileStream::GetMinizipFilefunc()); + CDefaultIOApi zipAPI; + auto zipStructure = zipAPI.getApiStructure(); + + unzFile archive = unzOpen2_64(from.c_str(), &zipStructure); auto onExit = vstd::makeScopeGuard([&]() { @@ -229,7 +234,7 @@ bool ZipArchive::extract(const boost::filesystem::path & from, const boost::file if (boost::algorithm::ends_with(file, "/")) continue; - FileStream destFile(fullName, std::ios::out | std::ios::binary); + std::fstream destFile(fullName.c_str(), std::ios::out | std::ios::binary); if (!destFile.good()) return false; diff --git a/lib/filesystem/CZipLoader.h b/lib/filesystem/CZipLoader.h index 255624164..139e49188 100644 --- a/lib/filesystem/CZipLoader.h +++ b/lib/filesystem/CZipLoader.h @@ -11,7 +11,7 @@ #include "ISimpleResourceLoader.h" #include "CInputStream.h" -#include "ResourceID.h" +#include "ResourcePath.h" #include "CCompressedStream.h" #include "MinizipExtensions.h" @@ -46,19 +46,19 @@ class DLL_LINKAGE CZipLoader : public ISimpleResourceLoader boost::filesystem::path archiveName; std::string mountPoint; - std::unordered_map files; + std::unordered_map files; - std::unordered_map listFiles(const std::string & mountPoint, const boost::filesystem::path &archive); + std::unordered_map listFiles(const std::string & mountPoint, const boost::filesystem::path &archive); public: CZipLoader(const std::string & mountPoint, const boost::filesystem::path & archive, std::shared_ptr api = std::shared_ptr(new CDefaultIOApi())); /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; }; namespace ZipArchive diff --git a/lib/filesystem/FileStream.cpp b/lib/filesystem/FileStream.cpp deleted file mode 100644 index 06f89fca5..000000000 --- a/lib/filesystem/FileStream.cpp +++ /dev/null @@ -1,175 +0,0 @@ -/* - * FileStream.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 "FileStream.h" - -#ifdef USE_SYSTEM_MINIZIP -#include -#include -#else -#include "../minizip/unzip.h" -#include "../minizip/ioapi.h" -#endif - -#include - -#define GETFILE static_cast(filePtr) - -#ifdef VCMI_WINDOWS - #ifndef _CRT_SECURE_NO_WARNINGS - #define _CRT_SECURE_NO_WARNINGS - #endif - #include - #define CHAR_LITERAL(s) L##s - using CharType = wchar_t; -#else - #define CHAR_LITERAL(s) s - using CharType = char; -#endif - -namespace -{ -inline FILE* do_open(const CharType* name, const CharType* mode) -{ - #ifdef VCMI_WINDOWS - return _wfopen(name, mode); - #else - return std::fopen(name, mode); - #endif -} - -voidpf ZCALLBACK MinizipOpenFunc(voidpf opaque, const void* filename, int mode) -{ - const CharType* mode_fopen = [mode]() -> const CharType* - { - if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) - return CHAR_LITERAL("rb"); - else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) - return CHAR_LITERAL("r+b"); - else if (mode & ZLIB_FILEFUNC_MODE_CREATE) - return CHAR_LITERAL("wb"); - return nullptr; - }(); - - if (filename != nullptr && mode_fopen != nullptr) - return do_open(static_cast(filename), mode_fopen); - else - return nullptr; -} -} // namespace - -template struct boost::iostreams::stream; - -VCMI_LIB_NAMESPACE_BEGIN - -zlib_filefunc64_def* FileStream::GetMinizipFilefunc() -{ - static zlib_filefunc64_def MinizipFilefunc; - static bool initialized = false; - if (!initialized) - { - fill_fopen64_filefunc(&MinizipFilefunc); - MinizipFilefunc.zopen64_file = &MinizipOpenFunc; - initialized = true; - } - return &MinizipFilefunc; -} - -/*static*/ -bool FileStream::createFile(const boost::filesystem::path& filename) -{ - FILE* f = do_open(filename.c_str(), CHAR_LITERAL("wb")); - bool result = (f != nullptr); - if(result) - fclose(f); - return result; -} - -FileBuf::FileBuf(const boost::filesystem::path& filename, std::ios_base::openmode mode) -{ - auto openmode = [mode]() -> std::basic_string - { - using namespace std; - switch (mode & (~ios_base::ate & ~ios_base::binary)) - { - case (ios_base::in): - return CHAR_LITERAL("r"); - case (ios_base::out): - case (ios_base::out | ios_base::trunc): - return CHAR_LITERAL("w"); - case (ios_base::app): - case (ios_base::out | ios_base::app): - return CHAR_LITERAL("a"); - case (ios_base::out | ios_base::in): - return CHAR_LITERAL("r+"); - case (ios_base::out | ios_base::in | ios_base::trunc): - return CHAR_LITERAL("w+"); - case (ios_base::out | ios_base::in | ios_base::app): - case (ios_base::in | ios_base::app): - return CHAR_LITERAL("a+"); - default: - throw std::ios_base::failure("invalid open mode"); - } - }(); - - if (mode & std::ios_base::binary) - openmode += CHAR_LITERAL('b'); - - filePtr = do_open(filename.c_str(), openmode.c_str()); - - if (filePtr == nullptr) - throw std::ios_base::failure("could not open file"); - - if (mode & std::ios_base::ate) { - if (std::fseek(GETFILE, 0, SEEK_END)) { - fclose(GETFILE); - throw std::ios_base::failure("could not open file"); - } - } -} - -void FileBuf::close() -{ - std::fclose(GETFILE); -} - -std::streamsize FileBuf::read(char* s, std::streamsize n) -{ - return static_cast(std::fread(s, 1, n, GETFILE)); -} - -std::streamsize FileBuf::write(const char* s, std::streamsize n) -{ - return static_cast(std::fwrite(s, 1, n, GETFILE)); -} - -std::streamoff FileBuf::seek(std::streamoff off, std::ios_base::seekdir way) -{ - const auto src = [way]() -> int - { - switch(way) - { - case std::ios_base::beg: - return SEEK_SET; - case std::ios_base::cur: - return SEEK_CUR; - case std::ios_base::end: - return SEEK_END; - default: - throw std::ios_base::failure("bad seek direction"); - } - }(); - if(std::fseek(GETFILE, static_cast(off), src)) - throw std::ios_base::failure("bad seek offset"); - - return static_cast(std::ftell(GETFILE)); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/FileStream.h b/lib/filesystem/FileStream.h deleted file mode 100644 index db57473e0..000000000 --- a/lib/filesystem/FileStream.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * FileStream.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE FileBuf -{ -public: - using char_type = char; - using category = struct category_ : - boost::iostreams::seekable_device_tag, - boost::iostreams::closable_tag - {}; - - FileBuf(const boost::filesystem::path& filename, std::ios_base::openmode mode); - - std::streamsize read(char* s, std::streamsize n); - std::streamsize write(const char* s, std::streamsize n); - std::streamoff seek(std::streamoff off, std::ios_base::seekdir way); - - void close(); -private: - void* filePtr; -}; - -VCMI_LIB_NAMESPACE_END - -struct zlib_filefunc64_def_s; -using zlib_filefunc64_def = zlib_filefunc64_def_s; - -#ifdef VCMI_DLL -#ifdef _MSC_VER -#pragma warning (push) -#pragma warning (disable : 4910) -#endif -extern template struct DLL_LINKAGE boost::iostreams::stream; -#ifdef _MSC_VER -#pragma warning (pop) -#endif -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE FileStream : public boost::iostreams::stream -{ -public: - FileStream() = default; - explicit FileStream(const boost::filesystem::path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out) - : boost::iostreams::stream(p, mode) {} - - static bool createFile(const boost::filesystem::path& filename); - - static zlib_filefunc64_def* GetMinizipFilefunc(); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index aa586efc5..84378f56b 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -20,7 +20,7 @@ #include "../GameConstants.h" #include "../VCMIDirs.h" #include "../CStopWatch.h" -#include "../CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -84,7 +84,7 @@ void CFilesystemGenerator::loadDirectory(const std::string &mountPoint, const Js if (!config["depth"].isNull()) depth = static_cast(config["depth"].Float()); - ResourceID resID(URI, EResType::DIRECTORY); + ResourcePath resID(URI, EResType::DIRECTORY); for(auto & loader : CResourceHandler::get("initial")->getResourcesWithName(resID)) { @@ -96,16 +96,16 @@ void CFilesystemGenerator::loadDirectory(const std::string &mountPoint, const Js void CFilesystemGenerator::loadZipArchive(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, EResType::ARCHIVE_ZIP)); + auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, EResType::ARCHIVE_ZIP)); if (filename) filesystem->addLoader(new CZipLoader(mountPoint, *filename), false); } -template +template void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, archiveType)); + auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, archiveType)); if (filename) filesystem->addLoader(new CArchiveLoader(mountPoint, *filename, extractArchives), false); } @@ -113,10 +113,10 @@ void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const Json void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, EResType::TEXT)); + auto filename = CResourceHandler::get("initial")->getResourceName(JsonPath::builtin(URI)); if (filename) { - auto configData = CResourceHandler::get("initial")->load(ResourceID(URI, EResType::TEXT))->readAll(); + auto configData = CResourceHandler::get("initial")->load(JsonPath::builtin(URI))->readAll(); const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); filesystem->addLoader(new CMappedFileLoader(mountPoint, configInitial), false); } @@ -131,7 +131,7 @@ ISimpleResourceLoader * CResourceHandler::createInitial() //recurse only into specific directories auto recurseInDir = [&](const std::string & URI, int depth) { - ResourceID ID(URI, EResType::DIRECTORY); + ResourcePath ID(URI, EResType::DIRECTORY); for(auto & loader : initialLoader->getResourcesWithName(ID)) { @@ -147,7 +147,7 @@ ISimpleResourceLoader * CResourceHandler::createInitial() for (auto & path : VCMIDirs::get().dataPaths()) { if (boost::filesystem::is_directory(path)) // some of system-provided paths may not exist - initialLoader->addLoader(new CFilesystemLoader("", path, 0, true), false); + initialLoader->addLoader(new CFilesystemLoader("", path, 1, true), false); } initialLoader->addLoader(new CFilesystemLoader("", VCMIDirs::get().userDataPath(), 0, true), false); @@ -210,11 +210,11 @@ ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives) { - auto fsConfigData = get("initial")->load(ResourceID(fsConfigURI, EResType::TEXT))->readAll(); + auto fsConfigData = get("initial")->load(JsonPath::builtin(fsConfigURI))->readAll(); const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); - addFilesystem("data", CModHandler::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); + addFilesystem("data", ModScope::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); } void CResourceHandler::addFilesystem(const std::string & parent, const std::string & identifier, ISimpleResourceLoader * loader) diff --git a/lib/filesystem/Filesystem.h b/lib/filesystem/Filesystem.h index a6b894ab0..1ead410f1 100644 --- a/lib/filesystem/Filesystem.h +++ b/lib/filesystem/Filesystem.h @@ -11,7 +11,7 @@ #include "CInputStream.h" #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +27,7 @@ class DLL_LINKAGE CFilesystemGenerator CFilesystemList * filesystem; std::string prefix; - template + template void loadArchive(const std::string & mountPoint, const JsonNode & config); void loadDirectory(const std::string & mountPoint, const JsonNode & config); void loadZipArchive(const std::string & mountPoint, const JsonNode & config); diff --git a/lib/filesystem/ISimpleResourceLoader.h b/lib/filesystem/ISimpleResourceLoader.h index 88c6a9fad..836a3a505 100644 --- a/lib/filesystem/ISimpleResourceLoader.h +++ b/lib/filesystem/ISimpleResourceLoader.h @@ -12,7 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; -class ResourceID; +class ResourcePath; /** * A class which knows the files containing in the archive or system and how to load them. @@ -28,14 +28,14 @@ public: * @param resourceName The unqiue resource name in space of the archive. * @return a input stream object */ - virtual std::unique_ptr load(const ResourceID & resourceName) const = 0; + virtual std::unique_ptr load(const ResourcePath & resourceName) const = 0; /** * Checks if the entry exists. * * @return Returns true if the entry exists, false if not. */ - virtual bool existsResource(const ResourceID & resourceName) const = 0; + virtual bool existsResource(const ResourcePath & resourceName) const = 0; /** * Gets mount point to which this loader was attached @@ -49,7 +49,7 @@ public: * * @return path or empty optional if file can't be accessed independently (e.g. file in archive) */ - virtual std::optional getResourceName(const ResourceID & resourceName) const + virtual std::optional getResourceName(const ResourcePath & resourceName) const { return std::optional(); } @@ -59,7 +59,7 @@ public: * * @return std::set with names. */ - virtual std::set getResourceNames(const ResourceID & resourceName) const + virtual std::set getResourceNames(const ResourcePath & resourceName) const { std::set result; auto rn = getResourceName(resourceName); @@ -83,14 +83,14 @@ public: * @param filter Filter that returns true if specified ID matches filter * @return Returns list of flies */ - virtual std::unordered_set getFilteredFiles(std::function filter) const = 0; + virtual std::unordered_set getFilteredFiles(std::function filter) const = 0; /** * Creates new resource with specified filename. * * @return true if new file was created, false on error or if file already exists */ - virtual bool createResource(std::string filename, bool update = false) + virtual bool createResource(const std::string & filename, bool update = false) { return false; } @@ -100,7 +100,7 @@ public: * * @return vector with all loaders */ - virtual std::vector getResourcesWithName(const ResourceID & resourceName) const + virtual std::vector getResourcesWithName(const ResourcePath & resourceName) const { if (existsResource(resourceName)) return std::vector(1, this); diff --git a/lib/filesystem/MinizipExtensions.cpp b/lib/filesystem/MinizipExtensions.cpp index e58fb3a9d..9c7a2a407 100644 --- a/lib/filesystem/MinizipExtensions.cpp +++ b/lib/filesystem/MinizipExtensions.cpp @@ -11,7 +11,6 @@ #include "MinizipExtensions.h" #include "CMemoryBuffer.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -86,10 +85,59 @@ inline int streamProxyClose(voidpf opaque, voidpf stream) } ///CDefaultIOApi +#define GETFILE static_cast(filePtr) + +#ifdef VCMI_WINDOWS + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #include + #define CHAR_LITERAL(s) L##s + using CharType = wchar_t; +#else + #define CHAR_LITERAL(s) s + using CharType = char; +#endif + +static inline FILE* do_open(const CharType* name, const CharType* mode) +{ + #ifdef VCMI_WINDOWS + return _wfopen(name, mode); + #else + return std::fopen(name, mode); + #endif +} + +static voidpf ZCALLBACK MinizipOpenFunc(voidpf opaque, const void* filename, int mode) +{ + const CharType* mode_fopen = [mode]() -> const CharType* + { + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) + return CHAR_LITERAL("rb"); + else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + return CHAR_LITERAL("r+b"); + else if (mode & ZLIB_FILEFUNC_MODE_CREATE) + return CHAR_LITERAL("wb"); + return nullptr; + }(); + + if (filename != nullptr && mode_fopen != nullptr) + return do_open(static_cast(filename), mode_fopen); + else + return nullptr; +} zlib_filefunc64_def CDefaultIOApi::getApiStructure() { - return * FileStream::GetMinizipFilefunc(); + static zlib_filefunc64_def MinizipFilefunc; + static bool initialized = false; + if (!initialized) + { + fill_fopen64_filefunc(&MinizipFilefunc); + MinizipFilefunc.zopen64_file = &MinizipOpenFunc; + initialized = true; + } + return MinizipFilefunc; } ///CProxyIOApi diff --git a/lib/filesystem/ResourceID.h b/lib/filesystem/ResourceID.h deleted file mode 100644 index de29dec48..000000000 --- a/lib/filesystem/ResourceID.h +++ /dev/null @@ -1,154 +0,0 @@ -/* - * ResourceID.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 - - -/** - * Specifies the resource type. - * - * Supported file extensions: - * - * Text: .txt .json - * Animation: .def - * Mask: .msk .msg - * Campaign: .h3c - * Map: .h3m - * Font: .fnt - * Image: .bmp, .jpg, .pcx, .png, .tga - * Sound: .wav .82m - * Video: .smk, .bik .mjpg .mpg - * Music: .mp3, .ogg - * Archive: .lod, .snd, .vid .pac .zip - * Palette: .pal - * Savegame: .v*gm1 - */ -namespace EResType -{ - enum Type - { - TEXT, - ANIMATION, - MASK, - CAMPAIGN, - MAP, - BMP_FONT, - TTF_FONT, - IMAGE, - VIDEO, - SOUND, - ARCHIVE_VID, - ARCHIVE_ZIP, - ARCHIVE_SND, - ARCHIVE_LOD, - PALETTE, - SAVEGAME, - DIRECTORY, - ERM, - ERT, - ERS, - OTHER, - UNDEFINED, - LUA - }; -} - -/** - * A struct which identifies a resource clearly. - */ -class DLL_LINKAGE ResourceID -{ -public: - /** - * Default c-tor. - */ - //ResourceID(); - - /** - * Ctor. Can be used to create identifier for resource loading using one parameter - * - * @param fullName The resource name including extension. - */ - explicit ResourceID(std::string fullName); - - /** - * Ctor. - * - * @param name The resource name. - * @param type The resource type. A constant from the enumeration EResType. - */ - ResourceID(std::string name, EResType::Type type); - - /** - * Compares this object with a another resource identifier. - * - * @param other The other resource identifier. - * @return Returns true if both are equally, false if not. - */ - inline bool operator==(ResourceID const & other) const - { - return name == other.name && type == other.type; - } - - std::string getName() const {return name;} - EResType::Type getType() const {return type;} - //void setName(std::string name); - //void setType(EResType::Type type); - -private: - /** - * Specifies the resource type. EResType::OTHER if not initialized. - * Required to prevent conflicts if files with different types (e.g. text and image) have the same name. - */ - EResType::Type type; - - /** Specifies the resource name. No extension so .pcx and .png can override each other, always in upper case. **/ - std::string name; -}; - -/** - * A helper class which provides a functionality to convert extension strings to EResTypes. - */ -class DLL_LINKAGE EResTypeHelper -{ -public: - /** - * Converts a extension string to a EResType enum object. - * - * @param extension The extension string e.g. .BMP, .PNG - * @return Returns a EResType enum object - */ - static EResType::Type getTypeFromExtension(std::string extension); - - /** - * Gets the EResType as a string representation. - * - * @param type the EResType - * @return the type as a string representation - */ - static std::string getEResTypeAsString(EResType::Type type); -}; - -VCMI_LIB_NAMESPACE_END - - -namespace std -{ -template <> struct hash -{ - size_t operator()(const VCMI_LIB_WRAP_NAMESPACE(ResourceID) & resourceIdent) const - { - std::hash intHasher; - std::hash stringHasher; - return stringHasher(resourceIdent.getName()) ^ intHasher(static_cast(resourceIdent.getType())); - } -}; -} diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourcePath.cpp similarity index 58% rename from lib/filesystem/ResourceID.cpp rename to lib/filesystem/ResourcePath.cpp index fb63fc428..5d982c2d1 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -1,5 +1,5 @@ /* - * ResourceID.cpp, part of VCMI engine + * ResourcePath.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,44 +8,26 @@ * */ #include "StdInc.h" -#include "ResourceID.h" +#include "ResourcePath.h" #include "FileInfo.h" +#include "../JsonNode.h" +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" + VCMI_LIB_NAMESPACE_BEGIN -// trivial to_upper that completely ignores localization and only work with ASCII -// Technically not a problem since -// 1) Right now VCMI does not supports unicode in filenames on Win -// 2) Filesystem case-sensivity is only problem for H3 data which uses ASCII-only symbols -// for me (Ivan) this define gives notable decrease in loading times -// #define ENABLE_TRIVIAL_TOUPPER - -#ifdef ENABLE_TRIVIAL_TOUPPER -static inline void toUpper(char & symbol) -{ - static const int diff = 'a' - 'A'; - if (symbol >= 'a' && symbol <= 'z') - symbol -= diff; -} - -static inline void toUpper(std::string & string) -{ - for (char & symbol : string) - toUpper(symbol); -} -#else static inline void toUpper(std::string & string) { boost::to_upper(string); } -#endif -static inline EResType::Type readType(const std::string& name) +static inline EResType readType(const std::string& name) { return EResTypeHelper::getTypeFromExtension(FileInfo::GetExtension(name).to_string()); } -static inline std::string readName(std::string name) +static inline std::string readName(std::string name, bool uppercase) { const auto dotPos = name.find_last_of('.'); @@ -61,74 +43,64 @@ static inline std::string readName(std::string name) name.resize(dotPos); } - toUpper(name); + if(uppercase) + toUpper(name); return name; } -#if 0 -ResourceID::ResourceID() - :type(EResType::OTHER) -{ -} -#endif - -ResourceID::ResourceID(std::string name_): - type{readType(name_)}, - name{readName(std::move(name_))} +ResourcePath::ResourcePath(const std::string & name_): + type(readType(name_)), + name(readName(name_, true)), + originalName(readName(name_, false)) {} -ResourceID::ResourceID(std::string name_, EResType::Type type_): - type{type_}, - name{readName(std::move(name_))} +ResourcePath::ResourcePath(const std::string & name_, EResType type_): + type(type_), + name(readName(name_, true)), + originalName(readName(name_, false)) {} -#if 0 -std::string ResourceID::getName() const + +ResourcePath::ResourcePath(const JsonNode & name, EResType type): + type(type), + name(readName(name.String(), true)), + originalName(readName(name.String(), false)) { - return name; } -EResType::Type ResourceID::getType() const +void ResourcePath::serializeJson(JsonSerializeFormat & handler) { - return type; -} - -void ResourceID::setName(std::string name) -{ - // setName shouldn't be used if type is UNDEFINED - assert(type != EResType::UNDEFINED); - - this->name = std::move(name); - - size_t dotPos = this->name.find_last_of("/."); - - if(dotPos != std::string::npos && this->name[dotPos] == '.' - && this->type == EResTypeHelper::getTypeFromExtension(this->name.substr(dotPos))) + if (!handler.saving) { - this->name.erase(dotPos); + JsonNode const & node = handler.getCurrent(); + + if (node.isString()) + { + name = readName(node.String(), true); + originalName = readName(node.String(), false); + return; + } } - toUpper(this->name); + handler.serializeInt("type", type); + handler.serializeString("name", name); + handler.serializeString("originalName", originalName); } -void ResourceID::setType(EResType::Type type) -{ - this->type = type; -} -#endif -EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) +EResType EResTypeHelper::getTypeFromExtension(std::string extension) { toUpper(extension); - static const std::map stringToRes = + static const std::map stringToRes = { {".TXT", EResType::TEXT}, - {".JSON", EResType::TEXT}, + {".JSON", EResType::JSON}, {".DEF", EResType::ANIMATION}, {".MSK", EResType::MASK}, {".MSG", EResType::MASK}, {".H3C", EResType::CAMPAIGN}, {".H3M", EResType::MAP}, + {".TUT", EResType::MAP}, {".FNT", EResType::BMP_FONT}, {".TTF", EResType::TTF_FONT}, {".BMP", EResType::IMAGE}, @@ -147,6 +119,7 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) {".MJPG", EResType::VIDEO}, {".MPG", EResType::VIDEO}, {".AVI", EResType::VIDEO}, + {".WEBM", EResType::VIDEO}, {".ZIP", EResType::ARCHIVE_ZIP}, {".LOD", EResType::ARCHIVE_LOD}, {".PAC", EResType::ARCHIVE_LOD}, @@ -169,13 +142,14 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) return iter->second; } -std::string EResTypeHelper::getEResTypeAsString(EResType::Type type) +std::string EResTypeHelper::getEResTypeAsString(EResType type) { #define MAP_ENUM(value) {EResType::value, #value}, - static const std::map stringToRes = + static const std::map stringToRes = { MAP_ENUM(TEXT) + MAP_ENUM(JSON) MAP_ENUM(ANIMATION) MAP_ENUM(MASK) MAP_ENUM(CAMPAIGN) diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h new file mode 100644 index 000000000..2a4086141 --- /dev/null +++ b/lib/filesystem/ResourcePath.h @@ -0,0 +1,226 @@ +/* + * ResourcePath.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; +class JsonSerializeFormat; + +/** + * Specifies the resource type. + * + * Supported file extensions: + * + * Text: .txt + * Json: .json + * Animation: .def + * Mask: .msk .msg + * Campaign: .h3c + * Map: .h3m + * Font: .fnt + * Image: .bmp, .jpg, .pcx, .png, .tga + * Sound: .wav .82m + * Video: .smk, .bik .mjpg .mpg .webm + * Music: .mp3, .ogg + * Archive: .lod, .snd, .vid .pac .zip + * Palette: .pal + * Savegame: .v*gm1 + */ +enum class EResType +{ + TEXT, + JSON, + ANIMATION, + MASK, + CAMPAIGN, + MAP, + BMP_FONT, + TTF_FONT, + IMAGE, + VIDEO, + SOUND, + ARCHIVE_VID, + ARCHIVE_ZIP, + ARCHIVE_SND, + ARCHIVE_LOD, + PALETTE, + SAVEGAME, + DIRECTORY, + ERM, + ERT, + ERS, + LUA, + OTHER, + UNDEFINED, +}; + +/** + * A struct which identifies a resource clearly. + */ +class DLL_LINKAGE ResourcePath +{ +protected: + /// Constructs resource path based on JsonNode and selected type. File extension is ignored + ResourcePath(const JsonNode & name, EResType type); + +public: + /// Constructs resource path based on full name including extension + explicit ResourcePath(const std::string & fullName); + + /// Constructs resource path based on filename and selected type. File extension is ignored + ResourcePath(const std::string & name, EResType type); + + inline bool operator==(const ResourcePath & other) const + { + return name == other.name && type == other.type; + } + + inline bool operator!=(const ResourcePath & other) const + { + return name != other.name || type != other.type; + } + + inline bool operator<(const ResourcePath & other) const + { + if (type != other.type) + return type < other.type; + return name < other.name; + } + + bool empty() const {return name.empty();} + std::string getName() const {return name;} + std::string getOriginalName() const {return originalName;} + EResType getType() const {return type;} + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler & h, const int version) + { + h & type; + h & name; + h & originalName; + } + +protected: + /// Specifies the resource type. EResType::OTHER if not initialized. + /// Required to prevent conflicts if files with different types (e.g. text and image) have the same name. + EResType type; + + /// Specifies the resource name. No extension so .pcx and .png can override each other, always in upper case. + std::string name; + + /// name in original case + std::string originalName; +}; + +template +class ResourcePathTempl : public ResourcePath +{ + template + friend class ResourcePathTempl; + + ResourcePathTempl(const ResourcePath & copy) + :ResourcePath(copy) + { + type = Type; + } + + ResourcePathTempl(const std::string & path) + :ResourcePath(path, Type) + {} + + ResourcePathTempl(const JsonNode & name) + :ResourcePath(name, Type) + {} + +public: + ResourcePathTempl() + :ResourcePath("", Type) + {} + + static ResourcePathTempl fromResource(const ResourcePath & resource) + { + assert(Type == resource.getType()); + return ResourcePathTempl(resource); + } + + static ResourcePathTempl builtin(const std::string & filename) + { + return ResourcePathTempl(filename); + } + + static ResourcePathTempl builtinTODO(const std::string & filename) + { + return ResourcePathTempl(filename); + } + + static ResourcePathTempl fromJson(const JsonNode & path) + { + return ResourcePathTempl(path); + } + + template + ResourcePathTempl toType() const + { + ResourcePathTempl result(static_cast(*this)); + return result; + } + + ResourcePathTempl addPrefix(const std::string & prefix) const + { + ResourcePathTempl result; + result.name = prefix + this->getName(); + result.originalName = prefix + this->getOriginalName(); + + return result; + } +}; + +using AnimationPath = ResourcePathTempl; +using ImagePath = ResourcePathTempl; +using TextPath = ResourcePathTempl; +using JsonPath = ResourcePathTempl; +using VideoPath = ResourcePathTempl; +using AudioPath = ResourcePathTempl; + +namespace EResTypeHelper +{ + /** + * Converts a extension string to a EResType enum object. + * + * @param extension The extension string e.g. .BMP, .PNG + * @return Returns a EResType enum object + */ + EResType getTypeFromExtension(std::string extension); + + /** + * Gets the EResType as a string representation. + * + * @param type the EResType + * @return the type as a string representation + */ + std::string getEResTypeAsString(EResType type); +}; + +VCMI_LIB_NAMESPACE_END + +namespace std +{ +template <> struct hash +{ + size_t operator()(const VCMI_LIB_WRAP_NAMESPACE(ResourcePath) & resourceIdent) const + { + std::hash intHasher; + std::hash stringHasher; + return stringHasher(resourceIdent.getName()) ^ intHasher(static_cast(resourceIdent.getType())); + } +}; +} diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c4bc828ab..519b23e8f 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -29,7 +29,8 @@ #include "../VCMI_Lib.h" #include "../battle/BattleInfo.h" #include "../campaign/CampaignState.h" -#include "../filesystem/ResourceID.h" +#include "../constants/StringConstants.h" +#include "../filesystem/ResourcePath.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/DwellingInstanceConstructor.h" @@ -38,9 +39,10 @@ #include "../mapping/CMap.h" #include "../mapping/CMapEditManager.h" #include "../mapping/CMapService.h" +#include "../modding/IdentifierStorage.h" #include "../pathfinder/CPathfinder.h" #include "../pathfinder/PathfinderOptions.h" -#include "../registerTypes/RegisterTypes.h" +#include "../registerTypes/RegisterTypesClientPacks.h" #include "../rmg/CMapGenerator.h" #include "../serializer/CMemorySerializer.h" #include "../serializer/CTypeList.h" @@ -75,38 +77,10 @@ public: } }; -static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & pos, const PlayerColor & owner) -{ - CGObjectInstance * nobj; - switch(id) - { - case Obj::HERO: - { - auto handler = VLC->objtypeh->getHandlerFor(id, VLC->heroh->objects[subid]->heroClass->getIndex()); - nobj = handler->create(handler->getTemplates().front()); - break; - } - case Obj::TOWN: - nobj = new CGTownInstance(); - break; - default: //rest of objects - nobj = new CGObjectInstance(); - break; - } - nobj->ID = id; - nobj->subID = subid; - nobj->pos = pos; - nobj->tempOwner = owner; - if (id != Obj::HERO) - nobj->appearance = VLC->objtypeh->getHandlerFor(id, subid)->getTemplates().front(); - - return nobj; -} - HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); - if(ps.hero >= 0 && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero + if(ps.hero >= HeroTypeID(0) && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero { return HeroTypeID(ps.hero); } @@ -135,7 +109,7 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return *RandomGeneratorUtil::nextItem(factionHeroes, getRandomGenerator()); } - logGlobal->warn("Cannot find free hero of appropriate faction for player %s - trying to get first available...", owner.getStr()); + logGlobal->warn("Cannot find free hero of appropriate faction for player %s - trying to get first available...", owner.toString()); if(!otherHeroes.empty()) { return *RandomGeneratorUtil::nextItem(otherHeroes, getRandomGenerator()); @@ -147,228 +121,10 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return *notAllowedHeroesButStillBetterThanCrash.begin(); logGlobal->error("No free heroes at all!"); - assert(0); //current code can't handle this situation - return HeroTypeID::NONE; // no available heroes at all + throw std::runtime_error("Can not allocate hero. All heroes are already used."); } -std::pair CGameState::pickObject (CGObjectInstance *obj) -{ - switch(obj->ID) - { - case Obj::RANDOM_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC)); - case Obj::RANDOM_TREASURE_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE)); - case Obj::RANDOM_MINOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MINOR)); - case Obj::RANDOM_MAJOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MAJOR)); - case Obj::RANDOM_RELIC_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_RELIC)); - case Obj::RANDOM_HERO: - return std::make_pair(Obj::HERO, pickNextHeroType(obj->tempOwner)); - case Obj::RANDOM_MONSTER: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator())); - case Obj::RANDOM_MONSTER_L1: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 1)); - case Obj::RANDOM_MONSTER_L2: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 2)); - case Obj::RANDOM_MONSTER_L3: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 3)); - case Obj::RANDOM_MONSTER_L4: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 4)); - case Obj::RANDOM_RESOURCE: - return std::make_pair(Obj::RESOURCE,getRandomGenerator().nextInt(6)); //now it's OH3 style, use %8 for mithril - case Obj::RANDOM_TOWN: - { - PlayerColor align = (dynamic_cast(obj))->alignmentToPlayer; - si32 f; // can be negative (for random) - if(align >= PlayerColor::PLAYER_LIMIT) //same as owner / random - { - if(obj->tempOwner >= PlayerColor::PLAYER_LIMIT) - f = -1; //random - else - f = scenarioOps->getIthPlayersSettings(obj->tempOwner).castle; - } - else - { - f = scenarioOps->getIthPlayersSettings(align).castle; - } - if(f<0) - { - do - { - f = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - } - while ((*VLC->townh)[f]->town == nullptr); // find playable faction - } - return std::make_pair(Obj::TOWN,f); - } - case Obj::RANDOM_MONSTER_L5: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 5)); - case Obj::RANDOM_MONSTER_L6: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 6)); - case Obj::RANDOM_MONSTER_L7: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 7)); - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - { - auto * dwl = dynamic_cast(obj); - int faction; - - //if castle alignment available - if(auto * info = dynamic_cast(dwl->info)) - { - faction = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - if(info->asCastle && !info->instanceId.empty()) - { - auto iter = map->instanceNames.find(info->instanceId); - - if(iter == map->instanceNames.end()) - logGlobal->error("Map object not found: %s", info->instanceId); - else - { - auto elem = iter->second; - if(elem->ID==Obj::RANDOM_TOWN) - { - randomizeObject(elem.get()); //we have to randomize the castle first - faction = elem->subID; - } - else if(elem->ID==Obj::TOWN) - faction = elem->subID; - else - logGlobal->error("Map object must be town: %s", info->instanceId); - } - } - else if(info->asCastle) - { - - for(auto & elem : map->objects) - { - if(!elem) - continue; - - if(elem->ID==Obj::RANDOM_TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - randomizeObject(elem); //we have to randomize the castle first - faction = elem->subID; - break; - } - else if(elem->ID==Obj::TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - faction = elem->subID; - break; - } - } - } - else - { - std::set temp; - - for(int i = 0; i < info->allowedFactions.size(); i++) - if(info->allowedFactions[i]) - temp.insert(i); - - if(temp.empty()) - logGlobal->error("Random faction selection failed. Nothing is allowed. Fall back to random."); - else - faction = *RandomGeneratorUtil::nextItem(temp, getRandomGenerator()); - } - } - else // castle alignment fixed - faction = obj->subID; - - int level; - - //if level set to range - if(auto * info = dynamic_cast(dwl->info)) - { - level = getRandomGenerator().nextInt(info->minLevel, info->maxLevel) - 1; - } - else // fixed level - { - level = obj->subID; - } - - delete dwl->info; - dwl->info = nullptr; - - std::pair result(Obj::NO_OBJ, -1); - CreatureID cid; - if((*VLC->townh)[faction]->town) - cid = (*VLC->townh)[faction]->town->creatures[level][0]; - else - { - //neutral faction - std::vector possibleCreatures; - std::copy_if(VLC->creh->objects.begin(), VLC->creh->objects.end(), std::back_inserter(possibleCreatures), [faction](const CCreature * c) - { - return c->getFaction() == faction; - }); - assert(!possibleCreatures.empty()); - cid = (*RandomGeneratorUtil::nextItem(possibleCreatures, getRandomGenerator()))->getId(); - } - - //NOTE: this will pick last dwelling with this creature (Mantis #900) - //check for block map equality is better but more complex solution - auto testID = [&](const Obj & primaryID) -> void - { - auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); - for (si32 entry : dwellingIDs) - { - const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); - - if (handler->producesCreature(VLC->creh->objects[cid])) - result = std::make_pair(primaryID, entry); - } - }; - - testID(Obj::CREATURE_GENERATOR1); - if (result.first == Obj::NO_OBJ) - testID(Obj::CREATURE_GENERATOR4); - - if (result.first == Obj::NO_OBJ) - { - logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); - result = std::make_pair(Obj::CREATURE_GENERATOR1, *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), getRandomGenerator())); - } - - return result; - } - } - return std::make_pair(Obj::NO_OBJ,-1); -} - -void CGameState::randomizeObject(CGObjectInstance *cur) -{ - std::pair ran = pickObject(cur); - if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything - { - if(cur->ID==Obj::TOWN || cur->ID==Obj::MONSTER) - cur->setType(cur->ID, cur->subID); // update def, if necessary - } - else if(ran.first==Obj::HERO)//special code for hero - { - auto * h = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->heroesOnMap.emplace_back(h); - } - else if(ran.first==Obj::TOWN)//special code for town - { - auto * t = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->towns.emplace_back(t); - } - else - { - cur->setType(ran.first, ran.second); - } -} - -int CGameState::getDate(Date::EDateType mode) const +int CGameState::getDate(Date mode) const { int temp; switch (mode) @@ -400,15 +156,17 @@ CGameState::CGameState() gs = this; heroesPool = std::make_unique(); applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); + registerTypesClientPacks(*applier); globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS); } CGameState::~CGameState() { - curB.dellNull(); + // explicitly delete all ongoing battles first - BattleInfo destructor requires valid CGameState + currentBattles.clear(); map.dellNull(); + scenarioOps.dellNull(); + initialOpts.dellNull(); } void CGameState::preInit(Services * services) @@ -416,7 +174,7 @@ void CGameState::preInit(Services * services) this->services = services; } -void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap) +void CGameState::init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator & progressTracking, bool allowSavingRandomMap) { preInitAuto(); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); @@ -428,7 +186,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow switch(scenarioOps->mode) { case StartInfo::NEW_GAME: - initNewGame(mapService, allowSavingRandomMap); + initNewGame(mapService, allowSavingRandomMap, progressTracking); break; case StartInfo::CAMPAIGN: initCampaign(); @@ -455,7 +213,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow initRandomFactionsForPlayers(); randomizeMapObjects(); placeStartingHeroes(); - initStartingResources(); + initDifficulty(); initHeroes(); initStartingBonus(); initTowns(); @@ -465,11 +223,6 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow initVisitingAndGarrisonedHeroes(); initFogOfWar(); - // Explicitly initialize static variables - for(auto & elem : players) - { - CGKeys::playerKeyMap[elem.first] = std::set(); - } for(auto & elem : teams) { CGObelisk::visited[elem.first] = 0; @@ -547,7 +300,7 @@ void CGameState::preInitAuto() } } -void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap) +void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking) { if(scenarioOps->createRandomMap()) { @@ -556,24 +309,29 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan // Gen map CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, scenarioOps->seedToBeUsed); + progressTracking.include(mapGenerator); std::unique_ptr randomMap = mapGenerator.generate(); + progressTracking.exclude(mapGenerator); if(allowSavingRandomMap) { try { - auto path = VCMIDirs::get().userCachePath() / "RandomMaps"; + auto path = VCMIDirs::get().userDataPath() / "Maps" / "RandomMaps"; boost::filesystem::create_directories(path); 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_%d.vmap") % templateName % seed ); + const std::string fileName = boost::str(boost::format("%s_%s_%d.vmap") % dt % templateName % seed ); const auto fullPath = path / fileName; + randomMap->name.appendRawString(boost::str(boost::format(" %s") % dt)); + mapService->saveMap(randomMap, fullPath); logGlobal->info("Random map has been saved to:"); @@ -612,7 +370,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan else { logGlobal->info("Open map file: %s", scenarioOps->mapname); - const ResourceID mapURI(scenarioOps->mapname, EResType::MAP); + const ResourcePath mapURI(scenarioOps->mapname, EResType::MAP); map = mapService->loadMap(mapURI).release(); } } @@ -649,12 +407,47 @@ void CGameState::initGlobalBonuses() { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::GLOBAL;//for all - bonus->sid = -1; //there is one global object + bonus->sid = BonusSourceID(); //there is one global object globalEffects.addNewBonus(bonus); } VLC->creh->loadCrExpBon(globalEffects); } +void CGameState::initDifficulty() +{ + logGlobal->debug("\tLoading difficulty settings"); + const JsonNode config = JsonUtils::assembleFromFiles("config/difficulty.json"); + + 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) + { + //set starting resources + state.resources = TResources(json["resources"]); + + //set global bonuses + for(auto & jsonBonus : json["globalBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.addNewBonus(bonus); + + //set battle bonuses + for(auto & jsonBonus : json["battleBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.battleBonuses.push_back(*bonus); + + }; + + for (auto & elem : players) + { + PlayerState &p = elem.second; + setDifficulty(p, p.human ? difficultyHuman : difficultyAI); + } + + if (campaign) + campaign->initStartingResources(); +} + void CGameState::initGrailPosition() { logGlobal->debug("\tPicking grail position"); @@ -707,7 +500,7 @@ void CGameState::initRandomFactionsForPlayers() logGlobal->debug("\tPicking random factions for players"); for(auto & elem : scenarioOps->playerInfos) { - if(elem.second.castle==-1) + if(elem.second.castle==FactionID::RANDOM) { auto randomID = getRandomGenerator().nextInt((int)map->players[elem.first.getNum()].allowedFactions.size() - 1); auto iter = map->players[elem.first.getNum()].allowedFactions.begin(); @@ -721,20 +514,21 @@ void CGameState::initRandomFactionsForPlayers() void CGameState::randomizeMapObjects() { logGlobal->debug("\tRandomizing objects"); - for(CGObjectInstance *obj : map->objects) + for(CGObjectInstance *object : map->objects) { - if(!obj) continue; + if(!object) + continue; - randomizeObject(obj); + object->pickRandomObject(getRandomGenerator()); //handle Favouring Winds - mark tiles under it - if(obj->ID == Obj::FAVORABLE_WINDS) + if(object->ID == Obj::FAVORABLE_WINDS) { - for (int i = 0; i < obj->getWidth() ; i++) + for (int i = 0; i < object->getWidth() ; i++) { - for (int j = 0; j < obj->getHeight() ; j++) + for (int j = 0; j < object->getHeight() ; j++) { - int3 pos = obj->pos - int3(i,j,0); + int3 pos = object->pos - int3(i,j,0); if(map->isInTheMap(pos)) map->getTile(pos).extTileFlags |= 128; } } @@ -767,7 +561,15 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } } - CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId.getNum(), townPos, playerColor); + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, heroTypeId.toHeroType()->heroClass->getIndex()); + CGObjectInstance * obj = handler->create(handler->getTemplates().front()); + CGHeroInstance * hero = dynamic_cast(obj); + + hero->ID = Obj::HERO; + hero->setHeroType(heroTypeId); + hero->tempOwner = playerColor; + + hero->pos = townPos; hero->pos += hero->getVisitableOffset(); map->getEditManager()->insertObject(hero); } @@ -786,8 +588,8 @@ void CGameState::placeStartingHeroes() if (campaign && campaign->playerHasStartingHero(playerColor)) continue; - int heroTypeId = pickNextHeroType(playerColor); - if(playerSettingPair.second.hero == -1) + HeroTypeID heroTypeId = pickNextHeroType(playerColor); + if(playerSettingPair.second.hero == HeroTypeID::NONE) playerSettingPair.second.hero = heroTypeId; placeStartingHero(playerColor, HeroTypeID(heroTypeId), playerInfo.posOfMainTown); @@ -811,30 +613,6 @@ void CGameState::removeHeroPlaceholders() } } -void CGameState::initStartingResources() -{ - logGlobal->debug("\tSetting up resources"); - const JsonNode config(ResourceID("config/startres.json")); - const JsonVector &vector = config["difficulty"].Vector(); - const JsonNode &level = vector[scenarioOps->difficulty]; - - TResources startresAI(level["ai"]); - TResources startresHuman(level["human"]); - - for (auto & elem : players) - { - PlayerState &p = elem.second; - - if (p.human) - p.resources = startresHuman; - else - p.resources = startresAI; - } - - if (campaign) - campaign->initStartingResources(); -} - void CGameState::initHeroes() { for(auto hero : map->heroesOnMap) //heroes instances initialization @@ -847,7 +625,7 @@ void CGameState::initHeroes() hero->initHero(getRandomGenerator()); getPlayerState(hero->getOwner())->heroes.push_back(hero); - map->allHeroes[hero->type->getIndex()] = hero; + map->allHeroes[hero->getHeroType().getNum()] = hero; } // generate boats for all heroes on water @@ -861,8 +639,6 @@ void CGameState::initHeroes() CGBoat * boat = dynamic_cast(handler->create()); handler->configureObject(boat, gs->getRandomGenerator()); - boat->ID = Obj::BOAT; - boat->subID = hero->getBoatType().getNum(); boat->pos = hero->pos; boat->appearance = handler->getTemplates().front(); boat->id = ObjectInstanceID(static_cast(gs->map->objects.size())); @@ -877,19 +653,23 @@ void CGameState::initHeroes() for(auto obj : map->objects) //prisons { if(obj && obj->ID == Obj::PRISON) - map->allHeroes[obj->subID] = dynamic_cast(obj.get()); + { + auto * hero = dynamic_cast(obj.get()); + hero->initHero(getRandomGenerator()); + map->allHeroes[hero->getHeroType().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, HeroTypeID(ph->subID))) + if(!vstd::contains(heroesToCreate, ph->getHeroType())) continue; ph->initHero(getRandomGenerator()); heroesPool->addHeroToPool(ph); heroesToCreate.erase(ph->type->getId()); - map->allHeroes[ph->subID] = ph; + map->allHeroes[ph->getHeroType().getNum()] = ph; } for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool @@ -916,7 +696,7 @@ void CGameState::initFogOfWar() int layers = map->levels(); for(auto & elem : teams) { - auto fow = elem.second.fogOfWarMap; + auto & fow = elem.second.fogOfWarMap; fow->resize(boost::extents[layers][map->width][map->height]); std::fill(fow->data(), fow->data() + fow->num_elements(), 0); @@ -925,7 +705,7 @@ void CGameState::initFogOfWar() if(!obj || !vstd::contains(elem.second.players, obj->tempOwner)) continue; //not a flagged object std::unordered_set tiles; - getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), obj->tempOwner, 1); + getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::HIDDEN, obj->tempOwner); for(const int3 & tile : tiles) { (*elem.second.fogOfWarMap)[tile.z][tile.x][tile.y] = 1; @@ -945,14 +725,15 @@ void CGameState::initStartingBonus() for(auto & elem : players) { //starting bonus - if(scenarioOps->playerInfos[elem.first].bonus==PlayerSettings::RANDOM) - scenarioOps->playerInfos[elem.first].bonus = static_cast(getRandomGenerator().nextInt(2)); + if(scenarioOps->playerInfos[elem.first].bonus == PlayerStartingBonus::RANDOM) + scenarioOps->playerInfos[elem.first].bonus = static_cast(getRandomGenerator().nextInt(2)); + switch(scenarioOps->playerInfos[elem.first].bonus) { - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: elem.second.resources[EGameResID::GOLD] += getRandomGenerator().nextInt(5, 10) * 100; break; - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { auto res = (*VLC->townh)[scenarioOps->playerInfos[elem.first].castle]->town->primaryRes; if(res == EGameResID::WOOD_AND_ORE) @@ -967,14 +748,14 @@ void CGameState::initStartingBonus() } break; } - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: { if(elem.second.heroes.empty()) { logGlobal->error("Cannot give starting artifact - no heroes!"); break; } - const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toArtifact(VLC->artifacts()); + const Artifact * toGive = pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); CGHeroInstance *hero = elem.second.heroes[0]; if(!giveHeroArtifact(hero, toGive->getId())) @@ -993,20 +774,20 @@ void CGameState::initTowns() campaign->initTowns(); CGTownInstance::universitySkills.clear(); - for ( int i=0; i<4; i++) - CGTownInstance::universitySkills.push_back(14+i);//skills for university + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::FIRE_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::AIR_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::WATER_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::EARTH_MAGIC)); for (auto & elem : map->towns) { CGTownInstance * vti =(elem); - if(!vti->town) - { - vti->town = (*VLC->townh)[vti->subID]->town; - } - if(vti->getNameTranslated().empty()) + assert(vti->town); + + if(vti->getNameTextID().empty()) { size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1); - vti->setNameTranslated(vti->getTown()->getRandomNameTranslated(nameID)); + vti->setNameTextId(vti->getTown()->getRandomNameTextID(nameID)); } static const BuildingID 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 }; @@ -1092,7 +873,7 @@ void CGameState::initTowns() for(ui32 z=0; zobligatorySpells.size();z++) { const auto * s = vti->obligatorySpells[z].toSpell(); - vti->spells[s->level-1].push_back(s->id); + vti->spells[s->getLevel()-1].push_back(s->id); vti->possibleSpells -= s->id; } while(!vti->possibleSpells.empty()) @@ -1120,7 +901,7 @@ void CGameState::initTowns() sel=0; const auto * s = vti->possibleSpells[sel].toSpell(); - vti->spells[s->level-1].push_back(s->id); + vti->spells[s->getLevel()-1].push_back(s->id); vti->possibleSpells -= s->id; } vti->possibleSpells.clear(); @@ -1137,17 +918,15 @@ void CGameState::initMapObjects() for(CGObjectInstance *obj : map->objects) { if(obj) - { - logGlobal->trace("Calling Init for object %d, %s, %s", obj->id.getNum(), obj->typeName, obj->subTypeName); obj->initObj(getRandomGenerator()); - } } + logGlobal->debug("\tObject initialization done"); for(CGObjectInstance *obj : map->objects) { if(!obj) continue; - switch (obj->ID) + switch(obj->ID.toEnum()) { case Obj::QUEST_GUARD: case Obj::SEER_HUT: @@ -1228,11 +1007,41 @@ void CGameState::initVisitingAndGarrisonedHeroes() } } +const BattleInfo * CGameState::getBattle(const PlayerColor & player) const +{ + if (!player.isValidPlayer()) + return nullptr; + + for (const auto & battlePtr : currentBattles) + if (battlePtr->sides[0].color == player || battlePtr->sides[1].color == player) + return battlePtr.get(); + + return nullptr; +} + +const BattleInfo * CGameState::getBattle(const BattleID & battle) const +{ + for (const auto & battlePtr : currentBattles) + if (battlePtr->battleID == battle) + return battlePtr.get(); + + return nullptr; +} + +BattleInfo * CGameState::getBattle(const BattleID & battle) +{ + for (const auto & battlePtr : currentBattles) + if (battlePtr->battleID == battle) + return battlePtr.get(); + + return nullptr; +} + BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & rand) { - if(!tile.valid() && curB) - tile = curB->tile; - else if(!tile.valid() && !curB) + assert(tile.valid()); + + if(!tile.valid()) return BattleField::NONE; const TerrainTile &t = map->getTile(tile); @@ -1256,7 +1065,7 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r } if(map->isCoastalTile(tile)) //coastal tile is always ground - return BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield.sand_shore")); + return BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.sand_shore")); return BattleField(*RandomGeneratorUtil::nextItem(t.terType->battleFields, rand)); } @@ -1307,7 +1116,7 @@ UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance &stack) const return ret; } -PlayerRelations::PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const +PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const { if ( color1 == color2 ) return PlayerRelations::SAME_PLAYER; @@ -1322,7 +1131,7 @@ PlayerRelations::PlayerRelations CGameState::getPlayerRelations( PlayerColor col void CGameState::apply(CPack *pack) { - ui16 typ = typeList.getTypeID(pack); + ui16 typ = CTypeList::getInstance().getTypeID(pack); applier->getApplier(typ)->applyOnGS(this, pack); } @@ -1569,7 +1378,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) - if(elem->hasArt(ArtifactID(condition.objectType))) + if(elem->hasArt(condition.objectType.as())) return true; return false; } @@ -1585,7 +1394,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio && (ai = dynamic_cast(object.get()))) //contains army { for(const auto & elem : ai->Slots()) //iterate through army - if(elem.second->type->getId() == condition.objectType) //it's searched creature + if(elem.second->getId() == condition.objectType.as()) //it's searched creature total += elem.second->count; } } @@ -1593,20 +1402,20 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::HAVE_RESOURCES: { - return p->resources[condition.objectType] >= condition.value; + return p->resources[condition.objectType.as()] >= condition.value; } case EventCondition::HAVE_BUILDING: { - if (condition.object) // specific town + if (condition.objectID != ObjectInstanceID::NONE) // specific town { - const auto * t = dynamic_cast(condition.object); - return (t->tempOwner == player && t->hasBuilt(BuildingID(condition.objectType))); + const auto * t = getTown(condition.objectID); + return (t->tempOwner == player && t->hasBuilt(condition.objectType.as())); } else // any town { for (const CGTownInstance * t : p->towns) { - if (t->hasBuilt(BuildingID(condition.objectType))) + if (t->hasBuilt(condition.objectType.as())) return true; } return false; @@ -1614,18 +1423,18 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::DESTROY: { - if (condition.object) // mode A - destroy specific object of this type + if (condition.objectID != ObjectInstanceID::NONE) // mode A - destroy specific object of this type { - if(const auto * hero = dynamic_cast(condition.object)) + if(const auto * hero = getHero(condition.objectID)) return boost::range::find(gs->map->heroesOnMap, hero) == gs->map->heroesOnMap.end(); else - return getObj(condition.object->id) == nullptr; + return getObj(condition.objectID) == nullptr; } else { for(const auto & elem : map->objects) // mode B - destroy all objects of this type { - if(elem && elem->ID == condition.objectType) + if(elem && elem->ID == condition.objectType.as()) return false; } return true; @@ -1637,16 +1446,16 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio // NOTE: cgameinfocallback specified explicitly in order to get const version const auto & team = CGameInfoCallback::getPlayerTeam(player)->players; - if (condition.object) // mode A - flag one specific object, like town + if (condition.objectID != ObjectInstanceID::NONE) // mode A - flag one specific object, like town { - return team.count(condition.object->tempOwner) != 0; + return team.count(getObjInstance(condition.objectID)->tempOwner) != 0; } else { for(const auto & elem : map->objects) // mode B - flag all objects of this type { //check not flagged objs - if ( elem && elem->ID == condition.objectType && team.count(elem->tempOwner) == 0 ) + if ( elem && elem->ID == condition.objectType.as() && team.count(elem->tempOwner) == 0 ) return false; } return true; @@ -1654,9 +1463,9 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::TRANSPORT: { - const auto * t = dynamic_cast(condition.object); - return (t->visitingHero && t->visitingHero->hasArt(ArtifactID(condition.objectType))) || - (t->garrisonHero && t->garrisonHero->hasArt(ArtifactID(condition.objectType))); + const auto * t = getTown(condition.objectID); + return (t->visitingHero && t->visitingHero->hasArt(condition.objectType.as())) || + (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType.as())); } case EventCondition::DAYS_PASSED: { @@ -1677,24 +1486,6 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { return condition.value; // just convert to bool } - case EventCondition::HAVE_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::HAVE_BUILDING_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::DESTROY_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } default: logGlobal->error("Invalid event condition type: %d", (int)condition.condition); return false; @@ -1709,7 +1500,7 @@ PlayerColor CGameState::checkForStandardWin() const TeamID winnerTeam = TeamID::NO_TEAM; for(const auto & elem : players) { - if(elem.second.status == EPlayerStatus::INGAME && elem.first < PlayerColor::PLAYER_LIMIT) + if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer()) { if(supposedWinner == PlayerColor::NEUTRAL) { @@ -1823,7 +1614,7 @@ struct statsHLP //Heroes can produce gold as well - skill, specialty or arts for(const auto & h : ps->heroes) { - totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, GameResID(EGameResID::GOLD))); + totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))); if(!heroOrTown) heroOrTown = h; @@ -1978,13 +1769,13 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { if(playerInactive(player.second.color)) //do nothing for neutral player continue; - int bestCre = -1; //best creature's ID + CreatureID bestCre; //best creature's ID for(const auto & elem : player.second.heroes) { for(const auto & it : elem->Slots()) { - int toCmp = it.second->type->getId(); //ID of creature we should compare with the best one - if(bestCre == -1 || VLC->creh->objects[bestCre]->getAIValue() < VLC->creh->objects[toCmp]->getAIValue()) + CreatureID toCmp = it.second->type->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; } @@ -2006,12 +1797,6 @@ void CGameState::buildBonusSystemTree() { t->deserializationFix(); } - // CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact - // are provided on initializing / deserializing - - // NOTE: calling deserializationFix() might be more correct option, but might lead to side effects - for (auto hero : map->heroesOnMap) - hero->boatDeserializationFix(); } void CGameState::deserializationFix() @@ -2049,13 +1834,12 @@ void CGameState::attachArmedObjects() bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) { - CArtifact * const artifact = VLC->arth->objects[aid]; //pointer to constant object - CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(artifact); + CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(aid); map->addNewArtifactInstance(ai); auto slot = ArtifactUtils::getArtAnyPosition(h, aid); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) { - ai->putAt(ArtifactLocation(h, slot)); + ai->putAt(*h, slot); return true; } else @@ -2066,14 +1850,11 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllowed) const { - std::set ret; - for(int i = 0; i < map->allowedHeroes.size(); i++) - if(map->allowedHeroes[i] || alsoIncludeNotAllowed) - ret.insert(HeroTypeID(i)); + std::set ret = map->allowedHeroes; for(const auto & playerSettingPair : scenarioOps->playerInfos) //remove uninitialized yet heroes picked for start by other players { - if(playerSettingPair.second.hero != PlayerSettings::RANDOM) + if(playerSettingPair.second.hero != HeroTypeID::RANDOM) ret -= HeroTypeID(playerSettingPair.second.hero); } @@ -2082,12 +1863,15 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow if(hero->type) ret -= hero->type->getId(); else - ret -= HeroTypeID(hero->subID); + ret -= hero->getHeroType(); } for(auto obj : map->objects) //prisons - if(obj && obj->ID == Obj::PRISON) - ret -= HeroTypeID(obj->subID); + { + auto * hero = dynamic_cast(obj.get()); + if(hero && hero->ID == Obj::PRISON) + ret -= hero->getHeroType(); + } return ret; } @@ -2099,23 +1883,19 @@ bool CGameState::isUsedHero(const HeroTypeID & hid) const CGHeroInstance * CGameState::getUsedHero(const HeroTypeID & hid) const { - for(auto hero : map->heroesOnMap) //heroes instances initialization - { - if(hero->type && hero->type->getId() == hid) - { - return hero; - } - } - for(auto obj : map->objects) //prisons { - if(obj && obj->ID == Obj::PRISON ) - { - auto * hero = dynamic_cast(obj.get()); - assert(hero); - if ( hero->type && hero->type->getId() == hid ) - return hero; - } + if (!obj) + continue; + + if ( obj->ID !=Obj::PRISON && obj->ID != Obj::HERO) + continue; + + auto * hero = dynamic_cast(obj.get()); + assert(hero); + + if (hero->getHeroType() == hid) + return hero; } return nullptr; @@ -2142,7 +1922,7 @@ bool RumorState::update(int id, int extra) TeamState::TeamState() { setNodeType(TEAM); - fogOfWarMap = std::make_shared>(); + fogOfWarMap = std::make_unique>(); } TeamState::TeamState(TeamState && other) noexcept: @@ -2158,4 +1938,73 @@ CRandomGenerator & CGameState::getRandomGenerator() return rand; } +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) +{ + std::set potentialPicks; + + // Select artifacts that satisfy provided criterias + for (auto const * artifact : VLC->arth->allowedArtifacts) + { + assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized + + if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) + continue; + + if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) + continue; + + if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) + continue; + + if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) + continue; + + if (!accepts(artifact->getId())) + continue; + + potentialPicks.insert(artifact->getId()); + } + + return pickRandomArtifact(rand, potentialPicks); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +{ + // No allowed artifacts at all - give Grail - this can't be banned (hopefully) + // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior + if (potentialPicks.empty()) + { + logGlobal->warn("Failed to find artifact that matches requested parameters!"); + return ArtifactID::GRAIL; + } + + // Find how many times least used artifacts were picked by randomizer + int leastUsedTimes = std::numeric_limits::max(); + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] < leastUsedTimes) + leastUsedTimes = allocatedArtifacts[artifact]; + + // Pick all artifacts that were used least number of times + std::set preferredPicks; + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] == leastUsedTimes) + preferredPicks.insert(artifact); + + assert(!preferredPicks.empty()); + + ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); + allocatedArtifacts[artID] += 1; // record +1 more usage + return artID; +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) +{ + return pickRandomArtifact(rand, 0xff, std::move(accepts)); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags) +{ + return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 0281aa18e..b2b03a56e 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -1,220 +1,244 @@ -/* - * CGameState.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 "bonuses/CBonusSystemNode.h" -#include "IGameCallback.h" - -namespace boost -{ -class shared_mutex; -} - -VCMI_LIB_NAMESPACE_BEGIN - -class EVictoryLossCheckResult; -class Services; -class IMapService; -class CMap; -struct CPack; -class CHeroClass; -struct EventCondition; -struct CampaignTravel; -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, const int version) - { - h & type; - h & last; - } -}; - -struct UpgradeInfo -{ - CreatureID oldID; //creature to be upgraded - std::vector newID; //possible upgrades - std::vector cost; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) - UpgradeInfo(){oldID = CreatureID::NONE;}; -}; - -class BattleInfo; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult); - -class DLL_LINKAGE CGameState : public CNonConstInfoCallback -{ - friend class CGameStateCampaign; - -public: - //we have here all heroes available on this map that are not hired - std::unique_ptr heroesPool; - - CGameState(); - virtual ~CGameState(); - - void preInit(Services * services); - - void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false); - void updateOnLoad(StartInfo * si); - - ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) - PlayerColor currentPlayer; //ID of player currently having turn - ConstTransitivePtr curB; //current battle - ui32 day; //total number of days in game - ConstTransitivePtr map; - std::map players; - std::map teams; - CBonusSystemNode globalEffects; - RumorState rumor; - - static boost::shared_mutex mutex; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; - - bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); - - void apply(CPack *pack); - BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); - - void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; - PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; - bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile - void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists - void calculatePaths(const std::shared_ptr & config) override; - int3 guardingCreaturePosition (int3 pos) const override; - std::vector guardingCreatures (int3 pos) const; - void updateRumor(); - - // ----- victory, loss condition checks ----- - - EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const; - bool checkForVictory(const PlayerColor & player, const EventCondition & condition) const; //checks if given player is winner - PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner - 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 - - bool isVisible(int3 pos, const std::optional & player) const override; - bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; - - int getDate(Date::EDateType 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 ----- - - /// 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 - /// - /// CGameHandler have it's own getter for CRandomGenerator::getDefault - /// Any server-side code outside of GH must use CRandomGenerator::getDefault - CRandomGenerator & getRandomGenerator(); - - template void serialize(Handler &h, const int version) - { - h & scenarioOps; - h & initialOpts; - h & currentPlayer; - h & day; - h & map; - h & players; - h & teams; - h & heroesPool; - h & globalEffects; - h & rand; - h & rumor; - h & campaign; - - BONUS_TREE_DESERIALIZATION_FIX - } - -private: - // ----- initialization ----- - void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap); - void checkMapChecksum(); - void initGlobalBonuses(); - void initGrailPosition(); - void initRandomFactionsForPlayers(); - void randomizeMapObjects(); - void randomizeObject(CGObjectInstance *cur); - void initPlayerStates(); - void placeStartingHeroes(); - void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); - void removeHeroPlaceholders(); - void initStartingResources(); - void initHeroes(); - void placeHeroesInTowns(); - void initFogOfWar(); - void initStartingBonus(); - void initTowns(); - void initMapObjects(); - void initVisitingAndGarrisonedHeroes(); - void initCampaign(); - - // ----- bonus system handling ----- - - void buildBonusSystemTree(); - void attachArmedObjects(); - void buildGlobalTeamPlayerTree(); - void deserializationFix(); - - // ---- misc helpers ----- - - CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; - bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons - std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; - std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns - HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly - HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly - UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; - - // ---- data ----- - std::shared_ptr> applier; - CRandomGenerator rand; - Services * services; - - /// Ponter to campaign state manager. Nullptr for single scenarios - std::unique_ptr campaign; - - friend class IGameCallback; - friend class CMapHandler; - friend class CGameHandler; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CGameState.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 "bonuses/CBonusSystemNode.h" +#include "IGameCallback.h" +#include "LoadProgress.h" +#include "ConstTransitivePtr.h" + +namespace boost +{ +class shared_mutex; +} + +VCMI_LIB_NAMESPACE_BEGIN + +class EVictoryLossCheckResult; +class Services; +class IMapService; +class CMap; +struct CPack; +class CHeroClass; +struct EventCondition; +struct CampaignTravel; +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, const int version) + { + h & type; + h & last; + } +}; + +struct UpgradeInfo +{ + CreatureID oldID; //creature to be upgraded + std::vector newID; //possible upgrades + std::vector cost; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) + UpgradeInfo(){oldID = CreatureID::NONE;}; +}; + +class BattleInfo; + +DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult); + +class DLL_LINKAGE CGameState : public CNonConstInfoCallback +{ + friend class CGameStateCampaign; + +public: + /// Stores number of times each artifact was placed on map via randomization + std::map allocatedArtifacts; + + /// List of currently ongoing battles + std::vector> currentBattles; + /// ID that can be allocated to next battle + BattleID nextBattleID = BattleID(0); + + //we have here all heroes available on this map that are not hired + std::unique_ptr heroesPool; + + /// list of players currently making turn. Usually - just one, except for simturns + std::set actingPlayers; + + CGameState(); + virtual ~CGameState(); + + void preInit(Services * services); + + void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = true); + void updateOnLoad(StartInfo * si); + + ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) + ui32 day; //total number of days in game + ConstTransitivePtr map; + std::map players; + std::map teams; + CBonusSystemNode globalEffects; + RumorState rumor; + + static boost::shared_mutex mutex; + + void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + + bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); + /// 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 fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; + PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; + bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile + void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists + 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); + + /// Returns battle in which selected player is engaged, or nullptr if none. + /// Can NOT be used with neutral player, use battle by ID instead + const BattleInfo * getBattle(const PlayerColor & player) const; + /// Returns battle by its unique identifier, or nullptr if not found + const BattleInfo * getBattle(const BattleID & battle) const; + BattleInfo * getBattle(const BattleID & battle); + + // ----- victory, loss condition checks ----- + + EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const; + bool checkForVictory(const PlayerColor & player, const EventCondition & condition) const; //checks if given player is winner + PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner + 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 + + bool isVisible(int3 pos, const std::optional & player) const override; + bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; + + 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 ----- + + /// 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 + /// + /// CGameHandler have it's own getter for CRandomGenerator::getDefault + /// Any server-side code outside of GH must use CRandomGenerator::getDefault + CRandomGenerator & getRandomGenerator(); + + template void serialize(Handler &h, const int version) + { + h & scenarioOps; + h & initialOpts; + h & actingPlayers; + h & day; + h & map; + h & players; + h & teams; + h & heroesPool; + h & globalEffects; + h & rand; + h & rumor; + h & campaign; + h & allocatedArtifacts; + + BONUS_TREE_DESERIALIZATION_FIX + } + +private: + // ----- initialization ----- + void preInitAuto(); + void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); + void checkMapChecksum(); + void initGlobalBonuses(); + void initGrailPosition(); + void initRandomFactionsForPlayers(); + void randomizeMapObjects(); + void initPlayerStates(); + void placeStartingHeroes(); + void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); + void removeHeroPlaceholders(); + void initDifficulty(); + void initHeroes(); + void placeHeroesInTowns(); + void initFogOfWar(); + void initStartingBonus(); + void initTowns(); + void initMapObjects(); + void initVisitingAndGarrisonedHeroes(); + void initCampaign(); + + // ----- bonus system handling ----- + + void buildBonusSystemTree(); + void attachArmedObjects(); + void buildGlobalTeamPlayerTree(); + void deserializationFix(); + + // ---- misc helpers ----- + + CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; + bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons + std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; + HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly + UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; + + // ---- data ----- + std::shared_ptr> applier; + CRandomGenerator rand; + Services * services; + + /// Ponter to campaign state manager. Nullptr for single scenarios + std::unique_ptr campaign; + + friend class IGameCallback; + friend class CMapHandler; + friend class CGameHandler; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 0d8729a94..b78828b57 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -16,7 +16,7 @@ #include "../campaign/CampaignState.h" #include "../mapping/CMapEditManager.h" #include "../mapObjects/CGHeroInstance.h" -#include "../registerTypes/RegisterTypes.h" +#include "../networkPacks/ArtifactLocation.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../StartInfo.h" @@ -84,13 +84,13 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorgetBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g]; + cgh->getBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g.getNum()]; } } } @@ -118,7 +118,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartType->getId()); - ArtifactLocation al(hero, artifactPosition); - if(!takeable && !al.getSlot()->locked) //don't try removing locked artifacts -> it crashes #1719 - al.removeArtifact(); + ArtifactLocation al(hero->id, artifactPosition); + if(!takeable && !hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719 + hero->getArt(al.slot)->removeFrom(*hero, al.slot); }; // process on copy - removal of artifact will invalidate container auto artifactsWorn = hero->artifactsWorn; - for (auto const & art : artifactsWorn) + for(const auto & art : artifactsWorn) checkAndRemoveArtifact(art.first); // process in reverse - removal of artifact will shift all artifacts after this one for(int slotNumber = hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - checkAndRemoveArtifact(ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); + checkAndRemoveArtifact(ArtifactPosition::BACKPACK_START + slotNumber); } } @@ -155,7 +155,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector & j) -> bool { - CreatureID::ECreatureID crid = j.second->getCreatureID().toEnum(); + CreatureID crid = j.second->getCreatureID(); return !travelOptions.monstersKeptByHero.count(crid); }; @@ -189,8 +189,8 @@ void CGameStateCampaign::placeCampaignHeroes() auto it = gameState->scenarioOps->playerInfos.find(playerColor); if(it != gameState->scenarioOps->playerInfos.end()) { - auto heroTypeId = campaignBonus->info2; - if(heroTypeId == 0xffff) // random bonus hero + HeroTypeID heroTypeId = HeroTypeID(campaignBonus->info2); + if(heroTypeId.getNum() == 0xffff) // random bonus hero { heroTypeId = gameState->pickUnusedHeroTypeRandomly(playerColor); } @@ -213,10 +213,14 @@ void CGameStateCampaign::placeCampaignHeroes() std::set heroesToRemove = campaignState->getReservedHeroes(); for(auto & campaignHeroReplacement : campaignHeroReplacements) - heroesToRemove.insert(HeroTypeID(campaignHeroReplacement.hero->subID)); + heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType()); for(auto & heroID : heroesToRemove) { + // Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear + if (campaignState->getHeroByType(heroID).isNull()) + continue; + auto * hero = gameState->getUsedHero(heroID); if(hero) { @@ -233,7 +237,7 @@ void CGameStateCampaign::placeCampaignHeroes() // now add removed heroes again with unused type ID for(auto * hero : removedHeroes) { - si32 heroTypeId = 0; + HeroTypeID heroTypeId; if(hero->ID == Obj::HERO) { heroTypeId = gameState->pickUnusedHeroTypeRandomly(hero->tempOwner); @@ -243,7 +247,7 @@ void CGameStateCampaign::placeCampaignHeroes() auto unusedHeroTypeIds = gameState->getUnusedAllowedHeroes(); if(!unusedHeroTypeIds.empty()) { - heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())).getNum(); + heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())); } else { @@ -256,8 +260,7 @@ void CGameStateCampaign::placeCampaignHeroes() assert(0); // should not happen } - hero->subID = heroTypeId; - hero->portrait = hero->subID; + hero->setHeroType(heroTypeId); gameState->map->getEditManager()->insertObject(hero); } } @@ -301,7 +304,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(ArtifactLocation(hero, slot)); + scroll->putAt(*hero, slot); else logGlobal->error("Cannot give starting scroll - no free slots!"); break; @@ -309,16 +312,14 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) case CampaignBonusType::PRIMARY_SKILL: { const ui8 * ptr = reinterpret_cast(&curBonus->info2); - for(int g = 0; g < GameConstants::PRIMARY_SKILLS; ++g) + for(auto g = PrimarySkill::BEGIN; g < PrimarySkill::END; ++g) { - int val = ptr[g]; + int val = ptr[g.getNum()]; if(val == 0) - { continue; - } - auto bb = std::make_shared( - BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, static_cast(*gameState->scenarioOps->campState->currentScenario()), g - ); + + auto currentScenario = *gameState->scenarioOps->campState->currentScenario(); + auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, BonusSourceID(currentScenario), BonusSubtypeID(g) ); hero->addNewBonus(bb); } break; @@ -339,9 +340,10 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vectorid = campaignHeroReplacement.heroPlaceholderId; - heroToPlace->tempOwner = heroPlaceholder->tempOwner; + if(heroPlaceholder->tempOwner.isValidPlayer()) + heroToPlace->tempOwner = heroPlaceholder->tempOwner; heroToPlace->pos = heroPlaceholder->pos; - heroToPlace->type = VLC->heroh->objects[heroToPlace->subID]; + heroToPlace->type = VLC->heroh->objects[heroToPlace->getHeroType().getNum()]; heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front(); gameState->map->removeBlockVisTiles(heroPlaceholder, true); @@ -377,7 +379,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT auto * heroPlaceholder = dynamic_cast(obj.get()); // only 1 field must be set - assert(heroPlaceholder->powerRank != heroPlaceholder->heroType); + assert(heroPlaceholder->powerRank.has_value() != heroPlaceholder->heroType.has_value()); if(heroPlaceholder->powerRank) placeholdersByPower.push_back(heroPlaceholder); @@ -387,9 +389,9 @@ std::vector CGameStateCampaign::generateCampaignHeroesT } //selecting heroes by type - for (auto const * placeholder : placeholdersByType) + for(const auto * placeholder : placeholdersByType) { - auto const & node = campaignState->getHeroByType(*placeholder->heroType); + const auto & node = campaignState->getHeroByType(*placeholder->heroType); if (node.isNull()) { logGlobal->info("Hero crossover: Unable to replace placeholder for %d (%s)!", placeholder->heroType->getNum(), VLC->heroTypes()->getById(*placeholder->heroType)->getNameTranslated()); @@ -398,7 +400,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT CGHeroInstance * hero = CampaignState::crossoverDeserialize(node, gameState->map); - logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } @@ -413,10 +415,10 @@ std::vector CGameStateCampaign::generateCampaignHeroesT return *a->powerRank > *b->powerRank; }); - auto const & nodeList = campaignState->getHeroesByPower(lastScenario.value()); + const auto & nodeList = campaignState->getHeroesByPower(lastScenario.value()); auto nodeListIter = nodeList.begin(); - for (auto const * placeholder : placeholdersByPower) + for(const auto * placeholder : placeholdersByPower) { if (nodeListIter == nodeList.end()) break; @@ -424,7 +426,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT CGHeroInstance * hero = CampaignState::crossoverDeserialize(*nodeListIter, gameState->map); nodeListIter++; - logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } @@ -470,7 +472,7 @@ void CGameStateCampaign::initHeroes() { for (auto & heroe : heroes) { - if (heroe->subID == chosenBonus->info1) + if (heroe->getHeroType().getNum() == chosenBonus->info1) { giveCampaignBonusToHero(heroe); break; @@ -500,7 +502,7 @@ void CGameStateCampaign::initStartingResources() std::vector people = getHumanPlayerInfo(); //players we will give resource bonus for(const PlayerSettings *ps : people) { - std::vector res; //resources we will give + std::vector res; //resources we will give switch (chosenBonus->info1) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: @@ -559,7 +561,7 @@ void CGameStateCampaign::initTowns() if(gameState->scenarioOps->campState->formatVCMI()) newBuilding = BuildingID(chosenBonus->info1); else - newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->subID, town->builtBuildings); + newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFaction(), town->builtBuildings); // Build granted building & all prerequisites - e.g. Mages Guild Lvl 3 should also give Mages Guild Lvl 1 & 2 while(true) diff --git a/lib/gameState/InfoAboutArmy.cpp b/lib/gameState/InfoAboutArmy.cpp index 21531787e..f5c807fe5 100644 --- a/lib/gameState/InfoAboutArmy.cpp +++ b/lib/gameState/InfoAboutArmy.cpp @@ -73,18 +73,18 @@ void InfoAboutHero::assign(const InfoAboutHero & iah) details = (iah.details ? new Details(*iah.details) : nullptr); hclass = iah.hclass; - portrait = iah.portrait; + portraitSource = iah.portraitSource; } -InfoAboutHero::InfoAboutHero(): portrait(-1) {} +InfoAboutHero::InfoAboutHero() +{} InfoAboutHero::InfoAboutHero(const InfoAboutHero & iah): InfoAboutArmy(iah) { assign(iah); } -InfoAboutHero::InfoAboutHero(const CGHeroInstance * h, InfoAboutHero::EInfoLevel infoLevel): - portrait(-1) +InfoAboutHero::InfoAboutHero(const CGHeroInstance * h, InfoAboutHero::EInfoLevel infoLevel) { initFromHero(h, infoLevel); } @@ -100,6 +100,11 @@ InfoAboutHero & InfoAboutHero::operator=(const InfoAboutHero & iah) return *this; } +int32_t InfoAboutHero::getIconIndex() const +{ + return VLC->heroTypes()->getById(portraitSource)->getIconIndex(); +} + void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLevel infoLevel) { vstd::clear_pointer(details); @@ -112,7 +117,7 @@ void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLe hclass = h->type->heroClass; name = h->getNameTranslated(); - portrait = h->portrait; + portraitSource = h->getPortraitSource(); if(detailed) { @@ -125,7 +130,7 @@ void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLe for (int i = 0; i < GameConstants::PRIMARY_SKILLS ; i++) { - details->primskills[i] = h->getPrimSkillLevel(static_cast(i)); + details->primskills[i] = h->getPrimSkillLevel(static_cast(i)); } if (infoLevel == EInfoLevel::INBATTLE) details->manaLimit = h->manaLimit(); diff --git a/lib/gameState/InfoAboutArmy.h b/lib/gameState/InfoAboutArmy.h index e62a2719e..569084c83 100644 --- a/lib/gameState/InfoAboutArmy.h +++ b/lib/gameState/InfoAboutArmy.h @@ -55,7 +55,7 @@ public: Details * details = nullptr; const CHeroClass *hclass; - int portrait; + HeroTypeID portraitSource; enum EInfoLevel { @@ -72,6 +72,7 @@ public: InfoAboutHero & operator=(const InfoAboutHero & iah); void initFromHero(const CGHeroInstance *h, EInfoLevel infoLevel); + int32_t getIconIndex() const; }; /// Struct which holds a int information about a town diff --git a/lib/gameState/SThievesGuildInfo.h b/lib/gameState/SThievesGuildInfo.h index f3423d05f..45ed3bdef 100644 --- a/lib/gameState/SThievesGuildInfo.h +++ b/lib/gameState/SThievesGuildInfo.h @@ -22,8 +22,8 @@ struct DLL_LINKAGE SThievesGuildInfo std::map colorToBestHero; //maps player's color to his best heros' - std::map personality; // color to personality // ai tactic - std::map bestCreature; // color to ID // id or -1 if not known + std::map personality; // color to personality // ai tactic + std::map bestCreature; // color to ID // id or -1 if not known // template void serialize(Handler &h, const int version) // { diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index f5e5a6138..40161ff2e 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -25,7 +25,7 @@ std::map TavernHeroesPool::unusedHeroesFromPool() c { std::map pool = heroesPool; for(const auto & slot : currentTavern) - pool.erase(HeroTypeID(slot.hero->subID)); + pool.erase(slot.hero->getHeroType()); return pool; } @@ -34,7 +34,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const { for (auto const & slot : currentTavern) { - if (HeroTypeID(slot.hero->subID) == hero) + if (slot.hero->getHeroType() == hero) return slot.role; } return TavernSlotRole::NONE; @@ -74,7 +74,7 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, bool TavernHeroesPool::isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const { if (perPlayerAvailability.count(hero)) - return perPlayerAvailability.at(hero) & (1 << color.getNum()); + return perPlayerAvailability.at(hero).count(color) != 0; return true; } @@ -117,30 +117,25 @@ void TavernHeroesPool::onNewDay() if(!hero.second) continue; + hero.second->removeBonusesRecursive(Bonus::OneDay); + hero.second->reduceBonusDurations(Bonus::NDays); + hero.second->reduceBonusDurations(Bonus::OneWeek); + // do not access heroes who are not present in tavern of any players if (vstd::contains(unusedHeroes, hero.first)) continue; hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); - hero.second->mana = hero.second->manaLimit(); - } - - for (auto & slot : currentTavern) - { - if (slot.role == TavernSlotRole::RETREATED_TODAY) - slot.role = TavernSlotRole::RETREATED; - - if (slot.role == TavernSlotRole::SURRENDERED_TODAY) - slot.role = TavernSlotRole::SURRENDERED; + hero.second->mana = hero.second->getManaNewTurn(); } } void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) { - heroesPool[HeroTypeID(hero->subID)] = hero; + heroesPool[hero->getHeroType()] = hero; } -void TavernHeroesPool::setAvailability(HeroTypeID hero, PlayerColor::Mask mask) +void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set mask) { perPlayerAvailability[hero] = mask; } diff --git a/lib/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h index 97c54879c..fb5dc136f 100644 --- a/lib/gameState/TavernHeroesPool.h +++ b/lib/gameState/TavernHeroesPool.h @@ -44,7 +44,7 @@ class DLL_LINKAGE TavernHeroesPool /// list of which players are able to purchase specific hero /// if hero is not present in list, he is available for everyone - std::map perPlayerAvailability; + std::map> perPlayerAvailability; /// list of heroes currently available in taverns std::vector currentTavern; @@ -71,7 +71,7 @@ public: void addHeroToPool(CGHeroInstance * hero); /// Marks hero as available to only specific set of players - void setAvailability(HeroTypeID hero, PlayerColor::Mask mask); + void setAvailability(HeroTypeID hero, std::set mask); /// Makes hero available in tavern of specified player void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role); diff --git a/lib/gameState/TavernSlot.h b/lib/gameState/TavernSlot.h index 192fd047d..698168cff 100644 --- a/lib/gameState/TavernSlot.h +++ b/lib/gameState/TavernSlot.h @@ -24,12 +24,8 @@ enum class TavernSlotRole : int8_t SINGLE_UNIT, // hero was added after buying hero from this slot, and only has 1 creature in army FULL_ARMY, // hero was added to tavern on new week and still has full army - RETREATED, // hero was owned by player before, but have retreated from battle and only has 1 creature in army - RETREATED_TODAY, - - SURRENDERED, // hero was owned by player before, but have surrendered in battle and kept some troops - SURRENDERED_TODAY, + SURRENDERED // hero was owned by player before, but have surrendered in battle and kept some troops }; VCMI_LIB_NAMESPACE_END diff --git a/lib/int3.h b/lib/int3.h index aa060703a..e8a68e8ef 100644 --- a/lib/int3.h +++ b/lib/int3.h @@ -1,217 +1,217 @@ -/* - * int3.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 which consists of three integer values. Represents position on adventure map. -class int3 -{ -public: - si32 x, y, z; - - //c-tor: x, y, z initialized to 0 - constexpr int3() : x(0), y(0), z(0) {} // I think that x, y, z should be left uninitialized. - //c-tor: x, y, z initialized to i - explicit constexpr int3(const si32 i) : x(i), y(i), z(i) {} - //c-tor: x, y, z initialized to X, Y, Z - constexpr int3(const si32 X, const si32 Y, const si32 Z) : x(X), y(Y), z(Z) {} - constexpr int3(const int3 & c) = default; - - constexpr int3 & operator=(const int3 & c) = default; - constexpr int3 operator-() const { return int3(-x, -y, -z); } - - constexpr int3 operator+(const int3 & i) const { return int3(x + i.x, y + i.y, z + i.z); } - constexpr int3 operator-(const int3 & i) const { return int3(x - i.x, y - i.y, z - i.z); } - //returns int3 with coordinates increased by given number - constexpr int3 operator+(const si32 i) const { return int3(x + i, y + i, z + i); } - //returns int3 with coordinates decreased by given number - constexpr int3 operator-(const si32 i) const { return int3(x - i, y - i, z - i); } - - //returns int3 with coordinates multiplied by given number - constexpr int3 operator*(const double i) const { return int3((int)(x * i), (int)(y * i), (int)(z * i)); } - //returns int3 with coordinates divided by given number - constexpr int3 operator/(const double i) const { return int3((int)(x / i), (int)(y / i), (int)(z / i)); } - - //returns int3 with coordinates multiplied by given number - constexpr int3 operator*(const si32 i) const { return int3(x * i, y * i, z * i); } - //returns int3 with coordinates divided by given number - constexpr int3 operator/(const si32 i) const { return int3(x / i, y / i, z / i); } - - constexpr int3 & operator+=(const int3 & i) - { - x += i.x; - y += i.y; - z += i.z; - return *this; - } - constexpr int3 & operator-=(const int3 & i) - { - x -= i.x; - y -= i.y; - z -= i.z; - return *this; - } - - //increases all coordinates by given number - constexpr int3 & operator+=(const si32 i) - { - x += i; - y += i; - z += i; - return *this; - } - //decreases all coordinates by given number - constexpr int3 & operator-=(const si32 i) - { - x -= i; - y -= i; - z -= i; - return *this; - } - - constexpr bool operator==(const int3 & i) const { return (x == i.x && y == i.y && z == i.z); } - constexpr bool operator!=(const int3 & i) const { return (x != i.x || y != i.y || z != i.z); } - - constexpr bool operator<(const int3 & i) const - { - if (z < i.z) - return true; - if (z > i.z) - return false; - if (y < i.y) - return true; - if (y > i.y) - return false; - if (x < i.x) - return true; - if (x > i.x) - return false; - return false; - } - - enum EDistanceFormula - { - DIST_2D = 0, - DIST_MANHATTAN, // patrol distance - DIST_CHEBYSHEV, // ambient sound distance - DIST_2DSQ - }; - - ui32 dist(const int3 & o, EDistanceFormula formula) const - { - switch(formula) - { - case DIST_2D: - return static_cast(dist2d(o)); - case DIST_MANHATTAN: - return static_cast(mandist2d(o)); - case DIST_CHEBYSHEV: - return static_cast(chebdist2d(o)); - case DIST_2DSQ: - return dist2dSQ(o); - default: - return 0; - } - } - - //returns squared distance on Oxy plane (z coord is not used) - constexpr ui32 dist2dSQ(const int3 & o) const - { - const si32 dx = (x - o.x); - const si32 dy = (y - o.y); - return (ui32)(dx*dx) + (ui32)(dy*dy); - } - //returns distance on Oxy plane (z coord is not used) - double dist2d(const int3 & o) const - { - return std::sqrt((double)dist2dSQ(o)); - } - //manhattan distance used for patrol radius (z coord is not used) - constexpr double mandist2d(const int3 & o) const - { - return vstd::abs(o.x - x) + vstd::abs(o.y - y); - } - //chebyshev distance used for ambient sounds (z coord is not used) - constexpr double chebdist2d(const int3 & o) const - { - return std::max(vstd::abs(o.x - x), vstd::abs(o.y - y)); - } - - constexpr bool areNeighbours(const int3 & o) const - { - return (dist2dSQ(o) < 4) && (z == o.z); - } - - //returns "(x y z)" string - std::string toString() const - { - //Performance is important here - std::string result = "(" + - std::to_string(x) + " " + - std::to_string(y) + " " + - std::to_string(z) + ")"; - - return result; - } - - constexpr bool valid() const //Should be named "isValid"? - { - return z >= 0; //minimal condition that needs to be fulfilled for tiles in the map - } - - template - void serialize(Handler &h, const int version) - { - h & x; - h & y; - h & z; - } - - constexpr static std::array getDirs() - { - return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), - int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } }; - } -}; - -template -int3 findClosestTile (Container & container, int3 dest) -{ - static_assert(std::is_same::value, - "findClosestTile requires container."); - - int3 result(-1, -1, -1); - ui32 distance = std::numeric_limits::max(); - for (const int3& tile : container) - { - const ui32 currentDistance = dest.dist2dSQ(tile); - if (currentDistance < distance) - { - result = tile; - distance = currentDistance; - } - } - return result; -} - -VCMI_LIB_NAMESPACE_END - - -template<> -struct std::hash { - size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const - { - size_t ret = std::hash()(pos.x); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z); - return ret; - } -}; +/* + * int3.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 which consists of three integer values. Represents position on adventure map. +class int3 +{ +public: + si32 x, y, z; + + //c-tor: x, y, z initialized to 0 + constexpr int3() : x(0), y(0), z(0) {} // I think that x, y, z should be left uninitialized. + //c-tor: x, y, z initialized to i + explicit constexpr int3(const si32 i) : x(i), y(i), z(i) {} + //c-tor: x, y, z initialized to X, Y, Z + constexpr int3(const si32 X, const si32 Y, const si32 Z) : x(X), y(Y), z(Z) {} + constexpr int3(const int3 & c) = default; + + constexpr int3 & operator=(const int3 & c) = default; + constexpr int3 operator-() const { return int3(-x, -y, -z); } + + constexpr int3 operator+(const int3 & i) const { return int3(x + i.x, y + i.y, z + i.z); } + constexpr int3 operator-(const int3 & i) const { return int3(x - i.x, y - i.y, z - i.z); } + //returns int3 with coordinates increased by given number + constexpr int3 operator+(const si32 i) const { return int3(x + i, y + i, z + i); } + //returns int3 with coordinates decreased by given number + constexpr int3 operator-(const si32 i) const { return int3(x - i, y - i, z - i); } + + //returns int3 with coordinates multiplied by given number + constexpr int3 operator*(const double i) const { return int3((int)(x * i), (int)(y * i), (int)(z * i)); } + //returns int3 with coordinates divided by given number + constexpr int3 operator/(const double i) const { return int3((int)(x / i), (int)(y / i), (int)(z / i)); } + + //returns int3 with coordinates multiplied by given number + constexpr int3 operator*(const si32 i) const { return int3(x * i, y * i, z * i); } + //returns int3 with coordinates divided by given number + constexpr int3 operator/(const si32 i) const { return int3(x / i, y / i, z / i); } + + constexpr int3 & operator+=(const int3 & i) + { + x += i.x; + y += i.y; + z += i.z; + return *this; + } + constexpr int3 & operator-=(const int3 & i) + { + x -= i.x; + y -= i.y; + z -= i.z; + return *this; + } + + //increases all coordinates by given number + constexpr int3 & operator+=(const si32 i) + { + x += i; + y += i; + z += i; + return *this; + } + //decreases all coordinates by given number + constexpr int3 & operator-=(const si32 i) + { + x -= i; + y -= i; + z -= i; + return *this; + } + + constexpr bool operator==(const int3 & i) const { return (x == i.x && y == i.y && z == i.z); } + constexpr bool operator!=(const int3 & i) const { return (x != i.x || y != i.y || z != i.z); } + + constexpr bool operator<(const int3 & i) const + { + if (z < i.z) + return true; + if (z > i.z) + return false; + if (y < i.y) + return true; + if (y > i.y) + return false; + if (x < i.x) + return true; + if (x > i.x) + return false; + return false; + } + + enum EDistanceFormula + { + DIST_2D = 0, + DIST_MANHATTAN, // patrol distance + DIST_CHEBYSHEV, // ambient sound distance + DIST_2DSQ + }; + + ui32 dist(const int3 & o, EDistanceFormula formula) const + { + switch(formula) + { + case DIST_2D: + return std::round(dist2d(o)); + case DIST_MANHATTAN: + return static_cast(mandist2d(o)); + case DIST_CHEBYSHEV: + return static_cast(chebdist2d(o)); + case DIST_2DSQ: + return dist2dSQ(o); + default: + return 0; + } + } + + //returns squared distance on Oxy plane (z coord is not used) + constexpr ui32 dist2dSQ(const int3 & o) const + { + const si32 dx = (x - o.x); + const si32 dy = (y - o.y); + return (ui32)(dx*dx) + (ui32)(dy*dy); + } + //returns distance on Oxy plane (z coord is not used) + double dist2d(const int3 & o) const + { + return std::sqrt((double)dist2dSQ(o)); + } + //manhattan distance used for patrol radius (z coord is not used) + constexpr double mandist2d(const int3 & o) const + { + return vstd::abs(o.x - x) + vstd::abs(o.y - y); + } + //chebyshev distance used for ambient sounds (z coord is not used) + constexpr double chebdist2d(const int3 & o) const + { + return std::max(vstd::abs(o.x - x), vstd::abs(o.y - y)); + } + + constexpr bool areNeighbours(const int3 & o) const + { + return (dist2dSQ(o) < 4) && (z == o.z); + } + + //returns "(x y z)" string + std::string toString() const + { + //Performance is important here + std::string result = "(" + + std::to_string(x) + " " + + std::to_string(y) + " " + + std::to_string(z) + ")"; + + return result; + } + + constexpr bool valid() const //Should be named "isValid"? + { + return z >= 0; //minimal condition that needs to be fulfilled for tiles in the map + } + + template + void serialize(Handler &h, const int version) + { + h & x; + h & y; + h & z; + } + + constexpr static std::array getDirs() + { + return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), + int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } }; + } +}; + +template +int3 findClosestTile (Container & container, int3 dest) +{ + static_assert(std::is_same::value, + "findClosestTile requires container."); + + int3 result(-1, -1, -1); + ui32 distance = std::numeric_limits::max(); + for (const int3& tile : container) + { + const ui32 currentDistance = dest.dist2dSQ(tile); + if (currentDistance < distance) + { + result = tile; + distance = currentDistance; + } + } + return result; +} + +VCMI_LIB_NAMESPACE_END + + +template<> +struct std::hash { + size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const + { + size_t ret = std::hash()(pos.x); + VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y); + VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z); + return ret; + } +}; diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index 9e23d372e..2f9ccdf6f 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -288,7 +288,7 @@ std::string CLogFormatter::format(const LogRecord & record) const boost::algorithm::replace_first(message, "%m", record.message); boost::algorithm::replace_first(message, "%c", boost::posix_time::to_simple_string(record.timeStamp)); - //return boost::to_string (boost::format("%d %d %d[%d] - %d") % dateStream.str() % level % record.domain.getName() % record.threadId % record.message); + //return boost::str (boost::format("%d %d %d[%d] - %d") % dateStream.str() % level % record.domain.getName() % record.threadId % record.message); return message; } @@ -425,7 +425,7 @@ const CColorMapping & CLogConsoleTarget::getColorMapping() const { return colorM void CLogConsoleTarget::setColorMapping(const CColorMapping & colorMapping) { this->colorMapping = colorMapping; } CLogFileTarget::CLogFileTarget(const boost::filesystem::path & filePath, bool append): - file(filePath, append ? std::ios_base::app : std::ios_base::out) + file(filePath.c_str(), append ? std::ios_base::app : std::ios_base::out) { // formatter.setPattern("%d %l %n [%t] - %m"); formatter.setPattern("%l %n [%t] - %m"); diff --git a/lib/logging/CLogger.h b/lib/logging/CLogger.h index 03a36a73c..72a0b929e 100644 --- a/lib/logging/CLogger.h +++ b/lib/logging/CLogger.h @@ -10,7 +10,6 @@ #pragma once #include "../CConsoleHandler.h" -#include "../filesystem/FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -220,7 +219,7 @@ public: void write(const LogRecord & record) override; private: - boost::filesystem::fstream file; + std::fstream file; CLogFormatter formatter; mutable std::mutex mx; }; diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 53a19c83b..1d915d5f8 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -13,7 +13,7 @@ #include "IObjectInfo.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" @@ -83,24 +83,28 @@ void AObjectTypeHandler::init(const JsonNode & input) } for(const JsonNode & node : input["sounds"]["ambient"].Vector()) - sounds.ambient.push_back(node.String()); + sounds.ambient.push_back(AudioPath::fromJson(node)); for(const JsonNode & node : input["sounds"]["visit"].Vector()) - sounds.visit.push_back(node.String()); + sounds.visit.push_back(AudioPath::fromJson(node)); for(const JsonNode & node : input["sounds"]["removal"].Vector()) - sounds.removal.push_back(node.String()); + sounds.removal.push_back(AudioPath::fromJson(node)); if(input["aiValue"].isNull()) aiValue = std::nullopt; else aiValue = static_cast>(input["aiValue"].Integer()); + // TODO: Define properties, move them to actual object instance + blockVisit = input["blockVisit"].Bool(); + removable = input["removable"].Bool(); + battlefield = BattleField::NONE; if(!input["battleground"].isNull()) { - VLC->modh->identifiers.requestIdentifier("battlefield", input["battleground"], [this](int32_t identifier) + VLC->identifiers()->requestIdentifier("battlefield", input["battleground"], [this](int32_t identifier) { battlefield = BattleField(identifier); }); @@ -120,6 +124,8 @@ void AObjectTypeHandler::preInitObject(CGObjectInstance * obj) const obj->subID = subtype; obj->typeName = typeName; obj->subTypeName = subTypeName; + obj->blockVisit = blockVisit; + obj->removable = removable; } void AObjectTypeHandler::initTypeData(const JsonNode & input) diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index ce35a9f82..04e7c2ca5 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -43,6 +43,9 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable si32 type; si32 subtype; + bool blockVisit; + bool removable; + protected: void preInitObject(CGObjectInstance * obj) const; virtual bool objectFilter(const CGObjectInstance * obj, std::shared_ptr tmpl) const; @@ -112,20 +115,6 @@ public: /// Returns object configuration, if available. Otherwise returns NULL virtual std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const; - - template void serialize(Handler &h, const int version) - { - h & type; - h & subtype; - h & templates; - h & rmgInfo; - h & modScope; - h & typeName; - h & subTypeName; - h & sounds; - h & aiValue; - h & battlefield; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index ba8e4ea2c..f61641a85 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -37,22 +37,15 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const { BankConfig bc; + JsonRandom::Variables emptyVariables; bc.chance = static_cast(level["chance"].Float()); - - bc.guards = JsonRandom::loadCreatures(level["guards"], rng); - bc.upgradeChance = static_cast(level["upgrade_chance"].Float()); - bc.combatValue = static_cast(level["combat_value"].Float()); - - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + bc.guards = JsonRandom::loadCreatures(level["guards"], rng, emptyVariables); bc.resources = ResourceSet(level["reward"]["resources"]); - bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng); - bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng); - bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, spells); - - bc.value = static_cast(level["value"].Float()); + bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng, emptyVariables); + bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables); + bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, emptyVariables); return bc; } @@ -110,10 +103,12 @@ static void addStackToArmy(IObjectInfo::CArmyStructure & army, const CCreature * IObjectInfo::CArmyStructure CBankInfo::minGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -131,10 +126,12 @@ IObjectInfo::CArmyStructure CBankInfo::minGuards() const IObjectInfo::CArmyStructure CBankInfo::maxGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -152,12 +149,13 @@ IObjectInfo::CArmyStructure CBankInfo::maxGuards() const TPossibleGuards CBankInfo::getPossibleGuards() const { + JsonRandom::Variables emptyVariables; TPossibleGuards out; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["guards"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); IObjectInfo::CArmyStructure army; @@ -192,12 +190,13 @@ std::vector> CBankInfo::getPossibleResourcesReward() std::vector> CBankInfo::getPossibleCreaturesReward() const { + JsonRandom::Variables emptyVariables; std::vector> aproximateReward; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["reward"]["creatures"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); for(auto stack : stacks) { diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.h b/lib/mapObjectConstructors/CBankInstanceConstructor.h index f09d256a2..e683f01df 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.h +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.h @@ -20,10 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct BankConfig { - ui32 value = 0; //overall value of given things ui32 chance = 0; //chance for this level being chosen - ui32 upgradeChance = 0; //chance for creatures to be in upgraded versions - ui32 combatValue = 0; //how hard are guards of this level std::vector guards; //creature ID, amount ResourceSet resources; //resources given in case of victory std::vector creatures; //creatures granted in case of victory (creature ID, amount) @@ -33,13 +30,10 @@ struct BankConfig template void serialize(Handler &h, const int version) { h & chance; - h & upgradeChance; h & guards; - h & combatValue; h & resources; h & creatures; h & artifacts; - h & value; h & spells; } }; @@ -98,15 +92,6 @@ public: bool hasNameTextID() const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - h & levels; - h & bankResetDuration; - h & blockVisit; - h & coastVisitable; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 99d5aa82c..928a72a29 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -14,9 +14,8 @@ #include "../filesystem/CBinaryReader.h" #include "../VCMI_Lib.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../GameSettings.h" #include "../JsonNode.h" #include "../CSoundBase.h" @@ -27,7 +26,6 @@ #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGPandoraBox.h" #include "../mapObjects/CQuest.h" @@ -36,7 +34,9 @@ #include "../mapObjects/MiscObjects.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" - +#include "../modding/IdentifierStorage.h" +#include "../modding/CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -54,7 +54,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER_CLASS("bank", CBankInstanceConstructor); SET_HANDLER_CLASS("boat", BoatInstanceConstructor); SET_HANDLER_CLASS("market", MarketInstanceConstructor); - SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor); SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor); SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor); SET_HANDLER_CLASS("monster", CreatureInstanceConstructor); @@ -71,7 +70,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("randomDwelling", CGDwelling); SET_HANDLER("generic", CGObjectInstance); - SET_HANDLER("cartographer", CCartographer); SET_HANDLER("artifact", CGArtifact); SET_HANDLER("borderGate", CGBorderGate); SET_HANDLER("borderGuard", CGBorderGuard); @@ -84,18 +82,15 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("magi", CGMagi); SET_HANDLER("mine", CGMine); SET_HANDLER("obelisk", CGObelisk); - SET_HANDLER("observatory", CGObservatory); SET_HANDLER("pandora", CGPandoraBox); SET_HANDLER("prison", CGHeroInstance); SET_HANDLER("questGuard", CGQuestGuard); - SET_HANDLER("scholar", CGScholar); SET_HANDLER("seerHut", CGSeerHut); SET_HANDLER("sign", CGSignBottle); SET_HANDLER("siren", CGSirens); SET_HANDLER("monolith", CGMonolith); SET_HANDLER("subterraneanGate", CGSubterraneanGate); SET_HANDLER("whirlpool", CGWhirlpool); - SET_HANDLER("witch", CGWitchHut); SET_HANDLER("terrain", CGTerrainPatch); #undef SET_HANDLER_CLASS @@ -112,7 +107,7 @@ std::vector CObjectClassesHandler::loadLegacyData() { size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_OBJECT); - CLegacyConfigParser parser("Data/Objects.txt"); + CLegacyConfigParser parser(TextPath::builtin("Data/Objects.txt")); auto totalNumber = static_cast(parser.readNumber()); // first line contains number of objects to read and nothing else parser.endLine(); @@ -123,7 +118,7 @@ std::vector CObjectClassesHandler::loadLegacyData() tmpl->readTxt(parser); parser.endLine(); - std::pair key(tmpl->id.num, tmpl->subid); + std::pair key(tmpl->id, tmpl->subid); legacyTemplates.insert(std::make_pair(key, std::shared_ptr(tmpl))); } @@ -132,7 +127,7 @@ std::vector CObjectClassesHandler::loadLegacyData() std::vector ret(dataSize);// create storage for 256 objects assert(dataSize == 256); - CLegacyConfigParser namesParser("Data/ObjNames.txt"); + CLegacyConfigParser namesParser(TextPath::builtin("Data/ObjNames.txt")); for (size_t i=0; i<256; i++) { ret[i]["name"].String() = namesParser.readString(); @@ -142,7 +137,7 @@ std::vector CObjectClassesHandler::loadLegacyData() JsonNode cregen1; JsonNode cregen4; - CLegacyConfigParser cregen1Parser("data/crgen1"); + CLegacyConfigParser cregen1Parser(TextPath::builtin("data/crgen1")); do { JsonNode subObject; @@ -151,7 +146,7 @@ std::vector CObjectClassesHandler::loadLegacyData() } while(cregen1Parser.endLine()); - CLegacyConfigParser cregen4Parser("data/crgen4"); + CLegacyConfigParser cregen4Parser(TextPath::builtin("data/crgen4")); do { JsonNode subObject; @@ -199,13 +194,16 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); - if(!handlerConstructors.count(obj->handlerName)) + std::string handler = obj->handlerName; + if(!handlerConstructors.count(handler)) { - logGlobal->error("Handler with name %s was not found!", obj->handlerName); - return nullptr; + logMod->error("Handler with name %s was not found!", handler); + // workaround for potential crash - if handler does not exists, continue with generic handler that is used for objects without any custom logc + handler = "generic"; + assert(handlerConstructors.count(handler) != 0); } - auto createdObject = handlerConstructors.at(obj->handlerName)(); + auto createdObject = handlerConstructors.at(handler)(); createdObject->modScope = scope; createdObject->typeName = obj->identifier;; @@ -270,6 +268,10 @@ ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, con else loadSubObject(subData.second.meta, subData.first, subData.second, obj); } + + if (obj->id == MapObjectID::MONOLITH_TWO_WAY) + generateExtraMonolithsForRMG(obj); + return obj; } @@ -277,7 +279,7 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons { auto * object = loadFromJson(scope, data, name, objects.size()); objects.push_back(object); - VLC->modh->identifiers.registerObject(scope, "object", name, object->id); + VLC->identifiersHandler->registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -285,40 +287,32 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons auto * object = loadFromJson(scope, data, name, index); assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before objects[static_cast(index)] = object; - VLC->modh->identifiers.registerObject(scope, "object", name, object->id); + VLC->identifiersHandler->registerObject(scope, "object", name, object->id); } -void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID) +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(ID < objects.size()); - assert(objects[ID]); + assert(objects[ID.getNum()]); - if ( subID >= objects[ID]->objects.size()) - objects[ID]->objects.resize(subID+1); + if ( subID.getNum() >= objects[ID.getNum()]->objects.size()) + objects[ID.getNum()]->objects.resize(subID.getNum()+1); - JsonUtils::inherit(config, objects.at(ID)->base); - loadSubObject(config.meta, identifier, config, objects[ID], subID); + JsonUtils::inherit(config, objects.at(ID.getNum())->base); + loadSubObject(config.meta, identifier, config, objects[ID.getNum()], subID.getNum()); } -void CObjectClassesHandler::removeSubObject(si32 ID, si32 subID) +void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(ID < objects.size()); - assert(objects[ID]); - assert(subID < objects[ID]->objects.size()); - objects[ID]->objects[subID] = nullptr; + assert(objects[ID.getNum()]); + objects[ID.getNum()]->objects[subID.getNum()] = nullptr; } -std::vector CObjectClassesHandler::getDefaultAllowed() const -{ - return std::vector(); //TODO? -} - -TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const +TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try { - auto result = objects.at(type)->objects.at(subtype); + auto result = objects.at(type.getNum())->objects.at(subtype.getNum()); if (result != nullptr) return result; @@ -328,18 +322,18 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) // Leave catch block silently } - std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype); + std::string errorString = "Failed to find object of type " + std::to_string(type.getNum()) + "::" + std::to_string(subtype.getNum()); logGlobal->error(errorString); throw std::runtime_error(errorString); } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const { - std::optional id = VLC->modh->identifiers.getIdentifier(scope, "object", type); + std::optional id = VLC->identifiers()->getIdentifier(scope, "object", type); if(id) { auto * object = objects[id.value()]; - std::optional subID = VLC->modh->identifiers.getIdentifier(scope, object->getJsonKey(), subtype); + std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); if (subID) return object->objects[subID.value()]; @@ -355,9 +349,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID); } -std::set CObjectClassesHandler::knownObjects() const +std::set CObjectClassesHandler::knownObjects() const { - std::set ret; + std::set ret; for(auto * entry : objects) if (entry) @@ -366,17 +360,17 @@ std::set CObjectClassesHandler::knownObjects() const return ret; } -std::set CObjectClassesHandler::knownSubObjects(si32 primaryID) const +std::set CObjectClassesHandler::knownSubObjects(MapObjectID primaryID) const { - std::set ret; + std::set ret; - if (!objects.at(primaryID)) + if (!objects.at(primaryID.getNum())) { logGlobal->error("Failed to find object %d", primaryID); return ret; } - for(const auto & entry : objects.at(primaryID)->objects) + for(const auto & entry : objects.at(primaryID.getNum())->objects) if (entry) ret.insert(entry->subtype); @@ -424,14 +418,12 @@ void CObjectClassesHandler::afterLoadFinalization() logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); } } - - generateExtraMonolithsForRMG(); } -void CObjectClassesHandler::generateExtraMonolithsForRMG() +void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container) { //duplicate existing two-way portals to make reserve for RMG - auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects; + auto& portalVec = container->objects; //FIXME: Monoliths in this vector can be already not useful for every terrain const size_t portalCount = portalVec.size(); @@ -460,20 +452,23 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG() newPortal->type = portal->getIndex(); newPortal->subtype = portalVec.size(); //indexes must be unique, they are returned as a set + portalVec.push_back(newPortal); + + registerObject(ModScope::scopeGame(), container->getJsonKey(), newPortal->subTypeName, newPortal->subtype); } } -std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const +std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubID subtype) const { const auto handler = getHandlerFor(type, subtype); if (handler && handler->hasNameTextID()) return handler->getNameTranslated(); else - return objects[type]->getNameTranslated(); + return objects[type.getNum()]->getNameTranslated(); } -SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) const +SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const { // TODO: these objects may have subID's that does not have associated handler: // Prison: uses hero type as subID @@ -482,16 +477,19 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) co if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - assert(type < objects.size()); - assert(objects[type]); - assert(subtype < objects[type]->objects.size()); + assert(objects[type.getNum()]); return getHandlerFor(type, subtype)->getSounds(); } -std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const +std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const { - return objects.at(type)->handlerName; + return objects.at(type.getNum())->handlerName; +} + +std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const +{ + return objects.at(type.getNum())->getJsonKey(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 16817063d..173fe8748 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -62,16 +62,6 @@ public: std::string getJsonKey() const; std::string getNameTextID() const; std::string getNameTranslated() const; - - template void serialize(Handler &h, const int version) - { - h & id; - h & handlerName; - h & base; - h & objects; - h & identifier; - h & modScope; - } }; /// Main class responsible for creation of all adventure map objects @@ -84,7 +74,7 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase std::map > handlerConstructors; /// container with H3 templates, used only during loading, no need to serialize it - using TTemplatesContainer = std::multimap, std::shared_ptr>; + using TTemplatesContainer = std::multimap, std::shared_ptr>; TTemplatesContainer legacyTemplates; TObjectTypeHandler loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index); @@ -94,7 +84,7 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); - void generateExtraMonolithsForRMG(); + void generateExtraMonolithsForRMG(ObjectClass * container); public: CObjectClassesHandler(); @@ -105,34 +95,29 @@ public: 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 loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID); - void removeSubObject(si32 ID, si32 subID); + void loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID); + void removeSubObject(MapObjectID ID, MapObjectSubID subID); void beforeValidate(JsonNode & object) override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - /// Queries to detect loaded objects - std::set knownObjects() const; - std::set knownSubObjects(si32 primaryID) const; + std::set knownObjects() const; + std::set knownSubObjects(MapObjectID primaryID) const; /// returns handler for specified object (ID-based). ObjectHandler keeps ownership - TObjectTypeHandler getHandlerFor(si32 type, si32 subtype) const; + 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; - std::string getObjectName(si32 type, si32 subtype) const; + std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const; - SObjectSounds getObjectSounds(si32 type, si32 subtype) const; + SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const; /// Returns handler string describing the handler (for use in client) - std::string getObjectHandlerName(si32 type) const; + std::string getObjectHandlerName(MapObjectID type) const; - template void serialize(Handler &h, const int version) - { - h & objects; - } + std::string getJsonKey(MapObjectID type) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 8d3bec191..c8b802586 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -49,13 +49,8 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG { for (auto & bonus : rewardInfo.reward.bonuses) { - bonus.source = BonusSource::OBJECT; - bonus.sid = rewardableObject->ID; - //TODO: bonus.description = object->getObjectName(); - if (bonus.type == BonusType::MORALE) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); - if (bonus.type == BonusType::LUCK) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); + bonus.source = BonusSource::OBJECT_TYPE; + bonus.sid = BonusSourceID(rewardableObject->ID); } } assert(!rewardableObject->configuration.info.empty()); diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index aeb8d6b62..a92836f7f 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -30,14 +30,6 @@ public: void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - AObjectTypeHandler::serialize(h, version); - - if (version >= 816) - h & objectInfo; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index d786d7f04..13f286cbf 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -12,11 +12,10 @@ #include "../CGeneralTextHandler.h" #include "../CHeroHandler.h" -#include "../CModHandler.h" #include "../CTownHandler.h" #include "../IGameCallback.h" #include "../JsonRandom.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../VCMI_Lib.h" @@ -26,6 +25,8 @@ #include "../mapObjects/MiscObjects.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/IdentifierStorage.h" + #include "../mapping/CMapDefines.h" VCMI_LIB_NAMESPACE_BEGIN @@ -57,7 +58,7 @@ std::string ResourceInstanceConstructor::getNameTextID() const void CTownInstanceConstructor::initTypeData(const JsonNode & input) { - VLC->modh->identifiers.requestIdentifier("faction", input["faction"], [&](si32 index) + VLC->identifiers()->requestIdentifier("faction", input["faction"], [&](si32 index) { faction = (*VLC->townh)[index]; }); @@ -76,7 +77,7 @@ void CTownInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [this](const JsonNode & node) { - return BuildingID(VLC->modh->identifiers.getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value()); + return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value()); }); } } @@ -118,7 +119,7 @@ std::string CTownInstanceConstructor::getNameTextID() const void CHeroInstanceConstructor::initTypeData(const JsonNode & input) { - VLC->modh->identifiers.requestIdentifier( + VLC->identifiers()->requestIdentifier( "heroClass", input["heroClass"], [&](si32 index) { heroClass = VLC->heroh->classes[index]; }); @@ -132,7 +133,7 @@ void CHeroInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [](const JsonNode & node) { - return HeroTypeID(VLC->modh->identifiers.getIdentifier("hero", node.Vector()[0]).value()); + return HeroTypeID(VLC->identifiers()->getIdentifier("hero", node.Vector()[0]).value()); }); } } @@ -179,12 +180,15 @@ void BoatInstanceConstructor::initTypeData(const JsonNode & input) int pos = vstd::find_pos(NPathfindingLayer::names, input["layer"].String()); if(pos != -1) layer = EPathfindingLayer(pos); + else + logMod->error("Unknown layer %s found in boat!", input["layer"].String()); + onboardAssaultAllowed = input["onboardAssaultAllowed"].Bool(); onboardVisitAllowed = input["onboardVisitAllowed"].Bool(); - actualAnimation = input["actualAnimation"].String(); - overlayAnimation = input["overlayAnimation"].String(); + actualAnimation = AnimationPath::fromJson(input["actualAnimation"]); + overlayAnimation = AnimationPath::fromJson(input["overlayAnimation"]); for(int i = 0; i < flagAnimations.size() && i < input["flagAnimations"].Vector().size(); ++i) - flagAnimations[i] = input["flagAnimations"].Vector()[i].String(); + flagAnimations[i] = AnimationPath::fromJson(input["flagAnimations"].Vector()[i]); bonuses = JsonRandom::loadBonuses(input["bonuses"]); } @@ -200,7 +204,7 @@ void BoatInstanceConstructor::initializeObject(CGBoat * boat) const boat->addNewBonus(std::make_shared(b)); } -std::string BoatInstanceConstructor::getBoatAnimationName() const +AnimationPath BoatInstanceConstructor::getBoatAnimationName() const { return actualAnimation; } @@ -252,10 +256,12 @@ void MarketInstanceConstructor::initializeObject(CGMarket * market) const void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const { + JsonRandom::Variables emptyVariables; + if(auto * university = dynamic_cast(object)) { - for(auto skill : JsonRandom::loadSecondary(predefinedOffer, rng)) - university->skills.push_back(skill.first.getNum()); + for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables)) + university->skills.push_back(skill.first); } } diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 53e73d8d8..76215b97d 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -66,14 +66,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & faction; - h & filters; - h & static_cast&>(*this); - } }; class CHeroInstanceConstructor : public CDefaultObjectTypeHandler @@ -93,14 +85,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & heroClass; - h & filters; - h & static_cast&>(*this); - } }; class DLL_LINKAGE BoatInstanceConstructor : public CDefaultObjectTypeHandler @@ -113,27 +97,15 @@ protected: bool onboardAssaultAllowed; //if true, hero can attack units from transport bool onboardVisitAllowed; //if true, hero can visit objects from transport - std::string actualAnimation; //for OH3 boats those have actual animations - std::string overlayAnimation; //waves animations - std::array flagAnimations; + AnimationPath actualAnimation; //for OH3 boats those have actual animations + AnimationPath overlayAnimation; //waves animations + std::array flagAnimations; public: void initializeObject(CGBoat * object) const override; /// Returns boat preview animation, for use in Shipyards - std::string getBoatAnimationName() const; - - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & layer; - h & onboardAssaultAllowed; - h & onboardVisitAllowed; - h & bonuses; - h & actualAnimation; - h & overlayAnimation; - h & flagAnimations; - } + AnimationPath getBoatAnimationName() const; }; class MarketInstanceConstructor : public CDefaultObjectTypeHandler @@ -141,7 +113,7 @@ class MarketInstanceConstructor : public CDefaultObjectTypeHandler protected: void initTypeData(const JsonNode & config) override; - std::set marketModes; + std::set marketModes; JsonNode predefinedOffer; int marketEfficiency; @@ -152,12 +124,6 @@ public: void initializeObject(CGMarket * object) const override; void randomizeObject(CGMarket * object, CRandomGenerator & rng) const override; - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & marketModes; - h & marketEfficiency; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index 14a28944d..6a137b976 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -12,10 +12,10 @@ #include "../CCreatureHandler.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../JsonRandom.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGDwelling.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +43,7 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input) for(auto currentCreature = 0; currentCreature < creaturesNumber; currentCreature++) { - VLC->modh->identifiers.requestIdentifier("creature", creaturesOnLevel[currentCreature], [=] (si32 index) + VLC->identifiers()->requestIdentifier("creature", creaturesOnLevel[currentCreature], [=] (si32 index) { availableCreatures[currentLevel][currentCreature] = VLC->creh->objects[index]; }); @@ -93,7 +93,8 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGe } else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux) { - for(auto & stack : JsonRandom::loadCreatures(guards, rng)) + JsonRandom::Variables emptyVariables; + for(auto & stack : JsonRandom::loadCreatures(guards, rng, emptyVariables)) { dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count)); } diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.h b/lib/mapObjectConstructors/DwellingInstanceConstructor.h index 4022be309..ee06219be 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.h +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.h @@ -33,13 +33,6 @@ public: bool producesCreature(const CCreature * crea) const; std::vector getProducedCreatures() const; - - template void serialize(Handler &h, const int version) - { - h & availableCreatures; - h & guards; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/HillFortInstanceConstructor.h b/lib/mapObjectConstructors/HillFortInstanceConstructor.h index 526e66571..ace75dd1d 100644 --- a/lib/mapObjectConstructors/HillFortInstanceConstructor.h +++ b/lib/mapObjectConstructors/HillFortInstanceConstructor.h @@ -24,11 +24,6 @@ protected: void initializeObject(HillFort * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/RandomMapInfo.h b/lib/mapObjectConstructors/RandomMapInfo.h index 5bb98daf6..fe5dc2bd3 100644 --- a/lib/mapObjectConstructors/RandomMapInfo.h +++ b/lib/mapObjectConstructors/RandomMapInfo.h @@ -33,14 +33,6 @@ struct DLL_LINKAGE RandomMapInfo {} void setMapLimit(ui32 val) { mapLimit = val; } - - template void serialize(Handler &h, const int version) - { - h & value; - h & mapLimit; - h & zoneLimit; - h & rarity; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/SObjectSounds.h b/lib/mapObjectConstructors/SObjectSounds.h index ca1b8fef9..5f94cd44a 100644 --- a/lib/mapObjectConstructors/SObjectSounds.h +++ b/lib/mapObjectConstructors/SObjectSounds.h @@ -9,20 +9,15 @@ */ #pragma once +#include "../filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN struct SObjectSounds { - std::vector ambient; - std::vector visit; - std::vector removal; - - template void serialize(Handler &h, const int version) - { - h & ambient; - h & visit; - h & removal; - } + std::vector ambient; + std::vector visit; + std::vector removal; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp b/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp index 20052b57f..cbbb57541 100644 --- a/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp @@ -11,7 +11,7 @@ #include "ShipyardInstanceConstructor.h" #include "../mapObjects/MiscObjects.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,7 +22,7 @@ void ShipyardInstanceConstructor::initTypeData(const JsonNode & config) void ShipyardInstanceConstructor::initializeObject(CGShipyard * shipyard) const { - shipyard->createdBoat = BoatId(*VLC->modh->identifiers.getIdentifier("core:boat", parameters["boat"])); + shipyard->createdBoat = BoatId(*VLC->identifiers()->getIdentifier("core:boat", parameters["boat"])); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h index c0d47be4f..845bf77b0 100644 --- a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h +++ b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h @@ -24,11 +24,6 @@ protected: void initializeObject(CGShipyard * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp b/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp deleted file mode 100644 index 84ef806e8..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp +++ /dev/null @@ -1,42 +0,0 @@ -/* -* ShrineInstanceConstructor.cpp, part of VCMI engine -* -* Authors: listed in file AUTHORS in main folder -* -* License: GNU General Public License v2.0 or later -* Full text of license available in license.txt file, in main folder -* -*/ -#include "StdInc.h" -#include "ShrineInstanceConstructor.h" - -#include "../mapObjects/MiscObjects.h" -#include "../JsonRandom.h" -#include "../IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void ShrineInstanceConstructor::initTypeData(const JsonNode & config) -{ - parameters = config; -} - -void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenerator & rng) const -{ - auto visitTextParameter = parameters["visitText"]; - - if (visitTextParameter.isNumber()) - shrine->visitText.appendLocalString(EMetaText::ADVOB_TXT, static_cast(visitTextParameter.Float())); - else - shrine->visitText.appendRawString(visitTextParameter.String()); - - if(shrine->spell == SpellID::NONE) // shrine has no predefined spell - { - std::vector possibilities; - shrine->cb->getAllowedSpells(possibilities); - - shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, possibilities); - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.h b/lib/mapObjectConstructors/ShrineInstanceConstructor.h deleted file mode 100644 index 0ceb22690..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -* ShrineInstanceConstructor.h, part of VCMI engine -* -* Authors: listed in file AUTHORS in main folder -* -* License: GNU General Public License v2.0 or later -* Full text of license available in license.txt file, in main folder -* -*/ -#pragma once - -#include "CDefaultObjectTypeHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGShrine; - -class ShrineInstanceConstructor final : public CDefaultObjectTypeHandler -{ - JsonNode parameters; - -protected: - void initTypeData(const JsonNode & config) override; - void randomizeObject(CGShrine * object, CRandomGenerator & rng) const override; - -public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index fc54bb44a..6c588df7b 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN -void CArmedInstance::randomizeArmy(int type) +void CArmedInstance::randomizeArmy(FactionID type) { for (auto & elem : stacks) { @@ -59,7 +59,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, -1); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, BonusSourceID()); addNewBonus(b); } @@ -73,7 +73,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(const auto & slot : Slots()) { const CStackInstance * inst = slot.second; - const CCreature * creature = VLC->creh->objects[inst->getCreatureID()]; + const auto * creature = inst->getCreatureID().toEntity(VLC); factions.insert(creature->getFaction()); // Check for undead flag instead of faction (undead mummies are neutral) @@ -92,7 +92,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(auto f : factions) { - if (VLC->factions()->getByIndex(f)->getAlignment() != EAlignment::EVIL) + if (VLC->factions()->getById(f)->getAlignment() != EAlignment::EVIL) mixableFactions++; } if (mixableFactions > 0) @@ -111,7 +111,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() { b->val = 2 - static_cast(factionsInArmy); description = boost::str(boost::format(VLC->generaltexth->arraytxt[114]) % factionsInArmy % b->val); //Troops of %d alignments %d - description = b->description.substr(0, description.size()-2);//trim value + description = description.substr(0, description.size()-3);//trim value } boost::algorithm::trim(description); @@ -120,13 +120,12 @@ void CArmedInstance::updateMoraleBonusFromArmy() CBonusSystemNode::treeHasChanged(); //-1 modifier for any Undead unit in army - const ui8 UNDEAD_MODIFIER_ID = -2; - auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, UNDEAD_MODIFIER_ID)); + auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, BonusCustomSource::undeadMoraleDebuff)); if(hasUndead) { if(!undeadModifier) { - undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, UNDEAD_MODIFIER_ID, VLC->generaltexth->arraytxt[116]); + undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, BonusCustomSource::undeadMoraleDebuff, VLC->generaltexth->arraytxt[116]); undeadModifier->description = undeadModifier->description.substr(0, undeadModifier->description.size()-2);//trim value addNewBonus(undeadModifier); } @@ -143,7 +142,7 @@ void CArmedInstance::armyChanged() CBonusSystemNode & CArmedInstance::whereShouldBeAttached(CGameState * gs) { - if(tempOwner < PlayerColor::PLAYER_LIMIT) + if(tempOwner.isValidPlayer()) if(auto * where = gs->getPlayerState(tempOwner)) return *where; @@ -160,4 +159,10 @@ const IBonusBearer* CArmedInstance::getBonusBearer() const return this; } +void CArmedInstance::serializeJsonOptions(JsonSerializeFormat & handler) +{ + CGObjectInstance::serializeJsonOptions(handler); + CCreatureSet::serializeJson(handler, "army", 7); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.h b/lib/mapObjects/CArmedInstance.h index e14447c55..95cb59d43 100644 --- a/lib/mapObjects/CArmedInstance.h +++ b/lib/mapObjects/CArmedInstance.h @@ -18,6 +18,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BattleInfo; class CGameState; +class JsonSerializeFormat; class DLL_LINKAGE CArmedInstance: public CGObjectInstance, public CBonusSystemNode, public CCreatureSet, public IConstBonusProvider { @@ -28,7 +29,7 @@ private: public: BattleInfo *battle; //set to the current battle, if engaged - void randomizeArmy(int type); + void randomizeArmy(FactionID type); virtual void updateMoraleBonusFromArmy(); void armyChanged() override; @@ -48,6 +49,8 @@ public: { return this->tempOwner; } + + void serializeJsonOptions(JsonSerializeFormat & handler) override; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index b2c1d3261..46661af86 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -14,12 +14,17 @@ #include #include -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../GameSettings.h" +#include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/Component.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../MetaString.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" @@ -41,7 +46,7 @@ void CBank::initObj(CRandomGenerator & rand) { daycounter = 0; resetDuration = 0; - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } bool CBank::isCoastVisitable() const @@ -51,8 +56,36 @@ bool CBank::isCoastVisitable() const std::string CBank::getHoverText(PlayerColor player) const { - // TODO: record visited players - return getObjectName() + " " + visitedTxt(bc == nullptr); + if (!wasVisited(player)) + return getObjectName(); + + return getObjectName() + "\n" + visitedTxt(bc == nullptr); +} + +std::vector CBank::getPopupComponents(PlayerColor player) const +{ + if (!wasVisited(player)) + return {}; + + if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + return {}; + + if (bc == nullptr) + 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; } void CBank::setConfig(const BankConfig & config) @@ -64,12 +97,12 @@ void CBank::setConfig(const BankConfig & config) setCreature (SlotID(stacksCount()), stack.type->getId(), stack.count); } -void CBank::setPropertyDer (ui8 what, ui32 val) +void CBank::setPropertyDer (ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::BANK_DAYCOUNTER: //daycounter - daycounter+=val; + daycounter+= identifier.getNum(); break; case ObjProperty::BANK_RESET: // FIXME: Object reset must be done by separate netpack from server @@ -89,22 +122,25 @@ void CBank::newTurn(CRandomGenerator & rand) const if (resetDuration != 0) { if (daycounter >= resetDuration) - cb->setObjProperty (id, ObjProperty::BANK_RESET, 0); //daycounter 0 + cb->setObjPropertyValue(id, ObjProperty::BANK_RESET); //daycounter 0 else - cb->setObjProperty (id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ + cb->setObjPropertyValue(id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ } } } bool CBank::wasVisited (PlayerColor player) const { - return !bc; //FIXME: player A should not know about visit done by player B + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); } void CBank::onHeroVisit(const CGHeroInstance * h) const { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + int banktext = 0; - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: banktext = 41; @@ -130,28 +166,10 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const bd.player = h->getOwner(); bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); + bd.components = getPopupComponents(h->getOwner()); if (banktext == 32) bd.text.replaceRawString(getObjectName()); - if (VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) - { - std::map guardsAmounts; - - for (auto const & slot : Slots()) - if (slot.second) - guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); - - for (auto const & guard : guardsAmounts) - { - Component comp; - comp.id = Component::EComponentType::CREATURE; - comp.subtype = guard.first.getNum(); - comp.val = guard.second; - - bd.components.push_back(comp); - } - } - cb->showBlockingDialog(&bd); } @@ -165,7 +183,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const if (bc) { - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: textID = 43; @@ -188,20 +206,20 @@ void CBank::doVisit(const CGHeroInstance * hero) const } else { - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: case Obj::DERELICT_SHIP: case Obj::CRYPT: { GiveBonus gbonus; - gbonus.id = hero->id.getNum(); + gbonus.id = hero->id; gbonus.bonus.duration = BonusDuration::ONE_BATTLE; - gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = ID; + gbonus.bonus.source = BonusSource::OBJECT_TYPE; + gbonus.bonus.sid = BonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: textID = 123; @@ -217,18 +235,18 @@ void CBank::doVisit(const CGHeroInstance * hero) const break; } cb->giveHeroBonus(&gbonus); - iw.components.emplace_back(Component::EComponentType::MORALE, 0, -1, 0); + iw.components.emplace_back(ComponentType::MORALE, -1); iw.soundID = soundBase::GRAVEYARD; break; } case Obj::PYRAMID: { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, id.getNum(), VLC->generaltexth->arraytxt[70]); - gb.id = hero->id.getNum(); + gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); + gb.id = hero->id; cb->giveHeroBonus(&gb); textID = 107; - iw.components.emplace_back(Component::EComponentType::LUCK, 0, -2, 0); + iw.components.emplace_back(ComponentType::LUCK, -2); break; } case Obj::CREATURE_BANK: @@ -248,24 +266,24 @@ void CBank::doVisit(const CGHeroInstance * hero) const //grant resources if (bc) { - for (int it = 0; it < bc->resources.size(); it++) + for (GameResID it : GameResID::ALL_RESOURCES()) { if (bc->resources[it] != 0) { - iw.components.emplace_back(Component::EComponentType::RESOURCE, it, bc->resources[it], 0); + iw.components.emplace_back(ComponentType::RESOURCE, it, bc->resources[it]); loot.appendRawString("%d %s"); - loot.replaceNumber(iw.components.back().val); - loot.replaceLocalString(EMetaText::RES_NAMES, iw.components.back().subtype); - cb->giveResource(hero->getOwner(), static_cast(it), bc->resources[it]); + loot.replaceNumber(bc->resources[it]); + loot.replaceName(it); + cb->giveResource(hero->getOwner(), it, bc->resources[it]); } } //grant artifacts for (auto & elem : bc->artifacts) { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE); + loot.replaceName(elem); + cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } //display loot if (!iw.components.empty()) @@ -278,7 +296,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const return a.type->getFightValue() < b.type->getFightValue(); })->type; - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, strongest->getId()); + iw.text.replaceNamePlural(strongest->getId()); iw.text.replaceRawString(loot.buildList()); } cb->showInfoDialog(&iw); @@ -299,14 +317,14 @@ void CBank::doVisit(const CGHeroInstance * hero) const } for(const SpellID & spellId : bc->spells) { - const auto * spell = spellId.toSpell(VLC->spells()); - iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId); + const auto * spell = spellId.toEntity(VLC); + iw.text.appendName(spellId); if(spell->getLevel() <= hero->maxSpellLevel()) { if(hero->canLearnSpell(spell)) { spells.insert(spellId); - iw.components.emplace_back(Component::EComponentType::SPELL, spellId, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, spellId); } } else @@ -337,9 +355,9 @@ void CBank::doVisit(const CGHeroInstance * hero) const for(const auto & elem : ourArmy.Slots()) { - iw.components.emplace_back(*elem.second); + iw.components.emplace_back(ComponentType::CREATURE, elem.second->getId(), elem.second->getCount()); loot.appendRawString("%s"); - loot.replaceCreatureName(*elem.second); + loot.replaceName(*elem.second); } if(ourArmy.stacksCount()) @@ -354,7 +372,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const cb->showInfoDialog(&iw); cb->giveCreatures(this, hero, ourArmy, false); } - cb->setObjProperty(id, ObjProperty::BANK_CLEAR, 0); //bc = nullptr + cb->setObjPropertyValue(id, ObjProperty::BANK_CLEAR); //bc = nullptr } } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 7d62f30a2..99bb4fdd7 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -23,7 +23,7 @@ class DLL_LINKAGE CBank : public CArmedInstance ui32 resetDuration; bool coastVisitable; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void doVisit(const CGHeroInstance * hero) const; public: @@ -41,6 +41,8 @@ public: void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + std::vector getPopupComponents(PlayerColor player) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index a5ecd1b60..f76915582 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -10,12 +10,15 @@ #include "StdInc.h" #include "CGCreature.h" +#include "CGHeroInstance.h" -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CConfigHandler.h" #include "../GameSettings.h" #include "../IGameCallback.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/StackLocation.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,11 +28,10 @@ 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(), subID, id.getNum()); + logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), getCreature(), id.getNum()); return "INVALID_STACK"; } - std::string hoverName; MetaString ms; CCreature::CreatureQuantityId monsterQuantityId = stacks.begin()->second->getQuantityID(); int quantityTextIndex = 172 + 3 * (int)monsterQuantityId; @@ -38,21 +40,34 @@ std::string CGCreature::getHoverText(PlayerColor player) const else ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); - hoverName = ms.toString(); - return hoverName; + ms.appendNamePlural(getCreature()); + + return ms.toString(); } std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { - std::string hoverName; - if(hero->hasVisions(this, 0)) + if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); + ms.appendName(getCreature(), stacks.begin()->second->count); + return ms.toString(); + } + else + { + return getHoverText(hero->tempOwner); + } +} +std::string CGCreature::getPopupText(const CGHeroInstance * hero) const +{ + std::string hoverName; + if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) + { + MetaString ms; + ms.appendRawString(getHoverText(hero)); ms.appendRawString("\n\n"); int decision = takenAction(hero, true); @@ -69,10 +84,10 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const ms.appendLocalString(EMetaText::GENERAL_TXT,243); break; default: //decision = cost in gold - ms.appendRawString(boost::to_string(boost::format(VLC->generaltexth->allTexts[244]) % decision)); + ms.appendLocalString(EMetaText::GENERAL_TXT,244); + ms.replaceNumber(decision); break; } - hoverName = ms.toString(); } else @@ -80,29 +95,54 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const hoverName = getHoverText(hero->tempOwner); } - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); + if (settings["general"]["enableUiEnhancements"].Bool()) + { + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); - int choice; - double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); - if (ratio < 0.1) choice = 0; - else if (ratio < 0.25) choice = 1; - else if (ratio < 0.6) choice = 2; - else if (ratio < 0.9) choice = 3; - else if (ratio < 1.1) choice = 4; - else if (ratio < 1.3) choice = 5; - else if (ratio < 1.8) choice = 6; - else if (ratio < 2.5) choice = 7; - else if (ratio < 4) choice = 8; - else if (ratio < 8) choice = 9; - else if (ratio < 20) choice = 10; - else choice = 11; + int choice; + double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); + if (ratio < 0.1) choice = 0; + else if (ratio < 0.25) choice = 1; + else if (ratio < 0.6) choice = 2; + else if (ratio < 0.9) choice = 3; + else if (ratio < 1.1) choice = 4; + else if (ratio < 1.3) choice = 5; + else if (ratio < 1.8) choice = 6; + else if (ratio < 2.5) choice = 7; + else if (ratio < 4) choice = 8; + else if (ratio < 8) choice = 9; + else if (ratio < 20) choice = 10; + else choice = 11; - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + } return hoverName; } +std::string CGCreature::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} + +std::vector CGCreature::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::CREATURE, getCreature()) + }; +} + void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { + //show message + if(!message.empty()) + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text = message; + iw.type = EInfoWindowMode::MODAL; + cb->showInfoDialog(&iw); + } + int action = takenAction(h); switch( action ) //decide what we do... { @@ -119,7 +159,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); break; } @@ -133,13 +173,50 @@ 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->creh->objects[subID]->getNamePluralTranslated()); + boost::algorithm::replace_first(tmp,"%s",VLC->creatures()->getById(getCreature())->getNamePluralTranslated()); ynd.text.appendRawString(tmp); cb->showBlockingDialog(&ynd); break; } } +} +CreatureID CGCreature::getCreature() const +{ + return CreatureID(getObjTypeIndex().getNum()); +} + +void CGCreature::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID.toEnum()) + { + case MapObjectID::RANDOM_MONSTER: + subID = VLC->creh->pickRandomMonster(rand); + break; + case MapObjectID::RANDOM_MONSTER_L1: + subID = VLC->creh->pickRandomMonster(rand, 1); + break; + case MapObjectID::RANDOM_MONSTER_L2: + subID = VLC->creh->pickRandomMonster(rand, 2); + break; + case MapObjectID::RANDOM_MONSTER_L3: + subID = VLC->creh->pickRandomMonster(rand, 3); + break; + case MapObjectID::RANDOM_MONSTER_L4: + subID = VLC->creh->pickRandomMonster(rand, 4); + break; + case MapObjectID::RANDOM_MONSTER_L5: + subID = VLC->creh->pickRandomMonster(rand, 5); + break; + case MapObjectID::RANDOM_MONSTER_L6: + subID = VLC->creh->pickRandomMonster(rand, 6); + break; + case MapObjectID::RANDOM_MONSTER_L7: + subID = VLC->creh->pickRandomMonster(rand, 7); + break; + } + ID = MapObjectID::MONSTER; + setType(ID, subID); } void CGCreature::initObj(CRandomGenerator & rand) @@ -164,16 +241,16 @@ void CGCreature::initObj(CRandomGenerator & rand) break; } - stacks[SlotID(0)]->setType(CreatureID(subID)); + stacks[SlotID(0)]->setType(getCreature()); TQuantity &amount = stacks[SlotID(0)]->count; - CCreature &c = *VLC->creh->objects[subID]; + const Creature * c = VLC->creatures()->getById(getCreature()); if(amount == 0) { - amount = rand.nextInt(c.ammMin, c.ammMax); + amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax()); if(amount == 0) //armies with 0 creatures are illegal { - logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName()); + logGlobal->warn("Stack cannot have 0 creatures. Check properties of %s", c->getJsonKey()); amount = 1; } } @@ -189,31 +266,28 @@ void CGCreature::newTurn(CRandomGenerator & rand) const 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) { ui32 power = static_cast(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100); - cb->setObjProperty(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, power); //increase temppower + cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->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->setObjProperty(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose + cb->setObjPropertyValue(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose } -void CGCreature::setPropertyDer(ui8 what, ui32 val) +void CGCreature::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::MONSTER_COUNT: - stacks[SlotID(0)]->count = val; + stacks[SlotID(0)]->count = identifier.getNum(); break; case ObjProperty::MONSTER_POWER: - temppower = val; + temppower = identifier.getNum(); break; case ObjProperty::MONSTER_EXP: - giveStackExp(val); - break; - case ObjProperty::MONSTER_RESTORE_TYPE: - formation.basicType = val; + giveStackExp(identifier.getNum()); break; case ObjProperty::MONSTER_REFUSED_JOIN: - refusedJoining = val; + refusedJoining = identifier.getNum(); break; } } @@ -239,7 +313,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const powerFactor = -3; std::set myKindCres; //what creatures are the same kind as we - const CCreature * myCreature = VLC->creh->objects[subID]; + const CCreature * myCreature = VLC->creh->objects[getCreature().getNum()]; myKindCres.insert(myCreature->getId()); //we myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades @@ -277,7 +351,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const return JOIN_FOR_FREE; else if(diplomacy * 2 + sympathy + 1 >= character) - return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold + return VLC->creatures()->getById(getCreature())->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold } //we are still here - creatures have not joined hero, flee or fight @@ -291,7 +365,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const { if(refusedJoining) - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, false); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, false); if(pursue) { @@ -299,7 +373,7 @@ void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const } else { - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } } @@ -309,7 +383,7 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co { if(takenAction(h,false) == FLEE) { - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, true); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, true); flee(h); } else //they fight @@ -344,10 +418,6 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co void CGCreature::fight( const CGHeroInstance *h ) const { //split stacks - //TODO: multiple creature types in a stack? - int basicType = stacks.begin()->second->type->getId(); - cb->setObjProperty(id, ObjProperty::MONSTER_RESTORE_TYPE, basicType); //store info about creature stack - int stacksCount = getNumberOfStacks(h); //source: http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266335#focus @@ -377,7 +447,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const if(!upgrades.empty()) { auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault()); - cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]); + cb->changeStackType(StackLocation(this, slotID), it->toCreature()); } } } @@ -391,7 +461,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); } @@ -400,18 +470,18 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & if(result.winner == 0) { giveReward(hero); - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else if(result.winner > 1) // draw { // guarded reward is lost forever on draw - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else { //merge stacks into one TSlots::const_iterator i; - CCreature * cre = VLC->creh->objects[formation.basicType]; + const CCreature * cre = getCreature().toCreature(); for(i = stacks.begin(); i != stacks.end(); i++) { if(cre->isMyUpgrade(i->second->type)) @@ -436,7 +506,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & cb->moveStack(StackLocation(this, i->first), StackLocation(this, slot), i->second->count); } - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties + cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties } } @@ -513,17 +583,17 @@ void CGCreature::giveReward(const CGHeroInstance * h) const if(!resources.empty()) { cb->giveResources(h->tempOwner, resources); - for(int i = 0; i < resources.size(); i++) + for(auto const & res : GameResID::ALL_RESOURCES()) { - if(resources[i] > 0) - iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0); + if(resources[res] > 0) + iw.components.emplace_back(ComponentType::RESOURCE, res, resources[res]); } } if(gainedArtifact != ArtifactID::NONE) { - cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE); - iw.components.emplace_back(Component::EComponentType::ARTIFACT, gainedArtifact, 0, 0); + cb->giveHeroNewArtifact(h, gainedArtifact.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); + iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact); } if(!iw.components.empty()) @@ -568,7 +638,7 @@ void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeBool("noGrowing", notGrowingTeam); handler.serializeBool("neverFlees", neverFlees); - handler.serializeString("rewardMessage", message); + handler.serializeStruct("rewardMessage", message); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 76df95aed..4d33ecdf3 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -11,6 +11,7 @@ #include "CArmedInstance.h" #include "../ResourceSet.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,12 +23,12 @@ public: }; enum Character { - COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 + COMPLIANT = 0, FRIENDLY = 1, AGGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 }; ui32 identifier; //unique code for this monster (used in missions) si8 character; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage) - std::string message; //message printed for attacking hero + MetaString message; //message printed for attacking hero TResources resources; // resources given to hero that has won with monsters ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none bool neverFlees; //if true, the troops will never flee @@ -39,26 +40,20 @@ public: void onHeroVisit(const CGHeroInstance * h) const 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; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void newTurn(CRandomGenerator & 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; //stack formation depends on position, bool containsUpgradedStack() const; int getNumberOfStacks(const CGHeroInstance *hero) const; - struct DLL_LINKAGE formationInfo // info about merging stacks after battle back into one - { - si32 basicType; - ui8 upgrade; //random seed used to determine number of stacks and is there's upgraded stack - template void serialize(Handler &h, const int version) - { - h & basicType; - h & upgrade; - } - } formation; - template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -74,7 +69,7 @@ public: h & formation; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 3253e3e36..4eeaad6e5 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -11,53 +11,27 @@ #include "StdInc.h" #include "CGDwelling.h" #include "../serializer/JsonSerializeFormat.h" +#include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/DwellingInstanceConstructor.h" +#include "../mapObjects/CGHeroInstance.h" +#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 "../NetPacks.h" #include "../GameSettings.h" #include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN -CSpecObjInfo::CSpecObjInfo(): - owner(nullptr) -{ - -} - -void CCreGenAsCastleInfo::serializeJson(JsonSerializeFormat & handler) +void CGDwellingRandomizationInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("sameAsTown", instanceId); - - if(!handler.saving) - { - asCastle = !instanceId.empty(); - allowedFactions.clear(); - } - - if(!asCastle) - { - std::vector standard; - standard.resize(VLC->townh->size(), true); - - JsonSerializeFormat::LIC allowedLIC(standard, &FactionID::decode, &FactionID::encode); - allowedLIC.any = allowedFactions; - - handler.serializeLIC("allowedFactions", allowedLIC); - - if(!handler.saving) - { - allowedFactions = allowedLIC.any; - } - } -} - -void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) -{ + handler.serializeIdArray("allowedFactions", allowedFactions); handler.serializeInt("minLevel", minLevel, static_cast(1)); handler.serializeInt("maxLevel", maxLevel, static_cast(7)); @@ -69,30 +43,139 @@ void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) } } -void CCreGenLeveledCastleInfo::serializeJson(JsonSerializeFormat & handler) +CGDwelling::CGDwelling() = default; +CGDwelling::~CGDwelling() = default; + +FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) { - CCreGenAsCastleInfo::serializeJson(handler); - CCreGenLeveledInfo::serializeJson(handler); + if (ID == Obj::RANDOM_DWELLING_FACTION) + return FactionID(subID.getNum()); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL); + assert(randomizationInfo.has_value()); + if (!randomizationInfo) + return FactionID::CASTLE; + + CGTownInstance * linkedTown = nullptr; + + if (!randomizationInfo->instanceId.empty()) + { + auto iter = cb->gameState()->map->instanceNames.find(randomizationInfo->instanceId); + + if(iter == cb->gameState()->map->instanceNames.end()) + logGlobal->error("Map object not found: %s", randomizationInfo->instanceId); + linkedTown = dynamic_cast(iter->second.get()); + } + + if (randomizationInfo->identifier != 0) + { + for(auto & elem : cb->gameState()->map->objects) + { + auto town = dynamic_cast(elem.get()); + if(town && town->identifier == randomizationInfo->identifier) + { + linkedTown = town; + break; + } + } + } + + if (linkedTown) + { + if(linkedTown->ID==Obj::RANDOM_TOWN) + linkedTown->pickRandomObject(rand); //we have to randomize the castle first + + assert(linkedTown->ID == Obj::TOWN); + if(linkedTown->ID==Obj::TOWN) + return linkedTown->getFaction(); + } + + if(!randomizationInfo->allowedFactions.empty()) + return *RandomGeneratorUtil::nextItem(randomizationInfo->allowedFactions, rand); + + + std::vector potentialPicks; + + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); } -CGDwelling::CGDwelling() - : info(nullptr) +int CGDwelling::randomizeLevel(CRandomGenerator & rand) { + if (ID == Obj::RANDOM_DWELLING_LVL) + return subID.getNum(); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_FACTION); + assert(randomizationInfo.has_value()); + + if (!randomizationInfo) + return rand.nextInt(1, 7) - 1; + + if(randomizationInfo->minLevel == randomizationInfo->maxLevel) + return randomizationInfo->minLevel - 1; + + return rand.nextInt(randomizationInfo->minLevel, randomizationInfo->maxLevel) - 1; } -CGDwelling::~CGDwelling() +void CGDwelling::pickRandomObject(CRandomGenerator & rand) { - vstd::clear_pointer(info); + if (ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL || ID == Obj::RANDOM_DWELLING_FACTION) + { + FactionID faction = randomizeFaction(rand); + int level = randomizeLevel(rand); + assert(faction != FactionID::NONE && faction != FactionID::NEUTRAL); + assert(level >= 0 && level <= 6); + randomizationInfo.reset(); + + CreatureID cid = (*VLC->townh)[faction]->town->creatures[level][0]; + + //NOTE: this will pick last dwelling with this creature (Mantis #900) + //check for block map equality is better but more complex solution + auto testID = [&](const Obj & primaryID) -> MapObjectSubID + { + auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); + for (MapObjectSubID entry : dwellingIDs) + { + const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); + + if (handler->producesCreature(cid.toCreature())) + return MapObjectSubID(entry); + } + return MapObjectSubID(); + }; + + ID = Obj::CREATURE_GENERATOR1; + subID = testID(Obj::CREATURE_GENERATOR1); + + if (subID == MapObjectSubID()) + { + ID = Obj::CREATURE_GENERATOR4; + subID = testID(Obj::CREATURE_GENERATOR4); + } + + if (subID == MapObjectSubID()) + { + logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); + ID = Obj::CREATURE_GENERATOR4; + subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand); + } + + setType(ID, subID); + } } void CGDwelling::initObj(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); if (getOwner() != PlayerColor::NEUTRAL) cb->gameState()->players[getOwner()].dwellings.emplace_back(this); @@ -118,24 +201,7 @@ void CGDwelling::initObj(CRandomGenerator & rand) } } -void CGDwelling::initRandomObjectInfo() -{ - vstd::clear_pointer(info); - switch(ID) - { - case Obj::RANDOM_DWELLING: info = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: info = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: info = new CCreGenLeveledInfo(); - break; - } - - if(info) - info->owner = this; -} - -void CGDwelling::setPropertyDer(ui8 what, ui32 val) +void CGDwelling::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -148,14 +214,14 @@ void CGDwelling::setPropertyDer(ui8 what, ui32 val) std::vector >* dwellings = &cb->gameState()->players[tempOwner].dwellings; dwellings->erase (std::find(dwellings->begin(), dwellings->end(), this)); } - if (PlayerColor(val) != PlayerColor::NEUTRAL) //can new owner be neutral? - cb->gameState()->players[PlayerColor(val)].dwellings.emplace_back(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); - creatures[0].second[0] = CreatureID(val); + creatures[0].second[0] = identifier.as(); break; } } @@ -168,33 +234,33 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; 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.replaceLocalString(EMetaText::OBJ_NAMES, ID); + iw.text.replaceName(ID); cb->sendAndApply(&iw); return; } - PlayerRelations::PlayerRelations relations = cb->gameState()->getPlayerRelations( h->tempOwner, tempOwner ); + PlayerRelations relations = cb->gameState()->getPlayerRelations( h->tempOwner, tempOwner ); if ( relations == PlayerRelations::ALLIES ) return;//do not allow recruiting or capturing - if( !relations && stacksCount() > 0) //object is guarded, owned by enemy + if(relations == PlayerRelations::ENEMIES && stacksCount() > 0) //object is guarded, owned by enemy { BlockingDialog bd(true,false); bd.player = h->tempOwner; bd.text.appendLocalString(EMetaText::GENERAL_TXT, 421); //Much to your dismay, the %s is guarded by %s %s. Do you wish to fight the guards? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) bd.text.replaceRawString(CCreature::getQuantityRangeStringForId(Slots().begin()->second->getQuantityID())); else bd.text.replaceLocalString(EMetaText::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3); - bd.text.replaceCreatureName(*Slots().begin()->second); + bd.text.replaceName(*Slots().begin()->second); cb->showBlockingDialog(&bd); return; } // TODO this shouldn't be hardcoded - if(!relations && ID != Obj::WAR_MACHINE_FACTORY && ID != Obj::REFUGEE_CAMP) + if(relations == PlayerRelations::ENEMIES && ID != Obj::WAR_MACHINE_FACTORY && ID != Obj::REFUGEE_CAMP) { cb->setOwner(this, h->tempOwner); } @@ -204,16 +270,16 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const if(ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR4) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, ID == Obj::CREATURE_GENERATOR1 ? 35 : 36); //{%s} Would you like to recruit %s? / {%s} Would you like to recruit %s, %s, %s, or %s? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::REFUGEE_CAMP) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, 35); //{%s} Would you like to recruit %s? - bd.text.replaceLocalString(EMetaText::OBJ_NAMES, ID); + bd.text.replaceName(ID); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::WAR_MACHINE_FACTORY) bd.text.appendLocalString(EMetaText::ADVOB_TXT, 157); //{War Machine Factory} Would you like to purchase War Machines? @@ -234,7 +300,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const if(ID == Obj::REFUGEE_CAMP) //if it's a refugee camp, we need to pick an available creature { - cb->setObjProperty(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); + cb->setObjPropertyID(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); } bool change = false; @@ -253,7 +319,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const else creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); - CCreature *cre = VLC->creh->objects[creatures[i].second[0]]; + 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); if (creaturesAccumulate && ID != Obj::REFUGEE_CAMP) //camp should not try to accumulate different kinds of creatures sac.creatures[i].first += amount; @@ -269,6 +335,30 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const updateGuards(); } +std::vector CGDwelling::getPopupComponents(PlayerColor player) const +{ + if (getOwner() != player) + return {}; + + std::vector result; + + if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty()) + { + for (auto const & creature : creatures.front().second) + result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + } + + if (ID == Obj::CREATURE_GENERATOR4) + { + for (auto const & creatureLevel : creatures) + { + if (!creatureLevel.second.empty()) + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + } + } + return result; +} + void CGDwelling::updateGuards() const { //TODO: store custom guard config and use it @@ -278,7 +368,7 @@ void CGDwelling::updateGuards() const //default condition - creatures are of level 5 or higher for (auto creatureEntry : creatures) { - if (VLC->creatures()->getByIndex(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) + if (VLC->creatures()->getById(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) { guarded = true; break; @@ -289,7 +379,7 @@ void CGDwelling::updateGuards() const { for (auto creatureEntry : creatures) { - const CCreature * crea = VLC->creh->objects[creatureEntry.second.at(0)]; + const CCreature * crea = creatureEntry.second.at(0).toCreature(); SlotID slot = getSlotFor(crea->getId()); if (hasStackAtSlot(slot)) //stack already exists, overwrite it @@ -346,7 +436,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 425);//The %s would join your hero, but there aren't enough provisions to support them. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); } else //give creatures @@ -362,7 +452,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 423); //%d %s join your army. iw.text.replaceNumber(count); - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); cb->sendAndApply(&sac); @@ -374,7 +464,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 422); //There are no %s here to recruit. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); iw.player = h->tempOwner; cb->sendAndApply(&iw); } @@ -393,13 +483,8 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const cb->sendAndApply(&sac); } - OpenWindow ow; - ow.id1 = id.getNum(); - ow.id2 = h->id.getNum(); - ow.window = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) - ? EOpenWindowMode::RECRUITMENT_FIRST - : EOpenWindowMode::RECRUITMENT_ALL; - cb->sendAndApply(&ow); + auto windowMode = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) ? EOpenWindowMode::RECRUITMENT_FIRST : EOpenWindowMode::RECRUITMENT_ALL; + cb->showObjectWindow(this, windowMode, h, true); } } @@ -427,10 +512,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) { - if(!handler.saving) - initRandomObjectInfo(); - - switch (ID) + switch (ID.toEnum()) { case Obj::WAR_MACHINE_FACTORY: case Obj::REFUGEE_CAMP: @@ -439,8 +521,10 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) case Obj::RANDOM_DWELLING: case Obj::RANDOM_DWELLING_LVL: case Obj::RANDOM_DWELLING_FACTION: - info->serializeJson(handler); - //fall through + if (!handler.saving) + randomizationInfo = CGDwellingRandomizationInfo(); + randomizationInfo->serializeJson(handler); + [[fallthrough]]; default: serializeJsonOwner(handler); break; diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index f27cf44e2..09fc96ca4 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -16,68 +16,46 @@ VCMI_LIB_NAMESPACE_BEGIN class CGDwelling; -class DLL_LINKAGE CSpecObjInfo +class DLL_LINKAGE CGDwellingRandomizationInfo { public: - CSpecObjInfo(); - virtual ~CSpecObjInfo() = default; - - virtual void serializeJson(JsonSerializeFormat & handler) = 0; - - const CGDwelling * owner; -}; - -class DLL_LINKAGE CCreGenAsCastleInfo : public virtual CSpecObjInfo -{ -public: - bool asCastle = false; - ui32 identifier = 0;//h3m internal identifier - - std::vector allowedFactions; + std::set allowedFactions; std::string instanceId;//vcmi map instance identifier - void serializeJson(JsonSerializeFormat & handler) override; + int32_t identifier = 0;//h3m internal identifier + + uint8_t minLevel = 1; + uint8_t maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> + + void serializeJson(JsonSerializeFormat & handler); }; -class DLL_LINKAGE CCreGenLeveledInfo : public virtual CSpecObjInfo -{ -public: - ui8 minLevel = 1; - ui8 maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> - - void serializeJson(JsonSerializeFormat & handler) override; -}; - -class DLL_LINKAGE CCreGenLeveledCastleInfo : public CCreGenAsCastleInfo, public CCreGenLeveledInfo -{ -public: - CCreGenLeveledCastleInfo() = default; - void serializeJson(JsonSerializeFormat & handler) override; -}; - - class DLL_LINKAGE CGDwelling : public CArmedInstance { public: typedef std::vector > > TCreaturesSet; - CSpecObjInfo * info; //random dwelling options; not serialized + std::optional randomizationInfo; //random dwelling options; not serialized TCreaturesSet creatures; //creatures[level] -> CGDwelling(); ~CGDwelling() override; - void initRandomObjectInfo(); protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; private: + FactionID randomizeFaction(CRandomGenerator & rand); + int randomizeLevel(CRandomGenerator & rand); + + void pickRandomObject(CRandomGenerator & rand) override; void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; - void setPropertyDer(ui8 what, ui32 val) 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; + std::vector getPopupComponents(PlayerColor player) const override; void updateGuards() const; void heroAcceptsCreatures(const CGHeroInstance *h) const; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 4b77c2546..4037cec39 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -14,14 +14,12 @@ #include #include -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../ArtifactUtils.h" #include "../CHeroHandler.h" #include "../TerrainHandler.h" #include "../RoadHandler.h" #include "../GameSettings.h" -#include "../CModHandler.h" #include "../CSoundBase.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" @@ -35,11 +33,36 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" -#include "../StringConstants.h" +#include "../modding/ModScope.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../constants/StringConstants.h" #include "../battle/Unit.h" +#include "CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN +void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) +{ + serializeJsonOwner(handler); + + bool isHeroType = heroType.has_value(); + handler.serializeBool("placeholderType", isHeroType, false); + + if(!handler.saving) + { + if(isHeroType) + heroType = HeroTypeID::NONE; + else + powerRank = 0; + } + + if(isHeroType) + handler.serializeId("heroType", heroType.value(), HeroTypeID::NONE); + else + handler.serializeInt("powerRank", powerRank.value()); +} + static int lowestSpeed(const CGHeroInstance * chi) { static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED); @@ -76,7 +99,7 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain } else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus - !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, from.terType->getIndex())) //no special movement bonus + !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.terType->getId()))) //no special movement bonus { ret = VLC->terrainTypeHandler->getById(from.terType->getId())->moveCost; @@ -189,13 +212,16 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const if ( !canLearnSkill()) return false; - if (!cb->isAllowed(2, which)) + if (!cb->isAllowed(which)) return false; if (getSecSkillLevel(which) > 0) return false; - if (type->heroClass->secSkillProbability[which] == 0) + if (type->heroClass->secSkillProbability.count(which) == 0) + return false; + + if (type->heroClass->secSkillProbability.at(which) == 0) return false; return true; @@ -230,14 +256,14 @@ void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) c lowestCreatureSpeed = realLowestSpeed; //Let updaters run again treeHasChanged(); - ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(!!onLand)); + ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea)); } } int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const { updateArmyMovementBonus(onLand, ti); - return ti->valOfBonuses(BonusType::MOVEMENT, !!onLand); + return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea); } CGHeroInstance::CGHeroInstance(): @@ -246,7 +272,6 @@ CGHeroInstance::CGHeroInstance(): moveDir(4), mana(UNINITIALIZED_MANA), movement(UNINITIALIZED_MOVEMENT), - portrait(UNINITIALIZED_PORTRAIT), level(1), exp(UNINITIALIZED_EXPERIENCE), gender(EHeroGender::DEFAULT), @@ -254,7 +279,7 @@ CGHeroInstance::CGHeroInstance(): { setNodeType(HERO); ID = Obj::HERO; - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); blockVisit = true; } @@ -263,27 +288,28 @@ PlayerColor CGHeroInstance::getOwner() const return tempOwner; } +HeroTypeID CGHeroInstance::getHeroType() const +{ + return HeroTypeID(getObjTypeIndex().getNum()); +} + +void CGHeroInstance::setHeroType(HeroTypeID heroType) +{ + assert(type == nullptr); + subID = heroType; +} + void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) { subID = SUBID.getNum(); initHero(rand); } -void CGHeroInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::HERO); // just in case - type = VLC->heroh->objects[subID]; - portrait = type->imageIndex; - CGObjectInstance::setType(ID, type->heroClass->getIndex()); // to find object handler we must use heroClass->id - this->subID = subID; // after setType subID used to store unique hero identify id. Check issue 2277 for details - randomizeArmy(type->heroClass->faction); -} - void CGHeroInstance::initHero(CRandomGenerator & rand) { assert(validTypes(true)); if(!type) - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType().getNum()]; if (ID == Obj::HERO) appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); @@ -301,30 +327,34 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { // hero starts with default spellbook presence status if(!getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) - putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK); + putArtifact(ArtifactPosition::SPELLBOOK, artifact); + } } else spells -= SpellID::SPELLBOOK_PRESET; if(!getArt(ArtifactPosition::MACH4)) - putArtifact(ArtifactPosition::MACH4, ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT)); //everyone has a catapult + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT); + putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult + } - if(portrait < 0 || portrait == 255) - portrait = type->imageIndex; if(!hasBonus(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))) { for(int g=0; g(g), type->heroClass->primarySkillInitial[g]); + pushPrimSkill(static_cast(g), type->heroClass->primarySkillInitial[g]); } } - if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::DEFAULT, -1)) //set secondary skills to default + if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::NONE, -1)) //set secondary skills to default secSkills = type->secSkillsInit; if (gender == EHeroGender::DEFAULT) gender = type->gender; - setFormation(false); + setFormation(EArmyFormation::LOOSE); if (!stacksCount()) //standard army//initial army { initArmy(rand); @@ -353,7 +383,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::HERO_BASE_SKILL; - bonus->sid = id.getNum(); + bonus->sid = BonusSourceID(id); bonus->duration = BonusDuration::PERMANENT; addNewBonus(bonus); } @@ -365,9 +395,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) commander->giveStackExp (exp); //after our exp is set } - skillsInfo.rand.setSeed(rand.nextInt()); - skillsInfo.resetMagicSchoolCounter(); - skillsInfo.resetWisdomCounter(); + skillsInfo = SecondarySkillsInfo(); //copy active (probably growing) bonuses from hero prototype to hero object for(const std::shared_ptr & b : type->specialty) @@ -424,7 +452,10 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) ArtifactPosition slot = art->getPossibleSlots().at(ArtBearer::HERO).front(); if(!getArt(slot)) - putArtifact(slot, ArtifactUtils::createNewArtifactInstance(aid)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(aid); + putArtifact(slot, artifact); + } else logGlobal->warn("Hero %s already has artifact at %d, omitting giving artifact %d", getNameTranslated(), slot.toEnum(), aid.toEnum()); } @@ -456,7 +487,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (ID == Obj::HERO) { - if( cb->gameState()->getPlayerRelations(tempOwner, h->tempOwner)) //our or ally hero + if( cb->gameState()->getPlayerRelations(tempOwner, h->tempOwner) != PlayerRelations::ENEMIES) { //exchange cb->heroExchange(h->id, id); @@ -477,7 +508,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const SetMovePoints smp; smp.hid = id; - cb->setManaPoints (id, manaLimit()); + cb->setManaPoints (id, manaLimit()); ObjectInstanceID boatId; const auto boatPos = visitablePos(); @@ -487,7 +518,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (!boat) { //Create a new boat for hero - cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum()); + cb->createObject(boatPos, h->getOwner(), Obj::BOAT, getBoatType().getNum()); boatId = cb->getTopObj(boatPos)->id; } } @@ -496,7 +527,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const smp.val = movementPointsLimit(true); } cb->giveHero(id, h->tempOwner, boatId); //recreates def and adds hero to player - cb->setObjProperty(id, ObjProperty::ID, Obj::HERO); //set ID to 34 AFTER hero gets correct flag color + cb->setObjPropertyID(id, ObjProperty::ID, Obj(Obj::HERO)); //set ID to 34 AFTER hero gets correct flag color cb->setMovePoints (&smp); h->showInfoDialog(102); @@ -533,31 +564,46 @@ ui8 CGHeroInstance::maxlevelsToWisdom() const CGHeroInstance::SecondarySkillsInfo::SecondarySkillsInfo(): magicSchoolCounter(1), wisdomCounter(1) -{ - rand.setSeed(0); -} +{} void CGHeroInstance::SecondarySkillsInfo::resetMagicSchoolCounter() { - magicSchoolCounter = 1; + magicSchoolCounter = 0; } void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() { - wisdomCounter = 1; + wisdomCounter = 0; +} + +void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO); + + if (ID == Obj::RANDOM_HERO) + { + ID = Obj::HERO; + subID = cb->gameState()->pickNextHeroType(getOwner()); + type = VLC->heroh->objects[getHeroType().getNum()]; + randomizeArmy(type->heroClass->faction); + } + else + type = VLC->heroh->objects[getHeroType().getNum()]; + + auto oldSubID = subID; + + // to find object handler we must use heroClass->id + // after setType subID used to store unique hero identify id. Check issue 2277 for details + if (ID != Obj::PRISON) + setType(ID, type->heroClass->getIndex()); + else + setType(ID, 0); + + this->subID = oldSubID; } void CGHeroInstance::initObj(CRandomGenerator & rand) { - if(!type) - initHero(rand); //TODO: set up everything for prison before specialties are configured - if (ID != Obj::PRISON) - { - auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); - auto customApp = VLC->objtypeh->getHandlerFor(ID, type->heroClass->getIndex())->getOverride(terrain, this); - if (customApp) - appearance = customApp; - } } void CGHeroInstance::recreateSecondarySkillsBonuses() @@ -573,16 +619,16 @@ void CGHeroInstance::recreateSecondarySkillsBonuses() void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val) { - removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, which)); + removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, BonusSourceID(which))); auto skillBonus = (*VLC->skillh)[which]->at(val).effects; for(const auto & b : skillBonus) addNewBonus(std::make_shared(*b)); } -void CGHeroInstance::setPropertyDer( ui8 what, ui32 val ) +void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::PRIMARY_STACK_COUNT) - setStackCount(SlotID(0), val); + setStackCount(SlotID(0), identifier.getNum()); } double CGHeroInstance::getFightingStrength() const @@ -616,23 +662,23 @@ int32_t CGHeroInstance::getCasterUnitId() const return id.getNum(); } -int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { int32_t skill = -1; //skill level - spell->forEachSchool([&, this](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, cnf.id); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; if(outSelectedSchool) - *outSelectedSchool = static_cast(cnf.id); + *outSelectedSchool = cnf; } }); - vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(ESpellSchool::ANY))); //any school bonus - vstd::amax(skill, valOfBonuses(BonusType::SPELL, spell->getIndex())); //given by artifact or other effect + vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); //any school bonus + vstd::amax(skill, valOfBonuses(BonusType::SPELL, BonusSubtypeID(spell->getId()))); //given by artifact or other effect vstd::amax(skill, 0); //in case we don't know any school vstd::amin(skill, 3); @@ -644,28 +690,28 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, //applying sorcery secondary skill if(spell->isMagical()) - base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, SpellSchool(ESpellSchool::ANY))) / 100.0); + base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(SpellSchool::ANY))) / 100.0); - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0); int maxSchoolBonus = 0; - spell->forEachSchool([&maxSchoolBonus, this](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([&maxSchoolBonus, this](const SpellSchool & cnf, bool & stop) { - vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, cnf.id)); + vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(cnf))); }); base = static_cast(base * (100 + maxSchoolBonus) / 100.0); if(affectedStack && affectedStack->creatureLevel() > 0) //Hero specials like Solmyr, Deemer - base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, spell->getIndex()) / affectedStack->creatureLevel()) / 100.0); + base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, BonusSubtypeID(spell->getId())) / affectedStack->creatureLevel()) / 100.0); return base; } int64_t CGHeroInstance::getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const { - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0); return base; } @@ -681,7 +727,11 @@ int32_t CGHeroInstance::getEffectPower(const spells::Spell * spell) const int32_t CGHeroInstance::getEnchantPower(const spells::Spell * spell) const { - return getPrimSkillLevel(PrimarySkill::SPELL_POWER) + valOfBonuses(BonusType::SPELL_DURATION); + int32_t spellpower = getPrimSkillLevel(PrimarySkill::SPELL_POWER); + int32_t durationCommon = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID()); + int32_t durationSpecific = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID(spell->getId())); + + return spellpower + durationCommon + durationSpecific; } int64_t CGHeroInstance::getEffectValue(const spells::Spell * spell) const @@ -707,7 +757,7 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std:: text.appendLocalString(EMetaText::GENERAL_TXT, textIndex); getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } @@ -732,22 +782,22 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const { - const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->getIndex()); + const bool isAllowed = IObjectInterface::cb->isAllowed(spell->getId()); const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook(); - const bool specificBonus = hasBonusOfType(BonusType::SPELL, spell->getIndex()); + const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId())); bool schoolBonus = false; - spell->forEachSchool([this, &schoolBonus](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop) { - if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, cnf.id)) + if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, BonusSubtypeID(cnf))) { schoolBonus = stop = true; } }); - const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, spell->getLevel()); + const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusCustomSubtype::spellLevel(spell->getLevel())); if(spell->isSpecial()) { @@ -773,9 +823,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const } } -bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const +bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const { - if(!hasSpellbook()) + if(!hasSpellbook()) return false; if(spell->getLevel() > maxSpellLevel()) //not enough wisdom @@ -796,7 +846,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const return false;//creature abilities can not be learned } - if(!IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if(!allowBanned && !IObjectInterface::cb->isAllowed(spell->getId())) { logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated()); return false;//banned spells should not be learned @@ -824,18 +874,11 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all... const std::map &casualties = battleResult.casualties[!battleResult.winner]; // figure out what to raise - pick strongest creature meeting requirements - auto creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode + CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode int requiredCasualtyLevel = 1; TConstBonusListPtr improvedNecromancy = getBonuses(Selector::type()(BonusType::IMPROVED_NECROMANCY)); if(!improvedNecromancy->empty()) { - auto getCreatureID = [](const std::shared_ptr & bonus) -> CreatureID - { - assert(bonus->subtype >=0); - if(bonus->subtype >= 0) - return CreatureID(bonus->subtype); - return CreatureID::NONE; - }; int maxCasualtyLevel = 1; for(const auto & casualty : casualties) vstd::amax(maxCasualtyLevel, VLC->creatures()->getByIndex(casualty.first)->getLevel()); @@ -852,9 +895,9 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } else { - auto quality = [getCreatureID](const std::shared_ptr & pick) -> std::tuple + auto quality = [](const std::shared_ptr & pick) -> std::tuple { - const auto * c = getCreatureID(pick).toCreature(); + const auto * c = pick->subtype.as().toCreature(); return std::tuple {c->getLevel(), static_cast(c->getFullRecruitCost().marketValue()), -pick->additionalInfo[1]}; }; if(quality(topPick) < quality(newPick)) @@ -863,7 +906,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } if(topPick) { - creatureTypeRaised = getCreatureID(topPick); + creatureTypeRaised = topPick->subtype.as(); requiredCasualtyLevel = std::max(topPick->additionalInfo[1], 1); } } @@ -871,7 +914,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b // raise upgraded creature (at 2/3 rate) if no space available otherwise if(getSlotFor(creatureTypeRaised) == SlotID()) { - for(const CreatureID & upgraded : VLC->creh->objects[creatureTypeRaised]->upgrades) + for(const CreatureID & upgraded : creatureTypeRaised.toCreature()->upgrades) { if(getSlotFor(upgraded) != SlotID()) { @@ -882,7 +925,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } } // calculate number of creatures raised - low level units contribute at 50% rate - const double raisedUnitHealth = VLC->creh->objects[creatureTypeRaised]->getMaxHealth(); + const double raisedUnitHealth = creatureTypeRaised.toCreature()->getMaxHealth(); double raisedUnits = 0; for(const auto & casualty : casualties) { @@ -909,7 +952,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::pickup01 + rand.nextInt(6); iw.player = tempOwner; - iw.components.emplace_back(raisedStack); + iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count); if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural) { @@ -920,7 +963,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta { iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146); } - iw.text.replaceCreatureName(raisedStack); + iw.text.replaceName(raisedStack); cb->showInfoDialog(&iw); } @@ -997,11 +1040,14 @@ int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const return sp->getCost(getSpellSchoolLevel(sp)); } -void CGHeroInstance::pushPrimSkill( PrimarySkill::PrimarySkill which, int val ) +void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) { - assert(!hasBonus(Selector::typeSubtype(BonusType::PRIMARY_SKILL, which) - .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)))); - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), which)); + auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)) + .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); + if(hasBonus(sel)) + removeBonuses(sel); + + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, BonusSourceID(id), BonusSubtypeID(which))); } EAlignment CGHeroInstance::getAlignment() const @@ -1025,17 +1071,28 @@ si32 CGHeroInstance::manaLimit() const * (valOfBonuses(BonusType::MANA_PER_KNOWLEDGE))); } +HeroTypeID CGHeroInstance::getPortraitSource() const +{ + if (customPortraitSource.isValid()) + return customPortraitSource; + else + return getHeroType(); +} + +int32_t CGHeroInstance::getIconIndex() const +{ + return VLC->heroTypes()->getById(getPortraitSource())->getIconIndex(); +} + std::string CGHeroInstance::getNameTranslated() const { - if (!nameCustom.empty()) - return nameCustom; return VLC->generaltexth->translate(getNameTextID()); } std::string CGHeroInstance::getNameTextID() const { - if (!nameCustom.empty()) - return nameCustom; + if (!nameCustomTextId.empty()) + return nameCustomTextId; if (type) return type->getNameTextID(); @@ -1046,30 +1103,26 @@ std::string CGHeroInstance::getNameTextID() const std::string CGHeroInstance::getBiographyTranslated() const { - if (!biographyCustom.empty()) - return biographyCustom; - return VLC->generaltexth->translate(getBiographyTextID()); } std::string CGHeroInstance::getBiographyTextID() const { - if (!biographyCustom.empty()) - return biographyCustom; + if (!biographyCustomTextId.empty()) + return biographyCustomTextId; if (type) return type->getBiographyTextID(); - - assert(0); - return ""; + + return ""; //for random hero } -void CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance *art) +CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { - assert(art->artType->canBePutAt(this, pos)); + assert(art->canBePutAt(this, pos)); - CArtifactSet::putArtifact(pos, art); if(ArtifactUtils::isSlotEquipment(pos)) attachTo(*art); + return CArtifactSet::putArtifact(pos, art); } void CGHeroInstance::removeArtifact(ArtifactPosition pos) @@ -1108,7 +1161,7 @@ void CGHeroInstance::removeSpellbook() if(hasSpellbook()) { - ArtifactLocation(this, ArtifactPosition(ArtifactPosition::SPELLBOOK)).removeArtifact(); + getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK); } } @@ -1208,113 +1261,86 @@ ArtBearer::ArtBearer CGHeroInstance::bearerType() const return ArtBearer::HERO; } -std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() const +std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const { - std::vector obligatorySkills; //hero is offered magic school or wisdom if possible - auto getObligatorySkills = [](CSkill::Obligatory obl){ - std::vector obligatory = {}; + std::set obligatory; for(auto i = 0; i < VLC->skillh->size(); i++) if((*VLC->skillh)[SecondarySkill(i)]->obligatory(obl)) - obligatory.emplace_back(i); //Always return all obligatory skills + obligatory.insert(i); //Always return all obligatory skills return obligatory; }; - auto selectObligatorySkill = [&](std::vector& ss) -> void + auto intersect = [](const std::set & left, const std::set & right) { - std::shuffle(ss.begin(), ss.end(), skillsInfo.rand.getStdGenerator()); - - for(const auto & skill : ss) - { - if (canLearnSkill(skill)) //only skills hero doesn't know yet - { - obligatorySkills.push_back(skill); - break; //only one - } - } + std::set intersect; + std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), + std::inserter(intersect, intersect.begin())); + return intersect; }; - if (!skillsInfo.wisdomCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MAJOR); - selectObligatorySkill(obligatory); - } - if (!skillsInfo.magicSchoolCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MINOR); - selectObligatorySkill(obligatory); - } + std::set wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR); + std::set schoolList = getObligatorySkills(CSkill::Obligatory::MINOR); - std::vector skills; - //picking sec. skills for choice std::set basicAndAdv; - std::set expert; std::set none; + for(int i = 0; i < VLC->skillh->size(); i++) if (canLearnSkill(SecondarySkill(i))) none.insert(SecondarySkill(i)); for(const auto & elem : secSkills) { - if(elem.second < SecSkillLevel::EXPERT) + if(elem.second < MasteryLevel::EXPERT) basicAndAdv.insert(elem.first); - else - expert.insert(elem.first); none.erase(elem.first); } - for(const auto & s : obligatorySkills) //don't duplicate them - { - none.erase (s); - basicAndAdv.erase (s); - expert.erase (s); - } - //first offered skill: - // 1) give obligatory skill - // 2) give any other new skill - // 3) upgrade existing - if(canLearnSkill() && !obligatorySkills.empty()) - { - skills.push_back (obligatorySkills[0]); - } - else if(!none.empty() && canLearnSkill()) //hero have free skill slot - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //new skill - none.erase(skills.back()); - } - else if(!basicAndAdv.empty()) - { - skills.push_back(type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand)); //upgrade existing - basicAndAdv.erase(skills.back()); - } + bool wantsWisdom = skillsInfo.wisdomCounter + 1 >= maxlevelsToWisdom(); + bool wantsSchool = skillsInfo.magicSchoolCounter + 1 >= maxlevelsToMagicSchool(); - //second offered skill: - //1) upgrade existing - //2) give obligatory skill - //3) give any other new skill - if(!basicAndAdv.empty()) - { - SecondarySkill s = type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand);//upgrade existing - skills.push_back(s); - basicAndAdv.erase(s); - } - else if (canLearnSkill() && obligatorySkills.size() > 1) - { - skills.push_back (obligatorySkills[1]); - } - else if(!none.empty() && canLearnSkill()) - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //give new skill - none.erase(skills.back()); - } + std::vector skills; + + auto chooseSkill = [&](std::set & options) + { + bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty(); + bool selectSchool = wantsSchool && !intersect(options, schoolList).empty(); + SecondarySkill selection; + + if (selectWisdom) + selection = type->heroClass->chooseSecSkill(intersect(options, wisdomList), rand); + else if (selectSchool) + selection = type->heroClass->chooseSecSkill(intersect(options, schoolList), rand); + else + selection = type->heroClass->chooseSecSkill(options, rand); + + skills.push_back(selection); + options.erase(selection); + + if (wisdomList.count(selection)) + wisdomList.clear(); + + if (schoolList.count(selection)) + schoolList.clear(); + }; + + if (!basicAndAdv.empty()) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty()) + chooseSkill(none); + + if (!basicAndAdv.empty() && skills.size() < 2) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty() && skills.size() < 2) + chooseSkill(none); - if (skills.size() == 2) // Fix for #1868 to avoid changing logic (possibly causing bugs in process) - std::swap(skills[0], skills[1]); return skills; } -PrimarySkill::PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const +PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const { assert(gainsLevel()); int randomValue = rand.nextInt(99); @@ -1338,7 +1364,7 @@ PrimarySkill::PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & r randomValue = 100 / GameConstants::PRIMARY_SKILLS; } logGlobal->trace("The hero gets the primary skill %d with a probability of %d %%.", primarySkill, randomValue); - return static_cast(primarySkill); + return static_cast(primarySkill); } std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerator & rand) const @@ -1346,7 +1372,7 @@ std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerato assert(gainsLevel()); std::optional chosenSecondarySkill; - const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); if(!proposedSecondarySkills.empty()) { std::vector learnedSecondarySkills; @@ -1372,12 +1398,12 @@ std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerato return chosenSecondarySkill; } -void CGHeroInstance::setPrimarySkill(PrimarySkill::PrimarySkill primarySkill, si64 value, ui8 abs) +void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 abs) { if(primarySkill < PrimarySkill::EXPERIENCE) { auto skill = getBonusLocalFirst(Selector::type()(BonusType::PRIMARY_SKILL) - .And(Selector::subtype()(primarySkill)) + .And(Selector::subtype()(BonusSubtypeID(primarySkill))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); assert(skill); @@ -1414,8 +1440,9 @@ void CGHeroInstance::levelUp(const std::vector & skills) ++level; //deterministic secondary skills - skillsInfo.magicSchoolCounter = (skillsInfo.magicSchoolCounter + 1) % maxlevelsToMagicSchool(); - skillsInfo.wisdomCounter = (skillsInfo.wisdomCounter + 1) % maxlevelsToWisdom(); + ++skillsInfo.magicSchoolCounter; + ++skillsInfo.wisdomCounter; + for(const auto & skill : skills) { if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MAJOR)) @@ -1435,7 +1462,7 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) const auto primarySkill = nextPrimarySkill(rand); setPrimarySkill(primarySkill, 1, false); - auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); const auto secondarySkill = nextSecondarySkill(rand); if(secondarySkill) @@ -1448,13 +1475,10 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) } } -bool CGHeroInstance::hasVisions(const CGObjectInstance * target, const int subtype) const +bool CGHeroInstance::hasVisions(const CGObjectInstance * target, BonusSubtypeID subtype) const { //VISIONS spell support - - const auto cached = "type_" + std::to_string(vstd::to_underlying(BonusType::VISIONS)) + "__subtype_" + std::to_string(subtype); - - const int visionsMultiplier = valOfBonuses(Selector::typeSubtype(BonusType::VISIONS,subtype), cached); + const int visionsMultiplier = valOfBonuses(BonusType::VISIONS, subtype); int visionsRange = visionsMultiplier * getPrimSkillLevel(PrimarySkill::SPELL_POWER); @@ -1463,7 +1487,7 @@ bool CGHeroInstance::hasVisions(const CGObjectInstance * target, const int subty const int distance = static_cast(target->pos.dist2d(visitablePos())); - //logGlobal->debug(boost::to_string(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange)); + //logGlobal->debug(boost::str(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange)); return (distance < visionsRange) && (target->pos.z == pos.z); } @@ -1478,7 +1502,7 @@ std::string CGHeroInstance::getHeroTypeName() const } else { - return VLC->heroh->objects[subID]->getJsonKey(); + return getHeroType().toEntity(VLC)->getJsonKey(); } } return ""; @@ -1486,12 +1510,36 @@ std::string CGHeroInstance::getHeroTypeName() const void CGHeroInstance::afterAddToMap(CMap * map) { - if(ID == Obj::HERO) + if(ID != Obj::RANDOM_HERO) + { + auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool + { + return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; + }); + + if(existingHero != map->objects.end()) + { + if(settings["session"]["editor"].Bool()) + { + logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + } + else + { + logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + + throw std::runtime_error("Hero is already on the map"); + } + } + } + + if(ID != Obj::PRISON) + { map->heroesOnMap.emplace_back(this); + } } void CGHeroInstance::afterRemoveFromMap(CMap* map) { - if (ID == Obj::HERO) + if (ID == Obj::PRISON) vstd::erase_if_present(map->heroesOnMap, this); } @@ -1499,7 +1547,7 @@ void CGHeroInstance::setHeroTypeName(const std::string & identifier) { if(ID == Obj::HERO || ID == Obj::PRISON) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); if(rawId) subID = rawId.value(); @@ -1517,7 +1565,7 @@ void CGHeroInstance::updateFrom(const JsonNode & data) void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { - handler.serializeString("biography", biographyCustom); + handler.serializeString("biography", biographyCustomTextId); handler.serializeInt("experience", exp, 0); if(!handler.saving && exp != UNINITIALIZED_EXPERIENCE) //do not gain levels if experience is not initialized @@ -1528,42 +1576,9 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) } } - handler.serializeString("name", nameCustom); + handler.serializeString("name", nameCustomTextId); handler.serializeInt("gender", gender, 0); - - { - const int legacyHeroes = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO); - const int moddedStart = legacyHeroes + GameConstants::HERO_PORTRAIT_SHIFT; - - if(handler.saving) - { - if(portrait >= 0) - { - if(portrait < legacyHeroes || portrait >= moddedStart) - { - int tempPortrait = portrait >= moddedStart - ? portrait - GameConstants::HERO_PORTRAIT_SHIFT - : portrait; - handler.serializeId("portrait", tempPortrait, -1); - } - else - handler.serializeInt("portrait", portrait, -1); - } - } - else - { - const JsonNode & portraitNode = handler.getCurrent()["portrait"]; - - if(portraitNode.getType() == JsonNode::JsonType::DATA_STRING) - { - handler.serializeId("portrait", portrait, -1); - if(portrait >= legacyHeroes) - portrait += GameConstants::HERO_PORTRAIT_SHIFT; - } - else - handler.serializeInt("portrait", portrait, -1); - } - } + handler.serializeId("portrait", customPortraitSource, HeroTypeID::NONE); //primary skills if(handler.saving) @@ -1574,11 +1589,11 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { auto primarySkills = handler.enterStruct("primarySkills"); - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + for(auto i = PrimarySkill::BEGIN; i < PrimarySkill::END; ++i) { - int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, i).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); + int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(i)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); - handler.serializeInt(PrimarySkill::names[i], value, 0); + handler.serializeInt(NPrimarySkill::names[i.getNum()], value, 0); } } } @@ -1591,8 +1606,8 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) { int value = 0; - handler.serializeInt(PrimarySkill::names[i], value, 0); - pushPrimSkill(static_cast(i), value); + handler.serializeInt(NPrimarySkill::names[i], value, 0); + pushPrimSkill(static_cast(i), value); } } } @@ -1605,7 +1620,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) bool normalSkills = false; for(const auto & p : secSkills) { - if(p.first == SecondarySkill(SecondarySkill::DEFAULT)) + if(p.first == SecondarySkill(SecondarySkill::NONE)) defaultSkills = true; else normalSkills = true; @@ -1624,15 +1639,11 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(size_t skillIndex = 0; skillIndex < secondarySkills.size(); ++skillIndex) { JsonArraySerializer inner = secondarySkills.enterArray(skillIndex); - const si32 rawId = secSkills.at(skillIndex).first; + SecondarySkill skillId = secSkills.at(skillIndex).first; - if(rawId < 0 || rawId >= VLC->skillh->size()) - logGlobal->error("Invalid secondary skill %d", rawId); - - auto value = (*VLC->skillh)[SecondarySkill(rawId)]->getJsonKey(); - handler.serializeString("skill", value); - value = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); - handler.serializeString("level", value); + handler.serializeId("skill", skillId); + std::string skillLevel = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); + handler.serializeString("level", skillLevel); } } } @@ -1643,13 +1654,13 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) secSkills.clear(); if(secondarySkills.getType() == JsonNode::JsonType::DATA_NULL) { - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); } else { auto addSkill = [this](const std::string & skillId, const std::string & levelId) { - const int rawId = CSkillHandler::decodeSkill(skillId); + const int rawId = SecondarySkill::decode(skillId); if(rawId < 0) { logGlobal->error("Invalid secondary skill %s", skillId); @@ -1706,10 +1717,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) setHeroTypeName(typeName); } - static const std::vector FORMATIONS = { "wide", "tight" }; - - CCreatureSet::serializeJson(handler, "army", 7); - handler.serializeEnum("formation", formation, FORMATIONS); + CArmedInstance::serializeJsonOptions(handler); { static constexpr int NO_PATROLING = -1; @@ -1727,7 +1735,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!appearance) { // crossoverDeserialize - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType().getNum()]; appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); } @@ -1752,11 +1760,8 @@ bool CGHeroInstance::isMissionCritical() const auto const & testFunctor = [&](const EventCondition & condition) { - if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object) - { - const auto * hero = dynamic_cast(condition.object); - return (hero != this); - } + if ((condition.condition == EventCondition::CONTROL) && condition.objectID != ObjectInstanceID::NONE) + return (id != condition.objectID); if(condition.condition == EventCondition::IS_HUMAN) return true; @@ -1772,7 +1777,7 @@ bool CGHeroInstance::isMissionCritical() const void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { - TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, stack.type->getId())); + TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.type->getId()))); for(const auto & it : *lista) { auto nid = CreatureID(it->additionalInfo[0]); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a2ca01d81..208ea27be 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -25,7 +25,7 @@ struct TerrainTile; struct TurnInfo; enum class EHeroGender : uint8_t; -class CGHeroPlaceholder : public CGObjectInstance +class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { public: /// if this is placeholder by power, then power rank of desired hero @@ -40,6 +40,9 @@ public: h & powerRank; h & heroType; } + +protected: + void serializeJsonOptions(JsonSerializeFormat & handler) override; }; @@ -69,20 +72,21 @@ public: ConstTransitivePtr type; TExpType exp; //experience points ui32 level; //current level of hero - si32 portrait; //may be custom + + /// If not NONE - then hero should use portrait from referenced hero type + HeroTypeID customPortraitSource; si32 mana; // remaining spell points std::vector > secSkills; //first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert); if hero has ability (-1, -1) it meansthat it should have default secondary abilities EHeroGender gender; - std::string nameCustom; - std::string biographyCustom; + std::string nameCustomTextId; + std::string biographyCustomTextId; bool inTownGarrison; // if hero is in town garrison ConstTransitivePtr visitedTown; //set if hero is visiting town or in the town garrison ConstTransitivePtr commander; const CGBoat * boat = nullptr; //set to CGBoat when sailing - static constexpr si32 UNINITIALIZED_PORTRAIT = -1; static constexpr si32 UNINITIALIZED_MANA = -1; static constexpr ui32 UNINITIALIZED_MOVEMENT = -1; static constexpr TExpType UNINITIALIZED_EXPERIENCE = std::numeric_limits::max(); @@ -107,9 +111,6 @@ public: struct DLL_LINKAGE SecondarySkillsInfo { - //skills are determined, initialized at map start - //FIXME remove mutable - mutable CRandomGenerator rand; ui8 magicSchoolCounter; ui8 wisdomCounter; @@ -122,7 +123,6 @@ public: { h & magicSchoolCounter; h & wisdomCounter; - h & rand; } } skillsInfo; @@ -144,6 +144,9 @@ public: std::string getBiographyTranslated() const; std::string getNameTranslated() const; + HeroTypeID getPortraitSource() const; + int32_t getIconIndex() const; + private: std::string getNameTextID() const; std::string getBiographyTextID() const; @@ -168,7 +171,7 @@ public: int getCurrentLuck(int stack=-1, bool town=false) const; int32_t getSpellCost(const spells::Spell * sp) const; //do not use during battles -> bonuses from army would be ignored - bool canLearnSpell(const spells::Spell * spell) const; + bool canLearnSpell(const spells::Spell * spell, bool allowBanned = false) const; bool canCastThisSpell(const spells::Spell * spell) const; //determines if this hero can cast given spell; takes into account existing spell in spellbook, existing spellbook and artifact bonuses /// convert given position between map position (CGObjectInstance::pos) and visitable position used for hero interactions @@ -181,13 +184,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::PrimarySkill nextPrimarySkill(CRandomGenerator & rand) const; + PrimarySkill nextPrimarySkill(CRandomGenerator & 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; /// Gets 0, 1 or 2 secondary skills which are proposed on hero level up. - std::vector getLevelUpProposedSecondarySkills() const; + std::vector getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const; ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill @@ -195,7 +198,7 @@ public: bool canLearnSkill() const; bool canLearnSkill(const SecondarySkill & which) const; - void setPrimarySkill(PrimarySkill::PrimarySkill primarySkill, si64 value, ui8 abs); + void setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 abs); void setSecSkillLevel(const SecondarySkill & which, int val, bool abs); // abs == 0 - changes by value; 1 - sets to value void levelUp(const std::vector & skills); @@ -224,16 +227,17 @@ public: ////////////////////////////////////////////////////////////////////////// - void setType(si32 ID, si32 subID) override; + HeroTypeID getHeroType() const; + void setHeroType(HeroTypeID type); void initHero(CRandomGenerator & rand); void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); - void putArtifact(ArtifactPosition pos, CArtifactInstance * art) override; + ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override; void removeArtifact(ArtifactPosition pos) override; void initExp(CRandomGenerator & rand); void initArmy(CRandomGenerator & rand, IArmyDescriptor *dst = nullptr); - void pushPrimSkill(PrimarySkill::PrimarySkill which, int val); + void pushPrimSkill(PrimarySkill which, int val); ui8 maxlevelsToMagicSchool() const; ui8 maxlevelsToWisdom() const; void recreateSecondarySkillsBonuses(); @@ -241,7 +245,7 @@ public: void fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const override; - bool hasVisions(const CGObjectInstance * target, const int subtype) const; + bool hasVisions(const CGObjectInstance * target, BonusSubtypeID masteryLevel) const; /// If this hero perishes, the scenario is failed bool isMissionCritical() const; @@ -266,7 +270,7 @@ public: ///spells::Caster int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const override; @@ -287,6 +291,7 @@ public: void deserializationFix(); void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; std::string getObjectName() const override; @@ -298,7 +303,7 @@ public: bool isCoastVisitable() const override; BattleField getBattlefield() const override; protected: - void setPropertyDer(ui8 what, ui32 val) override;//synchr + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;//synchr ///common part of hero instance and hero definition void serializeCommonOptions(JsonSerializeFormat & handler); @@ -319,9 +324,9 @@ public: h & static_cast(*this); h & exp; h & level; - h & nameCustom; - h & biographyCustom; - h & portrait; + h & nameCustomTextId; + h & biographyCustomTextId; + h & customPortraitSource; h & mana; h & secSkills; h & movement; diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index 2d182cfc8..951dd41e9 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "CGMarket.h" -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../CCreatureHandler.h" @@ -20,17 +19,18 @@ #include "../CSkillHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN void CGMarket::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } void CGMarket::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::MARKET_WINDOW, id.getNum(), h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::MARKET_WINDOW, h, true); } int CGMarket::getMarketEfficiency() const @@ -38,28 +38,28 @@ int CGMarket::getMarketEfficiency() const return marketEfficiency; } -bool CGMarket::allowsTrade(EMarketMode::EMarketMode mode) const +bool CGMarket::allowsTrade(EMarketMode mode) const { return marketModes.count(mode); } -int CGMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const +int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const { return -1; } -std::vector CGMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGMarket::availableItemsIds(EMarketMode mode) const { if(allowsTrade(mode)) return IMarket::availableItemsIds(mode); - return std::vector(); + return std::vector(); } CGMarket::CGMarket() { } -std::vector CGBlackMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const { switch(mode) { @@ -67,16 +67,16 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode::EMarketMode mode) return IMarket::availableItemsIds(mode); case EMarketMode::RESOURCE_ARTIFACT: { - std::vector ret; + std::vector ret; for(const CArtifact *a : artifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } default: - return std::vector(); + return std::vector(); } } @@ -91,26 +91,12 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const return; SetAvailableArtifacts saa; - saa.id = id.getNum(); + saa.id = id; cb->pickAllowedArtsSet(saa.arts, rand); cb->sendAndApply(&saa); } -void CGUniversity::initObj(CRandomGenerator & rand) -{ - CGMarket::initObj(rand); - - std::vector toChoose; - for(int i = 0; i < VLC->skillh->size(); ++i) - { - if(!vstd::contains(skills, i) && cb->isAllowed(2, i)) - { - toChoose.push_back(i); - } - } -} - -std::vector CGUniversity::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGUniversity::availableItemsIds(EMarketMode mode) const { switch (mode) { @@ -118,13 +104,13 @@ std::vector CGUniversity::availableItemsIds(EMarketMode::EMarketMode mode) return skills; default: - return std::vector(); + return std::vector(); } } void CGUniversity::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::UNIVERSITY_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index d9e98f975..abeefd284 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -18,7 +18,7 @@ class DLL_LINKAGE CGMarket : public CGObjectInstance, public IMarket { public: - std::set marketModes; + std::set marketModes; int marketEfficiency; //window variables @@ -32,9 +32,9 @@ public: ///IMarket int getMarketEfficiency() const override; - bool allowsTrade(EMarketMode::EMarketMode mode) const override; - int availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited - std::vector availableItemsIds(EMarketMode::EMarketMode mode) 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; template void serialize(Handler &h, const int version) { @@ -52,7 +52,7 @@ public: std::vector artifacts; //available artifacts void newTurn(CRandomGenerator & rand) const override; //reset artifacts for black market every month - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; template void serialize(Handler &h, const int version) { @@ -64,10 +64,9 @@ public: class DLL_LINKAGE CGUniversity : public CGMarket { public: - std::vector skills; //available skills + std::vector skills; //available skills - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; - void initObj(CRandomGenerator & rand) override;//set skills for trade + std::vector availableItemsIds(EMarketMode mode) const override; void onHeroVisit(const CGHeroInstance * h) const override; //open window template void serialize(Handler &h, const int version) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 3da66c7bd..b2f7138dd 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -17,12 +17,12 @@ #include "../gameState/CGameState.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" -#include "../NetPacks.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapping/CMap.h" +#include "../networkPacks/PacksForClient.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -33,19 +33,20 @@ CGObjectInstance::CGObjectInstance(): ID(Obj::NO_OBJ), subID(-1), tempOwner(PlayerColor::UNFLAGGABLE), - blockVisit(false) + blockVisit(false), + removable(false) { } //must be instantiated in .cpp file for access to complete types of all member fields CGObjectInstance::~CGObjectInstance() = default; -int32_t CGObjectInstance::getObjGroupIndex() const +MapObjectID CGObjectInstance::getObjGroupIndex() const { - return ID.num; + return ID; } -int32_t CGObjectInstance::getObjTypeIndex() const +MapObjectSubID CGObjectInstance::getObjTypeIndex() const { return subID; } @@ -64,15 +65,18 @@ void CGObjectInstance::setOwner(const PlayerColor & ow) { tempOwner = ow; } -int CGObjectInstance::getWidth() const//returns width of object graphic in tiles + +int CGObjectInstance::getWidth() const { return appearance->getWidth(); } -int CGObjectInstance::getHeight() const //returns height of object graphic in tiles + +int CGObjectInstance::getHeight() const { return appearance->getHeight(); } -bool CGObjectInstance::visitableAt(int x, int y) const //returns true if object is visitable at location (x, y) form left top tile of image (x, y in tiles) + +bool CGObjectInstance::visitableAt(int x, int y) const { return appearance->isVisitableAt(pos.x - x, pos.y - y); } @@ -86,6 +90,20 @@ 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); +} +bool CGObjectInstance::blockingAt(const int3 & testPos) const +{ + return pos.z == testPos.z && appearance->isBlockedAt(pos.x - testPos.x, pos.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); +} + std::set CGObjectInstance::getBlockedPos() const { std::set ret; @@ -100,12 +118,12 @@ std::set CGObjectInstance::getBlockedPos() const return ret; } -std::set CGObjectInstance::getBlockedOffsets() const +const std::set & CGObjectInstance::getBlockedOffsets() const { return appearance->getBlockedOffsets(); } -void CGObjectInstance::setType(si32 newID, si32 newSubID) +void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID) { auto position = visitablePos(); auto oldOffset = getVisitableOffset(); @@ -149,38 +167,41 @@ void CGObjectInstance::setType(si32 newID, si32 newSubID) cb->gameState()->map->addBlockVisTiles(this); } -void CGObjectInstance::initObj(CRandomGenerator & rand) +void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) { - switch(ID) - { - case Obj::TAVERN: - blockVisit = true; - break; - } + // no-op } -void CGObjectInstance::setProperty( ui8 what, ui32 val ) +void CGObjectInstance::initObj(CRandomGenerator & rand) { - setPropertyDer(what, val); // call this before any actual changes (needed at least for dwellings) + // no-op +} + +void CGObjectInstance::setProperty( ObjProperty what, ObjPropertyID identifier ) +{ + setPropertyDer(what, identifier); // call this before any actual changes (needed at least for dwellings) switch(what) { case ObjProperty::OWNER: - tempOwner = PlayerColor(val); + tempOwner = identifier.as(); break; case ObjProperty::BLOCKVIS: - blockVisit = val; + // Never actually used in code, but possible in ERM + blockVisit = identifier.getNum(); break; case ObjProperty::ID: - ID = Obj(val); - break; - case ObjProperty::SUBID: - subID = val; + ID = identifier.as(); break; } } -void CGObjectInstance::setPropertyDer( ui8 what, ui32 val ) +TObjectTypeHandler CGObjectInstance::getObjectHandler() const +{ + return VLC->objtypeh->getHandlerFor(ID, subID); +} + +void CGObjectInstance::setPropertyDer( ObjProperty what, ObjPropertyID identifier ) {} int3 CGObjectInstance::getSightCenter() const @@ -202,10 +223,10 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura { GiveBonus gbonus; gbonus.bonus.type = BonusType::NONE; - gbonus.id = heroID.getNum(); + gbonus.id = heroID; gbonus.bonus.duration = duration; - gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = ID; + gbonus.bonus.source = BonusSource::OBJECT_TYPE; + gbonus.bonus.sid = BonusSourceID(ID); cb->giveHeroBonus(&gbonus); } @@ -214,7 +235,7 @@ std::string CGObjectInstance::getObjectName() const return VLC->objtypeh->getObjectName(ID, subID); } -std::optional CGObjectInstance::getAmbientSound() const +std::optional CGObjectInstance::getAmbientSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).ambient; if(!sounds.empty()) @@ -223,7 +244,7 @@ std::optional CGObjectInstance::getAmbientSound() const return std::nullopt; } -std::optional CGObjectInstance::getVisitSound() const +std::optional CGObjectInstance::getVisitSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).visit; if(!sounds.empty()) @@ -232,7 +253,7 @@ std::optional CGObjectInstance::getVisitSound() const return std::nullopt; } -std::optional CGObjectInstance::getRemovalSound() const +std::optional CGObjectInstance::getRemovalSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).removal; if(!sounds.empty()) @@ -254,9 +275,28 @@ std::string CGObjectInstance::getHoverText(const CGHeroInstance * hero) const return getHoverText(hero->tempOwner); } +std::string CGObjectInstance::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} +std::string CGObjectInstance::getPopupText(const CGHeroInstance * hero) const +{ + return getHoverText(hero); +} + +std::vector CGObjectInstance::getPopupComponents(PlayerColor player) const +{ + return {}; +} + +std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance * hero) const +{ + return getPopupComponents(hero->getOwner()); +} + void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const { - switch(ID) + switch(ID.toEnum()) { case Obj::SANCTUARY: { @@ -266,7 +306,7 @@ void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const break; case Obj::TAVERN: { - openWindow(EOpenWindowMode::TAVERN_WINDOW,h->id.getNum(),id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::TAVERN_WINDOW, h, true); } break; } @@ -284,9 +324,16 @@ bool CGObjectInstance::isVisitable() const bool CGObjectInstance::isBlockedVisitable() const { + // TODO: Read from json return blockVisit; } +bool CGObjectInstance::isRemovable() const +{ + // TODO: Read from json + return removable; +} + bool CGObjectInstance::isCoastVisitable() const { return false; @@ -341,15 +388,7 @@ void CGObjectInstance::serializeJsonOptions(JsonSerializeFormat & handler) void CGObjectInstance::serializeJsonOwner(JsonSerializeFormat & handler) { - if(handler.saving && tempOwner == PlayerColor::NEUTRAL) - return; - - ui8 temp = tempOwner.getNum(); - - handler.serializeEnum("owner", temp, PlayerColor::NEUTRAL.getNum(), GameConstants::PLAYER_COLOR_NAMES); - - if(!handler.saving) - tempOwner = PlayerColor(temp); + handler.serializeId("owner", tempOwner, PlayerColor::NEUTRAL); } BattleField CGObjectInstance::getBattlefield() const diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 03e27ebc9..7292435ba 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -10,14 +10,19 @@ #pragma once #include "IObjectInterface.h" +#include "../constants/EntityIdentifiers.h" +#include "../filesystem/ResourcePath.h" #include "../int3.h" #include "../bonuses/BonusEnum.h" VCMI_LIB_NAMESPACE_BEGIN +struct Component; class JsonSerializeFormat; class ObjectTemplate; class CMap; +class AObjectTypeHandler; +using TObjectTypeHandler = std::shared_ptr; class DLL_LINKAGE CGObjectInstance : public IObjectInterface { @@ -25,9 +30,9 @@ public: /// Position of bottom-right corner of object on map int3 pos; /// Type of object, e.g. town, hero, creature. - Obj ID; + MapObjectID ID; /// Subtype of object, depends on type - si32 subID; + MapObjectSubID subID; /// Current owner of an object (when below PLAYER_LIMIT) PlayerColor tempOwner; /// Index of object in map's list of objects @@ -42,13 +47,14 @@ public: CGObjectInstance(); //TODO: remove constructor ~CGObjectInstance() override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; /// "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 bool blockVisit; + bool removable; PlayerColor getOwner() const override { @@ -60,14 +66,19 @@ public: int getWidth() const; //returns width of object graphic in tiles int getHeight() const; //returns height of object graphic in tiles - bool visitableAt(int x, int y) const; //returns true if object is visitable at location (x, y) (h3m pos) int3 visitablePos() const override; int3 getPosition() 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) + std::set getBlockedPos() const; //returns set of positions blocked by this object - std::set getBlockedOffsets() const; //returns set of relative positions blocked by this object + const std::set & getBlockedOffsets() const; //returns set of relative positions blocked by this object /// returns true if object is visitable bool isVisitable() const; @@ -75,6 +86,9 @@ public: /// If true hero can visit this object only from neighbouring tiles and can't stand on this object virtual bool isBlockedVisitable() const; + // If true, can be possibly removed from the map + virtual bool isRemovable() const; + /// If true this object can be visited by hero standing on the coast virtual bool isCoastVisitable() const; @@ -82,9 +96,11 @@ public: virtual bool isTile2Terrain() const { return false; } - std::optional getAmbientSound() const; - std::optional getVisitSound() const; - std::optional getRemovalSound() const; + std::optional getAmbientSound() const; + std::optional getVisitSound() const; + std::optional getRemovalSound() const; + + TObjectTypeHandler getObjectHandler() const; /** VIRTUAL METHODS **/ @@ -94,10 +110,6 @@ public: virtual int getSightRadius() const; /// returns (x,y,0) offset to a visitable tile of object virtual int3 getVisitableOffset() const; - /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") - virtual void setType(si32 ID, si32 subID); - - /// returns text visible in status bar with specific hero/player active. /// Returns generic name of object, without any player-specific info virtual std::string getObjectName() const; @@ -107,12 +119,19 @@ public: /// Returns hero-specific hover name, including visited/not visited info. Default = player-specific name virtual std::string getHoverText(const CGHeroInstance * hero) const; + virtual std::string getPopupText(PlayerColor player) const; + virtual std::string getPopupText(const CGHeroInstance * hero) const; + + virtual std::vector getPopupComponents(PlayerColor player) const; + virtual std::vector getPopupComponents(const CGHeroInstance * hero) const; + /** OVERRIDES OF IObjectInterface **/ void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; /// method for synchronous update. Note: For new properties classes should override setPropertyDer instead - void setProperty(ui8 what, ui32 val) final; + void setProperty(ObjProperty what, ObjPropertyID identifier) final; virtual void afterAddToMap(CMap * map); virtual void afterRemoveFromMap(CMap * map); @@ -125,10 +144,11 @@ public: h & subTypeName; h & pos; h & ID; - h & subID; + subID.serializeIdentifier(h, ID, version); h & id; h & tempOwner; h & blockVisit; + h & removable; h & appearance; //definfo is handled by map serializer } @@ -139,7 +159,10 @@ public: protected: /// virtual method that allows synchronously update object state on server and all clients - virtual void setPropertyDer(ui8 what, ui32 val); + virtual void setPropertyDer(ObjProperty what, ObjPropertyID identifier); + + /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") + void setType(MapObjectID ID, MapObjectSubID subID); /// Gives dummy bonus from this object to hero. Can be used to track visited state void giveDummyBonus(const ObjectInstanceID & heroID, BonusDuration::Type duration = BonusDuration::ONE_DAY) const; diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 40de75105..750ac50fe 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -14,308 +14,175 @@ #include #include -#include "../NetPacks.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" #include "../StartInfo.h" #include "../IGameCallback.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN +void CGPandoraBox::init() +{ + blockVisit = true; + configuration.info.emplace_back(); + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + + for(auto & i : configuration.info) + { + i.reward.removeObject = true; + if(!message.empty() && i.message.empty()) + i.message = message; + } +} + void CGPandoraBox::initObj(CRandomGenerator & rand) { - blockVisit = (ID==Obj::PANDORAS_BOX); //block only if it's really pandora's box (events also derive from that class) - hasGuardians = stacks.size(); + init(); + + CRewardableObject::initObj(rand); +} + +void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, bool markAsVisit) const +{ + auto vi = configuration.info.at(index); + if(!vi.message.empty()) + { + CRewardableObject::grantRewardWithMessage(h, index, markAsVisit); + return; + } + + //split reward message for pandora box + auto setText = [](bool cond, int posId, int negId, const CGHeroInstance * h) + { + MetaString text; + text.appendLocalString(EMetaText::ADVOB_TXT, cond ? posId : negId); + text.replaceRawString(h->getNameTranslated()); + return text; + }; + + auto sendInfoWindow = [h](const MetaString & text, const Rewardable::Reward & reward) + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text = text; + reward.loadComponents(iw.components, h); + iw.type = EInfoWindowMode::MODAL; + if(!iw.components.empty()) + cb->showInfoDialog(&iw); + }; + + Rewardable::Reward temp; + temp.spells = vi.reward.spells; + temp.heroExperience = vi.reward.heroExperience; + temp.heroLevel = vi.reward.heroLevel; + temp.primary = vi.reward.primary; + temp.secondary = vi.reward.secondary; + temp.bonuses = vi.reward.bonuses; + temp.manaDiff = vi.reward.manaDiff; + temp.manaPercentage = vi.reward.manaPercentage; + + MetaString txt; + if(!vi.reward.spells.empty()) + txt = setText(temp.spells.size() == 1, 184, 188, h); + + if(vi.reward.heroExperience || vi.reward.heroLevel || !vi.reward.secondary.empty()) + txt = setText(true, 175, 175, h); + + for(int i : vi.reward.primary) + { + if(i) + { + txt = setText(true, 175, 175, h); + break; + } + } + + if(vi.reward.manaDiff || vi.reward.manaPercentage >= 0) + txt = setText(temp.manaDiff > 0, 177, 176, h); + + for(auto b : vi.reward.bonuses) + { + if(b.val && b.type == BonusType::MORALE) + txt = setText(b.val > 0, 179, 178, h); + if(b.val && b.type == BonusType::LUCK) + txt = setText(b.val > 0, 181, 180, h); + } + sendInfoWindow(txt, temp); + + //resource message + temp = Rewardable::Reward{}; + temp.resources = vi.reward.resources; + sendInfoWindow(setText(vi.reward.resources.marketValue() > 0, 183, 182, h), temp); + + //artifacts message + temp = Rewardable::Reward{}; + temp.artifacts = vi.reward.artifacts; + sendInfoWindow(setText(true, 183, 183, h), temp); + + //creatures message + temp = Rewardable::Reward{}; + temp.creatures = vi.reward.creatures; + txt.clear(); + if(!vi.reward.creatures.empty()) + { + MetaString loot; + for(auto c : vi.reward.creatures) + { + loot.appendRawString("%s"); + loot.replaceName(c); + } + + if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].count == 1) + txt.appendLocalString(EMetaText::ADVOB_TXT, 185); + else + txt.appendLocalString(EMetaText::ADVOB_TXT, 186); + + txt.replaceRawString(loot.buildList()); + txt.replaceRawString(h->getNameTranslated()); + } + sendInfoWindow(txt, temp); + + //everything else + temp = vi.reward; + temp.heroExperience = 0; + temp.heroLevel = 0; + temp.secondary.clear(); + temp.primary.clear(); + temp.resources.amin(0); + temp.resources.amax(0); + temp.manaDiff = 0; + temp.manaPercentage = -1; + temp.spells.clear(); + temp.creatures.clear(); + temp.bonuses.clear(); + temp.artifacts.clear(); + sendInfoWindow(setText(true, 175, 175, h), temp); + + // grant reward afterwards. Note that it may remove object + if(markAsVisit) + markAsVisited(h); + grantReward(index, h); } 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); -} - -void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const -{ - afterSuccessfulVisit(); - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - - bool changesPrimSkill = false; - for(const auto & elem : primskills) - { - if(elem) - { - changesPrimSkill = true; - break; - } - } - - std::vector> unpossessedAbilities; //ability + ability level - int abilitiesRequiringSlot = 0; - - //filter out unnecessary secondary skills - for (int i = 0; i < abilities.size(); i++) - { - int curLev = h->getSecSkillLevel(abilities[i]); - bool abilityCanUseSlot = !curLev && ((h->secSkills.size() + abilitiesRequiringSlot) < GameConstants::SKILL_PER_HERO); //limit new abilities to number of slots - - if (abilityCanUseSlot) - abilitiesRequiringSlot++; - - if ((curLev && curLev < abilityLevels[i]) || abilityCanUseSlot) - { - unpossessedAbilities.emplace_back(abilities[i], abilityLevels[i]); - } - } - - if(gainedExp || changesPrimSkill || !unpossessedAbilities.empty()) - { - TExpType expVal = h->calculateXp(gainedExp); - //getText(iw,afterBattle,175,h); //wtf? - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 175); //%s learns something - iw.text.replaceRawString(h->getNameTranslated()); - - if(expVal) - iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(expVal), 0); - - for(int i=0; ishowInfoDialog(&iw); - - //give sec skills - for(const auto & abilityData : unpossessedAbilities) - cb->changeSecSkill(h, abilityData.first, abilityData.second, true); - - assert(h->secSkills.size() <= GameConstants::SKILL_PER_HERO); - - //give prim skills - for(int i=0; ichangePrimSkill(h,static_cast(i),primskills[i],false); - - assert(!cb->isVisitCoveredByAnotherQuery(this, h)); - - //give exp - if(expVal) - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, expVal, false); - } - //else { } //TODO:Create information that box was empty for now, and deliver to CGPandoraBox::giveContentsAfterExp or refactor - - if(!cb->isVisitCoveredByAnotherQuery(this, h)) - giveContentsAfterExp(h); - //Otherwise continuation occurs via post-level-up callback. -} - -void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const -{ - bool hadGuardians = hasGuardians; //copy, because flag will be emptied after issuing first post-battle message - - std::string msg = message; //in case box is removed in the meantime - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - - //TODO: reuse this code for Scholar skill - if(!spells.empty()) - { - std::set spellsToGive; - - auto i = spells.cbegin(); - while (i != spells.cend()) - { - iw.components.clear(); - iw.text.clear(); - spellsToGive.clear(); - - for (; i != spells.cend(); i++) - { - const auto * spell = (*i).toSpell(VLC->spells()); - if(h->canLearnSpell(spell)) - { - iw.components.emplace_back(Component::EComponentType::SPELL, *i, 0, 0); - spellsToGive.insert(*i); - } - if(spellsToGive.size() == 8) //display up to 8 spells at once - { - break; - } - } - if (!spellsToGive.empty()) - { - if (spellsToGive.size() > 1) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 188); //%s learns spells - } - else - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 184); //%s learns a spell - } - iw.text.replaceRawString(h->getNameTranslated()); - cb->changeSpells(h, true, spellsToGive); - cb->showInfoDialog(&iw); - } - } - } - - if(manaDiff) - { - getText(iw,hadGuardians,manaDiff,176,177,h); - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, manaDiff, 0); - cb->showInfoDialog(&iw); - cb->setManaPoints(h->id, h->mana + manaDiff); - } - - if(moraleDiff) - { - getText(iw,hadGuardians,moraleDiff,178,179,h); - iw.components.emplace_back(Component::EComponentType::MORALE, 0, moraleDiff, 0); - cb->showInfoDialog(&iw); - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE,BonusType::MORALE,BonusSource::OBJECT,moraleDiff,id.getNum(),""); - gb.id = h->id.getNum(); - cb->giveHeroBonus(&gb); - } - - if(luckDiff) - { - getText(iw,hadGuardians,luckDiff,180,181,h); - iw.components.emplace_back(Component::EComponentType::LUCK, 0, luckDiff, 0); - cb->showInfoDialog(&iw); - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE,BonusType::LUCK,BonusSource::OBJECT,luckDiff,id.getNum(),""); - gb.id = h->id.getNum(); - cb->giveHeroBonus(&gb); - } - - iw.components.clear(); - iw.text.clear(); - for(int i=0; ishowInfoDialog(&iw); - } - - iw.components.clear(); - iw.text.clear(); - for(int i=0; i 0) - iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0); - } - if(!iw.components.empty()) - { - getText(iw,hadGuardians,183,h); - cb->showInfoDialog(&iw); - } - - iw.components.clear(); - // getText(iw,afterBattle,183,h); - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure - iw.text.replaceRawString(h->getNameTranslated()); - for(const auto & elem : artifacts) - { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); - if(iw.components.size() >= 14) - { - cb->showInfoDialog(&iw); - iw.components.clear(); - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure - once more? - iw.text.replaceRawString(h->getNameTranslated()); - } - } - if(!iw.components.empty()) - { - cb->showInfoDialog(&iw); - } - - cb->giveResources(h->getOwner(), resources); - - for(const auto & elem : artifacts) - cb->giveHeroNewArtifact(h, VLC->arth->objects[elem],ArtifactPosition::FIRST_AVAILABLE); - - iw.components.clear(); - iw.text.clear(); - - if(creatures.stacksCount()) - { //this part is taken straight from creature bank - MetaString loot; - for(const auto & elem : creatures.Slots()) - { //build list of joined creatures - iw.components.emplace_back(*elem.second); - loot.appendRawString("%s"); - loot.replaceCreatureName(*elem.second); - } - - if(creatures.stacksCount() == 1 && creatures.Slots().begin()->second->count == 1) - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 185); - else - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 186); - - iw.text.replaceRawString(loot.buildList()); - iw.text.replaceRawString(h->getNameTranslated()); - - cb->showInfoDialog(&iw); - cb->giveCreatures(this, h, creatures, false); - } - if(!hasGuardians && !msg.empty()) - { - iw.text.appendRawString(msg); - cb->showInfoDialog(&iw); - } -} - -void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int text, const CGHeroInstance * h ) const -{ - if(afterBattle || message.empty()) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,text);//%s has lost treasure. - iw.text.replaceRawString(h->getNameTranslated()); - } - else - { - iw.text.appendRawString(message); - afterBattle = true; - } -} - -void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int val, int negative, int positive, const CGHeroInstance * h ) const -{ - iw.components.clear(); - iw.text.clear(); - if(afterBattle || message.empty()) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,val < 0 ? negative : positive); //%s's luck takes a turn for the worse / %s's luck increases - iw.text.replaceRawString(h->getNameTranslated()); - } - else - { - iw.text.appendRawString(message); - afterBattle = true; - } + BlockingDialog bd (true, false); + bd.player = h->getOwner(); + bd.text.appendLocalString(EMetaText::ADVOB_TXT, 14); + cb->showBlockingDialog(&bd); } void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { if(result.winner == 0) { - giveContentsUpToExp(hero); + CRewardableObject::onHeroVisit(hero); } } @@ -328,123 +195,123 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL); cb->startBattleI(hero, this); //grants things after battle } - else if(message.empty() && resources.empty() - && primskills.empty() && abilities.empty() - && abilityLevels.empty() && artifacts.empty() - && spells.empty() && creatures.stacksCount() == 0 - && gainedExp == 0 && manaDiff == 0 && moraleDiff == 0 && luckDiff == 0) //if it gives nothing without battle + else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { hero->showInfoDialog(15); - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else //if it gives something without battle { - giveContentsUpToExp(hero); + CRewardableObject::onHeroVisit(hero); } } } -void CGPandoraBox::heroLevelUpDone(const CGHeroInstance *hero) const -{ - giveContentsAfterExp(hero); -} - -void CGPandoraBox::afterSuccessfulVisit() const -{ - cb->removeAfterVisit(this); -} - void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "guards", 7); - handler.serializeString("guardMessage", message); - - handler.serializeInt("experience", gainedExp, 0); - handler.serializeInt("mana", manaDiff, 0); - handler.serializeInt("morale", moraleDiff, 0); - handler.serializeInt("luck", luckDiff, 0); - - resources.serializeJson(handler, "resources"); - + CRewardableObject::serializeJsonOptions(handler); + + handler.serializeStruct("guardMessage", message); + + if(!handler.saving) { - bool haveSkills = false; - - if(handler.saving) - { - for(int primskill : primskills) - if(primskill != 0) - haveSkills = true; - } - else - { - primskills.resize(GameConstants::PRIMARY_SKILLS,0); - haveSkills = true; - } - - if(haveSkills) + //backward compatibility for VCMI maps that use old Pandora Box format + if(!handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); + + bool hasSomething = false; + Rewardable::VisitInfo vinfo; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + + handler.serializeInt("experience", vinfo.reward.heroExperience, 0); + handler.serializeInt("mana", vinfo.reward.manaDiff, 0); + + int val; + handler.serializeInt("morale", val, 0); + if(val) + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); + + handler.serializeInt("luck", val, 0); + if(val) + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); + + vinfo.reward.resources.serializeJson(handler, "resources"); { auto s = handler.enterStruct("primarySkills"); - for(int idx = 0; idx < primskills.size(); idx ++) - handler.serializeInt(PrimarySkill::names[idx], primskills[idx], 0); + for(int idx = 0; idx < vinfo.reward.primary.size(); idx ++) + { + handler.serializeInt(NPrimarySkill::names[idx], vinfo.reward.primary[idx], 0); + if(vinfo.reward.primary[idx]) + hasSomething = true; + } } - } - - if(handler.saving) - { - if(!abilities.empty()) + + handler.serializeIdArray("artifacts", vinfo.reward.artifacts); + handler.serializeIdArray("spells", vinfo.reward.spells); + handler.enterArray("creatures").serializeStruct(vinfo.reward.creatures); + { auto s = handler.enterStruct("secondarySkills"); - - for(size_t idx = 0; idx < abilities.size(); idx++) + for(const auto & p : handler.getCurrent().Struct()) { - handler.serializeEnum(CSkillHandler::encodeSkill(abilities[idx]), abilityLevels[idx], NSecondarySkill::levels); + const std::string skillName = p.first; + const std::string levelId = p.second.String(); + + const int rawId = SecondarySkill::decode(skillName); + if(rawId < 0) + { + logGlobal->error("Invalid secondary skill %s", skillName); + continue; + } + + const int level = vstd::find_pos(NSecondarySkill::levels, levelId); + if(level < 0) + { + logGlobal->error("Invalid secondary skill level %s", levelId); + continue; + } + + vinfo.reward.secondary[rawId] = level; } } + + hasSomething = hasSomething + || vinfo.reward.heroExperience + || vinfo.reward.manaDiff + || vinfo.reward.resources.nonZero() + || !vinfo.reward.artifacts.empty() + || !vinfo.reward.bonuses.empty() + || !vinfo.reward.creatures.empty() + || !vinfo.reward.secondary.empty(); + + if(hasSomething) + configuration.info.push_back(vinfo); } - else +} + +void CGEvent::init() +{ + blockVisit = false; + configuration.infoWindowType = EInfoWindowMode::MODAL; + + for(auto & i : configuration.info) { - auto s = handler.enterStruct("secondarySkills"); - - const JsonNode & skillMap = handler.getCurrent(); - - abilities.clear(); - abilityLevels.clear(); - - for(const auto & p : skillMap.Struct()) - { - const std::string skillName = p.first; - const std::string levelId = p.second.String(); - - const int rawId = CSkillHandler::decodeSkill(skillName); - if(rawId < 0) - { - logGlobal->error("Invalid secondary skill %s", skillName); - continue; - } - - const int level = vstd::find_pos(NSecondarySkill::levels, levelId); - if(level < 0) - { - logGlobal->error("Invalid secondary skill level %s", levelId); - continue; - } - - abilities.emplace_back(rawId); - abilityLevels.push_back(level); - } + i.reward.removeObject = removeAfterVisit; + if(!message.empty() && i.message.empty()) + i.message = message; } +} - - handler.serializeIdArray("artifacts", artifacts); - handler.serializeIdArray("spells", spells); - - creatures.serializeJson(handler, "creatures"); +void CGEvent::grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const +{ + CRewardableObject::grantRewardWithMessage(contextHero, rewardIndex, markAsVisit); } void CGEvent::onHeroVisit( const CGHeroInstance * h ) const { - if(!(availableFor & (1 << h->tempOwner.getNum()))) + if(availableFor.count(h->tempOwner) == 0) return; + if(cb->getPlayerSettings(h->tempOwner)->isControlledByHuman()) { if(humanActivate) @@ -461,7 +328,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const InfoWindow iw; iw.player = h->tempOwner; if(!message.empty()) - iw.text.appendRawString(message); + iw.text = message; else iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16); cb->showInfoDialog(&iw); @@ -469,41 +336,18 @@ void CGEvent::activated( const CGHeroInstance * h ) const } else { - giveContentsUpToExp(h); + CRewardableObject::onHeroVisit(h); } } -void CGEvent::afterSuccessfulVisit() const -{ - if(removeAfterVisit) - { - cb->removeAfterVisit(this); - } - else if(hasGuardians) - hasGuardians = false; -} - void CGEvent::serializeJsonOptions(JsonSerializeFormat & handler) { CGPandoraBox::serializeJsonOptions(handler); - handler.serializeBool("aIActivable", computerActivate, true, false, false); - handler.serializeBool("humanActivable", humanActivate, true, false, true); - handler.serializeBool("removeAfterVisit", removeAfterVisit, true, false, false); - - { - auto decodePlayer = [](const std::string & id)->si32 - { - return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, id); - }; - - auto encodePlayer = [](si32 idx)->std::string - { - return GameConstants::PLAYER_COLOR_NAMES[idx]; - }; - - handler.serializeIdArray("availableFor", availableFor, GameConstants::ALL_PLAYERS, decodePlayer, encodePlayer); - } + handler.serializeBool("aIActivable", computerActivate, false); + handler.serializeBool("humanActivable", humanActivate, true); + handler.serializeBool("removeAfterVisit", removeAfterVisit, false); + handler.serializeIdArray("availableFor", availableFor); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index b6c94a7bd..4bbd5b2af 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -9,70 +9,40 @@ */ #pragma once -#include "CArmedInstance.h" +#include "CRewardableObject.h" #include "../ResourceSet.h" VCMI_LIB_NAMESPACE_BEGIN struct InfoWindow; -class DLL_LINKAGE CGPandoraBox : public CArmedInstance +class DLL_LINKAGE CGPandoraBox : public CRewardableObject { public: - std::string message; - mutable bool hasGuardians = false; //helper - after battle even though we have no stacks, allows us to know that there was battle - - //gained things: - ui32 gainedExp = 0; - si32 manaDiff = 0; //amount of gained / lost mana - si32 moraleDiff = 0; //morale modifier - si32 luckDiff = 0; //luck modifier - TResources resources;//gained / lost resources - std::vector primskills;//gained / lost prim skills - std::vector abilities; //gained abilities - std::vector abilityLevels; //levels of gained abilities - std::vector artifacts; //gained artifacts - std::vector spells; //gained spells - CCreatureSet creatures; //gained creatures + MetaString message; void initObj(CRandomGenerator & 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 heroLevelUpDone(const CGHeroInstance *hero) const override; template void serialize(Handler &h, const int version) { - h & static_cast(*this); + h & static_cast(*this); h & message; - h & hasGuardians; - h & gainedExp; - h & manaDiff; - h & moraleDiff; - h & luckDiff; - h & resources; - h & primskills; - h & abilities; - h & abilityLevels; - h & artifacts; - h & spells; - h & creatures; } protected: - void giveContentsUpToExp(const CGHeroInstance *h) const; - void giveContentsAfterExp(const CGHeroInstance *h) const; + void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const override; + + virtual void init(); void serializeJsonOptions(JsonSerializeFormat & handler) override; -private: - void getText( InfoWindow &iw, bool &afterBattle, int val, int negative, int positive, const CGHeroInstance * h ) const; - void getText( InfoWindow &iw, bool &afterBattle, int text, const CGHeroInstance * h ) const; - virtual void afterSuccessfulVisit() const; }; class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects { public: bool removeAfterVisit = false; //true if event is removed after occurring - ui8 availableFor = 0; //players whom this event is available for + std::set availableFor; //players whom this event is available for bool computerActivate = false; //true if computer player can activate this event bool humanActivate = false; //true if human player can activate this event @@ -87,10 +57,12 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; protected: + void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const override; + + void init() override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: void activated(const CGHeroInstance * h) const; - void afterSuccessfulVisit() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 556bc91cb..f00aa72f2 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -12,9 +12,10 @@ #include "CGTownBuilding.h" #include "CGTownInstance.h" #include "../CGeneralTextHandler.h" -#include "../NetPacks.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,12 +24,12 @@ PlayerColor CGTownBuilding::getOwner() const return town->getOwner(); } -int32_t CGTownBuilding::getObjGroupIndex() const +MapObjectID CGTownBuilding::getObjGroupIndex() const { return -1; } -int32_t CGTownBuilding::getObjTypeIndex() const +MapObjectSubID CGTownBuilding::getObjTypeIndex() const { return 0; } @@ -119,12 +120,12 @@ COPWBonus::COPWBonus(const BuildingID & bid, BuildingSubID::EBuildingSubID subId indexOnTV = static_cast(town->bonusingBuildings.size()); } -void COPWBonus::setProperty(ui8 what, ui32 val) +void COPWBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(val); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -143,11 +144,11 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const switch (this->bType) { case BuildingSubID::STABLES: - if(!h->hasBonusFrom(BonusSource::OBJECT, Obj::STABLES)) //does not stack with advMap 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, 600, 94, VLC->generaltexth->arraytxt[100], 1); - gb.id = heroID.getNum(); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT_TYPE, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); + gb.id = heroID; cb->giveHeroBonus(&gb); SetMovePoints mp; @@ -186,10 +187,10 @@ CTownBonus::CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID s indexOnTV = static_cast(town->bonusingBuildings.size()); } -void CTownBonus::setProperty (ui8 what, ui32 val) +void CTownBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::VISITORS) - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); } void CTownBonus::onHeroVisit (const CGHeroInstance * h) const @@ -199,43 +200,43 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const { si64 val = 0; InfoWindow iw; - PrimarySkill::PrimarySkill what = PrimarySkill::NONE; + PrimarySkill what = PrimarySkill::NONE; switch(bType) { case BuildingSubID::KNOWLEDGE_VISITING_BONUS: //wall of knowledge what = PrimarySkill::KNOWLEDGE; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 3, 1, 0); + 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(Component::EComponentType::PRIM_SKILL, 2, 1, 0); + 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(Component::EComponentType::PRIM_SKILL, 0, 1, 0); + 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(Component::EComponentType::EXPERIENCE, 0, val, 0); + 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(Component::EComponentType::PRIM_SKILL, 1, 1, 0); + 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, Bonus::getSid32(building->town->faction->getIndex(), building->bid))) + if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID()))) { const auto & bonuses = building->onVisitBonuses; applyBonuses(const_cast(h), bonuses); @@ -271,7 +272,7 @@ void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) con bonus->duration = BonusDuration::ONE_DAY; } gb.bonus = * bonus; - gb.id = h->id.getNum(); + gb.id = h->id; cb->giveHeroBonus(&gb); if(bonus->duration == BonusDuration::PERMANENT) @@ -306,11 +307,7 @@ void CTownRewardableBuilding::initObj(CRandomGenerator & rand) for (auto & bonus : rewardInfo.reward.bonuses) { bonus.source = BonusSource::TOWN_STRUCTURE; - bonus.sid = bID; - if (bonus.type == BonusType::MORALE) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); - if (bonus.type == BonusType::LUCK) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); + bonus.sid = BonusSourceID(building->getUniqueTypeID()); } } } @@ -321,21 +318,21 @@ void CTownRewardableBuilding::newTurn(CRandomGenerator & rand) const { if(configuration.resetParameters.rewards) { - cb->setObjProperty(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); } if(configuration.resetParameters.visitors) { - cb->setObjProperty(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); } } } -void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) +void CTownRewardableBuilding::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -344,7 +341,7 @@ void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; } } @@ -397,9 +394,14 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer case Rewardable::VISIT_PLAYER: return false; //not supported case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(town->town->faction->getIndex(), bID)); + { + 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; } diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h index 1fffb3710..47da63af5 100644 --- a/lib/mapObjects/CGTownBuilding.h +++ b/lib/mapObjects/CGTownBuilding.h @@ -45,8 +45,8 @@ public: } PlayerColor getOwner() const override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; int3 visitablePos() const override; int3 getPosition() const override; @@ -69,8 +69,8 @@ protected: class DLL_LINKAGE COPWBonus : public CGTownBuilding {///used for OPW bonusing structures public: - std::set visitors; - void setProperty(ui8 what, ui32 val) override; + 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); @@ -89,7 +89,7 @@ class DLL_LINKAGE CTownBonus : public CGTownBuilding ///feel free to merge inheritance tree public: std::set visitors; - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit (const CGHeroInstance * h) const override; CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); @@ -117,7 +117,7 @@ class DLL_LINKAGE CTownRewardableBuilding : public CGTownBuilding, public Reward void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; public: - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 10fd30409..ab3fe7cd7 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -14,23 +14,27 @@ #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" #include "../battle/IBattleInfoCallback.h" -#include "../NetPacks.h" #include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" +#include "../StartInfo.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../modding/ModScope.h" +#include "../networkPacks/StackLocation.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN std::vector CGTownInstance::merchantArtifacts; -std::vector CGTownInstance::universitySkills; +std::vector CGTownInstance::universitySkills; int CGTownInstance::getSightRadius() const //returns sight distance @@ -49,28 +53,28 @@ int CGTownInstance::getSightRadius() const //returns sight distance return ret; } -void CGTownInstance::setPropertyDer(ui8 what, ui32 val) +void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { ///this is freakin' overcomplicated solution switch (what) { case ObjProperty::STRUCTURE_ADD_VISITING_HERO: - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, visitingHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, visitingHero->id); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: - bonusingBuildings[val]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, NumericID(0)); break; case ObjProperty::STRUCTURE_ADD_GARRISONED_HERO: //add garrisoned hero to visitors - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, garrisonHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, garrisonHero->id); break; case ObjProperty::BONUS_VALUE_FIRST: - bonusValue.first = val; + bonusValue.first = identifier.getNum(); break; case ObjProperty::BONUS_VALUE_SECOND: - bonusValue.second = val; + bonusValue.second = identifier.getNum(); break; case ObjProperty::REWARD_RANDOMIZE: - bonusingBuildings[val]->setProperty(ObjProperty::REWARD_RANDOMIZE, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::REWARD_RANDOMIZE, NumericID(0)); break; } } @@ -132,7 +136,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const if (creatures[level].second.empty()) return ret; //no dwelling - const CCreature *creature = VLC->creh->objects[creatures[level].second.back()]; + const Creature *creature = creatures[level].second.back().toEntity(VLC); const int base = creature->getGrowth(); int castleBonus = 0; @@ -160,7 +164,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::type()(BonusType::CREATURE_GROWTH).And(Selector::subtype()(level))); + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); @@ -270,7 +274,7 @@ void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 ans void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const { - if(!cb->gameState()->getPlayerRelations( getOwner(), h->getOwner() ))//if this is enemy + if(cb->gameState()->getPlayerRelations( getOwner(), h->getOwner() ) == PlayerRelations::ENEMIES) { if(armedGarrison() || visitingHero) { @@ -321,13 +325,13 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const InfoWindow iw; iw.player = h->tempOwner; iw.text.appendRawString(h->commander->getName()); - iw.components.emplace_back(*h->commander); + iw.components.emplace_back(ComponentType::CREATURE, h->commander->getId(), h->commander->getCount()); cb->showInfoDialog(&iw); } } else { - logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), name); + logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), getNameTranslated()); } } @@ -337,15 +341,15 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const if(visitingHero == h) { cb->stopHeroVisitCastle(this, h); - logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), name); + logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), getNameTranslated()); } else - logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), name); + logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), getNameTranslated()); } std::string CGTownInstance::getObjectName() const { - return name + ", " + town->faction->getNameTranslated(); + return getNameTranslated() + ", " + town->faction->getNameTranslated(); } bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const @@ -364,11 +368,11 @@ void CGTownInstance::initOverriddenBids() } } -bool CGTownInstance::isBonusingBuildingAdded(BuildingID::EBuildingID bid) const +bool CGTownInstance::isBonusingBuildingAdded(BuildingID bid) const { auto present = std::find_if(bonusingBuildings.begin(), bonusingBuildings.end(), [&](CGTownBuilding* building) { - return building->getBuildingType().num == bid; + return building->getBuildingType() == bid; }); return present != bonusingBuildings.end(); @@ -428,7 +432,7 @@ DamageRange CGTownInstance::getKeepDamageRange() const }; } -void CGTownInstance::deleteTownBonus(BuildingID::EBuildingID bid) +void CGTownInstance::deleteTownBonus(BuildingID bid) { size_t i = 0; CGTownBuilding * freeIt = nullptr; @@ -456,6 +460,40 @@ void CGTownInstance::deleteTownBonus(BuildingID::EBuildingID bid) delete freeIt; } +FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) +{ + if(getOwner().isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()).castle; + + if(alignmentToPlayer.isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(alignmentToPlayer).castle; + + std::vector potentialPicks; + + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); +} + +void CGTownInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == MapObjectID::TOWN || ID == MapObjectID::RANDOM_TOWN); + if (ID == MapObjectID::RANDOM_TOWN) + { + ID = MapObjectID::TOWN; + subID = randomizeFaction(rand); + } + + assert(ID == Obj::TOWN); // just in case + setType(ID, subID); + town = (*VLC->townh)[getFaction()]->town; + randomizeArmy(getFaction()); + updateAppearance(); +} + void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structures { blockVisit = true; @@ -467,7 +505,7 @@ void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structu for (int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++) { - BuildingID buildID = BuildingID(BuildingID::DWELL_FIRST).advance(level); + BuildingID buildID = BuildingID(BuildingID::DWELL_FIRST + level); int upgradeNum = 0; for (; town->buildings.count(buildID); upgradeNum++, buildID.advance(GameConstants::CREATURES_PER_TOWN)) @@ -489,19 +527,19 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const //give resources if there's a Mystic Pond if (hasBuilt(BuildingSubID::MYSTIC_POND) && cb->getDate(Date::DAY) != 1 - && (tempOwner < PlayerColor::PLAYER_LIMIT) + && (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->setObjProperty (id, ObjProperty::BONUS_VALUE_FIRST, resID); - cb->setObjProperty (id, ObjProperty::BONUS_VALUE_SECOND, 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->setObjProperty(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex //get Mana Vortex or Stables bonuses //same code is in the CGameHandler::buildStructure method @@ -516,7 +554,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const std::vector nativeCrits; //slots for(const auto & elem : Slots()) { - if (elem.second->type->getFaction() == subID) //native + if (elem.second->type->getFaction() == getFaction()) //native { nativeCrits.push_back(elem.first); //collect matching slots } @@ -533,7 +571,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const } else //upgrade { - cb->changeStackType(sl, VLC->creh->objects[*c->upgrades.begin()]); + cb->changeStackType(sl, c->upgrades.begin()->toCreature()); } } if ((stacksCount() < GameConstants::ARMY_SIZE && rand.nextInt(99) < 25) || Slots().empty()) //add new stack @@ -546,7 +584,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const TQuantity count = creatureGrowth(i); if (!count) // no dwelling - count = VLC->creatures()->getByIndex(c)->getGrowth(); + count = VLC->creatures()->getById(c)->getGrowth(); {//no lower tiers or above current month @@ -554,7 +592,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const { StackLocation sl(this, n); if (slotEmpty(n)) - cb->insertNewStack(sl, VLC->creh->objects[c], count); + cb->insertNewStack(sl, c.toCreature(), count); else //add to existing cb->changeStackCount(sl, count); } @@ -701,7 +739,7 @@ int CGTownInstance::getMarketEfficiency() const return marketCount; } -bool CGTownInstance::allowsTrade(EMarketMode::EMarketMode mode) const +bool CGTownInstance::allowsTrade(EMarketMode mode) const { switch(mode) { @@ -727,16 +765,16 @@ bool CGTownInstance::allowsTrade(EMarketMode::EMarketMode mode) const } } -std::vector CGTownInstance::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const { if(mode == EMarketMode::RESOURCE_ARTIFACT) { - std::vector ret; + std::vector ret; for(const CArtifact *a : merchantArtifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } else if ( mode == EMarketMode::RESOURCE_SKILL ) @@ -747,27 +785,18 @@ std::vector CGTownInstance::availableItemsIds(EMarketMode::EMarketMode mode return IMarket::availableItemsIds(mode); } -void CGTownInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::TOWN); // just in case - CGObjectInstance::setType(ID, subID); - town = (*VLC->townh)[subID]->town; - randomizeArmy(subID); - updateAppearance(); -} - void CGTownInstance::updateAppearance() { auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); //FIXME: not the best way to do this - auto app = VLC->objtypeh->getHandlerFor(ID, subID)->getOverride(terrain, this); + auto app = getObjectHandler()->getOverride(terrain, this); if (app) appearance = app; } std::string CGTownInstance::nodeName() const { - return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + name; + return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + getNameTranslated(); } void CGTownInstance::deserializationFix() @@ -787,7 +816,7 @@ void CGTownInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, -1); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, BonusSourceID()); addNewBonus(b); } @@ -888,7 +917,7 @@ const CTown * CGTownInstance::getTown() const { if(nullptr == town) { - return (*VLC->townh)[subID]->town; + return (*VLC->townh)[getFaction()]->town; } else return town; @@ -915,12 +944,17 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached() std::string CGTownInstance::getNameTranslated() const { - return name; + return VLC->generaltexth->translate(nameTextId); } -void CGTownInstance::setNameTranslated( const std::string & newName ) +std::string CGTownInstance::getNameTextID() const { - name = newName; + return nameTextId; +} + +void CGTownInstance::setNameTextId( const std::string & newName ) +{ + nameTextId = newName; } const CArmedInstance * CGTownInstance::getUpperArmy() const @@ -967,9 +1001,9 @@ bool CGTownInstance::hasBuilt(const BuildingID & buildingID) const return vstd::contains(builtBuildings, buildingID); } -bool CGTownInstance::hasBuilt(const BuildingID & buildingID, int townID) const +bool CGTownInstance::hasBuilt(const BuildingID & buildingID, FactionID townID) const { - if (townID == town->faction->getIndex() || townID == ETownType::ANY) + if (townID == town->faction->getId() || townID == FactionID::ANY) return hasBuilt(buildingID); return false; } @@ -980,7 +1014,7 @@ TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const return town->buildings.at(buildingID)->resources; else { - logGlobal->error("Town %s at %s has no possible building %d!", name, pos.toString(), buildingID.toEnum()); + logGlobal->error("Town %s at %s has no possible building %d!", getNameTranslated(), pos.toString(), buildingID.toEnum()); return TResources(); } @@ -1043,14 +1077,14 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & void CGTownInstance::addHeroToStructureVisitors(const CGHeroInstance *h, si64 structureInstanceID ) const { if(visitingHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors else if(garrisonHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero else { //should never ever happen logGlobal->error("Cannot add hero %s to visitors of structure # %d", h->getNameTranslated(), structureInstanceID); - throw std::runtime_error("internal error"); + throw std::runtime_error("unexpected hero in CGTownInstance::addHeroToStructureVisitors"); } } @@ -1066,23 +1100,17 @@ void CGTownInstance::battleFinished(const CGHeroInstance * hero, const BattleRes void CGTownInstance::onTownCaptured(const PlayerColor & winner) const { setOwner(winner); - FoWChange fw; - fw.player = winner; - fw.mode = 1; - cb->getTilesInRange(fw.tiles, getSightCenter(), getSightRadius(), winner, 1); - cb->sendAndApply(& fw); + cb->changeFogOfWar(getSightCenter(), getSightRadius(), winner, ETileVisibility::REVEALED); } void CGTownInstance::afterAddToMap(CMap * map) { - if(ID == Obj::TOWN) - map->towns.emplace_back(this); + map->towns.emplace_back(this); } void CGTownInstance::afterRemoveFromMap(CMap * map) { - if (ID == Obj::TOWN) - vstd::erase_if_present(map->towns, this); + vstd::erase_if_present(map->towns, this); } void CGTownInstance::reset() @@ -1093,17 +1121,16 @@ void CGTownInstance::reset() void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { - static const std::vector FORMATIONS = { "wide", "tight" }; - CGObjectInstance::serializeJsonOwner(handler); - CCreatureSet::serializeJson(handler, "army", 7); - handler.serializeEnum("tightFormation", formation, FORMATIONS); - handler.serializeString("name", name); + if(!handler.saving) + handler.serializeEnum("tightFormation", formation, NArmyFormation::names); //for old format + CArmedInstance::serializeJsonOptions(handler); + handler.serializeString("name", nameTextId); { auto decodeBuilding = [this](const std::string & identifier) -> si32 { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), getTown()->getBuildingScope(), identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), getTown()->getBuildingScope(), identifier); if(rawId) return rawId.value(); @@ -1127,7 +1154,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) for(const BuildingID & id : forbiddenBuildings) { - buildingsLIC.none.insert(id); + buildingsLIC.none.insert(id.getNum()); customBuildings = true; } @@ -1144,7 +1171,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(id == BuildingID::FORT) hasFort = true; - buildingsLIC.all.insert(id); + buildingsLIC.all.insert(id.getNum()); customBuildings = true; } @@ -1179,42 +1206,14 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) } { - std::vector standard = VLC->spellh->getDefaultAllowed(); - JsonSerializeFormat::LIC spellsLIC(standard, SpellID::decode, SpellID::encode); - - if(handler.saving) - { - for(const SpellID & id : possibleSpells) - spellsLIC.any[id.num] = true; - - for(const SpellID & id : obligatorySpells) - spellsLIC.all[id.num] = true; - } - - handler.serializeLIC("spells", spellsLIC); - - if(!handler.saving) - { - possibleSpells.clear(); - for(si32 idx = 0; idx < spellsLIC.any.size(); idx++) - { - if(spellsLIC.any[idx]) - possibleSpells.emplace_back(idx); - } - - obligatorySpells.clear(); - for(si32 idx = 0; idx < spellsLIC.all.size(); idx++) - { - if(spellsLIC.all[idx]) - obligatorySpells.emplace_back(idx); - } - } + handler.serializeIdArray( "possibleSpells", possibleSpells); + handler.serializeIdArray( "obligatorySpells", obligatorySpells); } } FactionID CGTownInstance::getFaction() const { - return town->faction->getId(); + return FactionID(subID.getNum()); } TerrainId CGTownInstance::getNativeTerrain() const diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 1e914df89..0bc2976ba 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -44,7 +44,7 @@ struct DLL_LINKAGE GrowthInfo class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader { - std::string name; // name of town + std::string nameTextId; // name of town public: using CGDwelling::getPosition; @@ -68,12 +68,12 @@ public: ////////////////////////////////////////////////////////////////////////// static std::vector merchantArtifacts; //vector of artifacts available at Artifact merchant, NULLs possible (for making empty space when artifact is bought) - static std::vector universitySkills;//skills for university of magic + static std::vector universitySkills;//skills for university of magic template void serialize(Handler &h, const int version) { h & static_cast(*this); - h & name; + h & nameTextId; h & builded; h & destroyed; h & identifier; @@ -92,7 +92,18 @@ public: for(auto * bonusingBuilding : bonusingBuildings) bonusingBuilding->town = this; - h & town; + if (h.saving) + { + CFaction * faction = town ? town->faction : nullptr; + h & faction; + } + else + { + CFaction * faction = nullptr; + h & faction; + town = faction ? faction->town : nullptr; + } + h & townAndVis; BONUS_TREE_DESERIALIZATION_FIX @@ -102,7 +113,7 @@ public: { 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", name, pos.toString(), 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; @@ -126,7 +137,8 @@ public: const CArmedInstance *getUpperArmy() const; //garrisoned hero if present or the town itself std::string getNameTranslated() const; - void setNameTranslated(const std::string & newName); + std::string getNameTextID() const; + void setNameTextId(const std::string & newName); ////////////////////////////////////////////////////////////////////////// @@ -138,10 +150,9 @@ public: EGeneratorState shipyardStatus() const override; const IObjectInterface * getObject() const override; int getMarketEfficiency() const override; //=market count - bool allowsTrade(EMarketMode::EMarketMode mode) const override; - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + bool allowsTrade(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; - void setType(si32 ID, si32 subID) override; void updateAppearance(); ////////////////////////////////////////////////////////////////////////// @@ -161,7 +172,7 @@ public: 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, int townID) const; + bool hasBuilt(const BuildingID & buildingID, FactionID townID) const; TResources getBuildingCost(const BuildingID & buildingID) const; TResources dailyIncome() const; //calculates daily income of this town @@ -175,7 +186,7 @@ public: void removeCapitols(const PlayerColor & owner) const; void clearArmy() const; void addHeroToStructureVisitors(const CGHeroInstance *h, si64 structureInstanceID) const; //hero must be visiting or garrisoned in town - void deleteTownBonus(BuildingID::EBuildingID bid); + void deleteTownBonus(BuildingID bid); /// Returns damage range for secondary towers of this town DamageRange getTowerDamageRange() const; @@ -197,6 +208,7 @@ public: 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 battleFinished(const CGHeroInstance * hero, const BattleResult & result) const override; std::string getObjectName() const override; @@ -211,16 +223,17 @@ public: return defendingHero && garrisonHero && defendingHero != garrisonHero; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + 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); void setOwner(const PlayerColor & owner) const; void onTownCaptured(const PlayerColor & winner) const; int getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const; bool townEnvisagesBuilding(BuildingSubID::EBuildingSubID bid) const; - bool isBonusingBuildingAdded(BuildingID::EBuildingID bid) const; + bool isBonusingBuildingAdded(BuildingID bid) const; void initOverriddenBids(); void addTownBonuses(CRandomGenerator & rand); }; diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index dccbb6610..bf9c2158d 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -12,14 +12,15 @@ #include "CObjectHandler.h" #include "CGObjectInstance.h" -#include "../filesystem/ResourceID.h" +#include "../filesystem/ResourcePath.h" +#include "../JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN CObjectHandler::CObjectHandler() { logGlobal->trace("\t\tReading resources prices "); - const JsonNode config2(ResourceID("config/resources.json")); + const JsonNode config2(JsonPath::builtin("config/resources.json")); for(const JsonNode &price : config2["resources_prices"].Vector()) { resVals.push_back(static_cast(price.Float())); @@ -27,14 +28,4 @@ CObjectHandler::CObjectHandler() logGlobal->trace("\t\tDone loading resource prices!"); } -CGObjectInstanceBySubIdFinder::CGObjectInstanceBySubIdFinder(CGObjectInstance * obj) : obj(obj) -{ - -} - -bool CGObjectInstanceBySubIdFinder::operator()(CGObjectInstance * obj) const -{ - return this->obj->subID == obj->subID; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CObjectHandler.h b/lib/mapObjects/CObjectHandler.h index 128d579b4..7413918b2 100644 --- a/lib/mapObjects/CObjectHandler.h +++ b/lib/mapObjects/CObjectHandler.h @@ -16,28 +16,12 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; class int3; -/// function object which can be used to find an object with an specific sub ID -class CGObjectInstanceBySubIdFinder -{ -public: - CGObjectInstanceBySubIdFinder(CGObjectInstance * obj); - bool operator()(CGObjectInstance * obj) const; - -private: - CGObjectInstance * obj; -}; - class DLL_LINKAGE CObjectHandler { public: std::vector resVals; //default values of resources in gold CObjectHandler(); - - template void serialize(Handler &h, const int version) - { - h & resVals; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 2018ca8dc..3f4275a3a 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -14,7 +14,6 @@ #include #include "../ArtifactUtils.h" -#include "../NetPacks.h" #include "../CSoundBase.h" #include "../CGeneralTextHandler.h" #include "../CHeroHandler.h" @@ -22,31 +21,34 @@ #include "../IGameCallback.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../serializer/JsonSerializeFormat.h" -#include "../CModHandler.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" +#include "../CPlayerState.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../modding/ModScope.h" +#include "../modding/ModUtility.h" +#include "../networkPacks/PacksForClient.h" +#include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN -std::map > CGKeys::playerKeyMap; - //TODO: Remove constructor CQuest::CQuest(): qid(-1), - missionType(MISSION_NONE), - progress(NOT_ACTIVE), + isCompleted(false), lastDay(-1), - m13489val(0), + killTarget(ObjectInstanceID::NONE), textOption(0), completedOption(0), stackDirection(0), - heroPortrait(-1), isCustomFirst(false), isCustomNext(false), - isCustomComplete(false) + isCustomComplete(false), + repeatedQuest(false), + questName(CQuest::missionName(0)) { } @@ -56,9 +58,9 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -const std::string & CQuest::missionName(CQuest::Emission mission) +const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -69,7 +71,9 @@ const std::string & CQuest::missionName(CQuest::Emission mission) "bringResources", "bringHero", "bringPlayer", - "keymaster" + "keymaster", + "hota", + "other" }; if(static_cast(mission) < names.size()) @@ -99,7 +103,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) ui32 count = 0; ui32 slotsCount = 0; bool hasExtraCreatures = false; - for(cre = q->m6creatures.begin(); cre != q->m6creatures.end(); ++cre) + for(cre = q->mission.creatures.begin(); cre != q->mission.creatures.end(); ++cre) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { @@ -121,357 +125,222 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) bool CQuest::checkQuest(const CGHeroInstance * h) const { - switch (missionType) + if(!mission.heroAllowed(h)) + return false; + + if(killTarget != ObjectInstanceID::NONE) { - case MISSION_NONE: - return true; - case MISSION_LEVEL: - return m13489val <= h->level; - case MISSION_PRIMARY_STAT: - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - { - if(h->getPrimSkillLevel(static_cast(i)) < static_cast(m2stats[i])) - return false; - } - return true; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if(!CGHeroInstance::cb->getObjByQuestIdentifier(m13489val)) - return true; - return false; - case MISSION_ART: - { - // if the object was deserialized - if(artifactsRequirements.empty()) - for(const auto & id : m5arts) - ++artifactsRequirements[id]; - - size_t reqSlots = 0; - for(const auto & elem : artifactsRequirements) - { - // check required amount of artifacts - if(h->getArtPosCount(elem.first, false, true, true) < elem.second) - return false; - if(!h->hasArt(elem.first)) - reqSlots += h->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; - } - if(ArtifactUtils::isBackpackFreeSlots(h, reqSlots)) - return true; - else - return false; - } - case MISSION_ARMY: - return checkMissionArmy(this, h); - case MISSION_RESOURCES: - for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) //including Mithril ? - { //Quest has no direct access to callback - if(CGHeroInstance::cb->getResource(h->tempOwner, i) < static_cast(m7resources[i])) - return false; - } - return true; - case MISSION_HERO: - return m13489val == h->type->getIndex(); - case MISSION_PLAYER: - return m13489val == h->getOwner().getNum(); - default: + if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; } + + return true; } -void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const +void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const +{ + for(auto & elem : mission.artifacts) + { + if(h->hasArt(elem)) + { + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(elem, false))); + } + else + { + const auto * assembly = h->getAssemblyByConstituent(elem); + assert(assembly); + auto parts = assembly->getPartsInfo(); + + // Remove the assembly + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(assembly))); + + // Disassemble this backpack artifact + for(const auto & ci : parts) + { + if(ci.art->getTypeId() != elem) + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); + } + } + } + + cb->takeCreatures(h->id, mission.creatures); + cb->giveResources(h->getOwner(), mission.resources); +} + +void CQuest::addTextReplacements(MetaString & text, std::vector & components) const +{ + if(mission.heroLevel > 0) + text.replaceNumber(mission.heroLevel); + + if(mission.heroExperience > 0) + text.replaceNumber(mission.heroExperience); + + { //primary skills + MetaString loot; + for(int i = 0; i < 4; ++i) + { + if(mission.primary[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(mission.primary[i]); + loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); + } + } + + for(auto & skill : mission.secondary) + { + loot.appendTextID(VLC->skillh->getById(skill.first)->getNameTextID()); + } + + for(auto & spell : mission.spells) + { + loot.appendTextID(VLC->spellh->getById(spell)->getNameTextID()); + } + + if(!loot.empty()) + text.replaceRawString(loot.buildList()); + } + + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) + { + components.emplace_back(ComponentType::HERO_PORTRAIT, heroPortrait); + addKillTargetReplacements(text); + } + + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) + { + components.emplace_back(ComponentType::CREATURE, stackToKill); + addKillTargetReplacements(text); + } + + if(!mission.heroes.empty()) + text.replaceRawString(VLC->heroh->getById(mission.heroes.front())->getNameTranslated()); + + if(!mission.artifacts.empty()) + { + MetaString loot; + for(const auto & elem : mission.artifacts) + { + loot.appendRawString("%s"); + loot.replaceName(elem); + } + text.replaceRawString(loot.buildList()); + } + + if(!mission.creatures.empty()) + { + MetaString loot; + for(const auto & elem : mission.creatures) + { + loot.appendRawString("%s"); + loot.replaceName(elem); + } + text.replaceRawString(loot.buildList()); + } + + if(mission.resources.nonZero()) + { + MetaString loot; + for(auto i : GameResID::ALL_RESOURCES()) + { + if(mission.resources[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(mission.resources[i]); + loot.replaceName(i); + } + } + text.replaceRawString(loot.buildList()); + } + + if(!mission.players.empty()) + { + MetaString loot; + for(auto & p : mission.players) + loot.appendName(p); + + text.replaceRawString(loot.buildList()); + } + + if(lastDay >= 0) + text.replaceNumber(lastDay - IObjectInterface::cb->getDate(Date::DAY)); +} + +void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { - std::string text; bool failRequirements = (h ? !checkQuest(h) : true); + mission.loadComponents(components, h); if(firstVisit) - { - isCustom = isCustomFirst; - text = firstVisitText; - iwText.appendRawString(text); - } + iwText.appendRawString(firstVisitText.toString()); else if(failRequirements) - { - isCustom = isCustomNext; - text = nextVisitText; - iwText.appendRawString(text); - } - switch (missionType) - { - case MISSION_LEVEL: - components.emplace_back(Component::EComponentType::EXPERIENCE, 0, m13489val, 0); - if(!isCustom) - iwText.replaceNumber(m13489val); - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for(int i = 0; i < 4; ++i) - { - if(m2stats[i]) - { - components.emplace_back(Component::EComponentType::PRIM_SKILL, i, m2stats[i], 0); - loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); - if(!isCustom) - addReplacements(iwText, text); - break; - case MISSION_HERO: - //FIXME: portrait may not match hero, if custom portrait was set in map editor - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0); - if(!isCustom) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); - break; - case MISSION_KILL_CREATURE: - { - components.emplace_back(stackToKill); - if(!isCustom) - { - addReplacements(iwText, text); - } - } - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : m5arts) - { - components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : m6creatures) - { - components.emplace_back(elem); - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for(int i = 0; i < 7; ++i) - { - if(m7resources[i]) - { - components.emplace_back(Component::EComponentType::RESOURCE, i, m7resources[i], 0); - loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_PLAYER: - components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0); - if(!isCustom) - iwText.replaceRawString(VLC->generaltexth->colors[m13489val]); - break; - } + iwText.appendRawString(nextVisitText.toString()); + + if(lastDay >= 0) + iwText.appendTextID(TextIdentifier("core", "seerhut", "time", textOption).get()); + + addTextReplacements(iwText, components); } void CQuest::getRolloverText(MetaString &ms, bool onHover) const { - // Quests with MISSION_NONE type don't have a text for them - assert(missionType != MISSION_NONE); - if(onHover) ms.appendRawString("\n\n"); - std::string questName = missionName(missionType); std::string questState = missionState(onHover ? 3 : 4); - ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption)); + ms.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, questState, textOption).get()); - switch(missionType) - { - case MISSION_LEVEL: - ms.replaceNumber(m13489val); - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for (int i = 0; i < 4; ++i) - { - if (m2stats[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - ms.replaceRawString(heroName); - break; - case MISSION_KILL_CREATURE: - ms.replaceCreatureName(stackToKill); - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : m5arts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : m6creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (m7resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_HERO: - ms.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); - break; - case MISSION_PLAYER: - ms.replaceRawString(VLC->generaltexth->colors[m13489val]); - break; - default: - break; - } + std::vector components; + addTextReplacements(ms, components); } -void CQuest::getCompletionText(MetaString &iwText, std::vector &components, bool isCustom, const CGHeroInstance * h) const +void CQuest::getCompletionText(MetaString &iwText) const { - iwText.appendRawString(completedText); - switch(missionType) - { - case CQuest::MISSION_LEVEL: - if (!isCustomComplete) - iwText.replaceNumber(m13489val); - break; - case CQuest::MISSION_PRIMARY_STAT: - if (vstd::contains (completedText,'%')) //there's one case when there's nothing to replace - { - MetaString loot; - for (int i = 0; i < 4; ++i) - { - if (m2stats[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_ART: - { - MetaString loot; - for(const auto & elem : m5arts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : m6creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (m7resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if (!isCustomComplete) - addReplacements(iwText, completedText); - break; - case MISSION_HERO: - if (!isCustomComplete) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); - break; - case MISSION_PLAYER: - if (!isCustomComplete) - iwText.replaceRawString(VLC->generaltexth->colors[m13489val]); - break; - } + iwText.appendRawString(completedText.toString()); + + std::vector components; + addTextReplacements(iwText, components); } -void CQuest::addArtifactID(const ArtifactID & id) +void CQuest::defineQuestName() { - m5arts.push_back(id); - ++artifactsRequirements[id]; + //standard quests + questName = CQuest::missionName(0); + if(mission != Rewardable::Limiter{}) questName = CQuest::missionName(12); + if(mission.heroLevel > 0) questName = CQuest::missionName(1); + for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2); + if(!mission.spells.empty()) questName = CQuest::missionName(2); + if(!mission.secondary.empty()) questName = CQuest::missionName(2); + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3); + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) questName = CQuest::missionName(4); + if(!mission.artifacts.empty()) questName = CQuest::missionName(5); + if(!mission.creatures.empty()) questName = CQuest::missionName(6); + if(mission.resources.nonZero()) questName = CQuest::missionName(7); + if(!mission.heroes.empty()) questName = CQuest::missionName(8); + if(!mission.players.empty()) questName = CQuest::missionName(9); + if(mission.daysPassed > 0 || !mission.heroClasses.empty()) questName = CQuest::missionName(11); +} + +void CQuest::addKillTargetReplacements(MetaString &out) const +{ + if(!heroName.empty()) + out.replaceTextID(heroName); + if(stackToKill != CreatureID::NONE) + { + out.replaceNamePlural(stackToKill); + out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); + } } void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { auto q = handler.enterStruct(fieldName); - handler.serializeString("firstVisitText", firstVisitText); - handler.serializeString("nextVisitText", nextVisitText); - handler.serializeString("completedText", completedText); + handler.serializeStruct("firstVisitText", firstVisitText); + handler.serializeStruct("nextVisitText", nextVisitText); + handler.serializeStruct("completedText", completedText); + handler.serializeBool("repeatedQuest", repeatedQuest, false); if(!handler.saving) { @@ -479,82 +348,94 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi isCustomNext = !nextVisitText.empty(); isCustomComplete = !completedText.empty(); } - - static const std::vector MISSION_TYPE_JSON = - { - "None", "Level", "PrimaryStat", "KillHero", "KillCreature", "Artifact", "Army", "Resources", "Hero", "Player" - }; - - handler.serializeEnum("missionType", missionType, Emission::MISSION_NONE, MISSION_TYPE_JSON); + handler.serializeInt("timeLimit", lastDay, -1); + handler.serializeStruct("limiter", mission); + handler.serializeInstance("killTarget", killTarget, ObjectInstanceID::NONE); - switch (missionType) + if(!handler.saving) //compatibility with legacy vmaps { - case MISSION_NONE: - break; - case MISSION_LEVEL: - handler.serializeInt("heroLevel", m13489val, -1); - break; - case MISSION_PRIMARY_STAT: + std::string missionType = "None"; + handler.serializeString("missionType", missionType); + if(missionType == "None") + return; + + if(missionType == "Level") + handler.serializeInt("heroLevel", mission.heroLevel); + + if(missionType == "PrimaryStat") { auto primarySkills = handler.enterStruct("primarySkills"); - if(!handler.saving) - m2stats.resize(GameConstants::PRIMARY_SKILLS); - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(PrimarySkill::names[i], m2stats[i], 0); + handler.serializeInt(NPrimarySkill::names[i], mission.primary[i], 0); } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - handler.serializeInstance("killTarget", m13489val, static_cast(-1)); - break; - case MISSION_ART: - //todo: ban artifacts - handler.serializeIdArray("artifacts", m5arts); - break; - case MISSION_ARMY: + + if(missionType == "Artifact") + handler.serializeIdArray("artifacts", mission.artifacts); + + if(missionType == "Army") { auto a = handler.enterArray("creatures"); - a.serializeStruct(m6creatures); + a.serializeStruct(mission.creatures); } - break; - case MISSION_RESOURCES: + + if(missionType == "Resources") { auto r = handler.enterStruct("resources"); for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], m7resources[idx], 0); + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mission.resources[idx], 0); } } - break; - case MISSION_HERO: - handler.serializeId("hero", m13489val, 0); - break; - case MISSION_PLAYER: - handler.serializeEnum("player", m13489val, PlayerColor::CANNOT_DETERMINE.getNum(), GameConstants::PLAYER_COLOR_NAMES); - break; - default: - logGlobal->error("Invalid quest mission type"); - break; + + if(missionType == "Hero") + { + HeroTypeID temp; + handler.serializeId("hero", temp, HeroTypeID::NONE); + mission.heroes.emplace_back(temp); + } + + if(missionType == "Player") + { + PlayerColor temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); + mission.players.emplace_back(temp); + } } } +bool IQuestObject::checkQuest(const CGHeroInstance* h) const +{ + return quest->checkQuest(h); +} + +void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const +{ + quest->getVisitText(text, components, FirstVisit, h); +} + +void IQuestObject::afterAddToMapCommon(CMap * map) const +{ + map->addNewQuestInstance(quest); +} + void CGSeerHut::setObjToKill() { - if(quest->missionType == CQuest::MISSION_KILL_CREATURE) + if(quest->killTarget == ObjectInstanceID::NONE) + return; + + if(getCreatureToKill(true)) { - quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? - assert(quest->stackToKill.type); - quest->stackToKill.count = 0; //no count in info window + quest->stackToKill = getCreatureToKill(false)->getCreature(); + assert(quest->stackToKill != CreatureID::NONE); quest->stackDirection = checkDirection(); } - else if(quest->missionType == CQuest::MISSION_KILL_HERO) + else if(getHeroToKill(true)) { quest->heroName = getHeroToKill(false)->getNameTranslated(); - quest->heroPortrait = getHeroToKill(false)->portrait; + quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); } } @@ -566,34 +447,46 @@ void CGSeerHut::init(CRandomGenerator & rand) seerName = VLC->generaltexth->translate(seerNameID); quest->textOption = rand.nextInt(2); quest->completedOption = rand.nextInt(1, 3); + + configuration.canRefuse = true; + configuration.visitMode = Rewardable::EVisitMode::VISIT_ONCE; + configuration.selectMode = Rewardable::ESelectMode::SELECT_PLAYER; } void CGSeerHut::initObj(CRandomGenerator & rand) { init(rand); - - quest->progress = CQuest::NOT_ACTIVE; - if(quest->missionType) + + CRewardableObject::initObj(rand); + + setObjToKill(); + quest->defineQuestName(); + + if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE) + quest->isCompleted = true; + + if(quest->questName == quest->missionName(0)) { - std::string questName = quest->missionName(quest->missionType); - - if(!quest->isCustomFirst) - quest->firstVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(0), quest->textOption); - if(!quest->isCustomNext) - quest->nextVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(1), quest->textOption); - if(!quest->isCustomComplete) - quest->completedText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(2), quest->textOption); + quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } else { - quest->progress = CQuest::COMPLETE; - quest->firstVisitText = VLC->generaltexth->seerEmpty[quest->completedOption]; + if(!quest->isCustomFirst) + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(0), quest->textOption).get()); + if(!quest->isCustomNext) + quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(1), quest->textOption).get()); + if(!quest->isCustomComplete) + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } + + quest->getCompletionText(configuration.onSelect); + for(auto & i : configuration.info) + quest->getCompletionText(i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const { - quest->getRolloverText (text, onHover);//TODO: simplify? + quest->getRolloverText(text, onHover);//TODO: simplify? if(!onHover) text.replaceRawString(seerName); } @@ -601,13 +494,15 @@ void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const std::string CGSeerHut::getHoverText(PlayerColor player) const { std::string hoverName = getObjectName(); - if(ID == Obj::SEER_HUT && quest->progress != CQuest::NOT_ACTIVE) + if(ID == Obj::SEER_HUT && quest->activeForPlayers.count(player)) { hoverName = VLC->generaltexth->allTexts[347]; boost::algorithm::replace_first(hoverName, "%s", seerName); } - if(quest->progress & quest->missionType) //rollover when the quest is active + if(quest->activeForPlayers.count(player) + && (quest->mission != Rewardable::Limiter{} + || quest->killTarget != ObjectInstanceID::NONE)) //rollover when the quest is active { MetaString ms; getRolloverText (ms, true); @@ -616,91 +511,61 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CQuest::addReplacements(MetaString &out, const std::string &base) const +std::string CGSeerHut::getHoverText(const CGHeroInstance * hero) const { - switch(missionType) - { - case MISSION_KILL_CREATURE: - out.replaceCreatureName(stackToKill); - if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster - { - out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); - } - break; - case MISSION_KILL_HERO: - out.replaceRawString(heroName); - break; - } + return getHoverText(hero->getOwner()); } -bool IQuestObject::checkQuest(const CGHeroInstance* h) const +std::string CGSeerHut::getPopupText(PlayerColor player) const { - return quest->checkQuest(h); + return getHoverText(player); } -void IQuestObject::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const +std::string CGSeerHut::getPopupText(const CGHeroInstance * hero) const { - quest->getVisitText (text,components, isCustom, FirstVisit, h); + return getHoverText(hero->getOwner()); } -void IQuestObject::afterAddToMapCommon(CMap * map) const +std::vector CGSeerHut::getPopupComponents(PlayerColor player) const { - map->addNewQuestInstance(quest); + std::vector result; + if (quest->activeForPlayers.count(player)) + quest->mission.loadComponents(result, nullptr); + return result; } -void CGSeerHut::getCompletionText(MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h) const +std::vector CGSeerHut::getPopupComponents(const CGHeroInstance * hero) const { - quest->getCompletionText (text, components, isCustom, h); - switch(rewardType) - { - case EXPERIENCE: - components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(rVal)), 0); - break; - case MANA_POINTS: - components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, rVal, 0); - break; - case MORALE_BONUS: - components.emplace_back(Component::EComponentType::MORALE, 0, rVal, 0); - break; - case LUCK_BONUS: - components.emplace_back(Component::EComponentType::LUCK, 0, rVal, 0); - break; - case RESOURCES: - components.emplace_back(Component::EComponentType::RESOURCE, rID, rVal, 0); - break; - case PRIMARY_SKILL: - components.emplace_back(Component::EComponentType::PRIM_SKILL, rID, rVal, 0); - break; - case SECONDARY_SKILL: - components.emplace_back(Component::EComponentType::SEC_SKILL, rID, rVal, 0); - break; - case ARTIFACT: - components.emplace_back(Component::EComponentType::ARTIFACT, rID, 0, 0); - break; - case SPELL: - components.emplace_back(Component::EComponentType::SPELL, rID, 0, 0); - break; - case CREATURE: - components.emplace_back(Component::EComponentType::CREATURE, rID, rVal, 0); - break; - } + std::vector result; + if (quest->activeForPlayers.count(hero->getOwner())) + quest->mission.loadComponents(result, hero); + return result; } -void CGSeerHut::setPropertyDer (ui8 what, ui32 val) +void CGSeerHut::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case 10: - quest->progress = static_cast(val); + case ObjProperty::SEERHUT_VISITED: + { + quest->activeForPlayers.emplace(identifier.as()); break; + } + case ObjProperty::SEERHUT_COMPLETE: + { + quest->isCompleted = identifier.getNum(); + quest->activeForPlayers.clear(); + break; + } } } void CGSeerHut::newTurn(CRandomGenerator & rand) const { + CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { - cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, true); } } @@ -708,41 +573,30 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const { InfoWindow iw; iw.player = h->getOwner(); - if(quest->progress < CQuest::COMPLETE) + if(!quest->isCompleted) { - bool firstVisit = !quest->progress; + bool firstVisit = !quest->activeForPlayers.count(h->getOwner()); bool failRequirements = !checkQuest(h); - bool isCustom = false; if(firstVisit) { - isCustom = quest->isCustomFirst; - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::IN_PROGRESS); + cb->setObjPropertyID(id, ObjProperty::SEERHUT_VISITED, h->getOwner()); AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); aq.player = h->tempOwner; cb->sendAndApply(&aq); //TODO: merge with setObjProperty? } - else if(failRequirements) - { - isCustom = quest->isCustomNext; - } if(firstVisit || failRequirements) { - getVisitText (iw.text, iw.components, isCustom, firstVisit, h); + getVisitText (iw.text, iw.components, firstVisit, h); cb->showInfoDialog(&iw); } if(!failRequirements) // propose completion, also on first visit { - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - - getCompletionText (bd.text, bd.components, isCustom, h); - - cb->showBlockingDialog (&bd); + CRewardableObject::onHeroVisit(h); return; } } @@ -787,131 +641,30 @@ int CGSeerHut::checkDirection() const } } -void CGSeerHut::finishQuest(const CGHeroInstance * h, ui32 accept) const -{ - if (accept) - { - switch (quest->missionType) - { - case CQuest::MISSION_ART: - for(auto & elem : quest->m5arts) - { - if(h->hasArt(elem)) - { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); - } - else - { - const auto * assembly = h->getAssemblyByConstituent(elem); - assert(assembly); - auto parts = assembly->getPartsInfo(); - - // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); - - // Disassemble this backpack artifact - for(const auto & ci : parts) - { - if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, GameConstants::BACKPACK_START); - } - } - } - break; - case CQuest::MISSION_ARMY: - cb->takeCreatures(h->id, quest->m6creatures); - break; - case CQuest::MISSION_RESOURCES: - for (int i = 0; i < 7; ++i) - { - cb->giveResource(h->getOwner(), static_cast(i), -static_cast(quest->m7resources[i])); - } - break; - default: - break; - } - cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete - completeQuest(h); //make sure to remove QuestGuard at the very end - } -} - -void CGSeerHut::completeQuest (const CGHeroInstance * h) const //reward -{ - switch (rewardType) - { - case EXPERIENCE: - { - TExpType expVal = h->calculateXp(rVal); - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, expVal, false); - break; - } - case MANA_POINTS: - { - cb->setManaPoints(h->id, h->mana+rVal); - break; - } - case MORALE_BONUS: case LUCK_BONUS: - { - Bonus hb(BonusDuration::ONE_WEEK, (rewardType == 3 ? BonusType::MORALE : BonusType::LUCK), - BonusSource::OBJECT, rVal, h->id.getNum(), "", -1); - GiveBonus gb; - gb.id = h->id.getNum(); - gb.bonus = hb; - cb->giveHeroBonus(&gb); - } - break; - case RESOURCES: - cb->giveResource(h->getOwner(), static_cast(rID), rVal); - break; - case PRIMARY_SKILL: - cb->changePrimSkill(h, static_cast(rID), rVal, false); - break; - case SECONDARY_SKILL: - cb->changeSecSkill(h, SecondarySkill(rID), rVal, false); - break; - case ARTIFACT: - cb->giveHeroNewArtifact(h, VLC->arth->objects[rID],ArtifactPosition::FIRST_AVAILABLE); - break; - case SPELL: - { - std::set spell; - spell.insert (SpellID(rID)); - cb->changeSpells(h, true, spell); - } - break; - case CREATURE: - { - CCreatureSet creatures; - creatures.setCreature(SlotID(0), CreatureID(rID), rVal); - cb->giveCreatures(this, h, creatures, false); - } - break; - default: - break; - } -} - const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && (o->ID == Obj::HERO || o->ID == Obj::PRISON)); return dynamic_cast(o); } const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && o->ID == Obj::MONSTER); return dynamic_cast(o); } void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { - finishQuest(hero, answer); + CRewardableObject::blockingDialogAnswered(hero, answer); + if(answer) + { + quest->completeQuest(cb, hero); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete + } } void CGSeerHut::afterAddToMap(CMap* map) @@ -921,163 +674,74 @@ void CGSeerHut::afterAddToMap(CMap* map) void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) { - static const std::map REWARD_MAP = - { - {NOTHING, ""}, - {EXPERIENCE, "experience"}, - {MANA_POINTS, "mana"}, - {MORALE_BONUS, "morale"}, - {LUCK_BONUS, "luck"}, - {RESOURCES, "resource"}, - {PRIMARY_SKILL, "primarySkill"}, - {SECONDARY_SKILL,"secondarySkill"}, - {ARTIFACT, "artifact"}, - {SPELL, "spell"}, - {CREATURE, "creature"} - }; - - static const std::map REWARD_RMAP = - { - {"experience", EXPERIENCE}, - {"mana", MANA_POINTS}, - {"morale", MORALE_BONUS}, - {"luck", LUCK_BONUS}, - {"resource", RESOURCES}, - {"primarySkill", PRIMARY_SKILL}, - {"secondarySkill",SECONDARY_SKILL}, - {"artifact", ARTIFACT}, - {"spell", SPELL}, - {"creature", CREATURE} - }; - //quest and reward + CRewardableObject::serializeJsonOptions(handler); quest->serializeJson(handler, "quest"); - - //only one reward is supported - //todo: full reward format support after CRewardInfo integration - - auto s = handler.enterStruct("reward"); - std::string fullIdentifier; - std::string metaTypeName; - std::string scope; - std::string identifier; - - if(handler.saving) + + if(!handler.saving) { - si32 amount = rVal; - - metaTypeName = REWARD_MAP.at(rewardType); - switch (rewardType) - { - case NOTHING: - break; - case EXPERIENCE: - case MANA_POINTS: - case MORALE_BONUS: - case LUCK_BONUS: - identifier.clear(); - break; - case RESOURCES: - identifier = GameConstants::RESOURCE_NAMES[rID]; - break; - case PRIMARY_SKILL: - identifier = PrimarySkill::names[rID]; - break; - case SECONDARY_SKILL: - identifier = CSkillHandler::encodeSkill(rID); - break; - case ARTIFACT: - identifier = ArtifactID(rID).toArtifact(VLC->artifacts())->getJsonKey(); - amount = 1; - break; - case SPELL: - identifier = SpellID(rID).toSpell(VLC->spells())->getJsonKey(); - amount = 1; - break; - case CREATURE: - identifier = CreatureID(rID).toCreature(VLC->creatures())->getJsonKey(); - break; - default: - assert(false); - break; - } - if(rewardType != NOTHING) - { - fullIdentifier = CModHandler::makeFullIdentifier(scope, metaTypeName, identifier); - handler.serializeInt(fullIdentifier, amount); - } - } - else - { - rewardType = NOTHING; - + //backward compatibility for VCMI maps that use old SeerHut format + auto s = handler.enterStruct("reward"); const JsonNode & rewardsJson = handler.getCurrent(); + + std::string fullIdentifier; + std::string metaTypeName; + std::string scope; + std::string identifier; - fullIdentifier.clear(); + auto iter = rewardsJson.Struct().begin(); + fullIdentifier = iter->first; - if(rewardsJson.Struct().empty()) + ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); + if(!std::set{"resource", "primarySkill", "secondarySkill", "artifact", "spell", "creature", "experience", "mana", "morale", "luck"}.count(metaTypeName)) return; - else + + int val = 0; + handler.serializeInt(fullIdentifier, val); + + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + if(metaTypeName == "experience") + reward.heroExperience = val; + if(metaTypeName == "mana") + reward.manaDiff = val; + if(metaTypeName == "morale") + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); + if(metaTypeName == "luck") + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); + if(metaTypeName == "resource") { - auto iter = rewardsJson.Struct().begin(); - fullIdentifier = iter->first; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.resources[rawId] = val; } - - CModHandler::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - auto it = REWARD_RMAP.find(metaTypeName); - - if(it == REWARD_RMAP.end()) + if(metaTypeName == "primarySkill") { - logGlobal->error("%s: invalid metatype in reward item %s", instanceName, fullIdentifier); - return; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.primary.at(rawId) = val; } - else + if(metaTypeName == "secondarySkill") { - rewardType = it->second; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.secondary[rawId] = val; } - - bool doRequest = false; - - switch (rewardType) + if(metaTypeName == "artifact") { - case NOTHING: - return; - case EXPERIENCE: - case MANA_POINTS: - case MORALE_BONUS: - case LUCK_BONUS: - break; - case PRIMARY_SKILL: - doRequest = true; - break; - case RESOURCES: - case SECONDARY_SKILL: - case ARTIFACT: - case SPELL: - case CREATURE: - doRequest = true; - break; - default: - assert(false); - break; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.artifacts.push_back(rawId); } - - if(doRequest) + if(metaTypeName == "spell") { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), fullIdentifier, false); - - if(rawId) - { - rID = rawId.value(); - } - else - { - rewardType = NOTHING;//fallback in case of error - return; - } + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.spells.push_back(rawId); } - handler.serializeInt(fullIdentifier, rVal); + if(metaTypeName == "creature") + { + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.creatures.emplace_back(rawId, val); + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.info.push_back(vinfo); } } @@ -1086,11 +750,24 @@ void CGQuestGuard::init(CRandomGenerator & rand) blockVisit = true; quest->textOption = rand.nextInt(3, 5); quest->completedOption = rand.nextInt(4, 5); + + configuration.info.push_back({}); + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.info.back().reward.removeObject = subID.getNum() == 0 ? true : false; + configuration.canRefuse = true; } -void CGQuestGuard::completeQuest(const CGHeroInstance *h) const +void CGQuestGuard::onHeroVisit(const CGHeroInstance * h) const { - cb->removeObject(this); + if(!quest->isCompleted) + CGSeerHut::onHeroVisit(h); + else + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, false); +} + +bool CGQuestGuard::passableFor(PlayerColor color) const +{ + return quest->isCompleted; } void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) @@ -1099,25 +776,9 @@ void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) quest->serializeJson(handler, "quest"); } -void CGKeys::reset() -{ - playerKeyMap.clear(); -} - -void CGKeys::setPropertyDer (ui8 what, ui32 val) //101-108 - enable key for player 1-8 -{ - if (what >= 101 && what <= (100 + PlayerColor::PLAYER_LIMIT_I)) - { - PlayerColor player(what-101); - playerKeyMap[player].insert(static_cast(val)); - } - else - logGlobal->error("Unexpected properties requested to set: what=%d, val=%d", static_cast(what), val); -} - bool CGKeys::wasMyColorVisited(const PlayerColor & player) const { - return playerKeyMap.count(player) && vstd::contains(playerKeyMap[player], subID); + return cb->getPlayerState(player)->visitedObjectsGlobal.count({Obj::KEYMASTER, subID}) != 0; } std::string CGKeys::getHoverText(PlayerColor player) const @@ -1127,7 +788,7 @@ std::string CGKeys::getHoverText(PlayerColor player) const std::string CGKeys::getObjectName() const { - return VLC->generaltexth->tentColors[subID] + " " + CGObjectInstance::getObjectName(); + return VLC->generaltexth->tentColors[subID.getNum()] + " " + CGObjectInstance::getObjectName(); } bool CGKeymasterTent::wasVisited (PlayerColor player) const @@ -1140,7 +801,11 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const int txt_id; if (!wasMyColorVisited (h->getOwner()) ) { - cb->setObjProperty(id, h->tempOwner.getNum()+101, subID); + ChangeObjectVisitors cow; + cow.mode = ChangeObjectVisitors::VISITOR_GLOBAL; + cow.hero = h->id; + cow.object = id; + cb->sendAndApply(&cow); txt_id=19; } else @@ -1150,20 +815,19 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const void CGBorderGuard::initObj(CRandomGenerator & rand) { - //ui32 m13489val = subID; //store color as quest info blockVisit = true; } -void CGBorderGuard::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const +void CGBorderGuard::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - text.appendLocalString(EMetaText::ADVOB_TXT,18); + text.appendLocalString(EMetaText::ADVOB_TXT, 18); } -void CGBorderGuard::getRolloverText (MetaString &text, bool onHover) const +void CGBorderGuard::getRolloverText(MetaString &text, bool onHover) const { if (!onHover) { - text.appendRawString(VLC->generaltexth->tentColors[subID]); + text.appendRawString(VLC->generaltexth->tentColors[subID.getNum()]); text.appendRawString(" "); text.appendRawString(VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID)); } @@ -1198,7 +862,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { if (answer) - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } void CGBorderGuard::afterAddToMap(CMap * map) diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index e60135c56..cb3046a22 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -9,8 +9,9 @@ */ #pragma once -#include "CArmedInstance.h" +#include "CRewardableObject.h" #include "../ResourceSet.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -18,59 +19,35 @@ class CGCreature; class DLL_LINKAGE CQuest final { - mutable std::unordered_map artifactsRequirements; // artifact ID -> required count - public: - enum Emission { - MISSION_NONE = 0, - MISSION_LEVEL = 1, - MISSION_PRIMARY_STAT = 2, - MISSION_KILL_HERO = 3, - MISSION_KILL_CREATURE = 4, - MISSION_ART = 5, - MISSION_ARMY = 6, - MISSION_RESOURCES = 7, - MISSION_HERO = 8, - MISSION_PLAYER = 9, - MISSION_HOTA_MULTI = 10, - // end of H3 missions - MISSION_KEYMASTER = 100, - MISSION_HOTA_HERO_CLASS = 101, - MISSION_HOTA_REACH_DATE = 102 - }; - enum Eprogress { - NOT_ACTIVE, - IN_PROGRESS, - COMPLETE - }; - - static const std::string & missionName(Emission mission); - static const std::string & missionState(int index); + static const std::string & missionName(int index); + static const std::string & missionState(int index); + + std::string questName; si32 qid; //unique quest id for serialization / identification - Emission missionType; - Eprogress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit - - ui32 m13489val; - std::vector m2stats; - std::vector m5arts; // artifact IDs. Add IDs through addArtifactID(), not directly to the field. - std::vector m6creatures; //pair[cre id, cre count], CreatureSet info irrelevant - TResources m7resources; + ObjectInstanceID killTarget; + Rewardable::Limiter mission; + bool repeatedQuest; + bool isCompleted; + std::set activeForPlayers; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info // needed for messages / hover text ui8 textOption; ui8 completedOption; - CStackBasicDescriptor stackToKill; + CreatureID stackToKill; ui8 stackDirection; std::string heroName; //backup of hero name - si32 heroPortrait; + HeroTypeID heroPortrait; - std::string firstVisitText, nextVisitText, completedText; + MetaString firstVisitText; + MetaString nextVisitText; + MetaString completedText; bool isCustomFirst; bool isCustomNext; bool isCustomComplete; @@ -78,13 +55,14 @@ 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 (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; - virtual void getCompletionText (MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h = nullptr) const; + virtual bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not + virtual void getVisitText(MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry - virtual void completeQuest (const CGHeroInstance * h) const {}; - virtual void addReplacements(MetaString &out, const std::string &base) const; - void addArtifactID(const ArtifactID & id); + virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; + virtual void addTextReplacements(MetaString &out, std::vector & components) const; + virtual void addKillTargetReplacements(MetaString &out) const; + void defineQuestName(); bool operator== (const CQuest & quest) const { @@ -94,14 +72,9 @@ public: template void serialize(Handler &h, const int version) { h & qid; - h & missionType; - h & progress; + h & isCompleted; + h & activeForPlayers; h & lastDay; - h & m13489val; - h & m2stats; - h & m5arts; - h & m6creatures; - h & m7resources; h & textOption; h & stackToKill; h & stackDirection; @@ -114,6 +87,9 @@ public: h & isCustomNext; h & isCustomComplete; h & completedOption; + h & questName; + h & mission; + h & killTarget; } void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); @@ -127,7 +103,7 @@ public: ///Information about quest should remain accessible even if IQuestObject removed from map ///All CQuest objects are freed in CMap destructor virtual ~IQuestObject() = default; - virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual bool checkQuest (const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) @@ -138,17 +114,18 @@ protected: void afterAddToMapCommon(CMap * map) const; }; -class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward +class DLL_LINKAGE CGSeerHut : public CRewardableObject, public IQuestObject { public: - enum ERewardType {NOTHING, EXPERIENCE, MANA_POINTS, MORALE_BONUS, LUCK_BONUS, RESOURCES, PRIMARY_SKILL, SECONDARY_SKILL, ARTIFACT, SPELL, CREATURE}; - ERewardType rewardType = ERewardType::NOTHING; - si32 rID = -1; //reward ID - si32 rVal = -1; //reward value std::string seerName; void initObj(CRandomGenerator & 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 onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; @@ -159,25 +136,17 @@ public: const CGHeroInstance *getHeroToKill(bool allowNull = false) const; const CGCreature *getCreatureToKill(bool allowNull = false) const; void getRolloverText (MetaString &text, bool onHover) const; - void getCompletionText(MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h = nullptr) const; - void finishQuest (const CGHeroInstance * h, ui32 accept) const; //common for both objects - virtual void completeQuest (const CGHeroInstance * h) const; void afterAddToMap(CMap * map) override; template void serialize(Handler &h, const int version) { - h & static_cast(*this); + h & static_cast(*this); h & static_cast(*this); - h & rewardType; - h & rID; - h & rVal; h & seerName; } protected: - static constexpr int OBJPROP_VISITED = 10; - - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; }; @@ -186,7 +155,9 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: void init(CRandomGenerator & rand) override; - void completeQuest (const CGHeroInstance * h) const override; + + void onHeroVisit(const CGHeroInstance * h) const override; + bool passableFor(PlayerColor color) const override; template void serialize(Handler &h, const int version) { @@ -199,11 +170,6 @@ protected: class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards { public: - static std::map > playerKeyMap; //[players][keysowned] - //SubID 0 - lightblue, 1 - green, 2 - red, 3 - darkblue, 4 - brown, 5 - purple, 6 - white, 7 - black - - static void reset(); - bool wasMyColorVisited(const PlayerColor & player) const; std::string getObjectName() const override; //depending on color @@ -213,8 +179,6 @@ public: { h & static_cast(*this); } -protected: - void setPropertyDer(ui8 what, ui32 val) override; }; class DLL_LINKAGE CGKeymasterTent : public CGKeys @@ -236,7 +200,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; + void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; void getRolloverText (MetaString &text, bool onHover) const; bool checkQuest (const CGHeroInstance * h) const override; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 5d21cf278..c05fbd9aa 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -14,57 +14,66 @@ #include "../CGeneralTextHandler.h" #include "../CPlayerState.h" #include "../IGameCallback.h" -#include "../NetPacks.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN -// FIXME: copy-pasted from CObjectHandler -static std::string visitedTxt(const bool visited) +void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const { - int id = visited ? 352 : 353; - return VLC->generaltexth->allTexts[id]; + auto vi = configuration.info.at(index); + logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); + // show message only if it is not empty or in infobox + if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty()) + { + InfoWindow iw; + iw.player = contextHero->tempOwner; + iw.text = vi.message; + vi.reward.loadComponents(iw.components, contextHero); + iw.type = configuration.infoWindowType; + if(!iw.components.empty() || !iw.text.toString().empty()) + cb->showInfoDialog(&iw); + } + // grant reward afterwards. Note that it may remove object + if(markAsVisit) + markAsVisited(contextHero); + grantReward(index, contextHero); +} + +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); +} + +std::vector CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const +{ + std::vector result; + + if (rewardIndices.empty()) + return result; + + if (configuration.selectMode != Rewardable::SELECT_FIRST) + { + for (auto index : rewardIndices) + result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); + } + else + { + configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); + } + + return result; } void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { - auto grantRewardWithMessage = [&](int index, bool markAsVisit) -> void - { - 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 = h->tempOwner; - iw.text = vi.message; - vi.reward.loadComponents(iw.components, h); - iw.type = configuration.infoWindowType; - if(!iw.components.empty() || !iw.text.toString().empty()) - cb->showInfoDialog(&iw); - } - // grant reward afterwards. Note that it may remove object - if(markAsVisit) - markAsVisited(h); - 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(!wasVisitedBefore(h)) { auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); @@ -82,7 +91,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE); if (!emptyRewards.empty()) - grantRewardWithMessage(emptyRewards[0], false); + grantRewardWithMessage(h, emptyRewards[0], false); else logMod->warn("No applicable message for visiting empty object!"); break; @@ -90,22 +99,22 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const case 1: // one reward. Just give it with message { if (configuration.canRefuse) - selectRewardsMessage(rewards, configuration.info.at(rewards.front()).message); + selectRewardWthMessage(h, rewards, configuration.info.at(rewards.front()).message); else - grantRewardWithMessage(rewards.front(), true); + grantRewardWithMessage(h, rewards.front(), true); break; } default: // multiple rewards. Act according to select mode { switch (configuration.selectMode) { case Rewardable::SELECT_PLAYER: // player must select - selectRewardsMessage(rewards, configuration.onSelect); + selectRewardWthMessage(h, rewards, configuration.onSelect); break; case Rewardable::SELECT_FIRST: // give first available - grantRewardWithMessage(rewards.front(), true); + grantRewardWithMessage(h, rewards.front(), true); break; case Rewardable::SELECT_RANDOM: // give random - grantRewardWithMessage(*RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); + grantRewardWithMessage(h, *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); break; } break; @@ -122,9 +131,15 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { logGlobal->debug("Revisiting already visited object"); + if (!wasVisited(h->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + } + auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); if (!visitedRewards.empty()) - grantRewardWithMessage(visitedRewards[0], false); + grantRewardWithMessage(h, visitedRewards[0], false); else logMod->warn("No applicable message for visiting already visited object!"); } @@ -154,7 +169,7 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, true); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); cb->sendAndApply(&cov); @@ -162,7 +177,7 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID); + cb->setObjPropertyValue(id, ObjProperty::REWARD_SELECT, rewardID); grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); // hero is not blocked by levelup dialog - grant remainer immediately @@ -183,9 +198,11 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con case Rewardable::VISIT_PLAYER: return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id)); case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::OBJECT, ID); + return contextHero->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); default: return false; } @@ -198,6 +215,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const case Rewardable::VISIT_UNLIMITED: case Rewardable::VISIT_BONUS: case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: return false; case Rewardable::VISIT_ONCE: case Rewardable::VISIT_PLAYER: @@ -207,34 +225,120 @@ bool CRewardableObject::wasVisited(PlayerColor player) const } } +bool CRewardableObject::wasScouted(PlayerColor player) const +{ + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); +} + bool CRewardableObject::wasVisited(const CGHeroInstance * h) const { switch (configuration.visitMode) { case Rewardable::VISIT_BONUS: - return h->hasBonusFrom(BonusSource::OBJECT, ID); + return h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return wasScouted(h->getOwner()) && configuration.visitLimiter.heroAllowed(h); default: - return wasVisited(h->tempOwner); + return wasVisited(h->getOwner()); } } +std::string CRewardableObject::getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const +{ + std::string result = getObjectName(); + + if (includeDescription && !getDescriptionMessage(player, hero).empty()) + result += "\n" + getDescriptionMessage(player, hero); + + if (hero) + { + if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) + { + if (wasVisited(hero)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n " + configuration.notVisitedTooltip.toString(); + } + } + else + { + if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) + { + if (wasVisited(player)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n" + configuration.notVisitedTooltip.toString(); + } + } + return result; +} + std::string CRewardableObject::getHoverText(PlayerColor player) const { - if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) - return getObjectName() + " " + visitedTxt(wasVisited(player)); - return getObjectName(); + return getDisplayTextImpl(player, nullptr, false); } std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { - if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) - return getObjectName() + " " + visitedTxt(wasVisited(hero)); - return getObjectName(); + return getDisplayTextImpl(hero->getOwner(), hero, false); } -void CRewardableObject::setPropertyDer(ui8 what, ui32 val) +std::string CRewardableObject::getPopupText(PlayerColor player) const +{ + return getDisplayTextImpl(player, nullptr, true); +} + +std::string CRewardableObject::getPopupText(const CGHeroInstance * hero) const +{ + return getDisplayTextImpl(hero->getOwner(), hero, true); +} + +std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const +{ + if (!wasScouted(player) || configuration.info.empty()) + return configuration.description.toString(); + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() || !configuration.info[0].description.empty()) + return configuration.info[0].description.toString(); + + if (!configuration.info[rewardIndices.front()].description.empty()) + return configuration.info[rewardIndices.front()].description.toString(); + + return configuration.description.toString(); +} + +std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const +{ + if (!wasScouted(player)) + return {}; + + if (!configuration.showScoutedPreview) + return {}; + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty() && !configuration.info.empty()) + rewardIndices.push_back(0); + + if (rewardIndices.empty()) + return {}; + + return loadComponents(hero, rewardIndices); +} + +std::vector CRewardableObject::getPopupComponents(PlayerColor player) const +{ + return getPopupComponentsImpl(player, nullptr); +} + +std::vector CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const +{ + return getPopupComponentsImpl(hero->getOwner(), hero); +} + +void CRewardableObject::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -242,10 +346,10 @@ void CRewardableObject::setPropertyDer(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; case ObjProperty::REWARD_CLEARED: - onceVisitableObjectCleared = val; + onceVisitableObjectCleared = identifier.getNum(); break; } } @@ -256,11 +360,11 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const { if (configuration.resetParameters.rewards) { - cb->setObjProperty(id, ObjProperty::REWARD_RANDOMIZE, 0); + cb->setObjPropertyValue(id, ObjProperty::REWARD_RANDOMIZE, 0); } if (configuration.resetParameters.visitors) { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, false); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, false); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id); cb->sendAndApply(&cov); } @@ -269,11 +373,16 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const void CRewardableObject::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); - assert(!configuration.info.empty()); + getObjectHandler()->configureObject(this, rand); } CRewardableObject::CRewardableObject() {} +void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) +{ + CArmedInstance::serializeJsonOptions(handler); + handler.serializeStruct("rewardable", static_cast(*this)); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index ca8f918db..fa31f963d 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../mapObjects/CArmedInstance.h" +#include "CArmedInstance.h" #include "../rewardable/Interface.h" VCMI_LIB_NAMESPACE_BEGIN @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CRewardableObject : public CArmedInstance, public Rewardable::Interface { protected: - + bool onceVisitableObjectCleared = false; /// reward selected by player, no serialize @@ -31,11 +31,25 @@ protected: /// 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; + + 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; + + std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; + + std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const; + std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const; + std::vector getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const; public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; bool wasVisited(const CGHeroInstance * h) const override; + + /// Returns true if object was scouted by player and he is aware of its internal state + bool wasScouted(PlayerColor player) const; /// gives reward to player or ask for choice in case of multiple rewards void onHeroVisit(const CGHeroInstance *h) const override; @@ -51,13 +65,19 @@ public: void initObj(CRandomGenerator & rand) override; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; CRewardableObject(); std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + + std::vector getPopupComponents(PlayerColor player) const override; + std::vector getPopupComponents(const CGHeroInstance * hero) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -69,10 +89,6 @@ public: //TODO: // MAX -// class DLL_LINKAGE CGPandoraBox : public CArmedInstance -// class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects -// class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward -// class DLL_LINKAGE CGQuestGuard : public CGSeerHut // class DLL_LINKAGE CBank : public CArmedInstance // class DLL_LINKAGE CGPyramid : public CBank @@ -85,7 +101,5 @@ public: // POSSIBLE // class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles -// class DLL_LINKAGE CGWitchHut : public CPlayersVisited -// class DLL_LINKAGE CGScholar : public CGObjectInstance VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index a6c8528a9..907d90378 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -20,7 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN -bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMarketMode mode) const +bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const { switch(mode) { @@ -122,12 +122,12 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMar return true; } -bool IMarket::allowsTrade(EMarketMode::EMarketMode mode) const +bool IMarket::allowsTrade(EMarketMode mode) const { return false; } -int IMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const +int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const { switch(mode) { @@ -140,16 +140,16 @@ int IMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) } } -std::vector IMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector IMarket::availableItemsIds(EMarketMode mode) const { - std::vector ret; + std::vector ret; switch(mode) { case EMarketMode::RESOURCE_RESOURCE: case EMarketMode::ARTIFACT_RESOURCE: case EMarketMode::CREATURE_RESOURCE: - for (int i = 0; i < 7; i++) - ret.push_back(i); + for (auto res : GameResID::ALL_RESOURCES()) + ret.push_back(res); } return ret; } @@ -158,7 +158,13 @@ const IMarket * IMarket::castFrom(const CGObjectInstance *obj, bool verbose) { auto * imarket = dynamic_cast(obj); if(verbose && !imarket) - logGlobal->error("Cannot cast to IMarket object type %s", obj->typeName); + { + logGlobal->error("Cannot cast to IMarket"); + if(obj) + { + logGlobal->error("Object type %s", obj->typeName); + } + } return imarket; } @@ -166,12 +172,12 @@ IMarket::IMarket() { } -std::vector IMarket::availableModes() const +std::vector IMarket::availableModes() const { - std::vector ret; - for (int i = 0; i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; i++) - if(allowsTrade(static_cast(i))) - ret.push_back(static_cast(i)); + std::vector ret; + for (EMarketMode i = static_cast(0); i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; i = vstd::next(i, 1)) + if(allowsTrade(i)) + ret.push_back(i); return ret; } diff --git a/lib/mapObjects/IMarket.h b/lib/mapObjects/IMarket.h index 7e9d79359..cc20300ab 100644 --- a/lib/mapObjects/IMarket.h +++ b/lib/mapObjects/IMarket.h @@ -9,7 +9,8 @@ */ #pragma once -#include "../GameConstants.h" +#include "../networkPacks/TradeItem.h" +#include "../constants/Enumerations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,12 +23,12 @@ public: virtual ~IMarket() {} virtual int getMarketEfficiency() const = 0; - virtual bool allowsTrade(EMarketMode::EMarketMode mode) const; - virtual int availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const; //-1 if unlimited - virtual std::vector availableItemsIds(EMarketMode::EMarketMode mode) const; + virtual bool allowsTrade(EMarketMode mode) const; + virtual int availableUnits(EMarketMode mode, int marketItemSerial) const; //-1 if unlimited + virtual std::vector availableItemsIds(EMarketMode mode) const; - bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units - std::vector availableModes() 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; static const IMarket *castFrom(const CGObjectInstance *obj, bool verbose = true); }; diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index b76106232..eebc08478 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -14,24 +14,15 @@ #include "CGTownInstance.h" #include "MiscObjects.h" -#include "../NetPacks.h" #include "../IGameCallback.h" #include "../TerrainHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN IGameCallback * IObjectInterface::cb = nullptr; -///helpers -void IObjectInterface::openWindow(const EOpenWindowMode type, const int id1, const int id2) -{ - OpenWindow ow; - ow.window = type; - ow.id1 = id1; - ow.id2 = id2; - IObjectInterface::cb->sendAndApply(&ow); -} - void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInfoWindowMode mode) const { InfoWindow iw; @@ -55,7 +46,10 @@ void IObjectInterface::newTurn(CRandomGenerator & rand) const void IObjectInterface::initObj(CRandomGenerator & rand) {} -void IObjectInterface::setProperty( ui8 what, ui32 val ) +void IObjectInterface::pickRandomObject(CRandomGenerator & rand) +{} + +void IObjectInterface::setProperty(ObjProperty what, ObjPropertyID identifier) {} bool IObjectInterface::wasVisited (PlayerColor player) const @@ -95,11 +89,23 @@ int3 IBoatGenerator::bestLocation() const int3 targetTile = getObject()->visitablePos() + offset; const TerrainTile *tile = getObject()->cb->getTile(targetTile, false); - if(tile) //tile is in the map + if(!tile) + continue; // tile not visible / outside the map + + if(!tile->terType->isWater()) + continue; + + if (tile->blocked) { - if(tile->terType->isWater() && (!tile->blocked || tile->blockingObjects.front()->ID == Obj::BOAT)) //and is water and is not blocked or is blocked by boat - return targetTile; + bool hasBoat = false; + for (auto const * object : tile->blockingObjects) + if (object->ID == Obj::BOAT || object->ID == Obj::HERO) + hasBoat = true; + + if (!hasBoat) + continue; // tile is blocked, but not by boat -> check next potential position } + return targetTile; } return int3 (-1,-1,-1); } @@ -118,7 +124,7 @@ IBoatGenerator::EGeneratorState IBoatGenerator::shipyardStatus() const if(t->blockingObjects.empty()) return GOOD; //OK - if(t->blockingObjects.front()->ID == Obj::BOAT) + if(t->blockingObjects.front()->ID == Obj::BOAT || t->blockingObjects.front()->ID == Obj::HERO) return BOAT_ALREADY_BUILT; //blocked with boat return TILE_BLOCKED; //blocked diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index b27a66f00..7cf9c8525 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -9,18 +9,24 @@ */ #pragma once -#include "../NetPacksBase.h" +#include "../networkPacks/EInfoWindowMode.h" +#include "../networkPacks/ObjProperty.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN struct BattleResult; struct UpgradeInfo; +class BoatId; class CGObjectInstance; class CRandomGenerator; +class CStackInstance; +class CGHeroInstance; class IGameCallback; class ResourceSet; class int3; class MetaString; +class PlayerColor; class DLL_LINKAGE IObjectInterface { @@ -29,8 +35,8 @@ public: virtual ~IObjectInterface() = default; - virtual int32_t getObjGroupIndex() const = 0; - virtual int32_t getObjTypeIndex() const = 0; + virtual MapObjectID getObjGroupIndex() const = 0; + virtual MapObjectSubID getObjTypeIndex() const = 0; virtual PlayerColor getOwner() const = 0; virtual int3 visitablePos() const = 0; @@ -40,7 +46,8 @@ public: virtual void onHeroLeave(const CGHeroInstance * h) const; virtual void newTurn(CRandomGenerator & rand) const; virtual void initObj(CRandomGenerator & rand); //synchr - virtual void setProperty(ui8 what, ui32 val);//synchr + virtual void pickRandomObject(CRandomGenerator & 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 @@ -52,9 +59,6 @@ public: //unified helper to show info dialog for object owner virtual void showInfoDialog(const ui32 txtID, const ui16 soundID = 0, EInfoWindowMode mode = EInfoWindowMode::AUTO) const; - //unified helper to show a specific window - static void openWindow(const EOpenWindowMode type, const int id1, const int id2 = -1); - //unified interface, AI helpers virtual bool wasVisited (PlayerColor player) const; virtual bool wasVisited (const CGHeroInstance * h) const; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c5e4fdbda..11c532f13 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -11,11 +11,11 @@ #include "StdInc.h" #include "MiscObjects.h" -#include "../StringConstants.h" -#include "../NetPacks.h" +#include "../ArtifactUtils.h" +#include "../constants/StringConstants.h" +#include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" -#include "../CModHandler.h" #include "../CSkillHandler.h" #include "../spells/CSpellHandler.h" #include "../IGameCallback.h" @@ -25,10 +25,14 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../modding/ModScope.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/StackLocation.h" VCMI_LIB_NAMESPACE_BEGIN -std::map > CGMagi::eyelist; ui8 CGObelisk::obeliskCount = 0; //how many obelisks are on map std::map CGObelisk::visited; //map: team_id => how many obelisks has been visited @@ -39,10 +43,10 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -void CTeamVisited::setPropertyDer(ui8 what, ui32 val) +void CTeamVisited::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { - if(what == CTeamVisited::OBJPROP_VISITED) - players.insert(PlayerColor(val)); + if(what == ObjProperty::VISITED) + players.insert(identifier.as()); } bool CTeamVisited::wasVisited(PlayerColor player) const @@ -68,21 +72,21 @@ bool CTeamVisited::wasVisited(const TeamID & team) const //CGMine void CGMine::onHeroVisit( const CGHeroInstance * h ) const { - int relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); + auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); - if(relations == 2) //we're visiting our mine + if(relations == PlayerRelations::SAME_PLAYER) //we're visiting our mine { cb->showGarrisonDialog(id,h->id,true); return; } - else if (relations == 1)//ally + else if (relations == PlayerRelations::ALLIES)//ally return; if(stacksCount()) //Mine is guarded { BlockingDialog ynd(true,false); ynd.player = h->tempOwner; - ynd.text.appendLocalString(EMetaText::ADVOB_TXT, subID == 7 ? 84 : 187); + ynd.text.appendLocalString(EMetaText::ADVOB_TXT, isAbandoned() ? 84 : 187); cb->showBlockingDialog(&ynd); return; } @@ -116,19 +120,27 @@ void CGMine::initObj(CRandomGenerator & rand) } else { - producedResource = GameResID(subID); + producedResource = GameResID(getObjTypeIndex().getNum()); } producedQuantity = defaultResProduction(); } bool CGMine::isAbandoned() const { - return (subID >= 7); + return subID.getNum() >= 7; +} + +ResourceSet CGMine::dailyIncome() const +{ + ResourceSet result; + result[producedResource] += defaultResProduction(); + + return result; } std::string CGMine::getObjectName() const { - return VLC->generaltexth->translate("core.minename", subID); + return VLC->generaltexth->translate("core.minename", getObjTypeIndex()); } std::string CGMine::getHoverText(PlayerColor player) const @@ -136,7 +148,7 @@ std::string CGMine::getHoverText(PlayerColor player) const std::string hoverName = CArmedInstance::getHoverText(player); if (tempOwner != PlayerColor::NEUTRAL) - hoverName += "\n(" + VLC->generaltexth->restypes[producedResource] + ")"; + hoverName += "\n(" + VLC->generaltexth->restypes[producedResource.getNum()] + ")"; if(stacksCount()) { @@ -156,9 +168,9 @@ void CGMine::flagMine(const PlayerColor & player) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::FLAGMINE; - iw.text.appendLocalString(EMetaText::MINE_EVNTS, producedResource); //not use subID, abandoned mines uses default mine texts + 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(Component::EComponentType::RESOURCE, producedResource, producedQuantity, -1); + iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity); cb->showInfoDialog(&iw); } @@ -196,17 +208,17 @@ void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) con void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "army", 7); - + CArmedInstance::serializeJsonOptions(handler); + serializeJsonOwner(handler); if(isAbandoned()) { if(handler.saving) { JsonNode node(JsonNode::JsonType::DATA_VECTOR); - for(auto const & resID : abandonedMineResources) + for(const auto & resID : abandonedMineResources) { JsonNode one(JsonNode::JsonType::DATA_STRING); - one.String() = GameConstants::RESOURCE_NAMES[resID]; + one.String() = GameConstants::RESOURCE_NAMES[resID.getNum()]; node.Vector().push_back(one); } handler.serializeRaw("possibleResources", node, std::nullopt); @@ -228,15 +240,28 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) } } } - else - { - serializeJsonOwner(handler); - } +} + +GameResID CGResource::resourceID() const +{ + return getObjTypeIndex().getNum(); } std::string CGResource::getHoverText(PlayerColor player) const { - return VLC->generaltexth->restypes[subID]; + return VLC->generaltexth->restypes[resourceID().getNum()]; +} + +void CGResource::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::RESOURCE || ID == Obj::RANDOM_RESOURCE); + + if (ID == Obj::RANDOM_RESOURCE) + { + ID = Obj::RESOURCE; + subID = rand.nextInt(EGameResID::WOOD, EGameResID::GOLD); + setType(ID, subID); + } } void CGResource::initObj(CRandomGenerator & rand) @@ -245,7 +270,7 @@ void CGResource::initObj(CRandomGenerator & rand) if(amount == CGResource::RANDOM_AMOUNT) { - switch(static_cast(subID)) + switch(resourceID().toEnum()) { case EGameResID::GOLD: amount = rand.nextInt(5, 10) * 100; @@ -268,7 +293,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - ynd.text.appendRawString(message); + ynd.text = message; cb->showBlockingDialog(&ynd); } else @@ -282,24 +307,24 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const void CGResource::collectRes(const PlayerColor & player) const { - cb->giveResource(player, static_cast(subID), amount); + cb->giveResource(player, resourceID(), amount); InfoWindow sii; sii.player = player; if(!message.empty()) { sii.type = EInfoWindowMode::AUTO; - sii.text.appendRawString(message); + sii.text = message; } else { sii.type = EInfoWindowMode::INFO; sii.text.appendLocalString(EMetaText::ADVOB_TXT,113); - sii.text.replaceLocalString(EMetaText::RES_NAMES, subID); + sii.text.replaceName(resourceID()); } - sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0); + sii.components.emplace_back(ComponentType::RESOURCE, resourceID(), amount); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); cb->showInfoDialog(&sii); - cb->removeObject(this); + cb->removeObject(this, player); } void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const @@ -316,9 +341,11 @@ void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGResource::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "guards", 7); + CArmedInstance::serializeJsonOptions(handler); + if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); handler.serializeInt("amount", amount, 0); - handler.serializeString("guardMessage", message); + handler.serializeStruct("guardMessage", message); } bool CGTeleport::isEntrance() const @@ -370,7 +397,7 @@ ObjectInstanceID CGTeleport::getRandomExit(const CGHeroInstance * h) const bool CGTeleport::isTeleport(const CGObjectInstance * obj) { - return ((dynamic_cast(obj))); + return dynamic_cast(obj) != nullptr; } bool CGTeleport::isConnected(const CGTeleport * src, const CGTeleport * dst) @@ -437,14 +464,14 @@ void CGTeleport::addToChannel(std::map & IDs, int SubID) const +TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const { for(auto obj : cb->gameState()->map->objects) { if(!obj) continue; - const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); + const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); if(teleportObj && vstd::contains(IDs, teleportObj->ID) && teleportObj->subID == SubID) return teleportObj->channel; } @@ -453,7 +480,7 @@ TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, int Su void CGMonolith::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(isEntrance()) { if(cb->isTeleportChannelBidirectional(channel) && 1 < cb->getTeleportChannelExits(channel).size()) @@ -502,7 +529,7 @@ void CGMonolith::initObj(CRandomGenerator & rand) { std::vector IDs; IDs.push_back(ID); - switch(ID) + switch(ID.toEnum()) { case Obj::MONOLITH_ONE_WAY_ENTRANCE: type = ENTRANCE; @@ -527,7 +554,7 @@ void CGMonolith::initObj(CRandomGenerator & rand) void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(cb->isTeleportChannelImpassable(channel)) { h->showInfoDialog(153);//Just inside the entrance you find a large pile of rubble blocking the tunnel. You leave discouraged. @@ -611,7 +638,7 @@ void CGSubterraneanGate::postInit() //matches subterranean gates into pairs void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(cb->isTeleportChannelImpassable(channel)) { logGlobal->debug("Cannot find exit whirlpool for %d at %s", id.getNum(), pos.toString()); @@ -636,7 +663,7 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 168); - iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), -countToTake)); + iw.components.emplace_back(ComponentType::CREATURE, h->getCreature(targetstack)->getId(), -countToTake); cb->showInfoDialog(&iw); cb->changeStackCount(StackLocation(h, targetstack), -countToTake); } @@ -683,6 +710,44 @@ bool CGWhirlpool::isProtected(const CGHeroInstance * h) || (h->stacksCount() == 1 && h->Slots().begin()->second->count == 1); } +ArtifactID CGArtifact::getArtifact() const +{ + if(ID == Obj::SPELL_SCROLL) + return ArtifactID::SPELL_SCROLL; + else + return getObjTypeIndex().getNum(); +} + +void CGArtifact::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID.toEnum()) + { + case MapObjectID::RANDOM_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); + break; + case MapObjectID::RANDOM_TREASURE_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE); + break; + case MapObjectID::RANDOM_MINOR_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR); + break; + case MapObjectID::RANDOM_MAJOR_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR); + break; + case MapObjectID::RANDOM_RELIC_ART: + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_RELIC); + break; + } + + if (ID != MapObjectID::SPELL_SCROLL && ID != MapObjectID::ARTIFACT) + { + ID = MapObjectID::ARTIFACT; + setType(ID, subID); + } + else if (ID != MapObjectID::SPELL_SCROLL) + ID = MapObjectID::ARTIFACT; +} + void CGArtifact::initObj(CRandomGenerator & rand) { blockVisit = true; @@ -695,7 +760,7 @@ void CGArtifact::initObj(CRandomGenerator & rand) storedArtifact = a; } if(!storedArtifact->artType) - storedArtifact->setType(VLC->arth->objects[subID]); + storedArtifact->setType(VLC->arth->objects[getArtifact().getNum()]); } if(ID == Obj::SPELL_SCROLL) subID = 1; @@ -708,7 +773,32 @@ void CGArtifact::initObj(CRandomGenerator & rand) std::string CGArtifact::getObjectName() const { - return VLC->artifacts()->getByIndex(subID)->getNameTranslated(); + return VLC->artifacts()->getById(getArtifact())->getNameTranslated(); +} + +std::string CGArtifact::getPopupText(PlayerColor player) const +{ + if (settings["general"]["enableUiEnhancements"].Bool()) + { + std::string description = VLC->artifacts()->getById(getArtifact())->getDescriptionTranslated(); + if (getArtifact() == ArtifactID::SPELL_SCROLL) + ArtifactUtils::insertScrrollSpellName(description, SpellID::NONE); // erase text placeholder + return description; + } + else + return getObjectName(); +} + +std::string CGArtifact::getPopupText(const CGHeroInstance * hero) const +{ + return getPopupText(hero->getOwner()); +} + +std::vector CGArtifact::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::ARTIFACT, getArtifact()) + }; } void CGArtifact::onHeroVisit(const CGHeroInstance * h) const @@ -721,27 +811,27 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const if(storedArtifact->artType->canBePutAt(h)) { - switch (ID) + switch (ID.toEnum()) { case Obj::ARTIFACT: { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0); - if(message.length()) - iw.text.appendRawString(message); + iw.components.emplace_back(ComponentType::ARTIFACT, getArtifact()); + if(!message.empty()) + iw.text = message; else - iw.text.appendLocalString(EMetaText::ART_EVNTS, subID); + iw.text.appendTextID(getArtifact().toArtifact()->getEventTextID()); } break; case Obj::SPELL_SCROLL: { - int spellID = storedArtifact->getScrollSpellID(); - iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0); - if(message.length()) - iw.text.appendRawString(message); + SpellID spell = storedArtifact->getScrollSpellID(); + iw.components.emplace_back(ComponentType::SPELL, spell); + if(!message.empty()) + iw.text = message; else { iw.text.appendLocalString(EMetaText::ADVOB_TXT,135); - iw.text.replaceLocalString(EMetaText::SPELL_NAME, spellID); + iw.text.replaceName(spell); } } break; @@ -756,14 +846,14 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const } else { - switch(ID) + switch(ID.toEnum()) { case Obj::ARTIFACT: { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - if(message.length()) - ynd.text.appendRawString(message); + if(!message.empty()) + ynd.text = message; else { // TODO: Guard text is more complex in H3, see mantis issue 2325 for details @@ -777,11 +867,11 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const break; case Obj::SPELL_SCROLL: { - if(message.length()) + if(!message.empty()) { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - ynd.text.appendRawString(message); + ynd.text = message; cb->showBlockingDialog(&ynd); } else @@ -794,8 +884,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const void CGArtifact::pick(const CGHeroInstance * h) const { - if(cb->giveHeroArtifact(h, storedArtifact, ArtifactPosition::FIRST_AVAILABLE)) - cb->removeObject(this); + if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact)) + cb->removeObject(this, h->getOwner()); } BattleField CGArtifact::getBattlefield() const @@ -826,215 +916,20 @@ void CGArtifact::afterAddToMap(CMap * map) void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) { - handler.serializeString("guardMessage", message); - CCreatureSet::serializeJson(handler, "guards" ,7); + handler.serializeStruct("guardMessage", message); + CArmedInstance::serializeJsonOptions(handler); + if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); if(handler.saving && ID == Obj::SPELL_SCROLL) { const std::shared_ptr b = storedArtifact->getBonusLocalFirst(Selector::type()(BonusType::SPELL)); - SpellID spellId(b->subtype); + SpellID spellId(b->subtype.as()); handler.serializeId("spell", spellId, SpellID::NONE); } } -void CGWitchHut::initObj(CRandomGenerator & rand) -{ - if (allowedAbilities.empty()) //this can happen for RMG and RoE maps. - { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - - // Necromancy and Leadership can't be learned by default - defaultAllowed[SecondarySkill::NECROMANCY] = false; - defaultAllowed[SecondarySkill::LEADERSHIP] = false; - - for(int i = 0; i < defaultAllowed.size(); i++) - if (defaultAllowed[i] && cb->isAllowed(2, i)) - allowedAbilities.insert(SecondarySkill(i)); - } - ability = *RandomGeneratorUtil::nextItem(allowedAbilities, rand); -} - -void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGWitchHut::OBJPROP_VISITED, h->tempOwner.getNum()); - ui32 txt_id; - if(h->getSecSkillLevel(SecondarySkill(ability))) //you already know this skill - { - txt_id =172; - } - else if(!h->canLearnSkill()) //already all skills slots used - { - txt_id = 173; - } - else //give sec skill - { - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, ability, 1, 0); - txt_id = 171; - cb->changeSecSkill(h, SecondarySkill(ability), 1, true); - } - - iw.text.appendLocalString(EMetaText::ADVOB_TXT,txt_id); - iw.text.replaceLocalString(EMetaText::SEC_SKILL_NAME, ability); - cb->showInfoDialog(&iw); -} - -std::string CGWitchHut::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[356]; // + (learn %s) - boost::algorithm::replace_first(hoverName, "%s", VLC->skillh->getByIndex(ability)->getNameTranslated()); - } - return hoverName; -} - -std::string CGWitchHut::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability - hoverName += "\n\n" + VLC->generaltexth->allTexts[357]; // (Already learned) - return hoverName; -} - -void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler) -{ - //TODO: unify allowed abilities with others - make them std::vector - - std::vector temp; - size_t skillCount = VLC->skillh->size(); - temp.resize(skillCount, false); - - auto standard = VLC->skillh->getDefaultAllowed(); //todo: for WitchHut default is all except Leadership and Necromancy - - if(handler.saving) - { - for(si32 i = 0; i < skillCount; ++i) - if(vstd::contains(allowedAbilities, i)) - temp[i] = true; - } - - handler.serializeLIC("allowedSkills", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, standard, temp); - - if(!handler.saving) - { - allowedAbilities.clear(); - for(si32 i = 0; i < skillCount; ++i) - if(temp[i]) - allowedAbilities.insert(SecondarySkill(i)); - } -} - -void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->tempOwner; - switch (ID) - { - case Obj::REDWOOD_OBSERVATORY: - case Obj::PILLAR_OF_FIRE: - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE)); - - FoWChange fw; - fw.player = h->tempOwner; - fw.mode = 1; - cb->getTilesInRange (fw.tiles, pos, 20, h->tempOwner, 1); - cb->sendAndApply (&fw); - break; - } - case Obj::COVER_OF_DARKNESS: - { - iw.text.appendLocalString (EMetaText::ADVOB_TXT, 31); - for (auto & player : cb->gameState()->players) - { - if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && - cb->getPlayerRelations(player.first, h->tempOwner) == PlayerRelations::ENEMIES) - cb->changeFogOfWar(visitablePos(), 20, player.first, true); - } - break; - } - } - cb->showInfoDialog(&iw); -} - -void CGShrine::onHeroVisit( const CGHeroInstance * h ) const -{ - if(spell == SpellID::NONE) - { - logGlobal->error("Not initialized shrine visited!"); - return; - } - - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGShrine::OBJPROP_VISITED, h->tempOwner.getNum()); - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text = visitText; - iw.text.appendLocalString(EMetaText::SPELL_NAME,spell); - iw.text.appendRawString("."); - - if(!h->getArt(ArtifactPosition::SPELLBOOK)) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,131); - } - else if(h->spellbookContainsSpell(spell))//hero already knows the spell - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,174); - } - else if(spell.toSpell()->getLevel() > h->maxSpellLevel()) //it's third level spell and hero doesn't have wisdom - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,130); - } - else //give spell - { - std::set spells; - spells.insert(spell); - cb->changeSpells(h, true, spells); - - iw.components.emplace_back(Component::EComponentType::SPELL, spell, 0, 0); - } - - cb->showInfoDialog(&iw); -} - -void CGShrine::initObj(CRandomGenerator & rand) -{ - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); -} - -std::string CGShrine::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[355]; // + (learn %s) - boost::algorithm::replace_first(hoverName,"%s", spell.toSpell()->getNameTranslated()); - } - return hoverName; -} - -std::string CGShrine::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->spellbookContainsSpell(spell)) //know what spell there is and hero knows that spell - hoverName += "\n\n" + VLC->generaltexth->allTexts[354]; // (Already learned) - return hoverName; -} - -void CGShrine::serializeJsonOptions(JsonSerializeFormat & handler) -{ - handler.serializeId("spell", spell, SpellID::NONE); -} - void CGSignBottle::initObj(CRandomGenerator & rand) { //if no text is set than we pick random from the predefined ones @@ -1042,7 +937,7 @@ void CGSignBottle::initObj(CRandomGenerator & rand) { auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign"); std::string messageIdentifier = *RandomGeneratorUtil::nextItem(vector, rand); - message = VLC->generaltexth->translate(messageIdentifier); + message.appendTextID(TextIdentifier("core", "randsign", messageIdentifier).get()); } if(ID == Obj::OCEAN_BOTTLE) @@ -1055,155 +950,29 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->getOwner(); - iw.text.appendRawString(message); + iw.text = message; cb->showInfoDialog(&iw); if(ID == Obj::OCEAN_BOTTLE) - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) { - handler.serializeString("text", message); -} - -void CGScholar::onHeroVisit( const CGHeroInstance * h ) const -{ - EBonusType type = bonusType; - int bid = bonusID; - //check if the bonus if applicable, if not - give primary skill (always possible) - int ssl = h->getSecSkillLevel(SecondarySkill(bid)); //current sec skill level, used if bonusType == 1 - if((type == SECONDARY_SKILL && ((ssl == 3) || (!ssl && !h->canLearnSkill()))) ////hero already has expert level in the skill or (don't know skill and doesn't have free slot) - || (type == SPELL && !h->canLearnSpell(SpellID(bid).toSpell()))) - { - type = PRIM_SKILL; - bid = CRandomGenerator::getDefault().nextInt(GameConstants::PRIMARY_SKILLS - 1); - } - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text.appendLocalString(EMetaText::ADVOB_TXT,115); - - switch (type) - { - case PRIM_SKILL: - cb->changePrimSkill(h,static_cast(bid),+1); - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0); - break; - case SECONDARY_SKILL: - cb->changeSecSkill(h,SecondarySkill(bid),+1); - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, bid, ssl + 1, 0); - break; - case SPELL: - { - std::set hlp; - hlp.insert(SpellID(bid)); - cb->changeSpells(h,true,hlp); - iw.components.emplace_back(Component::EComponentType::SPELL, bid, 0, 0); - } - break; - default: - logGlobal->error("Error: wrong bonus type (%d) for Scholar!\n", static_cast(type)); - return; - } - - cb->showInfoDialog(&iw); - cb->removeObject(this); -} - -void CGScholar::initObj(CRandomGenerator & rand) -{ - blockVisit = true; - if(bonusType == RANDOM) - { - bonusType = static_cast(rand.nextInt(2)); - switch(bonusType) - { - case PRIM_SKILL: - bonusID = rand.nextInt(GameConstants::PRIMARY_SKILLS -1); - break; - case SECONDARY_SKILL: - bonusID = rand.nextInt(static_cast(VLC->skillh->size()) - 1); - break; - case SPELL: - std::vector possibilities; - cb->getAllowedSpells (possibilities); - bonusID = *RandomGeneratorUtil::nextItem(possibilities, rand); - break; - } - } -} - -void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) -{ - if(handler.saving) - { - std::string value; - switch(bonusType) - { - case PRIM_SKILL: - value = PrimarySkill::names[bonusID]; - handler.serializeString("rewardPrimSkill", value); - break; - case SECONDARY_SKILL: - value = CSkillHandler::encodeSkill(bonusID); - handler.serializeString("rewardSkill", value); - break; - case SPELL: - value = SpellID::encode(bonusID); - handler.serializeString("rewardSpell", value); - break; - case RANDOM: - break; - } - } - else - { - //TODO: unify - const JsonNode & json = handler.getCurrent(); - bonusType = RANDOM; - if(!json["rewardPrimSkill"].String().empty()) - { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); - if(raw) - { - bonusType = PRIM_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSkill"].String().empty()) - { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "skill", json["rewardSkill"].String()); - if(raw) - { - bonusType = SECONDARY_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSpell"].String().empty()) - { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", json["rewardSpell"].String()); - if(raw) - { - bonusType = SPELL; - bonusID = raw.value(); - } - } - } + handler.serializeStruct("text", message); } void CGGarrison::onHeroVisit (const CGHeroInstance *h) const { - int ally = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); - if (!ally && stacksCount() > 0) { + 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); return; } //New owner. - if (!ally) + if (relations == PlayerRelations::ENEMIES) cb->setOwner(this, h->tempOwner); cb->showGarrisonDialog(id, h->id, removableUnits); @@ -1233,29 +1002,30 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler) { handler.serializeBool("removableUnits", removableUnits); serializeJsonOwner(handler); - CCreatureSet::serializeJson(handler, "army", 7); -} - -void CGMagi::reset() -{ - eyelist.clear(); + CArmedInstance::serializeJsonOptions(handler); } void CGMagi::initObj(CRandomGenerator & rand) { if (ID == Obj::EYE_OF_MAGI) - { blockVisit = true; - eyelist[subID].push_back(id); - } } + void CGMagi::onHeroVisit(const CGHeroInstance * h) const { if (ID == Obj::HUT_OF_MAGI) { h->showInfoDialog(61); - if (!eyelist[subID].empty()) + std::vector eyes; + + for (auto object : cb->gameState()->map->objects) + { + if (object && object->ID == Obj::EYE_OF_MAGI && object->subID == this->subID) + eyes.push_back(object); + } + + if (!eyes.empty()) { CenterView cv; cv.player = h->tempOwner; @@ -1263,14 +1033,12 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const FoWChange fw; fw.player = h->tempOwner; - fw.mode = 1; + fw.mode = ETileVisibility::REVEALED; fw.waitForDialogs = true; - for(const auto & it : eyelist[subID]) + for(const auto & eye : eyes) { - const CGObjectInstance *eye = cb->getObj(it); - - cb->getTilesInRange (fw.tiles, eye->pos, 10, h->tempOwner, 1); + cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); cb->sendAndApply(&fw); cv.pos = eye->pos; @@ -1291,7 +1059,7 @@ CGBoat::CGBoat() { hero = nullptr; direction = 4; - layer = EPathfindingLayer::EEPathfindingLayer::SAIL; + layer = EPathfindingLayer::SAIL; } bool CGBoat::isCoastVisitable() const @@ -1306,14 +1074,14 @@ void CGSirens::initObj(CRandomGenerator & rand) std::string CGSirens::getHoverText(const CGHeroInstance * hero) const { - return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT,ID)); + return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID))); } void CGSirens::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->tempOwner; - if(h->hasBonusFrom(BonusSource::OBJECT,ID)) //has already visited Sirens + if(h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID))) //has already visited Sirens { iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::ADVOB_TXT,133); @@ -1395,7 +1163,7 @@ void CGShipyard::onHeroVisit( const CGHeroInstance * h ) const } else { - openWindow(EOpenWindowMode::SHIPYARD_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::SHIPYARD_WINDOW, h, false); } } @@ -1409,82 +1177,9 @@ BoatId CGShipyard::getBoatType() const return createdBoat; } -void CCartographer::onHeroVisit( const CGHeroInstance * h ) const -{ - //if player has not bought map of this subtype yet and underground exist for stalagmite cartographer - if (!wasVisited(h->getOwner()) && (subID != 2 || cb->gameState()->map->twoLevel)) - { - if (cb->getResource(h->tempOwner, EGameResID::GOLD) >= 1000) //if he can afford a map - { - //ask if he wants to buy one - int text=0; - switch (subID) - { - case 0: - text = 25; - break; - case 1: - text = 26; - break; - case 2: - text = 27; - break; - default: - logGlobal->warn("Unrecognized subtype of cartographer"); - } - assert(text); - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - bd.text.appendLocalString (EMetaText::ADVOB_TXT, text); - cb->showBlockingDialog (&bd); - } - else //if he cannot afford - { - h->showInfoDialog(28); - } - } - else //if he already visited carographer - { - h->showInfoDialog(24); - } -} - -void CCartographer::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const -{ - if(answer) //if hero wants to buy map - { - cb->giveResource(hero->tempOwner, EGameResID::GOLD, -1000); - FoWChange fw; - fw.mode = 1; - fw.player = hero->tempOwner; - - //subIDs of different types of cartographers: - //water = 0; land = 1; underground = 2; - - IGameCallback::MapTerrainFilterMode tileFilterMode = IGameCallback::MapTerrainFilterMode::NONE; - - switch(subID) - { - case 0: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::WATER; - break; - case 1: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::LAND_CARTOGRAPHER; - break; - case 2: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER; - break; - } - - cb->getAllTiles(fw.tiles, hero->tempOwner, -1, tileFilterMode); //reveal appropriate tiles - cb->sendAndApply(&fw); - cb->setObjProperty(id, CCartographer::OBJPROP_VISITED, hero->tempOwner.getNum()); - } -} - void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const { - cb->showThievesGuildWindow(h->tempOwner, id); + cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); } void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const @@ -1502,14 +1197,13 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const cb->sendAndApply(&iw); // increment general visited obelisks counter - cb->setObjProperty(id, CGObelisk::OBJPROP_INC, team.getNum()); - - openWindow(EOpenWindowMode::PUZZLE_MAP, h->tempOwner.getNum()); + cb->setObjPropertyID(id, ObjProperty::OBELISK_VISITED, team); + cb->showObjectWindow(this, EOpenWindowMode::PUZZLE_MAP, h, false); // mark that particular obelisk as visited for all players in the team for(const auto & color : ts->players) { - cb->setObjProperty(id, CGObelisk::OBJPROP_VISITED, color.getNum()); + cb->setObjPropertyID(id, ObjProperty::VISITED, color); } } else @@ -1536,25 +1230,25 @@ std::string CGObelisk::getHoverText(PlayerColor player) const return getObjectName() + " " + visitedTxt(wasVisited(player)); } -void CGObelisk::setPropertyDer( ui8 what, ui32 val ) +void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case CGObelisk::OBJPROP_INC: + case ObjProperty::OBELISK_VISITED: { - auto progress = ++visited[TeamID(val)]; - logGlobal->debug("Player %d: obelisk progress %d / %d", val, static_cast(progress) , static_cast(obeliskCount)); + auto progress = ++visited[identifier.as()]; + logGlobal->debug("Player %d: obelisk progress %d / %d", identifier.getNum(), static_cast(progress) , static_cast(obeliskCount)); if(progress > obeliskCount) { logGlobal->error("Visited %d of %d", static_cast(progress), obeliskCount); - throw std::runtime_error("internal error"); + throw std::runtime_error("Player visited more obelisks than present on map!"); } break; } default: - CTeamVisited::setPropertyDer(what, val); + CTeamVisited::setPropertyDer(what, identifier); break; } } @@ -1568,12 +1262,12 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const h->showInfoDialog(69); giveBonusTo(h->tempOwner); - if(oldOwner < PlayerColor::PLAYER_LIMIT) //remove bonus from old owner + if(oldOwner.isValidPlayer()) //remove bonus from old owner { RemoveBonus rb(GiveBonus::ETarget::PLAYER); - rb.whoID = oldOwner.getNum(); - rb.source = vstd::to_underlying(BonusSource::OBJECT); - rb.id = id.getNum(); + rb.whoID = oldOwner; + rb.source = BonusSource::OBJECT_INSTANCE; + rb.id = BonusSourceID(id); cb->sendAndApply(&rb); } } @@ -1581,7 +1275,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const void CGLighthouse::initObj(CRandomGenerator & rand) { - if(tempOwner < PlayerColor::PLAYER_LIMIT) + if(tempOwner.isValidPlayer()) { // FIXME: This is dirty hack giveBonusTo(tempOwner, true); @@ -1593,11 +1287,11 @@ 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.getNum(); + gb.id = player; gb.bonus.duration = BonusDuration::PERMANENT; - gb.bonus.source = BonusSource::OBJECT; - gb.bonus.sid = id.getNum(); - gb.bonus.subtype = 0; + 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 @@ -1615,7 +1309,7 @@ void CGLighthouse::serializeJsonOptions(JsonSerializeFormat& handler) void HillFort::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::HILL_FORT_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::HILL_FORT_WINDOW, h, false); } void HillFort::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index c2a9f0c04..b3fbe421b 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -29,21 +29,19 @@ public: bool wasVisited (const CGHeroInstance * h) const override; bool wasVisited(PlayerColor player) const override; bool wasVisited(const TeamID & team) const; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; template void serialize(Handler &h, const int version) { h & static_cast(*this); h & players; } - - static constexpr int OBJPROP_VISITED = 10; }; class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles { public: - std::string message; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; @@ -57,46 +55,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGWitchHut : public CTeamVisited -{ -public: - std::set allowedAbilities; - SecondarySkill ability; - - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & allowedAbilities; - h & ability; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - -class DLL_LINKAGE CGScholar : public CGObjectInstance -{ -public: - enum EBonusType {PRIM_SKILL, SECONDARY_SKILL, SPELL, RANDOM = 255}; - EBonusType bonusType; - ui16 bonusID; //ID of skill/spell - - CGScholar() : bonusType(EBonusType::RANDOM),bonusID(0){}; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & bonusType; - h & bonusID; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGGarrison : public CArmedInstance { public: @@ -119,20 +77,26 @@ class DLL_LINKAGE CGArtifact : public CArmedInstance { public: CArtifactInstance * storedArtifact = nullptr; - std::string message; + MetaString message; 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; std::string getObjectName() const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; void pick( const CGHeroInstance * h ) const; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void afterAddToMap(CMap * map) override; BattleField getBattlefield() const override; + ArtifactID getArtifact() const; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -149,15 +113,17 @@ public: static constexpr ui32 RANDOM_AMOUNT = 0; ui32 amount = RANDOM_AMOUNT; //0 if random - std::string message; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::string getHoverText(PlayerColor player) const override; void collectRes(const PlayerColor & player) const; + GameResID resourceID() const; template void serialize(Handler &h, const int version) { @@ -169,27 +135,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGShrine : public CTeamVisited -{ -public: - MetaString visitText; - SpellID spell; //id of spell or NONE if random - - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this);; - h & spell; - h & visitText; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGMine : public CArmedInstance { public: @@ -198,6 +143,7 @@ public: std::set abandonedMineResources; bool isAbandoned() const; + ResourceSet dailyIncome() const; private: void onHeroVisit(const CGHeroInstance * h) const override; @@ -280,7 +226,7 @@ public: class DLL_LINKAGE CGMonolith : public CGTeleport { - TeleportChannelID findMeChannel(const std::vector & IDs, int SubID) const; + TeleportChannelID findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const; protected: void onHeroVisit(const CGHeroInstance * h) const override; @@ -334,17 +280,6 @@ public: } }; -class DLL_LINKAGE CGObservatory : public CGObjectInstance //Redwood observatory -{ -public: - void onHeroVisit(const CGHeroInstance * h) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode { public: @@ -352,12 +287,12 @@ public: const CGHeroInstance *hero; //hero on board bool onboardAssaultAllowed; //if true, hero can attack units from transport bool onboardVisitAllowed; //if true, hero can visit objects from transport - EPathfindingLayer::EEPathfindingLayer layer; + EPathfindingLayer layer; //animation filenames. If empty - animations won't be used - std::string actualAnimation; //for OH3 boats those have actual animations - std::string overlayAnimation; //waves animations - std::array flagAnimations; + AnimationPath actualAnimation; //for OH3 boats those have actual animations + AnimationPath overlayAnimation; //waves animations + std::array flagAnimations; CGBoat(); bool isCoastVisitable() const override; @@ -403,10 +338,6 @@ protected: class DLL_LINKAGE CGMagi : public CGObjectInstance { public: - static std::map > eyelist; //[subID][id], supports multiple sets as in H5 - - static void reset(); - void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; @@ -416,19 +347,6 @@ public: } }; -class DLL_LINKAGE CCartographer : public CTeamVisited -{ -///behaviour varies depending on surface and floor -public: - void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance { void onHeroVisit(const CGHeroInstance * h) const override; @@ -437,7 +355,6 @@ class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance class DLL_LINKAGE CGObelisk : public CTeamVisited { public: - static constexpr int OBJPROP_INC = 20; static ui8 obeliskCount; //how many obelisks are on map static std::map visited; //map: team_id => how many obelisks has been visited @@ -451,7 +368,7 @@ public: h & static_cast(*this); } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; class DLL_LINKAGE CGLighthouse : public CGObjectInstance diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 83ac7236c..3b623c76c 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -14,17 +14,17 @@ #include "../filesystem/CBinaryReader.h" #include "../VCMI_Lib.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../JsonNode.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN -static bool isOnVisitableFromTopList(int identifier, int type) +static bool isOnVisitableFromTopList(Obj identifier, int type) { if(type == 2 || type == 3 || type == 4 || type == 5) //creature, hero, artifact, resource return true; @@ -116,8 +116,6 @@ void ObjectTemplate::afterLoadFixup() usedTiles[0][0] = VISITABLE; visitDir = 0xFF; } - boost::algorithm::replace_all(animationFile, "\\", "/"); - boost::algorithm::replace_all(editorAnimationFile, "\\", "/"); } void ObjectTemplate::readTxt(CLegacyConfigParser & parser) @@ -127,7 +125,7 @@ void ObjectTemplate::readTxt(CLegacyConfigParser & parser) boost::split(strings, data, boost::is_any_of(" ")); assert(strings.size() == 9); - animationFile = strings[0]; + animationFile = AnimationPath::builtin(strings[0]); stringID = strings[0]; std::string & blockStr = strings[1]; //block map, 0 = blocked, 1 = unblocked @@ -182,7 +180,7 @@ void ObjectTemplate::readTxt(CLegacyConfigParser & parser) void ObjectTemplate::readMsk() { - ResourceID resID("SPRITES/" + animationFile, EResType::MASK); + ResourcePath resID(animationFile.getName(), EResType::MASK); if (CResourceHandler::get()->existsResource(resID)) { @@ -197,7 +195,7 @@ void ObjectTemplate::readMsk() void ObjectTemplate::readMap(CBinaryReader & reader) { - animationFile = reader.readBaseString(); + animationFile = AnimationPath::builtin(reader.readBaseString()); setSize(8, 6); ui8 blockMask[6]; @@ -251,8 +249,8 @@ void ObjectTemplate::readMap(CBinaryReader & reader) void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) { - animationFile = node["animation"].String(); - editorAnimationFile = node["editorAnimation"].String(); + animationFile = AnimationPath::fromJson(node["animation"]); + editorAnimationFile = AnimationPath::fromJson(node["editorAnimation"]); const JsonVector & visitDirs = node["visitableFrom"].Vector(); if (!visitDirs.empty()) @@ -273,7 +271,7 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) { for(const auto & entry : node["allowedTerrains"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", entry, [this](int32_t identifier){ + VLC->identifiers()->requestIdentifier("terrain", entry, [this](int32_t identifier){ allowedTerrains.insert(TerrainId(identifier)); }); } @@ -325,8 +323,8 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) void ObjectTemplate::writeJson(JsonNode & node, const bool withTerrain) const { - node["animation"].String() = animationFile; - node["editorAnimation"].String() = editorAnimationFile; + node["animation"].String() = animationFile.getOriginalName(); + node["editorAnimation"].String() = editorAnimationFile.getOriginalName(); if(visitDir != 0x0 && isVisitable()) { @@ -577,7 +575,7 @@ void ObjectTemplate::recalculate() calculateTopVisibleOffset(); if (visitable && visitDir == 0) - logMod->warn("Template for %s is visitable but has no visitable directions!", animationFile); + logMod->warn("Template for %s is visitable but has no visitable directions!", animationFile.getOriginalName()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 4560d75ea..258e9aea0 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -11,6 +11,7 @@ #include "../GameConstants.h" #include "../int3.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,15 +43,15 @@ class DLL_LINKAGE ObjectTemplate public: /// H3 ID/subID of this object - Obj id; - si32 subid; + 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 - std::string animationFile; + AnimationPath animationFile; /// map editor only animation file - std::string editorAnimationFile; + AnimationPath editorAnimationFile; /// string ID, equals to def base name for h3m files (lower case, no extension) or specified in mod data std::string stringID; @@ -79,7 +80,7 @@ public: bool isVisibleAt(si32 X, si32 Y) const; bool isBlockedAt(si32 X, si32 Y) const; - inline std::set getBlockedOffsets() const + inline const std::set & getBlockedOffsets() const { return blockedOffsets; }; @@ -163,7 +164,7 @@ public: h & animationFile; h & stringID; h & id; - h & subid; + subid.serializeIdentifier(h, id, version); h & printPriority; h & visitDir; h & editorAnimationFile; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index b496eda98..c825899e2 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -1,676 +1,673 @@ -/* - * CMap.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 "CMap.h" - -#include "../CArtHandler.h" -#include "../VCMI_Lib.h" -#include "../CCreatureHandler.h" -#include "../CTownHandler.h" -#include "../CHeroHandler.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../CGeneralTextHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CSkillHandler.h" -#include "CMapEditManager.h" -#include "CMapOperation.h" -#include "../serializer/JsonSerializeFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void Rumor::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeString("name", name); - handler.serializeString("text", text); -} - -DisposedHero::DisposedHero() : heroId(0), portrait(255), players(0) -{ - -} - -CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), - firstOccurence(0), nextOccurence(0) -{ - -} - -bool CMapEvent::earlierThan(const CMapEvent & other) const -{ - return firstOccurence < other.firstOccurence; -} - -bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const -{ - return firstOccurence <= other.firstOccurence; -} - -CCastleEvent::CCastleEvent() : town(nullptr) -{ - -} - -TerrainTile::TerrainTile(): - terType(nullptr), - 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) -{ -} - -bool TerrainTile::entrableTerrain(const TerrainTile * from) const -{ - return entrableTerrain(from ? from->terType->isLand() : true, from ? from->terType->isWater() : true); -} - -bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const -{ - return terType->isPassable() - && ((allowSea && terType->isWater()) || (allowLand && terType->isLand())); -} - -bool TerrainTile::isClear(const TerrainTile * from) const -{ - return entrableTerrain(from) && !blocked; -} - -Obj TerrainTile::topVisitableId(bool excludeTop) const -{ - return topVisitableObj(excludeTop) ? topVisitableObj(excludeTop)->ID : Obj(Obj::NO_OBJ); -} - -CGObjectInstance * TerrainTile::topVisitableObj(bool excludeTop) const -{ - if(visitableObjects.empty() || (excludeTop && visitableObjects.size() == 1)) - return nullptr; - - if(excludeTop) - return visitableObjects[visitableObjects.size()-2]; - - return visitableObjects.back(); -} - -EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const -{ - if(terType->isWater() || !terType->isPassable()) - return EDiggingStatus::WRONG_TERRAIN; - - int allowedBlocked = excludeTop ? 1 : 0; - if(blockingObjects.size() > allowedBlocked || topVisitableObj(excludeTop)) - return EDiggingStatus::TILE_OCCUPIED; - else - return EDiggingStatus::CAN_DIG; -} - -bool TerrainTile::hasFavorableWinds() const -{ - return extTileFlags & 128; -} - -bool TerrainTile::isWater() const -{ - return terType->isWater(); -} - -CMap::CMap() - : checksum(0) - , grailPos(-1, -1, -1) - , grailRadius(0) - , uidCounter(0) -{ - allHeroes.resize(allowedHeroes.size()); - allowedAbilities = VLC->skillh->getDefaultAllowed(); - allowedArtifact = VLC->arth->getDefaultAllowed(); - allowedSpells = VLC->spellh->getDefaultAllowed(); -} - -CMap::~CMap() -{ - getEditManager()->getUndoManager().clearAll(); - - for(auto obj : objects) - obj.dellNull(); - - for(auto quest : quests) - quest.dellNull(); - - resetStaticData(); -} - -void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) -{ - const int zVal = obj->pos.z; - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - int xVal = obj->pos.x - fx; - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int yVal = obj->pos.y - fy; - if(xVal>=0 && xVal < width && yVal>=0 && yVal < height) - { - TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(total || obj->visitableAt(xVal, yVal)) - { - curt.visitableObjects -= obj; - curt.visitable = curt.visitableObjects.size(); - } - if(total || obj->blockingAt(xVal, yVal)) - { - curt.blockingObjects -= obj; - curt.blocked = curt.blockingObjects.size(); - } - } - } - } -} - -void CMap::addBlockVisTiles(CGObjectInstance * obj) -{ - const int zVal = obj->pos.z; - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - int xVal = obj->pos.x - fx; - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int yVal = obj->pos.y - fy; - if(xVal>=0 && xVal < width && yVal >= 0 && yVal < height) - { - TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(obj->visitableAt(xVal, yVal)) - { - curt.visitableObjects.push_back(obj); - curt.visitable = true; - } - if(obj->blockingAt(xVal, yVal)) - { - curt.blockingObjects.push_back(obj); - curt.blocked = true; - } - } - } - } -} - -void CMap::calculateGuardingGreaturePositions() -{ - int levels = twoLevel ? 2 : 1; - for(int z = 0; z < levels; z++) - { - for(int x = 0; x < width; x++) - { - for(int y = 0; y < height; y++) - { - guardingCreaturePositions[z][x][y] = guardingCreaturePosition(int3(x, y, z)); - } - } - } -} - -CGHeroInstance * CMap::getHero(int heroID) -{ - for(auto & elem : heroesOnMap) - if(elem->subID == heroID) - return elem; - return nullptr; -} - -bool CMap::isCoastalTile(const int3 & pos) const -{ - //todo: refactoring: extract neighbor tile iterator and use it in GameState - static const int3 dirs[] = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), - int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) }; - - if(!isInTheMap(pos)) - { - logGlobal->error("Coastal check outside of map: %s", pos.toString()); - return false; - } - - if(isWaterTile(pos)) - return false; - - for(const auto & dir : dirs) - { - const int3 hlp = pos + dir; - - if(!isInTheMap(hlp)) - continue; - const TerrainTile &hlpt = getTile(hlp); - if(hlpt.isWater()) - return true; - } - - 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)); - return terrain[tile.z][tile.x][tile.y]; -} - -const TerrainTile & CMap::getTile(const int3 & tile) const -{ - assert(isInTheMap(tile)); - return terrain[tile.z][tile.x][tile.y]; -} - -bool CMap::isWaterTile(const int3 &pos) const -{ - return isInTheMap(pos) && getTile(pos).isWater(); -} -bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const -{ - const TerrainTile * dstTile = &getTile(dst); - const TerrainTile * srcTile = &getTile(src); - return checkForVisitableDir(src, dstTile, dst) && checkForVisitableDir(dst, srcTile, src); -} - -bool CMap::checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const -{ - if (!pom->entrableTerrain()) //rock is never accessible - return false; - for(auto * obj : pom->visitableObjects) //checking destination tile - { - if(!vstd::contains(pom->blockingObjects, obj)) //this visitable object is not blocking, ignore - continue; - - if (!obj->appearance->isVisitableFrom(src.x - dst.x, src.y - dst.y)) - return false; - } - return true; -} - -int3 CMap::guardingCreaturePosition (int3 pos) const -{ - const int3 originalPos = pos; - // Give monster at position priority. - if (!isInTheMap(pos)) - return int3(-1, -1, -1); - const TerrainTile &posTile = getTile(pos); - if (posTile.visitable) - { - for (CGObjectInstance* obj : posTile.visitableObjects) - { - if(obj->isBlockedVisitable()) - { - if (obj->ID == Obj::MONSTER) // Monster - return pos; - else - return int3(-1, -1, -1); //blockvis objects are not guarded by neighbouring creatures - } - } - } - - // See if there are any monsters adjacent. - bool water = posTile.isWater(); - - pos -= int3(1, 1, 0); // Start with top left. - for (int dx = 0; dx < 3; dx++) - { - for (int dy = 0; dy < 3; dy++) - { - if (isInTheMap(pos)) - { - const auto & tile = getTile(pos); - if (tile.visitable && (tile.isWater() == water)) - { - for (CGObjectInstance* obj : tile.visitableObjects) - { - if (obj->ID == Obj::MONSTER && checkForVisitableDir(pos, &posTile, originalPos)) // Monster being able to attack investigated tile - { - return pos; - } - } - } - } - - pos.y++; - } - pos.y -= 3; - pos.x++; - } - - return int3(-1, -1, -1); -} - -const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj::EObj type) -{ - for (CGObjectInstance * object : getTile(pos).visitableObjects) - { - if (object->ID == type) - return object; - } - // There is weird bug because of which sometimes heroes will not be found properly despite having correct position - // Try to workaround that and find closest object that we can use - - logGlobal->error("Failed to find object of type %d at %s", static_cast(type), pos.toString()); - logGlobal->error("Will try to find closest matching object"); - - CGObjectInstance * bestMatch = nullptr; - for (CGObjectInstance * object : objects) - { - if (object && object->ID == type) - { - if (bestMatch == nullptr) - bestMatch = object; - else - { - if (object->pos.dist2dSQ(pos) < bestMatch->pos.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()); - return bestMatch; -} - -void CMap::checkForObjectives() -{ - // NOTE: probably should be moved to MapFormatH3M.cpp - for (TriggeredEvent & event : triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - switch (cond.condition) - { - case EventCondition::HAVE_ARTIFACT: - event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID()); - break; - - case EventCondition::HAVE_CREATURES: - event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID()); - event.onFulfill.replaceNumber(cond.value); - break; - - case EventCondition::HAVE_RESOURCES: - event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType); - event.onFulfill.replaceNumber(cond.value); - break; - - case EventCondition::HAVE_BUILDING: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); - break; - - case EventCondition::CONTROL: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); - - if (cond.object) - { - const auto * town = dynamic_cast(cond.object); - if (town) - event.onFulfill.replaceRawString(town->getNameTranslated()); - const auto * hero = dynamic_cast(cond.object); - if (hero) - event.onFulfill.replaceRawString(hero->getNameTranslated()); - } - break; - - case EventCondition::DESTROY: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); - - if (cond.object) - { - const auto * hero = dynamic_cast(cond.object); - if (hero) - event.onFulfill.replaceRawString(hero->getNameTranslated()); - } - break; - case EventCondition::TRANSPORT: - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); - break; - //break; case EventCondition::DAYS_PASSED: - //break; case EventCondition::IS_HUMAN: - //break; case EventCondition::DAYS_WITHOUT_TOWN: - //break; case EventCondition::STANDARD_WIN: - - //TODO: support new condition format - case EventCondition::HAVE_0: - break; - case EventCondition::DESTROY_0: - break; - case EventCondition::HAVE_BUILDING_0: - break; - } - return cond; - }; - event.trigger = event.trigger.morph(patcher); - } -} - -void CMap::addNewArtifactInstance(CArtifactInstance * art) -{ - art->setId(static_cast(artInstances.size())); - artInstances.emplace_back(art); -} - -void CMap::eraseArtifactInstance(CArtifactInstance * art) -{ - //TODO: handle for artifacts removed in map editor - assert(artInstances[art->getId().getNum()] == art); - artInstances[art->getId().getNum()].dellNull(); -} - -void CMap::addNewQuestInstance(CQuest* quest) -{ - quest->qid = static_cast(quests.size()); - quests.emplace_back(quest); -} - -void CMap::removeQuestInstance(CQuest * quest) - -{ - //TODO: should be called only by map editor. - //During game, completed quests or quests from removed objects stay forever - - //Shift indexes - auto iter = std::next(quests.begin(), quest->qid); - iter = quests.erase(iter); - for (int i = quest->qid; iter != quests.end(); ++i, ++iter) - { - (*iter)->qid = i; - } -} - -void CMap::setUniqueInstanceName(CGObjectInstance * obj) -{ - //this gives object unique name even if objects are removed later - - auto uid = uidCounter++; - - boost::format fmt("%s_%d"); - fmt % obj->typeName % uid; - obj->instanceName = fmt.str(); -} - -void CMap::addNewObject(CGObjectInstance * obj) -{ - if(obj->id != ObjectInstanceID(static_cast(objects.size()))) - throw std::runtime_error("Invalid object instance id"); - - if(obj->instanceName.empty()) - throw std::runtime_error("Object instance name missing"); - - if (vstd::contains(instanceNames, obj->instanceName)) - throw std::runtime_error("Object instance name duplicated: "+obj->instanceName); - - objects.emplace_back(obj); - instanceNames[obj->instanceName] = obj; - addBlockVisTiles(obj); - - //TODO: how about defeated heroes recruited again? - - obj->afterAddToMap(this); -} - -void CMap::moveObject(CGObjectInstance * obj, const int3 & pos) -{ - removeBlockVisTiles(obj); - obj->pos = pos; - addBlockVisTiles(obj); -} - -void CMap::removeObject(CGObjectInstance * obj) -{ - removeBlockVisTiles(obj); - instanceNames.erase(obj->instanceName); - - //update indeces - auto iter = std::next(objects.begin(), obj->id.getNum()); - iter = objects.erase(iter); - for(int i = obj->id.getNum(); iter != objects.end(); ++i, ++iter) - { - (*iter)->id = ObjectInstanceID(i); - } - - obj->afterRemoveFromMap(this); - - //TOOD: Clean artifact instances (mostly worn by hero?) and quests related to this object -} - -bool CMap::isWaterMap() const -{ - return waterMap; -} - -bool CMap::calculateWaterContent() -{ - size_t totalTiles = height * width * levels(); - size_t waterTiles = 0; - - for(auto tile = terrain.origin(); tile < (terrain.origin() + terrain.num_elements()); ++tile) - { - if (tile->isWater()) - { - waterTiles++; - } - } - - if (waterTiles >= totalTiles / 100) //At least 1% of area is water - { - waterMap = true; - } - - return waterMap; -} - -void CMap::banWaterContent() -{ - banWaterHeroes(); - banWaterArtifacts(); - banWaterSpells(); - banWaterSkills(); -} - -void CMap::banWaterSpells() -{ - for (int j = 0; j < allowedSpells.size(); j++) - { - if (allowedSpells[j]) - { - auto* spell = dynamic_cast(VLC->spells()->getByIndex(j)); - if (spell->onlyOnWaterMap && !isWaterMap()) - { - allowedSpells[j] = false; - } - } - } -} - -void CMap::banWaterArtifacts() -{ - for (int j = 0; j < allowedArtifact.size(); j++) - { - if (allowedArtifact[j]) - { - auto* art = dynamic_cast(VLC->artifacts()->getByIndex(j)); - if (art->onlyOnWaterMap && !isWaterMap()) - { - allowedArtifact[j] = false; - } - } - } -} - -void CMap::banWaterSkills() -{ - for (int j = 0; j < allowedAbilities.size(); j++) - { - if (allowedAbilities[j]) - { - auto* skill = dynamic_cast(VLC->skills()->getByIndex(j)); - if (skill->onlyOnWaterMap && !isWaterMap()) - { - allowedAbilities[j] = false; - } - } - } -} - -void CMap::banWaterHeroes() -{ - for (int j = 0; j < allowedHeroes.size(); j++) - { - if (allowedHeroes[j]) - { - auto* h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap()) || (h->onlyOnMapWithoutWater && isWaterMap())) - { - banHero(HeroTypeID(j)); - } - } - } -} - -void CMap::banHero(const HeroTypeID & id) -{ - allowedHeroes.at(id) = false; -} - -void CMap::initTerrain() -{ - terrain.resize(boost::extents[levels()][width][height]); - guardingCreaturePositions.resize(boost::extents[levels()][width][height]); -} - -CMapEditManager * CMap::getEditManager() -{ - if(!editManager) editManager = std::make_unique(this); - return editManager.get(); -} - -void CMap::resetStaticData() -{ - CGKeys::reset(); - CGMagi::reset(); - CGObelisk::reset(); - CGTownInstance::reset(); -} - -VCMI_LIB_NAMESPACE_END +/* + * CMap.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 "CMap.h" + +#include "../CArtHandler.h" +#include "../VCMI_Lib.h" +#include "../CCreatureHandler.h" +#include "../CTownHandler.h" +#include "../CHeroHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../TerrainHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../CGeneralTextHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CSkillHandler.h" +#include "CMapEditManager.h" +#include "CMapOperation.h" +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void Rumor::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeString("name", name); + handler.serializeStruct("text", text); +} + +DisposedHero::DisposedHero() : heroId(0), portrait(255) +{ + +} + +CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), + firstOccurence(0), nextOccurence(0) +{ + +} + +bool CMapEvent::earlierThan(const CMapEvent & other) const +{ + return firstOccurence < other.firstOccurence; +} + +bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const +{ + return firstOccurence <= other.firstOccurence; +} + +void CMapEvent::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeString("name", name); + handler.serializeStruct("message", message); + handler.serializeInt("players", players); + handler.serializeInt("humanAffected", humanAffected); + handler.serializeInt("computerAffected", computerAffected); + handler.serializeInt("firstOccurence", firstOccurence); + handler.serializeInt("nextOccurence", nextOccurence); + resources.serializeJson(handler, "resources"); +} + +void CCastleEvent::serializeJson(JsonSerializeFormat & handler) +{ + CMapEvent::serializeJson(handler); + + // TODO: handler.serializeIdArray("buildings", buildings); + { + std::vector temp(buildings.begin(), buildings.end()); + auto a = handler.enterArray("buildings"); + a.syncSize(temp); + for(int i = 0; i < temp.size(); ++i) + { + int buildingID = temp[i].getNum(); + a.serializeInt(i, buildingID); + buildings.insert(buildingID); + } + } + + { + auto a = handler.enterArray("creatures"); + a.syncSize(creatures); + for(int i = 0; i < creatures.size(); ++i) + a.serializeInt(i, creatures[i]); + } +} + +TerrainTile::TerrainTile(): + terType(nullptr), + 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) +{ +} + +bool TerrainTile::entrableTerrain(const TerrainTile * from) const +{ + return entrableTerrain(from ? from->terType->isLand() : true, from ? from->terType->isWater() : true); +} + +bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const +{ + return terType->isPassable() + && ((allowSea && terType->isWater()) || (allowLand && terType->isLand())); +} + +bool TerrainTile::isClear(const TerrainTile * from) const +{ + return entrableTerrain(from) && !blocked; +} + +Obj TerrainTile::topVisitableId(bool excludeTop) const +{ + return topVisitableObj(excludeTop) ? topVisitableObj(excludeTop)->ID : Obj(Obj::NO_OBJ); +} + +CGObjectInstance * TerrainTile::topVisitableObj(bool excludeTop) const +{ + if(visitableObjects.empty() || (excludeTop && visitableObjects.size() == 1)) + return nullptr; + + if(excludeTop) + return visitableObjects[visitableObjects.size()-2]; + + return visitableObjects.back(); +} + +EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const +{ + if(terType->isWater() || !terType->isPassable()) + return EDiggingStatus::WRONG_TERRAIN; + + int allowedBlocked = excludeTop ? 1 : 0; + if(blockingObjects.size() > allowedBlocked || topVisitableObj(excludeTop)) + return EDiggingStatus::TILE_OCCUPIED; + else + return EDiggingStatus::CAN_DIG; +} + +bool TerrainTile::hasFavorableWinds() const +{ + return extTileFlags & 128; +} + +bool TerrainTile::isWater() const +{ + return terType->isWater(); +} + +CMap::CMap() + : checksum(0) + , grailPos(-1, -1, -1) + , grailRadius(0) + , uidCounter(0) +{ + allHeroes.resize(VLC->heroh->objects.size()); + allowedAbilities = VLC->skillh->getDefaultAllowed(); + allowedArtifact = VLC->arth->getDefaultAllowed(); + allowedSpells = VLC->spellh->getDefaultAllowed(); +} + +CMap::~CMap() +{ + getEditManager()->getUndoManager().clearAll(); + + for(auto obj : objects) + obj.dellNull(); + + for(auto quest : quests) + quest.dellNull(); + + for(auto artInstance : artInstances) + artInstance.dellNull(); + + resetStaticData(); +} + +void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) +{ + const int zVal = obj->pos.z; + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + int xVal = obj->pos.x - fx; + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int yVal = obj->pos.y - fy; + if(xVal>=0 && xVal < width && yVal>=0 && yVal < height) + { + TerrainTile & curt = terrain[zVal][xVal][yVal]; + if(total || obj->visitableAt(xVal, yVal)) + { + curt.visitableObjects -= obj; + curt.visitable = curt.visitableObjects.size(); + } + if(total || obj->blockingAt(xVal, yVal)) + { + curt.blockingObjects -= obj; + curt.blocked = curt.blockingObjects.size(); + } + } + } + } +} + +void CMap::addBlockVisTiles(CGObjectInstance * obj) +{ + const int zVal = obj->pos.z; + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + int xVal = obj->pos.x - fx; + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int yVal = obj->pos.y - fy; + if(xVal>=0 && xVal < width && yVal >= 0 && yVal < height) + { + TerrainTile & curt = terrain[zVal][xVal][yVal]; + if(obj->visitableAt(xVal, yVal)) + { + curt.visitableObjects.push_back(obj); + curt.visitable = true; + } + if(obj->blockingAt(xVal, yVal)) + { + curt.blockingObjects.push_back(obj); + curt.blocked = true; + } + } + } + } +} + +void CMap::calculateGuardingGreaturePositions() +{ + int levels = twoLevel ? 2 : 1; + for(int z = 0; z < levels; z++) + { + for(int x = 0; x < width; x++) + { + for(int y = 0; y < height; y++) + { + guardingCreaturePositions[z][x][y] = guardingCreaturePosition(int3(x, y, z)); + } + } + } +} + +CGHeroInstance * CMap::getHero(HeroTypeID heroID) +{ + for(auto & elem : heroesOnMap) + if(elem->getHeroType() == heroID) + return elem; + return nullptr; +} + +bool CMap::isCoastalTile(const int3 & pos) const +{ + //todo: refactoring: extract neighbor tile iterator and use it in GameState + static const int3 dirs[] = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), + int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) }; + + if(!isInTheMap(pos)) + { + logGlobal->error("Coastal check outside of map: %s", pos.toString()); + return false; + } + + if(isWaterTile(pos)) + return false; + + for(const auto & dir : dirs) + { + const int3 hlp = pos + dir; + + if(!isInTheMap(hlp)) + continue; + const TerrainTile &hlpt = getTile(hlp); + if(hlpt.isWater()) + return true; + } + + 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)); + return terrain[tile.z][tile.x][tile.y]; +} + +const TerrainTile & CMap::getTile(const int3 & tile) const +{ + assert(isInTheMap(tile)); + return terrain[tile.z][tile.x][tile.y]; +} + +bool CMap::isWaterTile(const int3 &pos) const +{ + return isInTheMap(pos) && getTile(pos).isWater(); +} +bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const +{ + const TerrainTile * dstTile = &getTile(dst); + const TerrainTile * srcTile = &getTile(src); + return checkForVisitableDir(src, dstTile, dst) && checkForVisitableDir(dst, srcTile, src); +} + +bool CMap::checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const +{ + if (!pom->entrableTerrain()) //rock is never accessible + return false; + for(auto * obj : pom->visitableObjects) //checking destination tile + { + if(!vstd::contains(pom->blockingObjects, obj)) //this visitable object is not blocking, ignore + continue; + + if (!obj->appearance->isVisitableFrom(src.x - dst.x, src.y - dst.y)) + return false; + } + return true; +} + +int3 CMap::guardingCreaturePosition (int3 pos) const +{ + const int3 originalPos = pos; + // Give monster at position priority. + if (!isInTheMap(pos)) + return int3(-1, -1, -1); + const TerrainTile &posTile = getTile(pos); + if (posTile.visitable) + { + for (CGObjectInstance* obj : posTile.visitableObjects) + { + if (obj->ID == Obj::MONSTER) + return pos; + } + } + + // See if there are any monsters adjacent. + bool water = posTile.isWater(); + + pos -= int3(1, 1, 0); // Start with top left. + for (int dx = 0; dx < 3; dx++) + { + for (int dy = 0; dy < 3; dy++) + { + if (isInTheMap(pos)) + { + const auto & tile = getTile(pos); + if (tile.visitable && (tile.isWater() == water)) + { + for (CGObjectInstance* obj : tile.visitableObjects) + { + if (obj->ID == Obj::MONSTER && checkForVisitableDir(pos, &posTile, originalPos)) // Monster being able to attack investigated tile + { + return pos; + } + } + } + } + + pos.y++; + } + pos.y -= 3; + pos.x++; + } + + return int3(-1, -1, -1); +} + +const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type) +{ + for (CGObjectInstance * object : getTile(pos).visitableObjects) + { + if (object->ID == type) + return object; + } + // There is weird bug because of which sometimes heroes will not be found properly despite having correct position + // Try to workaround that and find closest object that we can use + + logGlobal->error("Failed to find object of type %d at %s", type.getNum(), pos.toString()); + logGlobal->error("Will try to find closest matching object"); + + CGObjectInstance * bestMatch = nullptr; + for (CGObjectInstance * object : objects) + { + if (object && object->ID == type) + { + if (bestMatch == nullptr) + bestMatch = object; + else + { + if (object->pos.dist2dSQ(pos) < bestMatch->pos.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()); + return bestMatch; +} + +void CMap::checkForObjectives() +{ + // NOTE: probably should be moved to MapFormatH3M.cpp + for (TriggeredEvent & event : triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + switch (cond.condition) + { + case EventCondition::HAVE_ARTIFACT: + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameTextID()); + break; + + case EventCondition::HAVE_CREATURES: + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameSingularTextID()); + event.onFulfill.replaceNumber(cond.value); + break; + + case EventCondition::HAVE_RESOURCES: + event.onFulfill.replaceName(cond.objectType.as()); + event.onFulfill.replaceNumber(cond.value); + break; + + case EventCondition::HAVE_BUILDING: + if (isInTheMap(cond.position)) + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; + break; + + case EventCondition::CONTROL: + if (isInTheMap(cond.position)) + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; + + if (cond.objectID != ObjectInstanceID::NONE) + { + const auto * town = dynamic_cast(objects[cond.objectID].get()); + if (town) + event.onFulfill.replaceRawString(town->getNameTranslated()); + const auto * hero = dynamic_cast(objects[cond.objectID].get()); + if (hero) + event.onFulfill.replaceRawString(hero->getNameTranslated()); + } + break; + + case EventCondition::DESTROY: + if (isInTheMap(cond.position)) + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; + + if (cond.objectID != ObjectInstanceID::NONE) + { + const auto * hero = dynamic_cast(objects[cond.objectID].get()); + if (hero) + event.onFulfill.replaceRawString(hero->getNameTranslated()); + } + break; + case EventCondition::TRANSPORT: + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; + break; + //break; case EventCondition::DAYS_PASSED: + //break; case EventCondition::IS_HUMAN: + //break; case EventCondition::DAYS_WITHOUT_TOWN: + //break; case EventCondition::STANDARD_WIN: + } + return cond; + }; + event.trigger = event.trigger.morph(patcher); + } +} + +void CMap::addNewArtifactInstance(ConstTransitivePtr art) +{ + art->setId(static_cast(artInstances.size())); + artInstances.emplace_back(art); +} + +void CMap::eraseArtifactInstance(CArtifactInstance * art) +{ + //TODO: handle for artifacts removed in map editor + assert(artInstances[art->getId().getNum()] == art); + artInstances[art->getId().getNum()].dellNull(); +} + +void CMap::addNewQuestInstance(CQuest* quest) +{ + quest->qid = static_cast(quests.size()); + quests.emplace_back(quest); +} + +void CMap::removeQuestInstance(CQuest * quest) + +{ + //TODO: should be called only by map editor. + //During game, completed quests or quests from removed objects stay forever + + //Shift indexes + auto iter = std::next(quests.begin(), quest->qid); + iter = quests.erase(iter); + for (int i = quest->qid; iter != quests.end(); ++i, ++iter) + { + (*iter)->qid = i; + } +} + +void CMap::setUniqueInstanceName(CGObjectInstance * obj) +{ + //this gives object unique name even if objects are removed later + + auto uid = uidCounter++; + + boost::format fmt("%s_%d"); + fmt % obj->typeName % uid; + obj->instanceName = fmt.str(); +} + +void CMap::addNewObject(CGObjectInstance * obj) +{ + if(obj->id != ObjectInstanceID(static_cast(objects.size()))) + throw std::runtime_error("Invalid object instance id"); + + if(obj->instanceName.empty()) + throw std::runtime_error("Object instance name missing"); + + if (vstd::contains(instanceNames, obj->instanceName)) + throw std::runtime_error("Object instance name duplicated: "+obj->instanceName); + + objects.emplace_back(obj); + instanceNames[obj->instanceName] = obj; + addBlockVisTiles(obj); + + //TODO: how about defeated heroes recruited again? + + obj->afterAddToMap(this); +} + +void CMap::moveObject(CGObjectInstance * obj, const int3 & pos) +{ + removeBlockVisTiles(obj); + obj->pos = pos; + addBlockVisTiles(obj); +} + +void CMap::removeObject(CGObjectInstance * obj) +{ + removeBlockVisTiles(obj); + instanceNames.erase(obj->instanceName); + + //update indeces + auto iter = std::next(objects.begin(), obj->id.getNum()); + iter = objects.erase(iter); + for(int i = obj->id.getNum(); iter != objects.end(); ++i, ++iter) + { + (*iter)->id = ObjectInstanceID(i); + } + + obj->afterRemoveFromMap(this); + + //TOOD: Clean artifact instances (mostly worn by hero?) and quests related to this object +} + +bool CMap::isWaterMap() const +{ + return waterMap; +} + +bool CMap::calculateWaterContent() +{ + size_t totalTiles = height * width * levels(); + size_t waterTiles = 0; + + for(auto tile = terrain.origin(); tile < (terrain.origin() + terrain.num_elements()); ++tile) + { + if (tile->isWater()) + { + waterTiles++; + } + } + + if (waterTiles >= totalTiles / 100) //At least 1% of area is water + { + waterMap = true; + } + + return waterMap; +} + +void CMap::banWaterContent() +{ + banWaterHeroes(); + banWaterArtifacts(); + banWaterSpells(); + banWaterSkills(); +} + +void CMap::banWaterSpells() +{ + vstd::erase_if(allowedSpells, [&](SpellID spell) + { + return spell.toSpell()->onlyOnWaterMap && !isWaterMap(); + }); +} + +void CMap::banWaterArtifacts() +{ + vstd::erase_if(allowedArtifact, [&](ArtifactID artifact) + { + return artifact.toArtifact()->onlyOnWaterMap && !isWaterMap(); + }); +} + +void CMap::banWaterSkills() +{ + vstd::erase_if(allowedAbilities, [&](SecondarySkill skill) + { + return skill.toSkill()->onlyOnWaterMap && !isWaterMap(); + }); +} + +void CMap::banWaterHeroes() +{ + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) + { + return hero.toHeroType()->onlyOnWaterMap && !isWaterMap(); + }); + + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) + { + return hero.toHeroType()->onlyOnMapWithoutWater && isWaterMap(); + }); +} + +void CMap::banHero(const HeroTypeID & id) +{ + allowedHeroes.erase(id); +} + +void CMap::initTerrain() +{ + terrain.resize(boost::extents[levels()][width][height]); + guardingCreaturePositions.resize(boost::extents[levels()][width][height]); +} + +CMapEditManager * CMap::getEditManager() +{ + if(!editManager) editManager = std::make_unique(this); + return editManager.get(); +} + +void CMap::resetStaticData() +{ + CGObelisk::reset(); + CGTownInstance::reset(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 0aa6afb06..7922df144 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -1,204 +1,203 @@ -/* - * CMap.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 "CMapHeader.h" -#include "../mapObjects/MiscObjects.h" // To serialize static props -#include "../mapObjects/CQuest.h" // To serialize static props -#include "../mapObjects/CGTownInstance.h" // To serialize static props -#include "CMapDefines.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CArtifactInstance; -class CGObjectInstance; -class CGHeroInstance; -class CCommanderInstance; -class CGCreature; -class CQuest; -class CGTownInstance; -class IModableArt; -class IQuestObject; -class CInputStream; -class CMapEditManager; -class JsonSerializeFormat; -struct TeleportChannel; - -/// The rumor struct consists of a rumor name and text. -struct DLL_LINKAGE Rumor -{ - std::string name; - std::string text; - - Rumor() = default; - ~Rumor() = default; - - template - void serialize(Handler & h, const int version) - { - h & name; - h & text; - } - - void serializeJson(JsonSerializeFormat & handler); -}; - -/// The disposed hero struct describes which hero can be hired from which player. -struct DLL_LINKAGE DisposedHero -{ - DisposedHero(); - - HeroTypeID heroId; - HeroTypeID portrait; /// The portrait id of the hero, -1 is default. - std::string name; - PlayerColor::Mask players; /// Who can hire this hero (bitfield). - - template - void serialize(Handler & h, const int version) - { - h & heroId; - h & portrait; - h & name; - h & players; - } -}; - -/// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... -class DLL_LINKAGE CMap : public CMapHeader -{ -public: - CMap(); - ~CMap(); - void initTerrain(); - - CMapEditManager * getEditManager(); - 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; - - bool canMoveBetween(const int3 &src, const int3 &dst) const; - bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; - int3 guardingCreaturePosition (int3 pos) const; - - void addBlockVisTiles(CGObjectInstance * obj); - void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); - void calculateGuardingGreaturePositions(); - - void addNewArtifactInstance(CArtifactInstance * art); - void eraseArtifactInstance(CArtifactInstance * art); - - void addNewQuestInstance(CQuest * quest); - void removeQuestInstance(CQuest * quest); - - void setUniqueInstanceName(CGObjectInstance * obj); - ///Use only this method when creating new map object instances - void addNewObject(CGObjectInstance * obj); - void moveObject(CGObjectInstance * obj, const int3 & dst); - void removeObject(CGObjectInstance * obj); - - bool isWaterMap() const; - bool calculateWaterContent(); - void banWaterArtifacts(); - void banWaterHeroes(); - void banHero(const HeroTypeID& id); - void banWaterSpells(); - void banWaterSkills(); - void banWaterContent(); - - /// Gets object of specified type on requested position - const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj::EObj type); - CGHeroInstance * getHero(int heroId); - - /// Sets the victory/loss condition objectives ?? - void checkForObjectives(); - - void resetStaticData(); - - ui32 checksum; - std::vector rumors; - std::vector disposedHeroes; - std::vector > predefinedHeroes; - std::vector allowedSpells; - std::vector allowedArtifact; - std::vector allowedAbilities; - std::list events; - int3 grailPos; - int grailRadius; - - //Central lists of items in game. Position of item in the vectors below is their (instance) id. - std::vector< ConstTransitivePtr > objects; - std::vector< ConstTransitivePtr > towns; - std::vector< ConstTransitivePtr > artInstances; - std::vector< ConstTransitivePtr > quests; - std::vector< ConstTransitivePtr > allHeroes; //indexed by [hero_type_id]; on map, disposed, prisons, etc. - - //Helper lists - std::vector< ConstTransitivePtr > heroesOnMap; - std::map > teleportChannels; - - /// associative list to identify which hero/creature id belongs to which object id(index for objects) - std::map questIdentifierToId; - - std::unique_ptr editManager; - boost::multi_array guardingCreaturePositions; - - std::map > instanceNames; - - bool waterMap; - -private: - /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground - boost::multi_array terrain; - - si32 uidCounter; //TODO: initialize when loading an old map - -public: - template - void serialize(Handler &h, const int formatVersion) - { - h & static_cast(*this); - h & triggeredEvents; //from CMapHeader - h & rumors; - h & allowedSpells; - h & allowedAbilities; - h & allowedArtifact; - h & events; - h & grailPos; - h & artInstances; - h & quests; - h & allHeroes; - h & questIdentifierToId; - - //TODO: viccondetails - h & terrain; - h & guardingCreaturePositions; - - h & objects; - h & heroesOnMap; - h & teleportChannels; - h & towns; - h & artInstances; - - // static members - h & CGKeys::playerKeyMap; - h & CGMagi::eyelist; - h & CGObelisk::obeliskCount; - h & CGObelisk::visited; - h & CGTownInstance::merchantArtifacts; - h & CGTownInstance::universitySkills; - - h & instanceNames; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMap.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 "CMapHeader.h" +#include "../MetaString.h" +#include "../mapObjects/MiscObjects.h" // To serialize static props +#include "../mapObjects/CQuest.h" // To serialize static props +#include "../mapObjects/CGTownInstance.h" // To serialize static props +#include "CMapDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArtifactInstance; +class CGObjectInstance; +class CGHeroInstance; +class CCommanderInstance; +class CGCreature; +class CQuest; +class CGTownInstance; +class IModableArt; +class IQuestObject; +class CInputStream; +class CMapEditManager; +class JsonSerializeFormat; +struct TeleportChannel; + +/// The rumor struct consists of a rumor name and text. +struct DLL_LINKAGE Rumor +{ + std::string name; + MetaString text; + + Rumor() = default; + ~Rumor() = default; + + template + void serialize(Handler & h, const int version) + { + h & name; + h & text; + } + + void serializeJson(JsonSerializeFormat & handler); +}; + +/// The disposed hero struct describes which hero can be hired from which player. +struct DLL_LINKAGE DisposedHero +{ + DisposedHero(); + + HeroTypeID heroId; + HeroTypeID portrait; /// The portrait id of the hero, -1 is default. + std::string name; + std::set players; /// Who can hire this hero (bitfield). + + template + void serialize(Handler & h, const int version) + { + h & heroId; + h & portrait; + h & name; + h & players; + } +}; + +/// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... +class DLL_LINKAGE CMap : public CMapHeader +{ +public: + CMap(); + ~CMap(); + void initTerrain(); + + CMapEditManager * getEditManager(); + 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; + + bool canMoveBetween(const int3 &src, const int3 &dst) const; + bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; + int3 guardingCreaturePosition (int3 pos) const; + + void addBlockVisTiles(CGObjectInstance * obj); + void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); + void calculateGuardingGreaturePositions(); + + void addNewArtifactInstance(ConstTransitivePtr art); + void eraseArtifactInstance(CArtifactInstance * art); + + void addNewQuestInstance(CQuest * quest); + void removeQuestInstance(CQuest * quest); + + void setUniqueInstanceName(CGObjectInstance * obj); + ///Use only this method when creating new map object instances + void addNewObject(CGObjectInstance * obj); + void moveObject(CGObjectInstance * obj, const int3 & dst); + void removeObject(CGObjectInstance * obj); + + bool isWaterMap() const; + bool calculateWaterContent(); + void banWaterArtifacts(); + void banWaterHeroes(); + void banHero(const HeroTypeID& id); + void banWaterSpells(); + void banWaterSkills(); + void banWaterContent(); + + /// Gets object of specified type on requested position + const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); + CGHeroInstance * getHero(HeroTypeID heroId); + + /// Sets the victory/loss condition objectives ?? + void checkForObjectives(); + + void resetStaticData(); + + ui32 checksum; + std::vector rumors; + std::vector disposedHeroes; + std::vector > predefinedHeroes; + std::set allowedSpells; + std::set allowedArtifact; + std::set allowedAbilities; + std::list events; + int3 grailPos; + int grailRadius; + + //Central lists of items in game. Position of item in the vectors below is their (instance) id. + std::vector< ConstTransitivePtr > objects; + std::vector< ConstTransitivePtr > towns; + std::vector< ConstTransitivePtr > artInstances; + std::vector< ConstTransitivePtr > quests; + std::vector< ConstTransitivePtr > allHeroes; //indexed by [hero_type_id]; on map, disposed, prisons, etc. + + //Helper lists + std::vector< ConstTransitivePtr > heroesOnMap; + std::map > teleportChannels; + + /// associative list to identify which hero/creature id belongs to which object id(index for objects) + std::map questIdentifierToId; + + std::unique_ptr editManager; + boost::multi_array guardingCreaturePositions; + + std::map > instanceNames; + + bool waterMap; + +private: + /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground + boost::multi_array terrain; + + si32 uidCounter; //TODO: initialize when loading an old map + +public: + template + void serialize(Handler &h, const int formatVersion) + { + h & static_cast(*this); + h & triggeredEvents; //from CMapHeader + h & rumors; + h & allowedSpells; + h & allowedAbilities; + h & allowedArtifact; + h & events; + h & grailPos; + h & artInstances; + h & quests; + h & allHeroes; + h & questIdentifierToId; + + //TODO: viccondetails + h & terrain; + h & guardingCreaturePositions; + + h & objects; + h & heroesOnMap; + h & teleportChannels; + h & towns; + h & artInstances; + + // static members + h & CGObelisk::obeliskCount; + h & CGObelisk::visited; + h & CGTownInstance::merchantArtifacts; + h & CGTownInstance::universitySkills; + + h & instanceNames; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 14c7512b9..85a2da1a1 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -1,125 +1,131 @@ -/* - * CMapDefines.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 - -#include "../ResourceSet.h" - -class TerrainType; -class RiverType; -class RoadType; -class CGObjectInstance; -class CGTownInstance; - -/// The map event is an event which e.g. gives or takes resources of a specific -/// amount to/from players and can appear regularly or once a time. -class DLL_LINKAGE CMapEvent -{ -public: - CMapEvent(); - - bool earlierThan(const CMapEvent & other) const; - bool earlierThanOrEqual(const CMapEvent & other) const; - - std::string name; - std::string message; - TResources resources; - ui8 players; // affected players, bit field? - ui8 humanAffected; - ui8 computerAffected; - ui32 firstOccurence; - ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time - - template - void serialize(Handler & h, const int version) - { - h & name; - h & message; - h & resources; - h & players; - h & humanAffected; - h & computerAffected; - h & firstOccurence; - h & nextOccurence; - } -}; - -/// The castle event builds/adds buildings/creatures for a specific town. -class DLL_LINKAGE CCastleEvent: public CMapEvent -{ -public: - CCastleEvent(); - - std::set buildings; - std::vector creatures; - CGTownInstance * town; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & buildings; - h & creatures; - } -}; - -/// The terrain tile describes the terrain type and the visual representation of the terrain. -/// Furthermore the struct defines whether the tile is visitable or/and blocked and which objects reside in it. -struct DLL_LINKAGE TerrainTile -{ - TerrainTile(); - - /// Gets true if the terrain is not a rock. If from is water/land, same type is also required. - bool entrableTerrain(const TerrainTile * from = nullptr) const; - bool entrableTerrain(bool allowLand, bool allowSea) const; - /// Checks for blocking objects and terraint type (water / land). - bool isClear(const TerrainTile * from = nullptr) const; - /// Gets the ID of the top visitable object or -1 if there is none. - Obj topVisitableId(bool excludeTop = false) const; - CGObjectInstance * topVisitableObj(bool excludeTop = false) const; - bool isWater() const; - EDiggingStatus getDiggingStatus(const bool excludeTop = true) const; - bool hasFavorableWinds() const; - - const TerrainType * terType; - 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; - - template - void serialize(Handler & h, const int version) - { - h & terType; - h & terView; - h & riverType; - h & riverDir; - h & roadType; - h & roadDir; - h & extTileFlags; - h & visitable; - h & blocked; - h & visitableObjects; - h & blockingObjects; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMapDefines.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 "../ResourceSet.h" +#include "../MetaString.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class TerrainType; +class RiverType; +class RoadType; +class CGObjectInstance; +class CGTownInstance; +class JsonSerializeFormat; + +/// The map event is an event which e.g. gives or takes resources of a specific +/// amount to/from players and can appear regularly or once a time. +class DLL_LINKAGE CMapEvent +{ +public: + CMapEvent(); + virtual ~CMapEvent() = default; + + bool earlierThan(const CMapEvent & other) const; + bool earlierThanOrEqual(const CMapEvent & other) const; + + std::string name; + MetaString message; + TResources resources; + ui8 players; // affected players, bit field? + ui8 humanAffected; + ui8 computerAffected; + ui32 firstOccurence; + ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time + + template + void serialize(Handler & h, const int version) + { + h & name; + h & message; + h & resources; + h & players; + h & humanAffected; + h & computerAffected; + h & firstOccurence; + h & nextOccurence; + } + + virtual void serializeJson(JsonSerializeFormat & handler); +}; + +/// The castle event builds/adds buildings/creatures for a specific town. +class DLL_LINKAGE CCastleEvent: public CMapEvent +{ +public: + CCastleEvent() = default; + + std::set buildings; + std::vector creatures; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & buildings; + h & creatures; + } + + void serializeJson(JsonSerializeFormat & handler) override; +}; + +/// The terrain tile describes the terrain type and the visual representation of the terrain. +/// Furthermore the struct defines whether the tile is visitable or/and blocked and which objects reside in it. +struct DLL_LINKAGE TerrainTile +{ + TerrainTile(); + + /// Gets true if the terrain is not a rock. If from is water/land, same type is also required. + bool entrableTerrain(const TerrainTile * from = nullptr) const; + bool entrableTerrain(bool allowLand, bool allowSea) const; + /// Checks for blocking objects and terraint type (water / land). + bool isClear(const TerrainTile * from = nullptr) const; + /// Gets the ID of the top visitable object or -1 if there is none. + Obj topVisitableId(bool excludeTop = false) const; + CGObjectInstance * topVisitableObj(bool excludeTop = false) const; + bool isWater() const; + EDiggingStatus getDiggingStatus(const bool excludeTop = true) const; + bool hasFavorableWinds() const; + + const TerrainType * terType; + 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; + + template + void serialize(Handler & h, const int version) + { + h & terType; + h & terView; + h & riverType; + h & riverDir; + h & roadType; + h & roadDir; + h & extTileFlags; + h & visitable; + h & blocked; + h & visitableObjects; + h & blockingObjects; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 98a8afdb6..cacfc4c24 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -15,7 +15,9 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" #include "../CGeneralTextHandler.h" +#include "../modding/CModHandler.h" #include "../CHeroHandler.h" +#include "../Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,29 +32,29 @@ PlayerInfo::PlayerInfo(): canHumanPlay(false), canComputerPlay(false), allowedFactions = VLC->townh->getAllowedFactions(); } -si8 PlayerInfo::defaultCastle() const +FactionID PlayerInfo::defaultCastle() const { //if random allowed set it as default if(isFactionRandom) - return -1; + return FactionID::RANDOM; if(!allowedFactions.empty()) return *allowedFactions.begin(); // fall back to random - return -1; + return FactionID::RANDOM; } -si8 PlayerInfo::defaultHero() const +HeroTypeID PlayerInfo::defaultHero() const { // we will generate hero in front of main town if((generateHeroAtMainTown && hasMainTown) || hasRandomHero) { //random hero - return -1; + return HeroTypeID::RANDOM; } - return -2; + return HeroTypeID::NONE; } bool PlayerInfo::canAnyonePlay() const @@ -62,26 +64,19 @@ bool PlayerInfo::canAnyonePlay() const bool PlayerInfo::hasCustomMainHero() const { - return !mainCustomHeroName.empty() && mainCustomHeroPortrait != -1; + return mainCustomHeroId.isValid(); } EventCondition::EventCondition(EWinLoseType condition): - object(nullptr), - metaType(EMetaclass::INVALID), value(-1), - objectType(-1), - objectSubtype(-1), position(-1, -1, -1), condition(condition) { } -EventCondition::EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position): - object(nullptr), - metaType(EMetaclass::INVALID), +EventCondition::EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position): value(value), objectType(objectType), - objectSubtype(-1), position(position), condition(condition) {} @@ -127,6 +122,12 @@ CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72), setupEvents(); allowedHeroes = VLC->heroh->getDefaultAllowed(); players.resize(PlayerColor::PLAYER_LIMIT_I); + VLC->generaltexth->addSubContainer(*this); +} + +CMapHeader::~CMapHeader() +{ + VLC->generaltexth->removeSubContainer(*this); } ui8 CMapHeader::levels() const @@ -134,4 +135,77 @@ ui8 CMapHeader::levels() const return (twoLevel ? 2 : 1); } +void CMapHeader::registerMapStrings() +{ + VLC->generaltexth->removeSubContainer(*this); + VLC->generaltexth->addSubContainer(*this); + + //get supported languages. Assuming that translation containing most strings is the base language + std::set mapLanguages, mapBaseLanguages; + int maxStrings = 0; + for(auto & translation : translations.Struct()) + { + if(translation.first.empty() || !translation.second.isStruct() || translation.second.Struct().empty()) + continue; + + if(translation.second.Struct().size() > maxStrings) + maxStrings = translation.second.Struct().size(); + mapLanguages.insert(translation.first); + } + + if(maxStrings == 0 || mapLanguages.empty()) + { + logGlobal->info("Map %s doesn't have any supported translation", name.toString()); + return; + } + + //identifying base languages + for(auto & translation : translations.Struct()) + { + if(translation.second.isStruct() && translation.second.Struct().size() == maxStrings) + mapBaseLanguages.insert(translation.first); + } + + std::string baseLanguage, language; + //english is preferrable as base language + if(mapBaseLanguages.count(Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier)) + baseLanguage = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; + else + baseLanguage = *mapBaseLanguages.begin(); + + if(mapBaseLanguages.count(CGeneralTextHandler::getPreferredLanguage())) + { + language = CGeneralTextHandler::getPreferredLanguage(); //preferred language is base language - use it + baseLanguage = language; + } + else + { + if(mapLanguages.count(CGeneralTextHandler::getPreferredLanguage())) + language = CGeneralTextHandler::getPreferredLanguage(); + else + language = baseLanguage; //preferred language is not supported, use base language + } + + assert(!language.empty()); + + JsonNode data = translations[baseLanguage]; + if(language != baseLanguage) + JsonUtils::mergeCopy(data, translations[language]); + + for(auto & s : data.Struct()) + registerString("map", TextIdentifier(s.first), s.second.String(), language); +} + +std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) +{ + return mapRegisterLocalizedString(modContext, mapHeader, UID, localized, VLC->modh->getModLanguage(modContext)); +} + +std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) +{ + mapHeader.registerString(modContext, UID, localized, language); + mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; + return UID.get(); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8814f4933..4ba9d9a0a 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,23 +10,25 @@ #pragma once -#include "../CModVersion.h" +#include "../constants/VariantIdentifier.h" +#include "../modding/CModInfo.h" #include "../LogicalExpression.h" #include "../int3.h" #include "../MetaString.h" +#include "../CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; enum class EMapFormat : uint8_t; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /// The hero name struct consists of the hero id and the hero name. struct DLL_LINKAGE SHeroName { SHeroName(); - int heroId; + HeroTypeID heroId; std::string heroName; template @@ -44,15 +46,15 @@ struct DLL_LINKAGE PlayerInfo PlayerInfo(); /// Gets the default faction id or -1 for a random faction. - si8 defaultCastle() const; + FactionID defaultCastle() const; /// Gets the default hero id or -1 for a random hero. - si8 defaultHero() const; + HeroTypeID defaultHero() const; bool canAnyonePlay() const; bool hasCustomMainHero() const; bool canHumanPlay; bool canComputerPlay; - EAiTactic::EAiTactic aiTactic; /// The default value is EAiTactic::RANDOM. + EAiTactic aiTactic; /// The default value is EAiTactic::RANDOM. std::set allowedFactions; bool isFactionRandom; @@ -62,10 +64,10 @@ struct DLL_LINKAGE PlayerInfo /// Player has a random main hero bool hasRandomHero; /// The default value is -1. - si32 mainCustomHeroPortrait; - std::string mainCustomHeroName; + HeroTypeID mainCustomHeroPortrait; + std::string mainCustomHeroNameTextId; /// ID of custom hero (only if portrait and hero name are set, otherwise unpredicted value), -1 if none (not always -1) - si32 mainCustomHeroId; + HeroTypeID mainCustomHeroId; std::vector heroesNames; /// list of placed heroes on the map bool hasMainTown; /// The default value is false. @@ -84,7 +86,7 @@ struct DLL_LINKAGE PlayerInfo h & allowedFactions; h & isFactionRandom; h & mainCustomHeroPortrait; - h & mainCustomHeroName; + h & mainCustomHeroNameTextId; h & heroesNames; h & hasMainTown; h & generateHeroAtMainTown; @@ -98,7 +100,6 @@ struct DLL_LINKAGE PlayerInfo struct DLL_LINKAGE EventCondition { enum EWinLoseType { - //internal use, deprecated HAVE_ARTIFACT, // type - required artifact HAVE_CREATURES, // type - creatures to collect, value - amount to collect HAVE_RESOURCES, // type - resource ID, value - amount to collect @@ -107,27 +108,21 @@ struct DLL_LINKAGE EventCondition DESTROY, // position - position of object, optional, type - type of object TRANSPORT, // position - where artifact should be transported, type - type of artifact - //map format version pre 1.0 DAYS_PASSED, // value - number of days from start of the game IS_HUMAN, // value - 0 = player is AI, 1 = player is human DAYS_WITHOUT_TOWN, // value - how long player can live without town, 0=instakill STANDARD_WIN, // normal defeat all enemies condition CONST_VALUE, // condition that always evaluates to "value" (0 = false, 1 = true) - - //map format version 1.0+ - HAVE_0, - HAVE_BUILDING_0, - DESTROY_0 }; - EventCondition(EWinLoseType condition = STANDARD_WIN); - EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position = int3(-1, -1, -1)); + using TargetTypeID = VariantIdentifier; - const CGObjectInstance * object; // object that was at specified position or with instance name on start - EMetaclass metaType; + EventCondition(EWinLoseType condition = STANDARD_WIN); + EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position = int3(-1, -1, -1)); + + ObjectInstanceID objectID; // object that was at specified position or with instance name on start si32 value; - si32 objectType; - si32 objectSubtype; + TargetTypeID objectType; std::string objectInstanceName; int3 position; EWinLoseType condition; @@ -135,14 +130,12 @@ struct DLL_LINKAGE EventCondition template void serialize(Handler & h, const int version) { - h & object; + h & objectID; h & value; h & objectType; h & position; h & condition; - h & objectSubtype; h & objectInstanceName; - h & metaType; } }; @@ -199,7 +192,7 @@ struct DLL_LINKAGE TriggeredEvent }; /// The map header holds information about loss/victory condition,map format, version, players, height, width,... -class DLL_LINKAGE CMapHeader +class DLL_LINKAGE CMapHeader: public TextLocalizationContainer { void setupEvents(); public: @@ -213,7 +206,7 @@ public: static const int MAP_SIZE_GIANT = 252; CMapHeader(); - virtual ~CMapHeader() = default; + virtual ~CMapHeader(); ui8 levels() const; @@ -223,8 +216,8 @@ public: si32 height; /// The default value is 72. si32 width; /// The default value is 72. bool twoLevel; /// The default value is true. - std::string name; - std::string description; + MetaString name; + MetaString description; ui8 difficulty; /// The default value is 1 representing a normal map difficulty. /// 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. @@ -237,20 +230,25 @@ public: std::vector players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT. ui8 howManyTeams; - std::vector allowedHeroes; - std::vector reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign + std::set allowedHeroes; + std::set reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign bool areAnyPlayers; /// Unused. True if there are any playable players on the map. /// "main quests" of the map that describe victory and loss conditions std::vector triggeredEvents; + + /// translations for map to be transferred over network + JsonNode translations; + + void registerMapStrings(); template void serialize(Handler & h, const int Version) { + h & static_cast(*this); h & version; - if(Version >= 821) - h & mods; + h & mods; h & name; h & description; h & width; @@ -268,7 +266,14 @@ public: h & victoryIconIndex; h & defeatMessage; h & defeatIconIndex; + h & translations; + if(!h.saving) + registerMapStrings(); } }; +/// wrapper functions to register string into the map and stores its translation +std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized); +std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language); + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index d2acd4e8e..f729cdbcf 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -1,195 +1,208 @@ -/* - * CMapInfo.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 "CMapInfo.h" - -#include "../filesystem/ResourceID.h" -#include "../StartInfo.h" -#include "../GameConstants.h" -#include "CMapService.h" -#include "CMapHeader.h" -#include "MapFormat.h" - -#include "../campaign/CampaignHandler.h" -#include "../filesystem/Filesystem.h" -#include "../serializer/CMemorySerializer.h" -#include "../CGeneralTextHandler.h" -#include "../rmg/CMapGenOptions.h" -#include "../CCreatureHandler.h" -#include "../GameSettings.h" -#include "../CHeroHandler.h" -#include "../CModHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CMapInfo::CMapInfo() - : scenarioOptionsOfSave(nullptr), amountOfPlayersOnMap(0), amountOfHumanControllablePlayers(0), amountOfHumanPlayersInSave(0), isRandomMap(false) -{ - -} - -CMapInfo::~CMapInfo() -{ - vstd::clear_pointer(scenarioOptionsOfSave); -} - -void CMapInfo::mapInit(const std::string & fname) -{ - fileURI = fname; - CMapService mapService; - mapHeader = mapService.loadMapHeader(ResourceID(fname, EResType::MAP)); - countPlayers(); -} - -void CMapInfo::saveInit(const ResourceID & file) -{ - CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); - lf.checkMagicBytes(SAVEGAME_MAGIC); - - mapHeader = std::make_unique(); - lf >> *(mapHeader) >> scenarioOptionsOfSave; - fileURI = file.getName(); - countPlayers(); - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - date = std::asctime(std::localtime(&time)); - // We absolutely not need this data for lobby and server will read it from save - // FIXME: actually we don't want them in CMapHeader! - mapHeader->triggeredEvents.clear(); -} - -void CMapInfo::campaignInit() -{ - campaign = CampaignHandler::getHeader(fileURI); -} - -void CMapInfo::countPlayers() -{ - for(int i=0; iplayers[i].canHumanPlay) - { - amountOfPlayersOnMap++; - amountOfHumanControllablePlayers++; - } - else if(mapHeader->players[i].canComputerPlay) - { - amountOfPlayersOnMap++; - } - } - - if(scenarioOptionsOfSave) - for(const auto & playerInfo : scenarioOptionsOfSave->playerInfos) - if(playerInfo.second.isControlledByHuman()) - amountOfHumanPlayersInSave++; -} - -std::string CMapInfo::getName() const -{ - if(campaign && !campaign->getName().empty()) - return campaign->getName(); - else if(mapHeader && mapHeader->name.length()) - return mapHeader->name; - else - return VLC->generaltexth->allTexts[508]; -} - -std::string CMapInfo::getNameForList() const -{ - if(scenarioOptionsOfSave) - { - // TODO: this could be handled differently - std::vector path; - boost::split(path, fileURI, boost::is_any_of("\\/")); - return path[path.size()-1]; - } - else - { - return getName(); - } -} - -std::string CMapInfo::getDescription() const -{ - if(campaign) - return campaign->getDescription(); - else - return mapHeader->description; -} - -int CMapInfo::getMapSizeIconId() const -{ - if(!mapHeader) - return 4; - - switch(mapHeader->width) - { - case CMapHeader::MAP_SIZE_SMALL: - return 0; - case CMapHeader::MAP_SIZE_MIDDLE: - return 1; - case CMapHeader::MAP_SIZE_LARGE: - return 2; - case CMapHeader::MAP_SIZE_XLARGE: - return 3; - case CMapHeader::MAP_SIZE_HUGE: - return 4; - case CMapHeader::MAP_SIZE_XHUGE: - return 5; - case CMapHeader::MAP_SIZE_GIANT: - return 6; - default: - return 4; - } -} - -int CMapInfo::getMapSizeFormatIconId() const -{ - switch(mapHeader->version) - { - case EMapFormat::ROE: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); - case EMapFormat::AB: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); - case EMapFormat::SOD: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); - case EMapFormat::WOG: - return VLC->settings()->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(); - case EMapFormat::VCMI: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); - } - return 0; -} - -std::string CMapInfo::getMapSizeName() const -{ - switch(mapHeader->width) - { - case CMapHeader::MAP_SIZE_SMALL: - return "S"; - case CMapHeader::MAP_SIZE_MIDDLE: - return "M"; - case CMapHeader::MAP_SIZE_LARGE: - return "L"; - case CMapHeader::MAP_SIZE_XLARGE: - return "XL"; - case CMapHeader::MAP_SIZE_HUGE: - return "H"; - case CMapHeader::MAP_SIZE_XHUGE: - return "XH"; - case CMapHeader::MAP_SIZE_GIANT: - return "G"; - default: - return "C"; - } -} - -VCMI_LIB_NAMESPACE_END +/* + * CMapInfo.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 "CMapInfo.h" + +#include + +#include "../filesystem/ResourcePath.h" +#include "../StartInfo.h" +#include "../GameConstants.h" +#include "CMapService.h" +#include "CMapHeader.h" +#include "MapFormat.h" + +#include "../campaign/CampaignHandler.h" +#include "../filesystem/Filesystem.h" +#include "../serializer/CLoadFile.h" +#include "../CGeneralTextHandler.h" +#include "../rmg/CMapGenOptions.h" +#include "../CCreatureHandler.h" +#include "../GameSettings.h" +#include "../CHeroHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CMapInfo::CMapInfo() + : scenarioOptionsOfSave(nullptr), amountOfPlayersOnMap(0), amountOfHumanControllablePlayers(0), amountOfHumanPlayersInSave(0), isRandomMap(false) +{ + +} + +CMapInfo::~CMapInfo() +{ + vstd::clear_pointer(scenarioOptionsOfSave); +} + +void CMapInfo::mapInit(const std::string & fname) +{ + fileURI = fname; + CMapService mapService; + ResourcePath resource = ResourcePath(fname, EResType::MAP); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); + mapHeader = mapService.loadMapHeader(resource); + countPlayers(); +} + +void CMapInfo::saveInit(const ResourcePath & file) +{ + CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + mapHeader = std::make_unique(); + lf >> *(mapHeader) >> scenarioOptionsOfSave; + fileURI = file.getName(); + originalFileURI = file.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); + countPlayers(); + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); + date = vstd::getFormattedDateTime(time); + + // We absolutely not need this data for lobby and server will read it from save + // FIXME: actually we don't want them in CMapHeader! + mapHeader->triggeredEvents.clear(); +} + +void CMapInfo::campaignInit() +{ + ResourcePath resource = ResourcePath(fileURI, EResType::CAMPAIGN); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); + campaign = CampaignHandler::getHeader(fileURI); +} + +void CMapInfo::countPlayers() +{ + for(int i=0; iplayers[i].canHumanPlay) + { + amountOfPlayersOnMap++; + amountOfHumanControllablePlayers++; + } + else if(mapHeader->players[i].canComputerPlay) + { + amountOfPlayersOnMap++; + } + } + + if(scenarioOptionsOfSave) + for(const auto & playerInfo : scenarioOptionsOfSave->playerInfos) + if(playerInfo.second.isControlledByHuman()) + amountOfHumanPlayersInSave++; +} + +std::string CMapInfo::getNameTranslated() const +{ + if(campaign && !campaign->getNameTranslated().empty()) + return campaign->getNameTranslated(); + else if(mapHeader && !mapHeader->name.empty()) + { + mapHeader->registerMapStrings(); + return mapHeader->name.toString(); + } + else + return VLC->generaltexth->allTexts[508]; +} + +std::string CMapInfo::getNameForList() const +{ + if(scenarioOptionsOfSave) + { + // TODO: this could be handled differently + std::vector path; + boost::split(path, originalFileURI, boost::is_any_of("\\/")); + return path[path.size()-1]; + } + else + { + return getNameTranslated(); + } +} + +std::string CMapInfo::getDescriptionTranslated() const +{ + if(campaign) + return campaign->getDescriptionTranslated(); + else + return mapHeader->description.toString(); +} + +int CMapInfo::getMapSizeIconId() const +{ + if(!mapHeader) + return 4; + + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return 0; + case CMapHeader::MAP_SIZE_MIDDLE: + return 1; + case CMapHeader::MAP_SIZE_LARGE: + return 2; + case CMapHeader::MAP_SIZE_XLARGE: + return 3; + case CMapHeader::MAP_SIZE_HUGE: + return 4; + case CMapHeader::MAP_SIZE_XHUGE: + return 5; + case CMapHeader::MAP_SIZE_GIANT: + return 6; + default: + return 4; + } +} + +int CMapInfo::getMapSizeFormatIconId() const +{ + switch(mapHeader->version) + { + case EMapFormat::ROE: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); + case EMapFormat::AB: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); + case EMapFormat::SOD: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); + case EMapFormat::WOG: + return VLC->settings()->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(); + case EMapFormat::VCMI: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); + } + return 0; +} + +std::string CMapInfo::getMapSizeName() const +{ + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return "S"; + case CMapHeader::MAP_SIZE_MIDDLE: + return "M"; + case CMapHeader::MAP_SIZE_LARGE: + return "L"; + case CMapHeader::MAP_SIZE_XLARGE: + return "XL"; + case CMapHeader::MAP_SIZE_HUGE: + return "H"; + case CMapHeader::MAP_SIZE_XHUGE: + return "XH"; + case CMapHeader::MAP_SIZE_GIANT: + return "G"; + default: + return "C"; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 5724988bd..512cff481 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -1,72 +1,74 @@ -/* - * CMapInfo.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 StartInfo; - -class CMapHeader; -class Campaign; -class ResourceID; - -/** - * A class which stores the count of human players and all players, the filename, - * scenario options, the map header information,... - */ -class DLL_LINKAGE CMapInfo -{ -public: - std::unique_ptr mapHeader; //may be nullptr if campaign - std::unique_ptr campaign; //may be nullptr if scenario - StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) - std::string fileURI; - std::string date; - int amountOfPlayersOnMap; - int amountOfHumanControllablePlayers; - int amountOfHumanPlayersInSave; - bool isRandomMap; - - CMapInfo(); - virtual ~CMapInfo(); - - CMapInfo(CMapInfo &&other) = delete; - CMapInfo(const CMapInfo &other) = delete; - - CMapInfo &operator=(CMapInfo &&other) = delete; - CMapInfo &operator=(const CMapInfo &other) = delete; - - void mapInit(const std::string & fname); - void saveInit(const ResourceID & file); - void campaignInit(); - void countPlayers(); - // TODO: Those must be on client-side - std::string getName() const; - std::string getNameForList() const; - std::string getDescription() const; - int getMapSizeIconId() const; - int getMapSizeFormatIconId() const; - std::string getMapSizeName() const; - - template void serialize(Handler &h, const int Version) - { - h & mapHeader; - h & campaign; - h & scenarioOptionsOfSave; - h & fileURI; - h & date; - h & amountOfPlayersOnMap; - h & amountOfHumanControllablePlayers; - h & amountOfHumanPlayersInSave; - h & isRandomMap; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMapInfo.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 StartInfo; + +class CMapHeader; +class Campaign; +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 +{ +public: + std::unique_ptr mapHeader; //may be nullptr if campaign + std::unique_ptr campaign; //may be nullptr if scenario + StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) + std::string fileURI; + std::string originalFileURI; + std::string fullFileURI; + std::string date; + int amountOfPlayersOnMap; + int amountOfHumanControllablePlayers; + int amountOfHumanPlayersInSave; + bool isRandomMap; + + CMapInfo(); + virtual ~CMapInfo(); + + CMapInfo(CMapInfo &&other) = delete; + CMapInfo(const CMapInfo &other) = delete; + + CMapInfo &operator=(CMapInfo &&other) = delete; + CMapInfo &operator=(const CMapInfo &other) = delete; + + void mapInit(const std::string & fname); + void saveInit(const ResourcePath & file); + void campaignInit(); + void countPlayers(); + + std::string getNameTranslated() const; + std::string getNameForList() const; + std::string getDescriptionTranslated() const; + int getMapSizeIconId() const; + int getMapSizeFormatIconId() const; + std::string getMapSizeName() const; + + template void serialize(Handler &h, const int Version) + { + h & mapHeader; + h & campaign; + h & scenarioOptionsOfSave; + h & fileURI; + h & date; + h & amountOfPlayersOnMap; + h & amountOfHumanControllablePlayers; + h & amountOfHumanPlayersInSave; + h & isRandomMap; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index b3a99597e..0d119bd5a 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -15,7 +15,9 @@ #include "../filesystem/CCompressedStream.h" #include "../filesystem/CMemoryStream.h" #include "../filesystem/CMemoryBuffer.h" -#include "../CModHandler.h" +#include "../modding/CModHandler.h" +#include "../modding/ModScope.h" +#include "../modding/CModInfo.h" #include "../Languages.h" #include "../VCMI_Lib.h" @@ -28,7 +30,7 @@ VCMI_LIB_NAMESPACE_BEGIN -std::unique_ptr CMapService::loadMap(const ResourceID & name) const +std::unique_ptr CMapService::loadMap(const ResourcePath & name) const { std::string modName = VLC->modh->findResourceOrigin(name); std::string language = VLC->modh->getModLanguage(modName); @@ -38,7 +40,7 @@ std::unique_ptr CMapService::loadMap(const ResourceID & name) const return getMapLoader(stream, name.getName(), modName, encoding)->loadMap(); } -std::unique_ptr CMapService::loadMapHeader(const ResourceID & name) const +std::unique_ptr CMapService::loadMapHeader(const ResourcePath & name) const { std::string modName = VLC->modh->findResourceOrigin(name); std::string language = VLC->modh->getModLanguage(modName); @@ -80,7 +82,7 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: } { boost::filesystem::remove(fullPath); - boost::filesystem::ofstream tmp(fullPath, boost::filesystem::ofstream::binary); + std::ofstream tmp(fullPath.c_str(), std::ofstream::binary); tmp.write(reinterpret_cast(serializeBuffer.getBuffer().data()), serializeBuffer.getSize()); tmp.flush(); @@ -90,23 +92,32 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) { - ModCompatibilityInfo modCompatibilityInfo; const auto & activeMods = VLC->modh->getActiveMods(); + + ModCompatibilityInfo missingMods, missingModsFiltered; for(const auto & mapMod : map.mods) { if(vstd::contains(activeMods, mapMod.first)) { const auto & modInfo = VLC->modh->getModInfo(mapMod.first); - if(modInfo.version.compatible(mapMod.second)) + if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) continue; } - - modCompatibilityInfo[mapMod.first] = mapMod.second; - } - return modCompatibilityInfo; + missingMods[mapMod.first] = mapMod.second; + } + + //filter child mods + for(const auto & mapMod : missingMods) + { + if(!mapMod.second.parent.empty() && missingMods.count(mapMod.second.parent)) + continue; + missingModsFiltered.insert(mapMod); + } + + return missingModsFiltered; } -std::unique_ptr CMapService::getStreamFromFS(const ResourceID & name) +std::unique_ptr CMapService::getStreamFromFS(const ResourcePath & name) { return CResourceHandler::get()->load(name); } @@ -152,13 +163,13 @@ std::unique_ptr CMapService::getMapLoader(std::unique_ptr; +using ModCompatibilityInfo = std::map; /** * The map service provides loading of VCMI/H3 map files. It can @@ -39,7 +40,7 @@ public: * @param name the name of the map * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const ResourceID & name) const = 0; + virtual std::unique_ptr loadMap(const ResourcePath & name) const = 0; /** * Loads the VCMI/H3 map header specified by the name. @@ -47,7 +48,7 @@ public: * @param name the name of the map * @return a unique ptr to the loaded map header class */ - virtual std::unique_ptr loadMapHeader(const ResourceID & name) const = 0; + virtual std::unique_ptr loadMapHeader(const ResourcePath & name) const = 0; /** * Loads the VCMI/H3 map file from a buffer. This method is temporarily @@ -81,8 +82,8 @@ public: CMapService() = default; virtual ~CMapService() = default; - std::unique_ptr loadMap(const ResourceID & name) const override; - std::unique_ptr loadMapHeader(const ResourceID & name) const override; + std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMapHeader(const ResourcePath & name) const override; std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; std::unique_ptr loadMapHeader(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const override; @@ -101,7 +102,7 @@ private: * @param name the name of the map * @return a unique ptr to the input stream class */ - static std::unique_ptr getStreamFromFS(const ResourceID & name); + static std::unique_ptr getStreamFromFS(const ResourcePath & name); /** * Gets a map input stream from a buffer. diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index e6ff84f86..eef87e147 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -174,7 +174,7 @@ void TerrainViewPattern::WeightedRule::setNative() CTerrainViewPatternConfig::CTerrainViewPatternConfig() { - const JsonNode config(ResourceID("config/terrainViewPatterns.json")); + const JsonNode config(JsonPath::builtin("config/terrainViewPatterns.json")); static const std::string patternTypes[] = { "terrainView", "terrainType" }; for (int i = 0; i < std::size(patternTypes); ++i) { diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index b5d3a4b7b..99bda2b09 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -1,154 +1,158 @@ -/* - * MapFeaturesH3M.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 "MapFeaturesH3M.h" - -#include "MapFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hotaVersion) -{ - switch(format) - { - case EMapFormat::ROE: - return getFeaturesROE(); - case EMapFormat::AB: - return getFeaturesAB(); - case EMapFormat::SOD: - return getFeaturesSOD(); - case EMapFormat::WOG: - return getFeaturesWOG(); - case EMapFormat::HOTA: - return getFeaturesHOTA(hotaVersion); - default: - throw std::runtime_error("Invalid map format!"); - } -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() -{ - MapFormatFeaturesH3M result; - result.levelROE = true; - - result.factionsBytes = 1; - result.heroesBytes = 16; - result.artifactsBytes = 16; - result.skillsBytes = 4; - result.resourcesBytes = 4; - result.spellsBytes = 9; - result.buildingsBytes = 6; - - result.factionsCount = 8; - result.heroesCount = 128; - result.heroesPortraitsCount = 128; - result.artifactsCount = 127; - result.resourcesCount = 7; - result.creaturesCount = 118; - result.spellsCount = 70; - result.skillsCount = 28; - result.terrainsCount = 10; - result.artifactSlotsCount = 18; - result.buildingsCount = 41; - - result.heroIdentifierInvalid = 0xff; - result.artifactIdentifierInvalid = 0xff; - result.creatureIdentifierInvalid = 0xff; - result.spellIdentifierInvalid = 0xff; - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesAB() -{ - MapFormatFeaturesH3M result = getFeaturesROE(); - result.levelAB = true; - - result.factionsBytes = 2; // + Conflux - result.factionsCount = 9; - - result.creaturesCount = 145; // + Conflux and new neutrals - - result.heroesCount = 156; // + Conflux and campaign heroes - result.heroesPortraitsCount = 163; - result.heroesBytes = 20; - - result.artifactsCount = 129; // + Armaggedon Blade and Vial of Dragon Blood - result.artifactsBytes = 17; - - result.artifactIdentifierInvalid = 0xffff; // Now uses 2 bytes / object - result.creatureIdentifierInvalid = 0xffff; // Now uses 2 bytes / object - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() -{ - MapFormatFeaturesH3M result = getFeaturesAB(); - result.levelSOD = true; - - result.artifactsCount = 141; // + Combined artifacts - result.artifactsBytes = 18; - - result.artifactSlotsCount = 19; // + MISC_5 slot - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() -{ - MapFormatFeaturesH3M result = getFeaturesSOD(); - result.levelWOG = true; - - return result; -} - -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 - if(hotaVersion > 3) - throw std::runtime_error("Invalid map format!"); - - MapFormatFeaturesH3M result = getFeaturesSOD(); - result.levelHOTA0 = true; - result.levelHOTA1 = hotaVersion > 0; - //result.levelHOTA2 = hotaVersion > 1; // HOTA2 seems to be identical to HOTA1 so far - result.levelHOTA3 = hotaVersion > 2; - - result.artifactsBytes = 21; - result.heroesBytes = 23; - - result.terrainsCount = 12; // +Highlands +Wasteland - result.skillsCount = 29; // + Interference - result.factionsCount = 10; // + Cove - result.creaturesCount = 171; // + Cove + neutrals - - if(hotaVersion < 3) - { - result.artifactsCount = 163; // + HotA artifacts - result.heroesCount = 178; // + Cove - result.heroesPortraitsCount = 186; // + Cove - } - if(hotaVersion == 3) - { - result.artifactsCount = 165; // + HotA artifacts - result.heroesCount = 179; // + Cove + Giselle - result.heroesPortraitsCount = 188; // + Cove + Giselle - } - - assert((result.heroesCount + 7) / 8 == result.heroesBytes); - assert((result.artifactsCount + 7) / 8 == result.artifactsBytes); - - return result; -} - -VCMI_LIB_NAMESPACE_END +/* + * MapFeaturesH3M.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 "MapFeaturesH3M.h" + +#include "MapFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hotaVersion) +{ + switch(format) + { + case EMapFormat::ROE: + return getFeaturesROE(); + case EMapFormat::AB: + return getFeaturesAB(); + case EMapFormat::SOD: + return getFeaturesSOD(); + case EMapFormat::WOG: + return getFeaturesWOG(); + case EMapFormat::HOTA: + return getFeaturesHOTA(hotaVersion); + default: + throw std::runtime_error("Invalid map format!"); + } +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() +{ + MapFormatFeaturesH3M result; + result.levelROE = true; + + result.factionsBytes = 1; + result.heroesBytes = 16; + result.artifactsBytes = 16; + result.skillsBytes = 4; + result.resourcesBytes = 4; + result.spellsBytes = 9; + result.buildingsBytes = 6; + + result.factionsCount = 8; + result.heroesCount = 128; + result.heroesPortraitsCount = 130; // +General Kendal, +Catherine (portrait-only in RoE) + result.artifactsCount = 127; + result.resourcesCount = 7; + result.creaturesCount = 118; + result.spellsCount = 70; + result.skillsCount = 28; + result.terrainsCount = 10; + result.artifactSlotsCount = 18; + result.buildingsCount = 41; + result.roadsCount = 3; + result.riversCount = 4; + + result.heroIdentifierInvalid = 0xff; + result.artifactIdentifierInvalid = 0xff; + result.creatureIdentifierInvalid = 0xff; + result.spellIdentifierInvalid = 0xff; + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesAB() +{ + MapFormatFeaturesH3M result = getFeaturesROE(); + result.levelAB = true; + + result.factionsBytes = 2; // + Conflux + result.factionsCount = 9; + + result.creaturesCount = 145; // + Conflux and new neutrals + + result.heroesCount = 156; // + Conflux and campaign heroes + result.heroesPortraitsCount = 159; // +Kendal, +young Cristian, +Ordwald + result.heroesBytes = 20; + + result.artifactsCount = 129; // + Armaggedon Blade and Vial of Dragon Blood + result.artifactsBytes = 17; + + result.artifactIdentifierInvalid = 0xffff; // Now uses 2 bytes / object + result.creatureIdentifierInvalid = 0xffff; // Now uses 2 bytes / object + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() +{ + MapFormatFeaturesH3M result = getFeaturesAB(); + result.levelSOD = true; + + result.artifactsCount = 144; // + Combined artifacts + 3 unfinished artifacts (required for some maps) + result.artifactsBytes = 18; + + result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog + + result.artifactSlotsCount = 19; // + MISC_5 slot + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() +{ + MapFormatFeaturesH3M result = getFeaturesSOD(); + result.levelWOG = true; + + return result; +} + +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 + if(hotaVersion > 3) + throw std::runtime_error("Invalid map format!"); + + MapFormatFeaturesH3M result = getFeaturesSOD(); + result.levelHOTA0 = true; + result.levelHOTA1 = hotaVersion > 0; + //result.levelHOTA2 = hotaVersion > 1; // HOTA2 seems to be identical to HOTA1 so far + result.levelHOTA3 = hotaVersion > 2; + + result.artifactsBytes = 21; + result.heroesBytes = 23; + + result.terrainsCount = 12; // +Highlands +Wasteland + result.skillsCount = 29; // + Interference + result.factionsCount = 10; // + Cove + result.creaturesCount = 171; // + Cove + neutrals + + if(hotaVersion < 3) + { + result.artifactsCount = 163; // + HotA artifacts + result.heroesCount = 178; // + Cove + result.heroesPortraitsCount = 186; // + Cove + } + if(hotaVersion == 3) + { + result.artifactsCount = 165; // + HotA artifacts + result.heroesCount = 179; // + Cove + Giselle + result.heroesPortraitsCount = 188; // + Cove + Giselle + } + + assert((result.heroesCount + 7) / 8 == result.heroesBytes); + assert((result.artifactsCount + 7) / 8 == result.artifactsBytes); + + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index 4757d4223..d9fe3fc68 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/lib/mapping/MapFeaturesH3M.h @@ -1,71 +1,73 @@ -/* - * MapFeaturesH3M.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 EMapFormat : uint8_t; - -struct MapFormatFeaturesH3M -{ -public: - static MapFormatFeaturesH3M find(EMapFormat format, uint32_t hotaVersion); - static MapFormatFeaturesH3M getFeaturesROE(); - static MapFormatFeaturesH3M getFeaturesAB(); - static MapFormatFeaturesH3M getFeaturesSOD(); - static MapFormatFeaturesH3M getFeaturesWOG(); - static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); - - MapFormatFeaturesH3M() = default; - - // number of bytes in bitmask of appropriate type - - int factionsBytes; - int heroesBytes; - int artifactsBytes; - int resourcesBytes; - int skillsBytes; - int spellsBytes; - int buildingsBytes; - - // total number of elements of appropriate type - - int factionsCount; - int heroesCount; - int heroesPortraitsCount; - int artifactsCount; - int resourcesCount; - int creaturesCount; - int spellsCount; - int skillsCount; - int terrainsCount; - int artifactSlotsCount; - int buildingsCount; - - // identifier that should be treated as "invalid", usually - '-1' - - int heroIdentifierInvalid; - int artifactIdentifierInvalid; - int creatureIdentifierInvalid; - int spellIdentifierInvalid; - - // features from which map format are available - - bool levelROE = false; - bool levelAB = false; - bool levelSOD = false; - bool levelWOG = false; - bool levelHOTA0 = false; - bool levelHOTA1 = false; - bool levelHOTA3 = false; -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFeaturesH3M.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 EMapFormat : uint8_t; + +struct MapFormatFeaturesH3M +{ +public: + static MapFormatFeaturesH3M find(EMapFormat format, uint32_t hotaVersion); + static MapFormatFeaturesH3M getFeaturesROE(); + static MapFormatFeaturesH3M getFeaturesAB(); + static MapFormatFeaturesH3M getFeaturesSOD(); + static MapFormatFeaturesH3M getFeaturesWOG(); + static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); + + MapFormatFeaturesH3M() = default; + + // number of bytes in bitmask of appropriate type + + int factionsBytes; + int heroesBytes; + int artifactsBytes; + int resourcesBytes; + int skillsBytes; + int spellsBytes; + int buildingsBytes; + + // total number of elements of appropriate type + + int factionsCount; + int heroesCount; + int heroesPortraitsCount; + int artifactsCount; + int resourcesCount; + int creaturesCount; + int spellsCount; + int skillsCount; + int terrainsCount; + int roadsCount; + int riversCount; + int artifactSlotsCount; + int buildingsCount; + + // identifier that should be treated as "invalid", usually - '-1' + + int heroIdentifierInvalid; + int artifactIdentifierInvalid; + int creatureIdentifierInvalid; + int spellIdentifierInvalid; + + // features from which map format are available + + bool levelROE = false; + bool levelAB = false; + bool levelSOD = false; + bool levelWOG = false; + bool levelHOTA0 = false; + bool levelHOTA1 = false; + bool levelHOTA3 = false; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 81381bab7..2d99c745f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1,2256 +1,2387 @@ -/* - * MapFormatH3M.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 "MapFormatH3M.h" - -#include "CMap.h" -#include "MapReaderH3M.h" -#include "MapFormat.h" - -#include "../ArtifactUtils.h" -#include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" -#include "../CSkillHandler.h" -#include "../CStopWatch.h" -#include "../CModHandler.h" -#include "../GameSettings.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../TextOperations.h" -#include "../VCMI_Lib.h" -#include "../filesystem/CBinaryReader.h" -#include "../filesystem/Filesystem.h" -#include "../mapObjectConstructors/AObjectTypeHandler.h" -#include "../mapObjectConstructors/CObjectClassesHandler.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../spells/CSpellHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -static std::string convertMapName(std::string input) -{ - boost::algorithm::to_lower(input); - boost::algorithm::trim(input); - - size_t slashPos = input.find_last_of('/'); - - if(slashPos != std::string::npos) - return input.substr(slashPos + 1); - - return input; -} - -CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream) - : map(nullptr) - , reader(new MapReaderH3M(stream)) - , inputStream(stream) - , mapName(convertMapName(mapName)) - , modName(modName) - , fileEncoding(encodingName) -{ -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CMapLoaderH3M::~CMapLoaderH3M() = default; - -std::unique_ptr CMapLoaderH3M::loadMap() -{ - // Init map object by parsing the input buffer - map = new CMap(); - mapHeader = std::unique_ptr(dynamic_cast(map)); - init(); - - return std::unique_ptr(dynamic_cast(mapHeader.release())); -} - -std::unique_ptr CMapLoaderH3M::loadMapHeader() -{ - // Read header - mapHeader = std::make_unique(); - readHeader(); - - return std::move(mapHeader); -} - -void CMapLoaderH3M::init() -{ - //TODO: get rid of double input process - si64 temp_size = inputStream->getSize(); - inputStream->seek(0); - - auto * temp_buffer = new ui8[temp_size]; - inputStream->read(temp_buffer, temp_size); - - // Compute checksum - boost::crc_32_type result; - result.process_bytes(temp_buffer, temp_size); - map->checksum = result.checksum(); - - delete[] temp_buffer; - inputStream->seek(0); - - readHeader(); - map->allHeroes.resize(map->allowedHeroes.size()); - - readDisposedHeroes(); - readMapOptions(); - readAllowedArtifacts(); - readAllowedSpellsAbilities(); - readRumors(); - readPredefinedHeroes(); - readTerrain(); - readObjectTemplates(); - readObjects(); - readEvents(); - - map->calculateGuardingGreaturePositions(); - afterRead(); - //map->banWaterContent(); //Not sure if force this for custom scenarios -} - -void CMapLoaderH3M::readHeader() -{ - // Map version - mapHeader->version = static_cast(reader->readUInt32()); - - if(mapHeader->version == EMapFormat::HOTA) - { - uint32_t hotaVersion = reader->readUInt32(); - features = MapFormatFeaturesH3M::find(mapHeader->version, hotaVersion); - reader->setFormatLevel(features); - - if(hotaVersion > 0) - { - bool isMirrorMap = reader->readBool(); - bool isArenaMap = reader->readBool(); - - //TODO: HotA - if (isMirrorMap) - logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName); - - if (isArenaMap) - logGlobal->warn("Map '%s': Arena maps are not supported!", mapName); - } - - if(hotaVersion > 1) - { - [[maybe_unused]] uint8_t unknown = reader->readUInt32(); - assert(unknown == 12); - } - } - else - { - features = MapFormatFeaturesH3M::find(mapHeader->version, 0); - reader->setFormatLevel(features); - } - MapIdentifiersH3M identifierMapper; - - if (features.levelROE) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); - if (features.levelAB) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); - if (features.levelSOD) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); - if (features.levelWOG) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); - if (features.levelHOTA0) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); - - 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 = readLocalizedString("header.name"); - mapHeader->description = readLocalizedString("header.description"); - mapHeader->difficulty = reader->readInt8(); - - if(features.levelAB) - mapHeader->levelLimit = reader->readUInt8(); - else - mapHeader->levelLimit = 0; - - readPlayerInfo(); - readVictoryLossConditions(); - readTeamInfo(); - readAllowedHeroes(); -} - -void CMapLoaderH3M::readPlayerInfo() -{ - for(int i = 0; i < mapHeader->players.size(); ++i) - { - auto & playerInfo = mapHeader->players[i]; - - playerInfo.canHumanPlay = reader->readBool(); - playerInfo.canComputerPlay = reader->readBool(); - - // If nobody can play with this player - skip loading of these properties - if((!(playerInfo.canHumanPlay || playerInfo.canComputerPlay))) - { - if(features.levelROE) - reader->skipUnused(6); - if(features.levelAB) - reader->skipUnused(6); - if(features.levelSOD) - reader->skipUnused(1); - continue; - } - - playerInfo.aiTactic = static_cast(reader->readUInt8()); - - if(features.levelSOD) - reader->skipUnused(1); //TODO: check meaning? - - std::set allowedFactions; - - reader->readBitmaskFactions(allowedFactions, false); - - const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool(); - const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount; - - if(!allFactionsAllowed) - playerInfo.allowedFactions = allowedFactions; - - playerInfo.hasMainTown = reader->readBool(); - if(playerInfo.hasMainTown) - { - if(features.levelAB) - { - playerInfo.generateHeroAtMainTown = reader->readBool(); - reader->skipUnused(1); //TODO: check meaning? - } - else - { - playerInfo.generateHeroAtMainTown = true; - } - - playerInfo.posOfMainTown = reader->readInt3(); - } - - playerInfo.hasRandomHero = reader->readBool(); - playerInfo.mainCustomHeroId = reader->readHero().getNum(); - - if(playerInfo.mainCustomHeroId != -1) - { - playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); - playerInfo.mainCustomHeroName = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); - } - - if(features.levelAB) - { - reader->skipUnused(1); //TODO: check meaning? - uint32_t heroCount = reader->readUInt32(); - for(int pp = 0; pp < heroCount; ++pp) - { - SHeroName vv; - vv.heroId = reader->readUInt8(); - vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); - - playerInfo.heroesNames.push_back(vv); - } - } - } -} - -enum class EVictoryConditionType : uint8_t -{ - ARTIFACT = 0, - GATHERTROOP = 1, - GATHERRESOURCE = 2, - BUILDCITY = 3, - BUILDGRAIL = 4, - BEATHERO = 5, - CAPTURECITY = 6, - BEATMONSTER = 7, - TAKEDWELLINGS = 8, - TAKEMINES = 9, - TRANSPORTITEM = 10, - HOTA_ELIMINATE_ALL_MONSTERS = 11, - HOTA_SURVIVE_FOR_DAYS = 12, - WINSTANDARD = 255 -}; - -enum class ELossConditionType : uint8_t -{ - LOSSCASTLE = 0, - LOSSHERO = 1, - TIMEEXPIRES = 2, - LOSSSTANDARD = 255 -}; - -void CMapLoaderH3M::readVictoryLossConditions() -{ - mapHeader->triggeredEvents.clear(); - mapHeader->victoryMessage.clear(); - mapHeader->defeatMessage.clear(); - - auto vicCondition = static_cast(reader->readUInt8()); - - EventCondition victoryCondition(EventCondition::STANDARD_WIN); - EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); - defeatCondition.value = 7; - - TriggeredEvent standardVictory; - standardVictory.effect.type = EventEffect::VICTORY; - standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - standardVictory.identifier = "standardVictory"; - standardVictory.description.clear(); // TODO: display in quest window - standardVictory.onFulfill.appendTextID("core.genrltxt.659"); - standardVictory.trigger = EventExpression(victoryCondition); - - TriggeredEvent standardDefeat; - standardDefeat.effect.type = EventEffect::DEFEAT; - standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); - standardDefeat.identifier = "standardDefeat"; - standardDefeat.description.clear(); // TODO: display in quest window - standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); - standardDefeat.trigger = EventExpression(defeatCondition); - - // Specific victory conditions - if(vicCondition == EVictoryConditionType::WINSTANDARD) - { - // create normal condition - mapHeader->triggeredEvents.push_back(standardVictory); - mapHeader->victoryIconIndex = 11; - mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); - } - else - { - TriggeredEvent specialVictory; - specialVictory.effect.type = EventEffect::VICTORY; - specialVictory.identifier = "specialVictory"; - specialVictory.description.clear(); // TODO: display in quest window - - mapHeader->victoryIconIndex = static_cast(vicCondition); - - bool allowNormalVictory = reader->readBool(); - bool appliesToAI = reader->readBool(); - - if(allowNormalVictory) - { - size_t playersOnMap = boost::range::count_if( - mapHeader->players, - [](const PlayerInfo & info) - { - return info.canAnyonePlay(); - } - ); - - if(playersOnMap == 1) - { - logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); - allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! - } - } - - switch(vicCondition) - { - case EVictoryConditionType::ARTIFACT: - { - EventCondition cond(EventCondition::HAVE_ARTIFACT); - cond.objectType = reader->readArtifact(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); - specialVictory.onFulfill.appendTextID("core.genrltxt.280"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.1"); - break; - } - case EVictoryConditionType::GATHERTROOP: - { - EventCondition cond(EventCondition::HAVE_CREATURES); - cond.objectType = reader->readCreature(); - cond.value = reader->readInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); - specialVictory.onFulfill.appendTextID("core.genrltxt.276"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.2"); - break; - } - case EVictoryConditionType::GATHERRESOURCE: - { - EventCondition cond(EventCondition::HAVE_RESOURCES); - cond.objectType = reader->readUInt8(); - cond.value = reader->readInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); - specialVictory.onFulfill.appendTextID("core.genrltxt.278"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.3"); - break; - } - case EVictoryConditionType::BUILDCITY: - { - EventExpression::OperatorAll oper; - EventCondition cond(EventCondition::HAVE_BUILDING); - cond.position = reader->readInt3(); - cond.objectType = BuildingID::TOWN_HALL + reader->readUInt8(); - oper.expressions.emplace_back(cond); - cond.objectType = BuildingID::FORT + reader->readUInt8(); - oper.expressions.emplace_back(cond); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); - specialVictory.onFulfill.appendTextID("core.genrltxt.282"); - specialVictory.trigger = EventExpression(oper); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.4"); - break; - } - case EVictoryConditionType::BUILDGRAIL: - { - EventCondition cond(EventCondition::HAVE_BUILDING); - cond.objectType = BuildingID::GRAIL; - cond.position = reader->readInt3(); - if(cond.position.z > 2) - cond.position = int3(-1, -1, -1); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285"); - specialVictory.onFulfill.appendTextID("core.genrltxt.284"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.5"); - break; - } - case EVictoryConditionType::BEATHERO: - { - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::HERO; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); - specialVictory.onFulfill.appendTextID("core.genrltxt.252"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.6"); - break; - } - case EVictoryConditionType::CAPTURECITY: - { - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); - specialVictory.onFulfill.appendTextID("core.genrltxt.249"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.7"); - break; - } - case EVictoryConditionType::BEATMONSTER: - { - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); - specialVictory.onFulfill.appendTextID("core.genrltxt.286"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.8"); - break; - } - case EVictoryConditionType::TAKEDWELLINGS: - { - EventExpression::OperatorAll oper; - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1)); - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4)); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); - specialVictory.onFulfill.appendTextID("core.genrltxt.288"); - specialVictory.trigger = EventExpression(oper); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.9"); - break; - } - case EVictoryConditionType::TAKEMINES: - { - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::MINE; - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); - specialVictory.onFulfill.appendTextID("core.genrltxt.290"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.10"); - break; - } - case EVictoryConditionType::TRANSPORTITEM: - { - EventCondition cond(EventCondition::TRANSPORT); - cond.objectType = reader->readUInt8(); - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); - specialVictory.onFulfill.appendTextID("core.genrltxt.292"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.11"); - break; - } - case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: - { - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; - - specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); - specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.12"); - mapHeader->victoryIconIndex = 12; - break; - } - case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: - { - EventCondition cond(EventCondition::DAYS_PASSED); - cond.value = reader->readUInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers"); - specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.13"); - mapHeader->victoryIconIndex = 13; - break; - } - default: - assert(0); - } - - // if condition is human-only turn it into following construction: AllOf(human, condition) - if(!appliesToAI) - { - EventExpression::OperatorAll oper; - EventCondition notAI(EventCondition::IS_HUMAN); - notAI.value = 1; - oper.expressions.emplace_back(notAI); - oper.expressions.push_back(specialVictory.trigger.get()); - specialVictory.trigger = EventExpression(oper); - } - - // if normal victory allowed - add one more quest - if(allowNormalVictory) - { - mapHeader->victoryMessage.appendRawString(" / "); - mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); - mapHeader->triggeredEvents.push_back(standardVictory); - } - mapHeader->triggeredEvents.push_back(specialVictory); - } - - // Read loss conditions - auto lossCond = static_cast(reader->readUInt8()); - if(lossCond == ELossConditionType::LOSSSTANDARD) - { - mapHeader->defeatIconIndex = 3; - mapHeader->defeatMessage.appendTextID("core.lcdesc.0"); - } - else - { - TriggeredEvent specialDefeat; - specialDefeat.effect.type = EventEffect::DEFEAT; - specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - specialDefeat.identifier = "specialDefeat"; - specialDefeat.description.clear(); // TODO: display in quest window - - mapHeader->defeatIconIndex = static_cast(lossCond); - - switch(lossCond) - { - case ELossConditionType::LOSSCASTLE: - { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - cond.position = reader->readInt3(); - - noneOf.expressions.emplace_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); - specialDefeat.trigger = EventExpression(noneOf); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.1"); - break; - } - case ELossConditionType::LOSSHERO: - { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; - cond.position = reader->readInt3(); - - noneOf.expressions.emplace_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); - specialDefeat.trigger = EventExpression(noneOf); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.2"); - break; - } - case ELossConditionType::TIMEEXPIRES: - { - EventCondition cond(EventCondition::DAYS_PASSED); - cond.value = reader->readUInt16(); - - specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); - specialDefeat.trigger = EventExpression(cond); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.3"); - break; - } - } - // turn simple loss condition into complete one that can be evaluated later: - // - any of : - // - days without town: 7 - // - all of: - // - is human - // - (expression) - - EventExpression::OperatorAll allOf; - EventCondition isHuman(EventCondition::IS_HUMAN); - isHuman.value = 1; - - allOf.expressions.emplace_back(isHuman); - allOf.expressions.push_back(specialDefeat.trigger.get()); - specialDefeat.trigger = EventExpression(allOf); - - mapHeader->triggeredEvents.push_back(specialDefeat); - } - mapHeader->triggeredEvents.push_back(standardDefeat); -} - -void CMapLoaderH3M::readTeamInfo() -{ - mapHeader->howManyTeams = reader->readUInt8(); - if(mapHeader->howManyTeams > 0) - { - // Teams - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) - mapHeader->players[i].team = TeamID(reader->readUInt8()); - } - else - { - // No alliances - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) - if(mapHeader->players[i].canComputerPlay || mapHeader->players[i].canHumanPlay) - mapHeader->players[i].team = TeamID(mapHeader->howManyTeams++); - } -} - -void CMapLoaderH3M::readAllowedHeroes() -{ - mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); - - if(features.levelHOTA0) - reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); - else - reader->readBitmaskHeroes(mapHeader->allowedHeroes, false); - - if(features.levelAB) - { - uint32_t placeholdersQty = reader->readUInt32(); - - for (uint32_t i = 0; i < placeholdersQty; ++i) - { - auto heroID = reader->readHero(); - mapHeader->reservedCampaignHeroes.push_back(heroID); - } - } -} - -void CMapLoaderH3M::readDisposedHeroes() -{ - // Reading disposed heroes (20 bytes) - if(features.levelSOD) - { - ui8 disp = reader->readUInt8(); - map->disposedHeroes.resize(disp); - for(int g = 0; g < disp; ++g) - { - map->disposedHeroes[g].heroId = reader->readHero().getNum(); - map->disposedHeroes[g].portrait = reader->readHeroPortrait(); - map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); - map->disposedHeroes[g].players = reader->readUInt8(); - } - } -} - -void CMapLoaderH3M::readMapOptions() -{ - //omitting NULLS - reader->skipZero(31); - - if(features.levelHOTA0) - { - //TODO: HotA - bool allowSpecialMonths = reader->readBool(); - if(!allowSpecialMonths) - logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName); - reader->skipZero(3); - } - - if(features.levelHOTA1) - { - // Unknown, may be another "sized bitmap", e.g - // 4 bytes - size of bitmap (16) - // 2 bytes - bitmap data (16 bits / 2 bytes) - [[maybe_unused]] uint8_t unknownConstant = reader->readUInt8(); - assert(unknownConstant == 16); - reader->skipZero(5); - } - - if(features.levelHOTA3) - { - //TODO: HotA - int32_t roundLimit = reader->readInt32(); - if(roundLimit != -1) - logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit); - } -} - -void CMapLoaderH3M::readAllowedArtifacts() -{ - map->allowedArtifact = VLC->arth->getDefaultAllowed(); - - if(features.levelAB) - { - if(features.levelHOTA0) - reader->readBitmaskArtifactsSized(map->allowedArtifact, true); - else - reader->readBitmaskArtifacts(map->allowedArtifact, true); - } - - // ban combo artifacts - if(!features.levelSOD) - { - for(CArtifact * artifact : VLC->arth->objects) - if(artifact->isCombined()) - map->allowedArtifact[artifact->getId()] = false; - } - - if(!features.levelAB) - { - map->allowedArtifact[ArtifactID::VIAL_OF_DRAGON_BLOOD] = false; - map->allowedArtifact[ArtifactID::ARMAGEDDONS_BLADE] = false; - } - - // Messy, but needed - for(TriggeredEvent & event : map->triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) - { - map->allowedArtifact[cond.objectType] = false; - } - return cond; - }; - - event.trigger = event.trigger.morph(patcher); - } -} - -void CMapLoaderH3M::readAllowedSpellsAbilities() -{ - map->allowedSpells = VLC->spellh->getDefaultAllowed(); - map->allowedAbilities = VLC->skillh->getDefaultAllowed(); - - if(features.levelSOD) - { - reader->readBitmaskSpells(map->allowedSpells, true); - reader->readBitmaskSkills(map->allowedAbilities, true); - } -} - -void CMapLoaderH3M::readRumors() -{ - uint32_t rumorsCount = reader->readUInt32(); - assert(rumorsCount < 1000); // sanity check - - for(int it = 0; it < rumorsCount; it++) - { - Rumor ourRumor; - ourRumor.name = readBasicString(); - ourRumor.text = readLocalizedString(TextIdentifier("header", "rumor", it, "text")); - map->rumors.push_back(ourRumor); - } -} - -void CMapLoaderH3M::readPredefinedHeroes() -{ - if(!features.levelSOD) - return; - - uint32_t heroesCount = features.heroesCount; - - if(features.levelHOTA0) - heroesCount = reader->readUInt32(); - - assert(heroesCount <= features.heroesCount); - - for(int heroID = 0; heroID < heroesCount; heroID++) - { - bool custom = reader->readBool(); - if(!custom) - continue; - - auto * hero = new CGHeroInstance(); - hero->ID = Obj::HERO; - hero->subID = heroID; - - bool hasExp = reader->readBool(); - if(hasExp) - { - hero->exp = reader->readUInt32(); - } - else - { - hero->exp = 0; - } - - bool hasSecSkills = reader->readBool(); - if(hasSecSkills) - { - uint32_t howMany = reader->readUInt32(); - hero->secSkills.resize(howMany); - for(int yy = 0; yy < howMany; ++yy) - { - hero->secSkills[yy].first = reader->readSkill(); - hero->secSkills[yy].second = reader->readUInt8(); - } - } - - loadArtifactsOfHero(hero); - - bool hasCustomBio = reader->readBool(); - if(hasCustomBio) - hero->biographyCustom = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); - - // 0xFF is default, 00 male, 01 female - hero->gender = static_cast(reader->readUInt8()); - assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); - - bool hasCustomSpells = reader->readBool(); - if(hasCustomSpells) - reader->readBitmaskSpells(hero->spells, false); - - bool hasCustomPrimSkills = reader->readBool(); - if(hasCustomPrimSkills) - { - for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++) - { - hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); - } - } - map->predefinedHeroes.emplace_back(hero); - - logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getByIndex(hero->subID)->getJsonKey()); - } -} - -void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) -{ - bool hasArtSet = reader->readBool(); - - // True if artifact set is not default (hero has some artifacts) - if(!hasArtSet) - return; - - // Workaround - if hero has customized artifacts game should not attempt to add spellbook based on hero type - hero->spells.insert(SpellID::SPELLBOOK_PRESET); - - if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) - { - logGlobal->warn("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); - - hero->artifactsInBackpack.clear(); - while(!hero->artifactsWorn.empty()) - hero->eraseArtSlot(hero->artifactsWorn.begin()->first); - } - - for(int i = 0; i < features.artifactSlotsCount; i++) - loadArtifactToSlot(hero, i); - - // bag artifacts - // number of artifacts in hero's bag - int amount = reader->readUInt16(); - for(int i = 0; i < amount; ++i) - { - loadArtifactToSlot(hero, GameConstants::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); - } -} - -bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) -{ - ArtifactID artifactID = reader->readArtifact(); - - if(artifactID == ArtifactID::NONE) - return false; - - const Artifact * art = artifactID.toArtifact(VLC->artifacts()); - - if(!art) - { - logGlobal->warn("Map '%s': Invalid artifact in hero's backpack, ignoring...", mapName); - return false; - } - - if(art->isBig() && slot >= GameConstants::BACKPACK_START) - { - logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName); - return false; - } - - // 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); - auto artifactPos = ArtifactPosition(slot); - if(artifact->canBePutAt(ArtifactLocation(hero, artifactPos))) - { - hero->putArtifact(artifactPos, artifact); - } - else - { - logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->getNameTranslated(), slot); - return false; - } - - return true; -} - -void CMapLoaderH3M::readTerrain() -{ - map->initTerrain(); - - // Read terrain - int3 pos; - for(pos.z = 0; pos.z < map->levels(); ++pos.z) - { - //OH3 format is [z][y][x] - for(pos.y = 0; pos.y < map->height; pos.y++) - { - for(pos.x = 0; pos.x < map->width; pos.x++) - { - auto & tile = map->getTile(pos); - tile.terType = VLC->terrainTypeHandler->getById(reader->readTerrain()); - tile.terView = reader->readUInt8(); - tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver()); - tile.riverDir = reader->readUInt8(); - tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad()); - tile.roadDir = reader->readUInt8(); - tile.extTileFlags = reader->readUInt8(); - tile.blocked = !tile.terType->isPassable(); - tile.visitable = false; - - assert(tile.terType->getId() != ETerrainId::NONE); - } - } - } - map->calculateWaterContent(); -} - -void CMapLoaderH3M::readObjectTemplates() -{ - uint32_t defAmount = reader->readUInt32(); - - templates.reserve(defAmount); - - // Read custom defs - for(int defID = 0; defID < defAmount; ++defID) - { - auto tmpl = reader->readObjectTemplate(); - templates.push_back(tmpl); - - if (!CResourceHandler::get()->existsResource(ResourceID( "SPRITES/" + tmpl->animationFile, EResType::ANIMATION))) - logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile, tmpl->id, tmpl->subid ); - } -} - -CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition) -{ - auto * object = new CGEvent(); - - readBoxContent(object, mapPosition); - - object->availableFor = reader->readUInt8(); - object->computerActivate = reader->readBool(); - object->removeAfterVisit = reader->readBool(); - - reader->skipZero(4); - - if(features.levelHOTA3) - object->humanActivate = reader->readBool(); - else - object->humanActivate = true; - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition) -{ - auto * object = new CGPandoraBox(); - readBoxContent(object, mapPosition); - return object; -} - -void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition) -{ - readMessageAndGuards(object->message, object, mapPosition); - - object->gainedExp = reader->readUInt32(); - object->manaDiff = reader->readInt32(); - object->moraleDiff = reader->readInt8(); - object->luckDiff = reader->readInt8(); - - reader->readResourses(object->resources); - - object->primskills.resize(GameConstants::PRIMARY_SKILLS); - for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) - object->primskills[x] = static_cast(reader->readUInt8()); - - int gabn = reader->readUInt8(); //number of gained abilities - for(int oo = 0; oo < gabn; ++oo) - { - object->abilities.emplace_back(reader->readSkill()); - object->abilityLevels.push_back(reader->readUInt8()); - } - int gart = reader->readUInt8(); //number of gained artifacts - for(int oo = 0; oo < gart; ++oo) - object->artifacts.emplace_back(reader->readArtifact()); - - int gspel = reader->readUInt8(); //number of gained spells - for(int oo = 0; oo < gspel; ++oo) - object->spells.emplace_back(reader->readSpell()); - - int gcre = reader->readUInt8(); //number of gained creatures - readCreatureSet(&object->creatures, gcre); - reader->skipZero(8); -} - -CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - auto * object = new CGCreature(); - - if(features.levelAB) - { - object->identifier = reader->readUInt32(); - map->questIdentifierToId[object->identifier] = objectInstanceID; - } - - auto * hlp = new CStackInstance(); - hlp->count = reader->readUInt16(); - - //type will be set during initialization - object->putStack(SlotID(0), hlp); - - object->character = reader->readInt8(); - - bool hasMessage = reader->readBool(); - if(hasMessage) - { - object->message = readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message")); - reader->readResourses(object->resources); - object->gainedArtifact = reader->readArtifact(); - } - object->neverFlees = reader->readBool(); - object->notGrowingTeam = reader->readBool(); - reader->skipZero(2); - - if(features.levelHOTA3) - { - //TODO: HotA - int32_t agressionExact = 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 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) - logGlobal->warn( - "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemeted!", - mapName, - mapPosition.toString(), - agressionExact, - int(joinOnlyForMoney), - joinPercent, - upgradedStack, - stacksCount - ); - } - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) -{ - auto * object = new CGSignBottle(); - object->message = readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message")); - reader->skipZero(4); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readWitchHut() -{ - auto * object = new CGWitchHut(); - - // AB and later maps have allowed abilities defined in H3M - if(features.levelAB) - { - reader->readBitmaskSkills(object->allowedAbilities, false); - - if(object->allowedAbilities.size() != 1) - { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - - for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) - if(defaultAllowed[skillID]) - object->allowedAbilities.insert(SecondarySkill(skillID)); - } - } - return object; -} - -CGObjectInstance * CMapLoaderH3M::readScholar() -{ - auto * object = new CGScholar(); - object->bonusType = static_cast(reader->readUInt8()); - object->bonusID = reader->readUInt8(); - reader->skipZero(6); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) -{ - auto * object = new CGGarrison(); - - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - readCreatureSet(object, 7); - if(features.levelAB) - object->removableUnits = reader->readBool(); - else - object->removableUnits = true; - - reader->skipZero(8); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto artID = ArtifactID::NONE; //random, set later - int spellID = -1; - auto * object = new CGArtifact(); - - readMessageAndGuards(object->message, object, mapPosition); - - if(objectTemplate->id == Obj::SPELL_SCROLL) - { - spellID = reader->readSpell32(); - artID = ArtifactID::SPELL_SCROLL; - } - else if(objectTemplate->id == Obj::ARTIFACT) - { - //specific artifact - artID = ArtifactID(objectTemplate->subid); - } - - object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGResource(); - - readMessageAndGuards(object->message, object, mapPosition); - - object->amount = reader->readUInt32(); - if(objectTemplate->subid == GameResID(EGameResID::GOLD)) - { - // Gold is multiplied by 100. - object->amount *= 100; - } - reader->skipZero(4); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGMine(); - if(objectTemplate->subid < 7) - { - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - } - else - { - object->setOwner(PlayerColor::NEUTRAL); - reader->readBitmaskResources(object->abandonedMineResources, false); - } - return object; -} - -CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position) -{ - auto * object = new CGDwelling(); - setOwnerAndValidate(position, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGDwelling(); - - CSpecObjInfo * spec = nullptr; - switch(objectTemplate->id) - { - case Obj::RANDOM_DWELLING: - spec = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: - spec = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: - spec = new CCreGenLeveledInfo(); - break; - default: - throw std::runtime_error("Invalid random dwelling format"); - } - spec->owner = object; - - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - - //216 and 217 - if(auto * castleSpec = dynamic_cast(spec)) - { - castleSpec->instanceId = ""; - castleSpec->identifier = reader->readUInt32(); - if(!castleSpec->identifier) - { - castleSpec->asCastle = false; - const int MASK_SIZE = 8; - ui8 mask[2]; - mask[0] = reader->readUInt8(); - mask[1] = reader->readUInt8(); - - castleSpec->allowedFactions.clear(); - castleSpec->allowedFactions.resize(VLC->townh->size(), false); - - for(int i = 0; i < MASK_SIZE; i++) - castleSpec->allowedFactions[i] = ((mask[0] & (1 << i)) > 0); - - for(int i = 0; i < (GameConstants::F_NUMBER - MASK_SIZE); i++) - castleSpec->allowedFactions[i + MASK_SIZE] = ((mask[1] & (1 << i)) > 0); - } - else - { - castleSpec->asCastle = true; - } - } - - //216 and 218 - if(auto * lvlSpec = dynamic_cast(spec)) - { - lvlSpec->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; - lvlSpec->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; - } - object->info = spec; - return object; -} - -CGObjectInstance * CMapLoaderH3M::readShrine() -{ - auto * object = new CGShrine(); - object->spell = reader->readSpell32(); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) -{ - auto * object = new CGHeroPlaceholder(); - - setOwnerAndValidate(mapPosition, object, reader->readPlayer()); - - HeroTypeID htid = reader->readHero(); //hero type id - - if(htid.getNum() == -1) - { - object->powerRank = reader->readUInt8(); - logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().getStr()); - } - else - { - object->heroType = htid; - logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().getStr()); - } - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if (objectTemplate->subid < 1000) - { - map->grailPos = mapPosition; - map->grailRadius = reader->readInt32(); - } - else - { - // Battle location for arena mode in HotA - logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); - } - return nullptr; -} - -CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) - return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); - - logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile, mapPosition.toString()); - return new CGObjectInstance(); -} - -CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(objectTemplate->subid == 0) - return new CBank(); - - return new CGObjectInstance(); -} - -CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) -{ - auto * guard = new CGQuestGuard(); - readQuest(guard, mapPosition); - return guard; -} - -CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = readGeneric(mapPosition, objectTemplate); - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) -{ - auto * object = new CGLighthouse(); - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(features.levelHOTA3) - { - //TODO: HotA - // index of guards preset. -1 = random, 0-4 = index of possible guards settings - int32_t guardsPresetIndex = reader->readInt32(); - - // presence of upgraded stack: -1 = random, 0 = never, 1 = always - int8_t upgradedStackPresence = reader->readInt8(); - - assert(vstd::iswithin(guardsPresetIndex, -1, 4)); - assert(vstd::iswithin(upgradedStackPresence, -1, 1)); - - // list of possible artifacts in reward - // - if list is empty, artifacts are either not present in reward or random - // - if non-empty, then list always have same number of elements as number of artifacts in bank - // - ArtifactID::NONE indictates random artifact, other values indicate artifact that should be used as reward - std::vector artifacts; - int artNumber = reader->readUInt32(); - for(int yy = 0; yy < artNumber; ++yy) - { - artifacts.push_back(reader->readArtifact32()); - } - - if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty()) - logGlobal->warn( - "Map '%s: creature bank at %s settings %d %d %d are not implemented!", - mapName, - mapPosition.toString(), - guardsPresetIndex, - int(upgradedStackPresence), - artifacts.size() - ); - } - - return readGeneric(mapPosition, objectTemplate); -} - -CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - switch(objectTemplate->id) - { - case Obj::EVENT: - return readEvent(mapPosition); - - case Obj::HERO: - case Obj::RANDOM_HERO: - case Obj::PRISON: - return readHero(mapPosition, objectInstanceID); - - case Obj::MONSTER: - case Obj::RANDOM_MONSTER: - case Obj::RANDOM_MONSTER_L1: - case Obj::RANDOM_MONSTER_L2: - case Obj::RANDOM_MONSTER_L3: - case Obj::RANDOM_MONSTER_L4: - case Obj::RANDOM_MONSTER_L5: - case Obj::RANDOM_MONSTER_L6: - case Obj::RANDOM_MONSTER_L7: - return readMonster(mapPosition, objectInstanceID); - - case Obj::OCEAN_BOTTLE: - case Obj::SIGN: - return readSign(mapPosition); - - case Obj::SEER_HUT: - return readSeerHut(mapPosition); - - case Obj::WITCH_HUT: - return readWitchHut(); - case Obj::SCHOLAR: - return readScholar(); - - case Obj::GARRISON: - case Obj::GARRISON2: - return readGarrison(mapPosition); - - case Obj::ARTIFACT: - case Obj::RANDOM_ART: - case Obj::RANDOM_TREASURE_ART: - case Obj::RANDOM_MINOR_ART: - case Obj::RANDOM_MAJOR_ART: - case Obj::RANDOM_RELIC_ART: - case Obj::SPELL_SCROLL: - return readArtifact(mapPosition, objectTemplate); - - case Obj::RANDOM_RESOURCE: - case Obj::RESOURCE: - return readResource(mapPosition, objectTemplate); - case Obj::RANDOM_TOWN: - case Obj::TOWN: - return readTown(mapPosition, objectTemplate); - - case Obj::MINE: - case Obj::ABANDONED_MINE: - return readMine(mapPosition, objectTemplate); - - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR2: - case Obj::CREATURE_GENERATOR3: - case Obj::CREATURE_GENERATOR4: - return readDwelling(mapPosition); - - case Obj::SHRINE_OF_MAGIC_INCANTATION: - case Obj::SHRINE_OF_MAGIC_GESTURE: - case Obj::SHRINE_OF_MAGIC_THOUGHT: - return readShrine(); - - case Obj::PANDORAS_BOX: - return readPandora(mapPosition); - - case Obj::GRAIL: - return readGrail(mapPosition, objectTemplate); - - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - return readDwellingRandom(mapPosition, objectTemplate); - - case Obj::QUEST_GUARD: - return readQuestGuard(mapPosition); - - case Obj::SHIPYARD: - return readShipyard(mapPosition, objectTemplate); - - case Obj::HERO_PLACEHOLDER: - return readHeroPlaceholder(mapPosition); - - case Obj::PYRAMID: - return readPyramid(mapPosition, objectTemplate); - - case Obj::LIGHTHOUSE: - return readLighthouse(mapPosition); - - case Obj::CREATURE_BANK: - case Obj::DERELICT_SHIP: - case Obj::DRAGON_UTOPIA: - case Obj::CRYPT: - case Obj::SHIPWRECK: - return readBank(mapPosition, objectTemplate); - - default: //any other object - return readGeneric(mapPosition, objectTemplate); - } -} - -void CMapLoaderH3M::readObjects() -{ - uint32_t objectsCount = reader->readUInt32(); - - for(uint32_t i = 0; i < objectsCount; ++i) - { - int3 mapPosition = reader->readInt3(); - - uint32_t defIndex = reader->readUInt32(); - ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast(map->objects.size())); - - std::shared_ptr objectTemplate = templates.at(defIndex); - reader->skipZero(5); - - CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID); - - if(!newObject) - continue; - - newObject->pos = mapPosition; - newObject->ID = objectTemplate->id; - newObject->id = objectInstanceID; - if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) - { - newObject->subID = objectTemplate->subid; - } - newObject->appearance = objectTemplate; - assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); - - { - //TODO: define valid typeName and subtypeName for H3M maps - //boost::format fmt("%s_%d"); - //fmt % nobj->typeName % nobj->id.getNum(); - boost::format fmt("obj_%d"); - fmt % newObject->id.getNum(); - newObject->instanceName = fmt.str(); - } - map->addNewObject(newObject); - } - - std::sort( - map->heroesOnMap.begin(), - map->heroesOnMap.end(), - [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) - { - return a->subID < b->subID; - } - ); -} - -void CMapLoaderH3M::readCreatureSet(CCreatureSet * out, int number) -{ - for(int index = 0; index < number; ++index) - { - CreatureID creatureID = reader->readCreature(); - int count = reader->readUInt16(); - - // Empty slot - if(creatureID == CreatureID::NONE) - continue; - - auto * result = new CStackInstance(); - result->count = count; - - if(creatureID < CreatureID::NONE) - { - int value = -creatureID.getNum() - 2; - assert(value >= 0 && value < 14); - uint8_t level = value / 2; - uint8_t upgrade = value % 2; - - //this will happen when random object has random army - result->randomStack = CStackInstance::RandomStackInfo{level, upgrade}; - } - else - { - result->setType(creatureID); - } - - out->putStack(SlotID(index), result); - } - - out->validTypes(true); -} - -void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner) -{ - assert(owner.isValidPlayer() || owner == PlayerColor::NEUTRAL); - - if(owner == PlayerColor::NEUTRAL) - { - object->setOwner(PlayerColor::NEUTRAL); - return; - } - - if(!owner.isValidPlayer()) - { - object->setOwner(PlayerColor::NEUTRAL); - logGlobal->warn("Map '%s': Object at %s - owned by invalid player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum())); - return; - } - - if(!mapHeader->players[owner.getNum()].canAnyonePlay()) - { - object->setOwner(PlayerColor::NEUTRAL); - logGlobal->warn("Map '%s': Object at %s - owned by non-existing player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum()) - ); - return; - } - - object->setOwner(owner); -} - -CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - auto * object = new CGHeroInstance(); - - if(features.levelAB) - { - unsigned int identifier = reader->readUInt32(); - map->questIdentifierToId[identifier] = objectInstanceID; - } - - PlayerColor owner = reader->readPlayer(); - object->subID = reader->readHero().getNum(); - - //If hero of this type has been predefined, use that as a base. - //Instance data will overwrite the predefined values where appropriate. - for(auto & elem : map->predefinedHeroes) - { - if(elem->subID == object->subID) - { - logGlobal->debug("Hero %d will be taken from the predefined heroes list.", object->subID); - delete object; - object = elem; - break; - } - } - - setOwnerAndValidate(mapPosition, object, owner); - object->portrait = CGHeroInstance::UNINITIALIZED_PORTRAIT; - - for(auto & elem : map->disposedHeroes) - { - if(elem.heroId == object->subID) - { - object->nameCustom = elem.name; - object->portrait = elem.portrait; - break; - } - } - - bool hasName = reader->readBool(); - if(hasName) - object->nameCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); - - if(features.levelSOD) - { - bool hasCustomExperience = reader->readBool(); - if(hasCustomExperience) - object->exp = reader->readUInt32(); - } - else - { - object->exp = reader->readUInt32(); - - //0 means "not set" in <=AB maps - if(!object->exp) - object->exp = CGHeroInstance::UNINITIALIZED_EXPERIENCE; - } - - bool hasPortrait = reader->readBool(); - if(hasPortrait) - object->portrait = reader->readHeroPortrait(); - - bool hasSecSkills = reader->readBool(); - if(hasSecSkills) - { - if(!object->secSkills.empty()) - { - if(object->secSkills[0].first != SecondarySkill::DEFAULT) - logGlobal->warn("Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); - object->secSkills.clear(); - } - - uint32_t skillsCount = reader->readUInt32(); - object->secSkills.resize(skillsCount); - for(int i = 0; i < skillsCount; ++i) - { - object->secSkills[i].first = reader->readSkill(); - object->secSkills[i].second = reader->readUInt8(); - } - } - - bool hasGarison = reader->readBool(); - if(hasGarison) - readCreatureSet(object, 7); - - object->formation = static_cast(reader->readUInt8()); - assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); - - loadArtifactsOfHero(object); - object->patrol.patrolRadius = reader->readUInt8(); - object->patrol.patrolling = (object->patrol.patrolRadius != 0xff); - - if(features.levelAB) - { - bool hasCustomBiography = reader->readBool(); - if(hasCustomBiography) - object->biographyCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); - - object->gender = static_cast(reader->readUInt8()); - assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); - } - else - { - object->gender = EHeroGender::DEFAULT; - } - - // Spells - if(features.levelSOD) - { - bool hasCustomSpells = reader->readBool(); - if(hasCustomSpells) - { - if(!object->spells.empty()) - { - object->spells.clear(); - logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); - } - - object->spells.insert(SpellID::PRESET); //placeholder "preset spells" - - reader->readBitmaskSpells(object->spells, false); - } - } - else if(features.levelAB) - { - //we can read one spell - SpellID spell = reader->readSpell(); - - if(spell != SpellID::NONE) - object->spells.insert(spell); - } - - if(features.levelSOD) - { - bool hasCustomPrimSkills = reader->readBool(); - if(hasCustomPrimSkills) - { - auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); - if(ps->size()) - { - logGlobal->warn("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); - for(const auto & b : *ps) - object->removeBonus(b); - } - - for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx) - { - object->pushPrimSkill(static_cast(xx), reader->readUInt8()); - } - } - } - - if (object->subID != -1) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().getStr()); - else - logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().getStr()); - - reader->skipZero(16); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position) -{ - auto * hut = new CGSeerHut(); - - uint32_t questsCount = 1; - - if(features.levelHOTA3) - questsCount = reader->readUInt32(); - - //TODO: HotA - if(questsCount > 1) - logGlobal->warn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount); - - for(size_t i = 0; i < questsCount; ++i) - readSeerHutQuest(hut, position); - - if(features.levelHOTA3) - { - uint32_t repeateableQuestsCount = reader->readUInt32(); - - if(repeateableQuestsCount != 0) - logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); - - for(size_t i = 0; i < repeateableQuestsCount; ++i) - readSeerHutQuest(hut, position); - } - - reader->skipZero(2); - - return hut; -} - -void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) -{ - if(features.levelAB) - { - readQuest(hut, position); - } - else - { - //RoE - auto artID = reader->readArtifact(); - if(artID != ArtifactID::NONE) - { - //not none quest - hut->quest->addArtifactID(artID); - hut->quest->missionType = CQuest::MISSION_ART; - } - else - { - hut->quest->missionType = CQuest::MISSION_NONE; - } - hut->quest->lastDay = -1; //no timeout - hut->quest->isCustomFirst = false; - hut->quest->isCustomNext = false; - hut->quest->isCustomComplete = false; - } - - if(hut->quest->missionType) - { - auto rewardType = static_cast(reader->readUInt8()); - hut->rewardType = rewardType; - switch(rewardType) - { - case CGSeerHut::EXPERIENCE: - { - hut->rVal = reader->readUInt32(); - break; - } - case CGSeerHut::MANA_POINTS: - { - hut->rVal = reader->readUInt32(); - break; - } - case CGSeerHut::MORALE_BONUS: - { - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::LUCK_BONUS: - { - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::RESOURCES: - { - hut->rID = reader->readUInt8(); - hut->rVal = reader->readUInt32(); - - assert(hut->rID < features.resourcesCount); - assert((hut->rVal & 0x00ffffff) == hut->rVal); - break; - } - case CGSeerHut::PRIMARY_SKILL: - { - hut->rID = reader->readUInt8(); - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::SECONDARY_SKILL: - { - hut->rID = reader->readSkill(); - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::ARTIFACT: - { - hut->rID = reader->readArtifact(); - break; - } - case CGSeerHut::SPELL: - { - hut->rID = reader->readSpell(); - break; - } - case CGSeerHut::CREATURE: - { - hut->rID = reader->readCreature(); - hut->rVal = reader->readUInt16(); - break; - } - case CGSeerHut::NOTHING: - { - // no-op - break; - } - default: - { - assert(0); - } - } - } - else - { - // missionType==255 - reader->skipZero(1); - } -} - -void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) -{ - guard->quest->missionType = static_cast(reader->readUInt8()); - - switch(guard->quest->missionType) - { - case CQuest::MISSION_NONE: - return; - case CQuest::MISSION_PRIMARY_STAT: - { - guard->quest->m2stats.resize(4); - for(int x = 0; x < 4; ++x) - { - guard->quest->m2stats[x] = reader->readUInt8(); - } - } - break; - case CQuest::MISSION_LEVEL: - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: - { - guard->quest->m13489val = reader->readUInt32(); - break; - } - case CQuest::MISSION_ART: - { - int artNumber = reader->readUInt8(); - for(int yy = 0; yy < artNumber; ++yy) - { - auto artid = reader->readArtifact(); - guard->quest->addArtifactID(artid); - map->allowedArtifact[artid] = false; //these are unavailable for random generation - } - break; - } - case CQuest::MISSION_ARMY: - { - int typeNumber = reader->readUInt8(); - guard->quest->m6creatures.resize(typeNumber); - for(int hh = 0; hh < typeNumber; ++hh) - { - guard->quest->m6creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->m6creatures[hh].count = reader->readUInt16(); - } - break; - } - case CQuest::MISSION_RESOURCES: - { - for(int x = 0; x < 7; ++x) - guard->quest->m7resources[x] = reader->readUInt32(); - - break; - } - case CQuest::MISSION_HERO: - { - guard->quest->m13489val = reader->readHero().getNum(); - break; - } - case CQuest::MISSION_PLAYER: - { - guard->quest->m13489val = reader->readPlayer().getNum(); - break; - } - case CQuest::MISSION_HOTA_MULTI: - { - uint32_t missionSubID = reader->readUInt32(); - - if(missionSubID == 0) - { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_HERO_CLASS; - std::set heroClasses; - reader->readBitmaskHeroClassesSized(heroClasses, false); - - logGlobal->warn("Map '%s': Quest at %s 'Belong to one of %d classes' is not implemented!", mapName, position.toString(), heroClasses.size()); - break; - } - if(missionSubID == 1) - { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_REACH_DATE; - uint32_t daysPassed = reader->readUInt32(); - - logGlobal->warn("Map '%s': Quest at %s 'Wait till %d days passed' is not implemented!", mapName, position.toString(), daysPassed); - break; - } - assert(0); - break; - } - default: - { - assert(0); - } - } - - guard->quest->lastDay = reader->readInt32(); - guard->quest->firstVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit")); - guard->quest->nextVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit")); - guard->quest->completedText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed")); - guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); - guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); - guard->quest->isCustomComplete = !guard->quest->completedText.empty(); -} - -CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) -{ - auto * object = new CGTownInstance(); - if(features.levelAB) - object->identifier = reader->readUInt32(); - - setOwnerAndValidate(position, object, reader->readPlayer()); - - std::optional faction; - if (objectTemplate->id == Obj::TOWN) - faction = FactionID(objectTemplate->subid); - - bool hasName = reader->readBool(); - if(hasName) - object->setNameTranslated(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); - - bool hasGarrison = reader->readBool(); - if(hasGarrison) - readCreatureSet(object, 7); - - object->formation = static_cast(reader->readUInt8()); - assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); - - bool hasCustomBuildings = reader->readBool(); - if(hasCustomBuildings) - { - reader->readBitmaskBuildings(object->builtBuildings, faction); - reader->readBitmaskBuildings(object->forbiddenBuildings, faction); - } - // Standard buildings - else - { - bool hasFort = reader->readBool(); - if(hasFort) - object->builtBuildings.insert(BuildingID::FORT); - - //means that set of standard building should be included - object->builtBuildings.insert(BuildingID::DEFAULT); - } - - if(features.levelAB) - { - std::set spellsMask; - - reader->readBitmaskSpells(spellsMask, false); - std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->obligatorySpells)); - } - - { - std::set spellsMask; - - reader->readBitmaskSpells(spellsMask, true); - std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells)); - - auto defaultAllowed = VLC->spellh->getDefaultAllowed(); - - //add all spells from mods - for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) - if(defaultAllowed[i]) - object->possibleSpells.emplace_back(i); - } - - if(features.levelHOTA1) - { - // TODO: HOTA support - [[maybe_unused]] bool spellResearchAvailable = reader->readBool(); - } - - // Read castle events - uint32_t eventsCount = reader->readUInt32(); - - for(int eventID = 0; eventID < eventsCount; ++eventID) - { - CCastleEvent event; - event.town = object; - event.name = readBasicString(); - event.message = readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")); - - reader->readResourses(event.resources); - - event.players = reader->readUInt8(); - if(features.levelSOD) - event.humanAffected = reader->readBool(); - else - event.humanAffected = true; - - event.computerAffected = reader->readUInt8(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); - - reader->skipZero(17); - - // New buildings - reader->readBitmaskBuildings(event.buildings, faction); - - event.creatures.resize(7); - for(int i = 0; i < 7; ++i) - event.creatures[i] = reader->readUInt16(); - - reader->skipZero(4); - object->events.push_back(event); - } - - if(features.levelSOD) - { - object->alignmentToPlayer = PlayerColor::NEUTRAL; // "same as owner or random" - - uint8_t alignment = reader->readUInt8(); - - if(alignment != PlayerColor::NEUTRAL.getNum()) - { - if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) - { - 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)); - } - else - { - // TODO: HOTA support - uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum(); - - 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()); - } - else - { - logGlobal->warn("%s - Aligment of town at %s is corrupted!!", mapName, position.toString()); - } - } - } - } - reader->skipZero(3); - - return object; -} - -void CMapLoaderH3M::readEvents() -{ - uint32_t eventsCount = reader->readUInt32(); - for(int eventID = 0; eventID < eventsCount; ++eventID) - { - CMapEvent event; - event.name = readBasicString(); - event.message = readLocalizedString(TextIdentifier("event", eventID, "description")); - - reader->readResourses(event.resources); - event.players = reader->readUInt8(); - if(features.levelSOD) - { - event.humanAffected = reader->readBool(); - } - else - { - event.humanAffected = true; - } - event.computerAffected = reader->readBool(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); - - reader->skipZero(17); - - map->events.push_back(event); - } -} - -void CMapLoaderH3M::readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position) -{ - bool hasMessage = reader->readBool(); - if(hasMessage) - { - message = readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message")); - bool hasGuards = reader->readBool(); - if(hasGuards) - readCreatureSet(guards, 7); - - reader->skipZero(4); - } -} - -std::string CMapLoaderH3M::readBasicString() -{ - return TextOperations::toUnicode(reader->readBaseString(), fileEncoding); -} - -std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIdentifier) -{ - std::string mapString = TextOperations::toUnicode(reader->readBaseString(), fileEncoding); - TextIdentifier fullIdentifier("map", mapName, stringIdentifier.get()); - - if(mapString.empty()) - return ""; - - VLC->generaltexth->registerString(modName, fullIdentifier, mapString); - return VLC->generaltexth->translate(fullIdentifier.get()); -} - -void CMapLoaderH3M::afterRead() -{ - //convert main town positions for all players to actual object position, in H3M it is position of active tile - - for(auto & p : map->players) - { - int3 posOfMainTown = p.posOfMainTown; - if(posOfMainTown.valid() && map->isInTheMap(posOfMainTown)) - { - const TerrainTile & t = map->getTile(posOfMainTown); - - const CGObjectInstance * mainTown = nullptr; - - for(auto * obj : t.visitableObjects) - { - if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN) - { - mainTown = obj; - break; - } - } - - if(mainTown == nullptr) - continue; - - p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset(); - } - } -} - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatH3M.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 "MapFormatH3M.h" + +#include "CMap.h" +#include "MapReaderH3M.h" +#include "MapFormat.h" + +#include "../ArtifactUtils.h" +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CHeroHandler.h" +#include "../CSkillHandler.h" +#include "../CStopWatch.h" +#include "../GameSettings.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../TerrainHandler.h" +#include "../TextOperations.h" +#include "../VCMI_Lib.h" +#include "../constants/StringConstants.h" +#include "../filesystem/CBinaryReader.h" +#include "../filesystem/Filesystem.h" +#include "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGCreature.h" +#include "../mapObjects/MapObjects.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../modding/ModScope.h" +#include "../networkPacks/Component.h" +#include "../networkPacks/ArtifactLocation.h" +#include "../spells/CSpellHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +static std::string convertMapName(std::string input) +{ + boost::algorithm::to_lower(input); + boost::algorithm::trim(input); + boost::algorithm::erase_all(input, "."); + + size_t slashPos = input.find_last_of('/'); + + if(slashPos != std::string::npos) + return input.substr(slashPos + 1); + + return input; +} + +CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream) + : map(nullptr) + , reader(new MapReaderH3M(stream)) + , inputStream(stream) + , mapName(convertMapName(mapName)) + , modName(modName) + , fileEncoding(encodingName) +{ +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CMapLoaderH3M::~CMapLoaderH3M() = default; + +std::unique_ptr CMapLoaderH3M::loadMap() +{ + // Init map object by parsing the input buffer + map = new CMap(); + mapHeader = std::unique_ptr(dynamic_cast(map)); + init(); + + return std::unique_ptr(dynamic_cast(mapHeader.release())); +} + +std::unique_ptr CMapLoaderH3M::loadMapHeader() +{ + // Read header + mapHeader = std::make_unique(); + readHeader(); + + return std::move(mapHeader); +} + +void CMapLoaderH3M::init() +{ + //TODO: get rid of double input process + si64 temp_size = inputStream->getSize(); + inputStream->seek(0); + + auto * temp_buffer = new ui8[temp_size]; + inputStream->read(temp_buffer, temp_size); + + // Compute checksum + boost::crc_32_type result; + result.process_bytes(temp_buffer, temp_size); + map->checksum = result.checksum(); + + delete[] temp_buffer; + inputStream->seek(0); + + readHeader(); + readDisposedHeroes(); + readMapOptions(); + readAllowedArtifacts(); + readAllowedSpellsAbilities(); + readRumors(); + readPredefinedHeroes(); + readTerrain(); + readObjectTemplates(); + readObjects(); + readEvents(); + + map->calculateGuardingGreaturePositions(); + afterRead(); + //map->banWaterContent(); //Not sure if force this for custom scenarios +} + +void CMapLoaderH3M::readHeader() +{ + // Map version + mapHeader->version = static_cast(reader->readUInt32()); + + if(mapHeader->version == EMapFormat::HOTA) + { + uint32_t hotaVersion = reader->readUInt32(); + features = MapFormatFeaturesH3M::find(mapHeader->version, hotaVersion); + reader->setFormatLevel(features); + + if(hotaVersion > 0) + { + bool isMirrorMap = reader->readBool(); + bool isArenaMap = reader->readBool(); + + //TODO: HotA + if (isMirrorMap) + logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName); + + if (isArenaMap) + logGlobal->warn("Map '%s': Arena maps are not supported!", mapName); + } + + if(hotaVersion > 1) + { + [[maybe_unused]] uint8_t unknown = reader->readUInt32(); + assert(unknown == 12); + } + } + else + { + features = MapFormatFeaturesH3M::find(mapHeader->version, 0); + reader->setFormatLevel(features); + } + MapIdentifiersH3M identifierMapper; + + if (features.levelROE) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); + if (features.levelAB) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); + if (features.levelSOD) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); + if (features.levelWOG) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); + if (features.levelHOTA0) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); + + 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->difficulty = reader->readInt8(); + + if(features.levelAB) + mapHeader->levelLimit = reader->readUInt8(); + else + mapHeader->levelLimit = 0; + + readPlayerInfo(); + readVictoryLossConditions(); + readTeamInfo(); + readAllowedHeroes(); +} + +void CMapLoaderH3M::readPlayerInfo() +{ + for(int i = 0; i < mapHeader->players.size(); ++i) + { + auto & playerInfo = mapHeader->players[i]; + + playerInfo.canHumanPlay = reader->readBool(); + playerInfo.canComputerPlay = reader->readBool(); + + // If nobody can play with this player - skip loading of these properties + if((!(playerInfo.canHumanPlay || playerInfo.canComputerPlay))) + { + if(features.levelROE) + reader->skipUnused(6); + if(features.levelAB) + reader->skipUnused(6); + if(features.levelSOD) + reader->skipUnused(1); + continue; + } + + playerInfo.aiTactic = static_cast(reader->readUInt8()); + + if(features.levelSOD) + reader->skipUnused(1); //TODO: check meaning? + + std::set allowedFactions; + + reader->readBitmaskFactions(allowedFactions, false); + + const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool(); + const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount; + + if(!allFactionsAllowed) + playerInfo.allowedFactions = allowedFactions; + + playerInfo.hasMainTown = reader->readBool(); + if(playerInfo.hasMainTown) + { + if(features.levelAB) + { + playerInfo.generateHeroAtMainTown = reader->readBool(); + reader->skipUnused(1); //TODO: check meaning? + } + else + { + playerInfo.generateHeroAtMainTown = true; + } + + playerInfo.posOfMainTown = reader->readInt3(); + } + + playerInfo.hasRandomHero = reader->readBool(); + playerInfo.mainCustomHeroId = reader->readHero(); + + if(playerInfo.mainCustomHeroId != HeroTypeID::NONE) + { + playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); + playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); + } + + if(features.levelAB) + { + reader->skipUnused(1); //TODO: check meaning? + uint32_t heroCount = reader->readUInt32(); + for(int pp = 0; pp < heroCount; ++pp) + { + SHeroName vv; + vv.heroId = reader->readHero(); + vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId.getNum())); + + playerInfo.heroesNames.push_back(vv); + } + } + } +} + +enum class EVictoryConditionType : uint8_t +{ + ARTIFACT = 0, + GATHERTROOP = 1, + GATHERRESOURCE = 2, + BUILDCITY = 3, + BUILDGRAIL = 4, + BEATHERO = 5, + CAPTURECITY = 6, + BEATMONSTER = 7, + TAKEDWELLINGS = 8, + TAKEMINES = 9, + TRANSPORTITEM = 10, + HOTA_ELIMINATE_ALL_MONSTERS = 11, + HOTA_SURVIVE_FOR_DAYS = 12, + WINSTANDARD = 255 +}; + +enum class ELossConditionType : uint8_t +{ + LOSSCASTLE = 0, + LOSSHERO = 1, + TIMEEXPIRES = 2, + LOSSSTANDARD = 255 +}; + +void CMapLoaderH3M::readVictoryLossConditions() +{ + mapHeader->triggeredEvents.clear(); + mapHeader->victoryMessage.clear(); + mapHeader->defeatMessage.clear(); + + auto vicCondition = static_cast(reader->readUInt8()); + + EventCondition victoryCondition(EventCondition::STANDARD_WIN); + EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); + defeatCondition.value = 7; + + TriggeredEvent standardVictory; + standardVictory.effect.type = EventEffect::VICTORY; + standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + standardVictory.identifier = "standardVictory"; + standardVictory.description.clear(); // TODO: display in quest window + standardVictory.onFulfill.appendTextID("core.genrltxt.659"); + standardVictory.trigger = EventExpression(victoryCondition); + + TriggeredEvent standardDefeat; + standardDefeat.effect.type = EventEffect::DEFEAT; + standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); + standardDefeat.identifier = "standardDefeat"; + standardDefeat.description.clear(); // TODO: display in quest window + standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); + standardDefeat.trigger = EventExpression(defeatCondition); + + // Specific victory conditions + if(vicCondition == EVictoryConditionType::WINSTANDARD) + { + // create normal condition + mapHeader->triggeredEvents.push_back(standardVictory); + mapHeader->victoryIconIndex = 11; + mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); + } + else + { + TriggeredEvent specialVictory; + specialVictory.effect.type = EventEffect::VICTORY; + specialVictory.identifier = "specialVictory"; + specialVictory.description.clear(); // TODO: display in quest window + + mapHeader->victoryIconIndex = static_cast(vicCondition); + + bool allowNormalVictory = reader->readBool(); + bool appliesToAI = reader->readBool(); + + switch(vicCondition) + { + case EVictoryConditionType::ARTIFACT: + { + assert(allowNormalVictory == true); // not selectable in editor + EventCondition cond(EventCondition::HAVE_ARTIFACT); + cond.objectType = reader->readArtifact(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); + specialVictory.onFulfill.appendTextID("core.genrltxt.280"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.1"); + break; + } + case EVictoryConditionType::GATHERTROOP: + { + EventCondition cond(EventCondition::HAVE_CREATURES); + cond.objectType = reader->readCreature(); + cond.value = reader->readInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); + specialVictory.onFulfill.appendTextID("core.genrltxt.276"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.2"); + break; + } + case EVictoryConditionType::GATHERRESOURCE: + { + EventCondition cond(EventCondition::HAVE_RESOURCES); + cond.objectType = reader->readGameResID(); + cond.value = reader->readInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); + specialVictory.onFulfill.appendTextID("core.genrltxt.278"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.3"); + break; + } + case EVictoryConditionType::BUILDCITY: + { + assert(appliesToAI == true); // not selectable in editor + EventExpression::OperatorAll oper; + EventCondition cond(EventCondition::HAVE_BUILDING); + cond.position = reader->readInt3(); + cond.objectType = BuildingID::HALL_LEVEL(reader->readUInt8() + 1); + oper.expressions.emplace_back(cond); + cond.objectType = BuildingID::FORT_LEVEL(reader->readUInt8()); + oper.expressions.emplace_back(cond); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); + specialVictory.onFulfill.appendTextID("core.genrltxt.282"); + specialVictory.trigger = EventExpression(oper); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.4"); + break; + } + case EVictoryConditionType::BUILDGRAIL: + { + assert(allowNormalVictory == true); // not selectable in editor + assert(appliesToAI == true); // not selectable in editor + EventCondition cond(EventCondition::HAVE_BUILDING); + cond.objectType = BuildingID(BuildingID::GRAIL); + cond.position = reader->readInt3(); + if(cond.position.z > 2) + cond.position = int3(-1, -1, -1); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285"); + specialVictory.onFulfill.appendTextID("core.genrltxt.284"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.5"); + break; + } + case EVictoryConditionType::BEATHERO: + { + if (!allowNormalVictory) + logGlobal->debug("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); + allowNormalVictory = true; // H3 behavior + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = MapObjectID(MapObjectID::HERO); + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); + specialVictory.onFulfill.appendTextID("core.genrltxt.252"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.6"); + break; + } + case EVictoryConditionType::CAPTURECITY: + { + EventCondition cond(EventCondition::CONTROL); + cond.objectType = MapObjectID(MapObjectID::TOWN); + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); + specialVictory.onFulfill.appendTextID("core.genrltxt.249"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.7"); + break; + } + case EVictoryConditionType::BEATMONSTER: + { + assert(appliesToAI == true); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = MapObjectID(MapObjectID::MONSTER); + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); + specialVictory.onFulfill.appendTextID("core.genrltxt.286"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.8"); + break; + } + case EVictoryConditionType::TAKEDWELLINGS: + { + EventExpression::OperatorAll oper; + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR1))); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR4))); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); + specialVictory.onFulfill.appendTextID("core.genrltxt.288"); + specialVictory.trigger = EventExpression(oper); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.9"); + break; + } + case EVictoryConditionType::TAKEMINES: + { + EventCondition cond(EventCondition::CONTROL); + cond.objectType = MapObjectID(MapObjectID::MINE); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); + specialVictory.onFulfill.appendTextID("core.genrltxt.290"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.10"); + break; + } + case EVictoryConditionType::TRANSPORTITEM: + { + assert(allowNormalVictory == true); // not selectable in editor + EventCondition cond(EventCondition::TRANSPORT); + cond.objectType = reader->readArtifact8(); + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); + specialVictory.onFulfill.appendTextID("core.genrltxt.292"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.11"); + break; + } + case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: + { + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = MapObjectID(MapObjectID::MONSTER); + + specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); + specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.12"); + mapHeader->victoryIconIndex = 12; + break; + } + case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: + { + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DAYS_PASSED); + cond.value = reader->readUInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers"); + specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.13"); + mapHeader->victoryIconIndex = 13; + break; + } + default: + assert(0); + } + + if(allowNormalVictory) + { + size_t playersOnMap = boost::range::count_if( + mapHeader->players, + [](const PlayerInfo & info) + { + return info.canAnyonePlay(); + } + ); + + if(playersOnMap == 1) + { + logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); + allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! + } + } + + // if condition is human-only turn it into following construction: AllOf(human, condition) + if(!appliesToAI) + { + EventExpression::OperatorAll oper; + EventCondition notAI(EventCondition::IS_HUMAN); + notAI.value = 1; + oper.expressions.emplace_back(notAI); + oper.expressions.push_back(specialVictory.trigger.get()); + specialVictory.trigger = EventExpression(oper); + } + + // if normal victory allowed - add one more quest + if(allowNormalVictory) + { + mapHeader->victoryMessage.appendRawString(" / "); + mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); + mapHeader->triggeredEvents.push_back(standardVictory); + } + mapHeader->triggeredEvents.push_back(specialVictory); + } + + // Read loss conditions + auto lossCond = static_cast(reader->readUInt8()); + if(lossCond == ELossConditionType::LOSSSTANDARD) + { + mapHeader->defeatIconIndex = 3; + mapHeader->defeatMessage.appendTextID("core.lcdesc.0"); + } + else + { + TriggeredEvent specialDefeat; + specialDefeat.effect.type = EventEffect::DEFEAT; + specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + specialDefeat.identifier = "specialDefeat"; + specialDefeat.description.clear(); // TODO: display in quest window + + mapHeader->defeatIconIndex = static_cast(lossCond); + + switch(lossCond) + { + case ELossConditionType::LOSSCASTLE: + { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj(Obj::TOWN); + cond.position = reader->readInt3(); + + noneOf.expressions.emplace_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); + specialDefeat.trigger = EventExpression(noneOf); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.1"); + break; + } + case ELossConditionType::LOSSHERO: + { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj(Obj::HERO); + cond.position = reader->readInt3(); + + noneOf.expressions.emplace_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); + specialDefeat.trigger = EventExpression(noneOf); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.2"); + break; + } + case ELossConditionType::TIMEEXPIRES: + { + EventCondition cond(EventCondition::DAYS_PASSED); + cond.value = reader->readUInt16(); + + specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); + specialDefeat.trigger = EventExpression(cond); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.3"); + break; + } + } + // turn simple loss condition into complete one that can be evaluated later: + // - any of : + // - days without town: 7 + // - all of: + // - is human + // - (expression) + + EventExpression::OperatorAll allOf; + EventCondition isHuman(EventCondition::IS_HUMAN); + isHuman.value = 1; + + allOf.expressions.emplace_back(isHuman); + allOf.expressions.push_back(specialDefeat.trigger.get()); + specialDefeat.trigger = EventExpression(allOf); + + mapHeader->triggeredEvents.push_back(specialDefeat); + } + mapHeader->triggeredEvents.push_back(standardDefeat); +} + +void CMapLoaderH3M::readTeamInfo() +{ + mapHeader->howManyTeams = reader->readUInt8(); + if(mapHeader->howManyTeams > 0) + { + // Teams + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + mapHeader->players[i].team = TeamID(reader->readUInt8()); + } + else + { + // No alliances + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) + if(mapHeader->players[i].canComputerPlay || mapHeader->players[i].canHumanPlay) + mapHeader->players[i].team = TeamID(mapHeader->howManyTeams++); + } +} + +void CMapLoaderH3M::readAllowedHeroes() +{ + mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); + + if(features.levelHOTA0) + reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); + else + reader->readBitmaskHeroes(mapHeader->allowedHeroes, false); + + if(features.levelAB) + { + uint32_t placeholdersQty = reader->readUInt32(); + + for (uint32_t i = 0; i < placeholdersQty; ++i) + { + auto heroID = reader->readHero(); + mapHeader->reservedCampaignHeroes.insert(heroID); + } + } +} + +void CMapLoaderH3M::readDisposedHeroes() +{ + // Reading disposed heroes (20 bytes) + if(features.levelSOD) + { + ui8 disp = reader->readUInt8(); + map->disposedHeroes.resize(disp); + for(int g = 0; g < disp; ++g) + { + map->disposedHeroes[g].heroId = reader->readHero(); + map->disposedHeroes[g].portrait = reader->readHeroPortrait(); + map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId.getNum())); + reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); + } + } +} + +void CMapLoaderH3M::readMapOptions() +{ + //omitting NULLS + reader->skipZero(31); + + if(features.levelHOTA0) + { + //TODO: HotA + bool allowSpecialMonths = reader->readBool(); + if(!allowSpecialMonths) + logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName); + reader->skipZero(3); + } + + if(features.levelHOTA1) + { + // Unknown, may be another "sized bitmap", e.g + // 4 bytes - size of bitmap (16) + // 2 bytes - bitmap data (16 bits / 2 bytes) + [[maybe_unused]] uint8_t unknownConstant = reader->readUInt8(); + assert(unknownConstant == 16); + reader->skipZero(5); + } + + if(features.levelHOTA3) + { + //TODO: HotA + int32_t roundLimit = reader->readInt32(); + if(roundLimit != -1) + logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit); + } +} + +void CMapLoaderH3M::readAllowedArtifacts() +{ + map->allowedArtifact = VLC->arth->getDefaultAllowed(); + + if(features.levelAB) + { + if(features.levelHOTA0) + reader->readBitmaskArtifactsSized(map->allowedArtifact, true); + else + reader->readBitmaskArtifacts(map->allowedArtifact, true); + } + + // ban combo artifacts + if(!features.levelSOD) + { + for(CArtifact * artifact : VLC->arth->objects) + if(artifact->isCombined()) + map->allowedArtifact.erase(artifact->getId()); + } + + if(!features.levelAB) + { + map->allowedArtifact.erase(ArtifactID::VIAL_OF_DRAGON_BLOOD); + map->allowedArtifact.erase(ArtifactID::ARMAGEDDONS_BLADE); + } + + // Messy, but needed + for(TriggeredEvent & event : map->triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) + { + map->allowedArtifact.erase(cond.objectType.as()); + } + return cond; + }; + + event.trigger = event.trigger.morph(patcher); + } +} + +void CMapLoaderH3M::readAllowedSpellsAbilities() +{ + map->allowedSpells = VLC->spellh->getDefaultAllowed(); + map->allowedAbilities = VLC->skillh->getDefaultAllowed(); + + if(features.levelSOD) + { + reader->readBitmaskSpells(map->allowedSpells, true); + reader->readBitmaskSkills(map->allowedAbilities, true); + } +} + +void CMapLoaderH3M::readRumors() +{ + uint32_t rumorsCount = reader->readUInt32(); + assert(rumorsCount < 1000); // sanity check + + for(int it = 0; it < rumorsCount; it++) + { + Rumor ourRumor; + ourRumor.name = readBasicString(); + ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text"))); + map->rumors.push_back(ourRumor); + } +} + +void CMapLoaderH3M::readPredefinedHeroes() +{ + if(!features.levelSOD) + return; + + uint32_t heroesCount = features.heroesCount; + + if(features.levelHOTA0) + heroesCount = reader->readUInt32(); + + assert(heroesCount <= features.heroesCount); + + for(int heroID = 0; heroID < heroesCount; heroID++) + { + bool custom = reader->readBool(); + if(!custom) + continue; + + auto * hero = new CGHeroInstance(); + hero->ID = Obj::HERO; + hero->subID = heroID; + + bool hasExp = reader->readBool(); + if(hasExp) + { + hero->exp = reader->readUInt32(); + } + else + { + hero->exp = 0; + } + + bool hasSecSkills = reader->readBool(); + if(hasSecSkills) + { + uint32_t howMany = reader->readUInt32(); + hero->secSkills.resize(howMany); + for(int yy = 0; yy < howMany; ++yy) + { + hero->secSkills[yy].first = reader->readSkill(); + hero->secSkills[yy].second = reader->readUInt8(); + } + } + + loadArtifactsOfHero(hero); + + bool hasCustomBio = reader->readBool(); + if(hasCustomBio) + hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); + + // 0xFF is default, 00 male, 01 female + hero->gender = static_cast(reader->readUInt8()); + assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); + + bool hasCustomSpells = reader->readBool(); + if(hasCustomSpells) + reader->readBitmaskSpells(hero->spells, false); + + bool hasCustomPrimSkills = reader->readBool(); + if(hasCustomPrimSkills) + { + for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++) + { + hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); + } + } + map->predefinedHeroes.emplace_back(hero); + + logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getById(hero->getHeroType())->getJsonKey()); + } +} + +void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) +{ + bool hasArtSet = reader->readBool(); + + // True if artifact set is not default (hero has some artifacts) + if(!hasArtSet) + return; + + // Workaround - if hero has customized artifacts game should not attempt to add spellbook based on hero type + hero->spells.insert(SpellID::SPELLBOOK_PRESET); + + 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()); + + hero->artifactsInBackpack.clear(); + while(!hero->artifactsWorn.empty()) + hero->eraseArtSlot(hero->artifactsWorn.begin()->first); + } + + for(int i = 0; i < features.artifactSlotsCount; i++) + loadArtifactToSlot(hero, i); + + // bag artifacts + // number of artifacts in hero's bag + int amount = reader->readUInt16(); + for(int i = 0; i < amount; ++i) + { + loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); + } +} + +bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) +{ + ArtifactID artifactID = reader->readArtifact(); + + if(artifactID == ArtifactID::NONE) + return false; + + const Artifact * art = artifactID.toEntity(VLC); + + if(!art) + { + logGlobal->warn("Map '%s': Invalid artifact in hero's backpack, ignoring...", mapName); + return false; + } + + if(art->isBig() && slot >= ArtifactPosition::BACKPACK_START) + { + logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName); + return false; + } + + // 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))) + { + artifact->putAt(*hero, ArtifactPosition(slot)); + } + else + { + logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->getNameTranslated(), slot); + return false; + } + + return true; +} + +void CMapLoaderH3M::readTerrain() +{ + map->initTerrain(); + + // Read terrain + int3 pos; + for(pos.z = 0; pos.z < map->levels(); ++pos.z) + { + //OH3 format is [z][y][x] + for(pos.y = 0; pos.y < map->height; pos.y++) + { + for(pos.x = 0; pos.x < map->width; pos.x++) + { + auto & tile = map->getTile(pos); + tile.terType = VLC->terrainTypeHandler->getById(reader->readTerrain()); + tile.terView = reader->readUInt8(); + tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver()); + tile.riverDir = reader->readUInt8(); + tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad()); + tile.roadDir = reader->readUInt8(); + tile.extTileFlags = reader->readUInt8(); + tile.blocked = !tile.terType->isPassable(); + tile.visitable = false; + + assert(tile.terType->getId() != ETerrainId::NONE); + } + } + } + map->calculateWaterContent(); +} + +void CMapLoaderH3M::readObjectTemplates() +{ + uint32_t defAmount = reader->readUInt32(); + + templates.reserve(defAmount); + + // Read custom defs + for(int defID = 0; defID < defAmount; ++defID) + { + auto tmpl = reader->readObjectTemplate(); + templates.push_back(tmpl); + + if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/"))) + logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); + } +} + +CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + auto * object = new CGEvent(); + + readBoxContent(object, mapPosition, idToBeGiven); + + reader->readBitmaskPlayers(object->availableFor, false); + object->computerActivate = reader->readBool(); + object->removeAfterVisit = reader->readBool(); + + reader->skipZero(4); + + if(features.levelHOTA3) + object->humanActivate = reader->readBool(); + else + object->humanActivate = true; + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + auto * object = new CGPandoraBox(); + readBoxContent(object, mapPosition, idToBeGiven); + return object; +} + +void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + readMessageAndGuards(object->message, object, mapPosition); + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + + reward.heroExperience = reader->readUInt32(); + reward.manaDiff = reader->readInt32(); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); + + reader->readResourses(reward.resources); + for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) + reward.primary.at(x) = reader->readUInt8(); + + int gabn = reader->readUInt8(); //number of gained abilities + for(int oo = 0; oo < gabn; ++oo) + { + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; + } + int gart = reader->readUInt8(); //number of gained artifacts + for(int oo = 0; oo < gart; ++oo) + reward.artifacts.push_back(reader->readArtifact()); + + int gspel = reader->readUInt8(); //number of gained spells + for(int oo = 0; oo < gspel; ++oo) + reward.spells.push_back(reader->readSpell()); + + int gcre = reader->readUInt8(); //number of gained creatures + for(int oo = 0; oo < gcre; ++oo) + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + object->configuration.info.push_back(vinfo); + + reader->skipZero(8); +} + +CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + auto * object = new CGCreature(); + + if(features.levelAB) + { + object->identifier = reader->readUInt32(); + map->questIdentifierToId[object->identifier] = objectInstanceID; + } + + auto * hlp = new CStackInstance(); + hlp->count = reader->readUInt16(); + + //type will be set during initialization + object->putStack(SlotID(0), hlp); + + object->character = reader->readInt8(); + + bool hasMessage = reader->readBool(); + if(hasMessage) + { + object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); + reader->readResourses(object->resources); + object->gainedArtifact = reader->readArtifact(); + } + object->neverFlees = reader->readBool(); + object->notGrowingTeam = reader->readBool(); + reader->skipZero(2); + + if(features.levelHOTA3) + { + //TODO: HotA + int32_t agressionExact = 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 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) + logGlobal->warn( + "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", + mapName, + mapPosition.toString(), + agressionExact, + int(joinOnlyForMoney), + joinPercent, + upgradedStack, + stacksCount + ); + } + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) +{ + auto * object = new CGSignBottle(); + object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); + reader->skipZero(4); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr objectTemplate) +{ + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); + + // AB and later maps have allowed abilities defined in H3M + if(features.levelAB) + { + std::set allowedAbilities; + reader->readBitmaskSkills(allowedAbilities, false); + + if(allowedAbilities.size() != 1) + { + auto defaultAllowed = VLC->skillh->getDefaultAllowed(); + + for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) + if(defaultAllowed.count(skillID)) + allowedAbilities.insert(SecondarySkill(skillID)); + } + + JsonNode variable; + if (allowedAbilities.size() == 1) + { + variable.String() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey(); + } + else + { + JsonVector anyOfList; + for (auto const & skill : allowedAbilities) + { + JsonNode entry; + entry.String() = VLC->skills()->getById(skill)->getJsonKey(); + anyOfList.push_back(entry); + } + variable["anyOf"].Vector() = anyOfList; + } + + variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); + } + return object; +} + +CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr objectTemplate) +{ + enum class ScholarBonusType : uint8_t { + PRIM_SKILL = 0, + SECONDARY_SKILL = 1, + SPELL = 2, + RANDOM = 255 + }; + + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + uint8_t bonusTypeRaw = reader->readUInt8(); + auto bonusType = static_cast(bonusTypeRaw); + auto bonusID = reader->readUInt8(); + + switch (bonusType) + { + case ScholarBonusType::PRIM_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = NPrimarySkill::names[bonusID]; + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 80; + rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SECONDARY_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 50; + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SPELL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 20; + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::RANDOM: + break;// No-op + default: + logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName); + } + + reader->skipZero(6); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) +{ + auto * object = new CGGarrison(); + + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + readCreatureSet(object, 7); + if(features.levelAB) + object->removableUnits = reader->readBool(); + else + object->removableUnits = true; + + reader->skipZero(8); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + ArtifactID artID = ArtifactID::NONE; //random, set later + SpellID spellID = SpellID::NONE; + auto * object = new CGArtifact(); + + readMessageAndGuards(object->message, object, mapPosition); + + if(objectTemplate->id == Obj::SPELL_SCROLL) + { + spellID = reader->readSpell32(); + artID = ArtifactID::SPELL_SCROLL; + } + else if(objectTemplate->id == Obj::ARTIFACT) + { + //specific artifact + artID = ArtifactID(objectTemplate->subid); + } + + object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGResource(); + + readMessageAndGuards(object->message, object, mapPosition); + + object->amount = reader->readUInt32(); + if(GameResID(objectTemplate->subid) == GameResID(EGameResID::GOLD)) + { + // Gold is multiplied by 100. + object->amount *= 100; + } + reader->skipZero(4); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGMine(); + if(objectTemplate->subid < 7) + { + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + } + else + { + object->setOwner(PlayerColor::NEUTRAL); + reader->readBitmaskResources(object->abandonedMineResources, false); + } + return object; +} + +CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position) +{ + auto * object = new CGDwelling(); + setOwnerAndValidate(position, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGDwelling(); + + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + + object->randomizationInfo = CGDwellingRandomizationInfo(); + + bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL; + bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION; + + if (hasFactionInfo) + { + object->randomizationInfo->identifier = reader->readUInt32(); + + if(object->randomizationInfo->identifier == 0) + reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false); + } + else + object->randomizationInfo->allowedFactions.insert(FactionID(objectTemplate->subid)); + + if(hasLevelInfo) + { + object->randomizationInfo->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; + object->randomizationInfo->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; + } + else + { + object->randomizationInfo->minLevel = objectTemplate->subid; + object->randomizationInfo->maxLevel = objectTemplate->subid; + } + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr objectTemplate) +{ + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); + + SpellID spell = reader->readSpell32(); + + if(spell != SpellID::NONE) + { + JsonNode variable; + variable.String() = VLC->spells()->getById(spell)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + } + return object; +} + +CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) +{ + auto * object = new CGHeroPlaceholder(); + + setOwnerAndValidate(mapPosition, object, reader->readPlayer()); + + HeroTypeID htid = reader->readHero(); //hero type id + + if(htid.getNum() == -1) + { + object->powerRank = reader->readUInt8(); + logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); + } + else + { + object->heroType = htid; + logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + } + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if (objectTemplate->subid < 1000) + { + map->grailPos = mapPosition; + map->grailRadius = reader->readInt32(); + } + else + { + // Battle location for arena mode in HotA + logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); + } + return nullptr; +} + +CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) + return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); + + logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString()); + return new CGObjectInstance(); +} + +CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(objectTemplate->subid == 0) + return new CBank(); + + return new CGObjectInstance(); +} + +CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) +{ + auto * guard = new CGQuestGuard(); + readQuest(guard, mapPosition); + return guard; +} + +CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = readGeneric(mapPosition, objectTemplate); + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) +{ + auto * object = new CGLighthouse(); + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(features.levelHOTA3) + { + //TODO: HotA + // index of guards preset. -1 = random, 0-4 = index of possible guards settings + int32_t guardsPresetIndex = reader->readInt32(); + + // presence of upgraded stack: -1 = random, 0 = never, 1 = always + int8_t upgradedStackPresence = reader->readInt8(); + + assert(vstd::iswithin(guardsPresetIndex, -1, 4)); + assert(vstd::iswithin(upgradedStackPresence, -1, 1)); + + // list of possible artifacts in reward + // - if list is empty, artifacts are either not present in reward or random + // - if non-empty, then list always have same number of elements as number of artifacts in bank + // - ArtifactID::NONE indictates random artifact, other values indicate artifact that should be used as reward + std::vector artifacts; + int artNumber = reader->readUInt32(); + for(int yy = 0; yy < artNumber; ++yy) + { + artifacts.push_back(reader->readArtifact32()); + } + + if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty()) + logGlobal->warn( + "Map '%s: creature bank at %s settings %d %d %d are not implemented!", + mapName, + mapPosition.toString(), + guardsPresetIndex, + int(upgradedStackPresence), + artifacts.size() + ); + } + + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + switch(objectTemplate->id.toEnum()) + { + case Obj::EVENT: + return readEvent(mapPosition, objectInstanceID); + + case Obj::HERO: + case Obj::RANDOM_HERO: + case Obj::PRISON: + return readHero(mapPosition, objectInstanceID); + + case Obj::MONSTER: + case Obj::RANDOM_MONSTER: + case Obj::RANDOM_MONSTER_L1: + case Obj::RANDOM_MONSTER_L2: + case Obj::RANDOM_MONSTER_L3: + case Obj::RANDOM_MONSTER_L4: + case Obj::RANDOM_MONSTER_L5: + case Obj::RANDOM_MONSTER_L6: + case Obj::RANDOM_MONSTER_L7: + return readMonster(mapPosition, objectInstanceID); + + case Obj::OCEAN_BOTTLE: + case Obj::SIGN: + return readSign(mapPosition); + + case Obj::SEER_HUT: + return readSeerHut(mapPosition, objectInstanceID); + + case Obj::WITCH_HUT: + return readWitchHut(mapPosition, objectTemplate); + case Obj::SCHOLAR: + return readScholar(mapPosition, objectTemplate); + + case Obj::GARRISON: + case Obj::GARRISON2: + return readGarrison(mapPosition); + + case Obj::ARTIFACT: + case Obj::RANDOM_ART: + case Obj::RANDOM_TREASURE_ART: + case Obj::RANDOM_MINOR_ART: + case Obj::RANDOM_MAJOR_ART: + case Obj::RANDOM_RELIC_ART: + case Obj::SPELL_SCROLL: + return readArtifact(mapPosition, objectTemplate); + + case Obj::RANDOM_RESOURCE: + case Obj::RESOURCE: + return readResource(mapPosition, objectTemplate); + case Obj::RANDOM_TOWN: + case Obj::TOWN: + return readTown(mapPosition, objectTemplate); + + case Obj::MINE: + case Obj::ABANDONED_MINE: + return readMine(mapPosition, objectTemplate); + + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + return readDwelling(mapPosition); + + case Obj::SHRINE_OF_MAGIC_INCANTATION: + case Obj::SHRINE_OF_MAGIC_GESTURE: + case Obj::SHRINE_OF_MAGIC_THOUGHT: + return readShrine(mapPosition, objectTemplate); + + case Obj::PANDORAS_BOX: + return readPandora(mapPosition, objectInstanceID); + + case Obj::GRAIL: + return readGrail(mapPosition, objectTemplate); + + case Obj::RANDOM_DWELLING: + case Obj::RANDOM_DWELLING_LVL: + case Obj::RANDOM_DWELLING_FACTION: + return readDwellingRandom(mapPosition, objectTemplate); + + case Obj::QUEST_GUARD: + return readQuestGuard(mapPosition); + + case Obj::SHIPYARD: + return readShipyard(mapPosition, objectTemplate); + + case Obj::HERO_PLACEHOLDER: + return readHeroPlaceholder(mapPosition); + + case Obj::PYRAMID: + return readPyramid(mapPosition, objectTemplate); + + case Obj::LIGHTHOUSE: + return readLighthouse(mapPosition); + + case Obj::CREATURE_BANK: + case Obj::DERELICT_SHIP: + case Obj::DRAGON_UTOPIA: + case Obj::CRYPT: + case Obj::SHIPWRECK: + return readBank(mapPosition, objectTemplate); + + default: //any other object + return readGeneric(mapPosition, objectTemplate); + } +} + +void CMapLoaderH3M::readObjects() +{ + uint32_t objectsCount = reader->readUInt32(); + + for(uint32_t i = 0; i < objectsCount; ++i) + { + int3 mapPosition = reader->readInt3(); + + uint32_t defIndex = reader->readUInt32(); + ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast(map->objects.size())); + + std::shared_ptr objectTemplate = templates.at(defIndex); + reader->skipZero(5); + + CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID); + + if(!newObject) + continue; + + newObject->pos = mapPosition; + newObject->ID = objectTemplate->id; + newObject->id = objectInstanceID; + if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) + { + newObject->subID = objectTemplate->subid; + } + newObject->appearance = objectTemplate; + assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); + + if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) + logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString()); + + { + //TODO: define valid typeName and subtypeName for H3M maps + //boost::format fmt("%s_%d"); + //fmt % nobj->typeName % nobj->id.getNum(); + boost::format fmt("obj_%d"); + fmt % newObject->id.getNum(); + newObject->instanceName = fmt.str(); + } + map->addNewObject(newObject); + } + + std::sort( + map->heroesOnMap.begin(), + map->heroesOnMap.end(), + [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) + { + return a->subID < b->subID; + } + ); +} + +void CMapLoaderH3M::readCreatureSet(CCreatureSet * out, int number) +{ + for(int index = 0; index < number; ++index) + { + CreatureID creatureID = reader->readCreature(); + int count = reader->readUInt16(); + + // Empty slot + if(creatureID == CreatureID::NONE) + continue; + + auto * result = new CStackInstance(); + result->count = count; + + if(creatureID < CreatureID::NONE) + { + int value = -creatureID.getNum() - 2; + assert(value >= 0 && value < 14); + uint8_t level = value / 2; + uint8_t upgrade = value % 2; + + //this will happen when random object has random army + result->randomStack = CStackInstance::RandomStackInfo{level, upgrade}; + } + else + { + result->setType(creatureID); + } + + out->putStack(SlotID(index), result); + } + + out->validTypes(true); +} + +void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner) +{ + assert(owner.isValidPlayer() || owner == PlayerColor::NEUTRAL); + + if(owner == PlayerColor::NEUTRAL) + { + object->setOwner(PlayerColor::NEUTRAL); + return; + } + + if(!owner.isValidPlayer()) + { + object->setOwner(PlayerColor::NEUTRAL); + logGlobal->warn("Map '%s': Object at %s - owned by invalid player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum())); + return; + } + + if(!mapHeader->players[owner.getNum()].canAnyonePlay()) + { + object->setOwner(PlayerColor::NEUTRAL); + logGlobal->warn("Map '%s': Object at %s - owned by non-existing player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum()) + ); + return; + } + + object->setOwner(owner); +} + +CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + auto * object = new CGHeroInstance(); + + if(features.levelAB) + { + unsigned int identifier = reader->readUInt32(); + map->questIdentifierToId[identifier] = objectInstanceID; + } + + PlayerColor owner = reader->readPlayer(); + object->subID = reader->readHero().getNum(); + + //If hero of this type has been predefined, use that as a base. + //Instance data will overwrite the predefined values where appropriate. + for(auto & elem : map->predefinedHeroes) + { + if(elem->subID == object->subID) + { + logGlobal->debug("Hero %d will be taken from the predefined heroes list.", object->subID); + delete object; + object = elem; + break; + } + } + + setOwnerAndValidate(mapPosition, object, owner); + + for(auto & elem : map->disposedHeroes) + { + if(elem.heroId == object->getHeroType()) + { + object->nameCustomTextId = elem.name; + object->customPortraitSource = elem.portrait; + break; + } + } + + bool hasName = reader->readBool(); + if(hasName) + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroType().getNum(), "name")); + + if(features.levelSOD) + { + bool hasCustomExperience = reader->readBool(); + if(hasCustomExperience) + object->exp = reader->readUInt32(); + } + else + { + object->exp = reader->readUInt32(); + + //0 means "not set" in <=AB maps + if(!object->exp) + object->exp = CGHeroInstance::UNINITIALIZED_EXPERIENCE; + } + + bool hasPortrait = reader->readBool(); + if(hasPortrait) + object->customPortraitSource = reader->readHeroPortrait(); + + bool hasSecSkills = reader->readBool(); + if(hasSecSkills) + { + if(!object->secSkills.empty()) + { + if(object->secSkills[0].first != SecondarySkill::NONE) + logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); + object->secSkills.clear(); + } + + uint32_t skillsCount = reader->readUInt32(); + object->secSkills.resize(skillsCount); + for(int i = 0; i < skillsCount; ++i) + { + object->secSkills[i].first = reader->readSkill(); + object->secSkills[i].second = reader->readUInt8(); + } + } + + bool hasGarison = reader->readBool(); + if(hasGarison) + readCreatureSet(object, 7); + + object->formation = static_cast(reader->readUInt8()); + assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); + + loadArtifactsOfHero(object); + object->patrol.patrolRadius = reader->readUInt8(); + object->patrol.patrolling = (object->patrol.patrolRadius != 0xff); + + if(features.levelAB) + { + bool hasCustomBiography = reader->readBool(); + if(hasCustomBiography) + object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); + + object->gender = static_cast(reader->readUInt8()); + assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); + } + else + { + object->gender = EHeroGender::DEFAULT; + } + + // Spells + if(features.levelSOD) + { + bool hasCustomSpells = reader->readBool(); + if(hasCustomSpells) + { + if(!object->spells.empty()) + { + object->spells.clear(); + logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); + } + + reader->readBitmaskSpells(object->spells, false); + object->spells.insert(SpellID::PRESET); //placeholder "preset spells" + } + } + else if(features.levelAB) + { + //we can read one spell + SpellID spell = reader->readSpell(); + + if(spell != SpellID::NONE) + object->spells.insert(spell); + } + + if(features.levelSOD) + { + bool hasCustomPrimSkills = reader->readBool(); + if(hasCustomPrimSkills) + { + 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() ); + for(const auto & b : *ps) + object->removeBonus(b); + } + + for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx) + { + object->pushPrimSkill(static_cast(xx), reader->readUInt8()); + } + } + } + + 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()); + else + logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); + + reader->skipZero(16); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven) +{ + auto * hut = new CGSeerHut(); + + uint32_t questsCount = 1; + + if(features.levelHOTA3) + questsCount = reader->readUInt32(); + + //TODO: HotA + if(questsCount > 1) + logGlobal->warn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount); + + for(size_t i = 0; i < questsCount; ++i) + readSeerHutQuest(hut, position, idToBeGiven); + + if(features.levelHOTA3) + { + uint32_t repeateableQuestsCount = reader->readUInt32(); + hut->quest->repeatedQuest = repeateableQuestsCount != 0; + + if(repeateableQuestsCount != 0) + logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); + + for(size_t i = 0; i < repeateableQuestsCount; ++i) + readSeerHutQuest(hut, position, idToBeGiven); + } + + reader->skipZero(2); + + return hut; +} + +enum class ESeerHutRewardType : uint8_t +{ + NOTHING = 0, + EXPERIENCE = 1, + MANA_POINTS = 2, + MORALE = 3, + LUCK = 4, + RESOURCES = 5, + PRIMARY_SKILL = 6, + SECONDARY_SKILL = 7, + ARTIFACT = 8, + SPELL = 9, + CREATURE = 10, +}; + +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + +void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) +{ + EQuestMission missionType = EQuestMission::NONE; + if(features.levelAB) + { + missionType = static_cast(readQuest(hut, position)); + } + else + { + //RoE + auto artID = reader->readArtifact(); + if(artID != ArtifactID::NONE) + { + //not none quest + hut->quest->mission.artifacts.push_back(artID); + missionType = EQuestMission::ARTIFACT; + } + hut->quest->lastDay = -1; //no timeout + hut->quest->isCustomFirst = false; + hut->quest->isCustomNext = false; + hut->quest->isCustomComplete = false; + } + + if(missionType != EQuestMission::NONE) + { + auto rewardType = static_cast(reader->readUInt8()); + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + switch(rewardType) + { + case ESeerHutRewardType::NOTHING: + { + // no-op + break; + } + case ESeerHutRewardType::EXPERIENCE: + { + reward.heroExperience = reader->readUInt32(); + break; + } + case ESeerHutRewardType::MANA_POINTS: + { + reward.manaDiff = reader->readUInt32(); + break; + } + case ESeerHutRewardType::MORALE: + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + break; + } + case ESeerHutRewardType::LUCK: + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); + break; + } + case ESeerHutRewardType::RESOURCES: + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt32(); + + assert(rId < features.resourcesCount); + + reward.resources[rId] = rVal; + break; + } + case ESeerHutRewardType::PRIMARY_SKILL: + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt8(); + + reward.primary.at(rId) = rVal; + break; + } + case ESeerHutRewardType::SECONDARY_SKILL: + { + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; + break; + } + case ESeerHutRewardType::ARTIFACT: + { + reward.artifacts.push_back(reader->readArtifact()); + break; + } + case ESeerHutRewardType::SPELL: + { + reward.spells.push_back(reader->readSpell()); + break; + } + case ESeerHutRewardType::CREATURE: + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + break; + } + default: + { + assert(0); + } + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + hut->configuration.info.push_back(vinfo); + } + else + { + // missionType==255 + reader->skipZero(1); + } +} + +int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +{ + auto missionId = reader->readUInt8(); + + switch(static_cast(missionId)) + { + case EQuestMission::NONE: + return missionId; + case EQuestMission::PRIMARY_SKILL: + { + for(int x = 0; x < 4; ++x) + { + guard->quest->mission.primary[x] = reader->readUInt8(); + } + break; + } + case EQuestMission::LEVEL: + { + guard->quest->mission.heroLevel = reader->readUInt32(); + break; + } + case EQuestMission::KILL_HERO: + case EQuestMission::KILL_CREATURE: + { + guard->quest->killTarget = ObjectInstanceID(reader->readUInt32()); + break; + } + case EQuestMission::ARTIFACT: + { + int artNumber = reader->readUInt8(); + for(int yy = 0; yy < artNumber; ++yy) + { + auto artid = reader->readArtifact(); + guard->quest->mission.artifacts.push_back(artid); + map->allowedArtifact.erase(artid); //these are unavailable for random generation + } + break; + } + case EQuestMission::ARMY: + { + int typeNumber = reader->readUInt8(); + guard->quest->mission.creatures.resize(typeNumber); + for(int hh = 0; hh < typeNumber; ++hh) + { + guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature(); + guard->quest->mission.creatures[hh].count = reader->readUInt16(); + } + break; + } + case EQuestMission::RESOURCES: + { + for(int x = 0; x < 7; ++x) + guard->quest->mission.resources[x] = reader->readUInt32(); + + break; + } + case EQuestMission::HERO: + { + guard->quest->mission.heroes.push_back(reader->readHero()); + break; + } + case EQuestMission::PLAYER: + { + guard->quest->mission.players.push_back(reader->readPlayer()); + break; + } + case EQuestMission::HOTA_MULTI: + { + uint32_t missionSubID = reader->readUInt32(); + + if(missionSubID == 0) + { + missionId = int(EQuestMission::HOTA_HERO_CLASS); + std::set heroClasses; + reader->readBitmaskHeroClassesSized(heroClasses, false); + for(auto & hc : heroClasses) + guard->quest->mission.heroClasses.push_back(hc); + break; + } + if(missionSubID == 1) + { + missionId = int(EQuestMission::HOTA_REACH_DATE); + guard->quest->mission.daysPassed = reader->readUInt32() + 1; + break; + } + break; + } + default: + { + assert(0); + } + } + + guard->quest->lastDay = reader->readInt32(); + guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit"))); + guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit"))); + guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed"))); + guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); + guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); + guard->quest->isCustomComplete = !guard->quest->completedText.empty(); + return missionId; +} + +CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) +{ + auto * object = new CGTownInstance(); + if(features.levelAB) + object->identifier = reader->readUInt32(); + + setOwnerAndValidate(position, object, reader->readPlayer()); + + std::optional faction; + if (objectTemplate->id == Obj::TOWN) + faction = FactionID(objectTemplate->subid); + + bool hasName = reader->readBool(); + if(hasName) + object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); + + bool hasGarrison = reader->readBool(); + if(hasGarrison) + readCreatureSet(object, 7); + + object->formation = static_cast(reader->readUInt8()); + assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); + + bool hasCustomBuildings = reader->readBool(); + if(hasCustomBuildings) + { + reader->readBitmaskBuildings(object->builtBuildings, faction); + reader->readBitmaskBuildings(object->forbiddenBuildings, faction); + } + // Standard buildings + else + { + bool hasFort = reader->readBool(); + if(hasFort) + object->builtBuildings.insert(BuildingID::FORT); + + //means that set of standard building should be included + object->builtBuildings.insert(BuildingID::DEFAULT); + } + + if(features.levelAB) + { + std::set spellsMask; + + reader->readBitmaskSpells(spellsMask, false); + std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->obligatorySpells)); + } + + { + std::set spellsMask; + + reader->readBitmaskSpells(spellsMask, true); + std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells)); + + auto defaultAllowed = VLC->spellh->getDefaultAllowed(); + + //add all spells from mods + for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) + if(defaultAllowed.count(i)) + object->possibleSpells.emplace_back(i); + } + + if(features.levelHOTA1) + { + // TODO: HOTA support + [[maybe_unused]] bool spellResearchAvailable = reader->readBool(); + } + + // Read castle events + uint32_t eventsCount = reader->readUInt32(); + + for(int eventID = 0; eventID < eventsCount; ++eventID) + { + CCastleEvent event; + event.name = readBasicString(); + event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); + + reader->readResourses(event.resources); + + event.players = reader->readUInt8(); + if(features.levelSOD) + event.humanAffected = reader->readBool(); + else + event.humanAffected = true; + + event.computerAffected = reader->readUInt8(); + event.firstOccurence = reader->readUInt16(); + event.nextOccurence = reader->readUInt8(); + + reader->skipZero(17); + + // New buildings + reader->readBitmaskBuildings(event.buildings, faction); + + event.creatures.resize(7); + for(int i = 0; i < 7; ++i) + event.creatures[i] = reader->readUInt16(); + + reader->skipZero(4); + object->events.push_back(event); + } + + if(features.levelSOD) + { + object->alignmentToPlayer = PlayerColor::NEUTRAL; // "same as owner or random" + + uint8_t alignment = reader->readUInt8(); + + if(alignment != 255) + { + if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) + { + 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)); + } + else + { + // TODO: HOTA support + uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum(); + + 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()); + } + else + { + logGlobal->warn("%s - Aligment of town at %s is corrupted!!", mapName, position.toString()); + } + } + } + } + reader->skipZero(3); + + return object; +} + +void CMapLoaderH3M::readEvents() +{ + uint32_t eventsCount = reader->readUInt32(); + for(int eventID = 0; eventID < eventsCount; ++eventID) + { + CMapEvent event; + event.name = readBasicString(); + event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); + + reader->readResourses(event.resources); + event.players = reader->readUInt8(); + if(features.levelSOD) + { + event.humanAffected = reader->readBool(); + } + else + { + event.humanAffected = true; + } + event.computerAffected = reader->readBool(); + event.firstOccurence = reader->readUInt16(); + event.nextOccurence = reader->readUInt8(); + + reader->skipZero(17); + + map->events.push_back(event); + } +} + +void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position) +{ + bool hasMessage = reader->readBool(); + if(hasMessage) + { + message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message"))); + bool hasGuards = reader->readBool(); + if(hasGuards) + readCreatureSet(guards, 7); + + reader->skipZero(4); + } +} + +std::string CMapLoaderH3M::readBasicString() +{ + return TextOperations::toUnicode(reader->readBaseString(), fileEncoding); +} + +std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIdentifier) +{ + std::string mapString = TextOperations::toUnicode(reader->readBaseString(), fileEncoding); + TextIdentifier fullIdentifier("map", mapName, stringIdentifier.get()); + + if(mapString.empty()) + return ""; + + return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString); +} + +void CMapLoaderH3M::afterRead() +{ + //convert main town positions for all players to actual object position, in H3M it is position of active tile + + for(auto & p : map->players) + { + int3 posOfMainTown = p.posOfMainTown; + if(posOfMainTown.valid() && map->isInTheMap(posOfMainTown)) + { + const TerrainTile & t = map->getTile(posOfMainTown); + + const CGObjectInstance * mainTown = nullptr; + + for(auto * obj : t.visitableObjects) + { + if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN) + { + mainTown = obj; + break; + } + } + + if(mainTown == nullptr) + continue; + + p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset(); + } + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index c0407a35a..d1c1591c6 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -1,253 +1,254 @@ -/* - * MapFormatH3M.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 "CMapService.h" -#include "MapFeaturesH3M.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class MapReaderH3M; -class CArtifactInstance; -class CGObjectInstance; -class CGSeerHut; -class IQuestObject; -class CGTownInstance; -class CCreatureSet; -class CInputStream; -class TextIdentifier; -class CGPandoraBox; - -class ObjectInstanceID; -class BuildingID; -class ObjectTemplate; -class SpellID; -class PlayerColor; -class int3; - -class DLL_LINKAGE CMapLoaderH3M : public IMapLoader -{ -public: - /** - * Default constructor. - * - * @param stream a stream containing the map data - */ - CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream); - - /** - * Destructor. - */ - ~CMapLoaderH3M(); - - /** - * Loads the VCMI/H3 map file. - * - * @return a unique ptr of the loaded map class - */ - std::unique_ptr loadMap() override; - - /** - * Loads the VCMI/H3 map header. - * - * @return a unique ptr of the loaded map header class - */ - std::unique_ptr loadMapHeader() override; - -private: - /** - * Initializes the map object from parsing the input buffer. - */ - void init(); - - /** - * Reads the map header. - */ - void readHeader(); - - /** - * Reads player information. - */ - void readPlayerInfo(); - - /** - * Reads victory/loss conditions. - */ - void readVictoryLossConditions(); - - /** - * Reads team information. - */ - void readTeamInfo(); - - /** - * Reads the list of map flags. - */ - void readMapOptions(); - - /** - * Reads the list of allowed heroes. - */ - void readAllowedHeroes(); - - /** - * Reads the list of disposed heroes. - */ - void readDisposedHeroes(); - - /** - * Reads the list of allowed artifacts. - */ - void readAllowedArtifacts(); - - /** - * Reads the list of allowed spells and abilities. - */ - void readAllowedSpellsAbilities(); - - /** - * Loads artifacts of a hero. - * - * @param hero the hero which should hold those artifacts - */ - void loadArtifactsOfHero(CGHeroInstance * hero); - - /** - * Loads an artifact to the given slot of the specified hero. - * - * @param hero the hero which should hold that artifact - * @param slot the artifact slot where to place that artifact - * @return true if it loaded an artifact - */ - bool loadArtifactToSlot(CGHeroInstance * hero, int slot); - - /** - * Read rumors. - */ - void readRumors(); - - /** - * Reads predefined heroes. - */ - void readPredefinedHeroes(); - - /** - * Reads terrain data. - */ - void readTerrain(); - - /** - * Reads custom(map) def information. - */ - void readObjectTemplates(); - - /** - * Reads objects(towns, mines,...). - */ - void readObjects(); - - /// Reads single object from input stream based on template - CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - - CGObjectInstance * readEvent(const int3 & objectPosition); - CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readHero(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readSeerHut(const int3 & initialPos); - CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readSign(const int3 & position); - CGObjectInstance * readWitchHut(); - CGObjectInstance * readScholar(); - CGObjectInstance * readGarrison(const int3 & mapPosition); - CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readPandora(const int3 & position); - CGObjectInstance * readDwelling(const int3 & position); - CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readShrine(); - CGObjectInstance * readHeroPlaceholder(const int3 & position); - CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); - 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 * readGeneric(const int3 & position, std::shared_ptr objectTemplate); - CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); - - /** - * Reads a creature set. - * - * @param out the loaded creature set - * @param number the count of creatures to read - */ - void readCreatureSet(CCreatureSet * out, int number); - - /** - * Reads a quest for the given quest guard. - * - * @param guard the quest guard where that quest should be applied to - */ - void readBoxContent(CGPandoraBox * object, const int3 & position); - - /** - * Reads a quest for the given quest guard. - * - * @param guard the quest guard where that quest should be applied to - */ - void readQuest(IQuestObject * guard, const int3 & position); - - void readSeerHutQuest(CGSeerHut * hut, const int3 & position); - - /** - * Reads events. - */ - void readEvents(); - - /** - * read optional message and optional guards - */ - void readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position); - - /// reads string from input stream and converts it to unicode - std::string readBasicString(); - - /// reads string from input stream, converts it to unicode and attempts to translate it - std::string readLocalizedString(const TextIdentifier & identifier); - - void setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner); - - void afterRead(); - - MapFormatFeaturesH3M features; - - /** List of templates loaded from the map, used on later stage to create - * objects but not needed for fully functional CMap */ - std::vector> templates; - - /** ptr to the map object which gets filled by data from the buffer */ - CMap * map; - - /** - * ptr to the map header object which gets filled by data from the buffer. - * (when loading a map then the mapHeader ptr points to the same object) - */ - std::unique_ptr mapHeader; - std::unique_ptr reader; - CInputStream * inputStream; - - std::string mapName; - std::string modName; - std::string fileEncoding; - -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatH3M.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 "CMapService.h" +#include "MapFeaturesH3M.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class MapReaderH3M; +class MetaString; +class CArtifactInstance; +class CGObjectInstance; +class CGSeerHut; +class IQuestObject; +class CGTownInstance; +class CCreatureSet; +class CInputStream; +class TextIdentifier; +class CGPandoraBox; + +class ObjectInstanceID; +class BuildingID; +class ObjectTemplate; +class SpellID; +class PlayerColor; +class int3; + +class DLL_LINKAGE CMapLoaderH3M : public IMapLoader +{ +public: + /** + * Default constructor. + * + * @param stream a stream containing the map data + */ + CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream); + + /** + * Destructor. + */ + ~CMapLoaderH3M(); + + /** + * Loads the VCMI/H3 map file. + * + * @return a unique ptr of the loaded map class + */ + std::unique_ptr loadMap() override; + + /** + * Loads the VCMI/H3 map header. + * + * @return a unique ptr of the loaded map header class + */ + std::unique_ptr loadMapHeader() override; + +private: + /** + * Initializes the map object from parsing the input buffer. + */ + void init(); + + /** + * Reads the map header. + */ + void readHeader(); + + /** + * Reads player information. + */ + void readPlayerInfo(); + + /** + * Reads victory/loss conditions. + */ + void readVictoryLossConditions(); + + /** + * Reads team information. + */ + void readTeamInfo(); + + /** + * Reads the list of map flags. + */ + void readMapOptions(); + + /** + * Reads the list of allowed heroes. + */ + void readAllowedHeroes(); + + /** + * Reads the list of disposed heroes. + */ + void readDisposedHeroes(); + + /** + * Reads the list of allowed artifacts. + */ + void readAllowedArtifacts(); + + /** + * Reads the list of allowed spells and abilities. + */ + void readAllowedSpellsAbilities(); + + /** + * Loads artifacts of a hero. + * + * @param hero the hero which should hold those artifacts + */ + void loadArtifactsOfHero(CGHeroInstance * hero); + + /** + * Loads an artifact to the given slot of the specified hero. + * + * @param hero the hero which should hold that artifact + * @param slot the artifact slot where to place that artifact + * @return true if it loaded an artifact + */ + bool loadArtifactToSlot(CGHeroInstance * hero, int slot); + + /** + * Read rumors. + */ + void readRumors(); + + /** + * Reads predefined heroes. + */ + void readPredefinedHeroes(); + + /** + * Reads terrain data. + */ + void readTerrain(); + + /** + * Reads custom(map) def information. + */ + void readObjectTemplates(); + + /** + * Reads objects(towns, mines,...). + */ + void readObjects(); + + /// Reads single object from input stream based on template + CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + + CGObjectInstance * readEvent(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readHero(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readSign(const int3 & position); + CGObjectInstance * readWitchHut(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readScholar(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readGarrison(const int3 & mapPosition); + CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readDwelling(const int3 & position); + CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readShrine(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readHeroPlaceholder(const int3 & position); + CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readQuestGuard(const int3 & position); + CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate); + CGObjectInstance * readLighthouse(const int3 & mapPosition); + CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); + + /** + * Reads a creature set. + * + * @param out the loaded creature set + * @param number the count of creatures to read + */ + void readCreatureSet(CCreatureSet * out, int number); + + /** + * Reads a quest for the given quest guard. + * + * @param guard the quest guard where that quest should be applied to + */ + void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); + + /** + * Reads a quest for the given quest guard. + * + * @param guard the quest guard where that quest should be applied to + */ + int readQuest(IQuestObject * guard, const int3 & position); + + void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); + + /** + * Reads events. + */ + void readEvents(); + + /** + * read optional message and optional guards + */ + void readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position); + + /// reads string from input stream and converts it to unicode + std::string readBasicString(); + + /// reads string from input stream, converts it to unicode and attempts to translate it + std::string readLocalizedString(const TextIdentifier & identifier); + + void setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner); + + void afterRead(); + + MapFormatFeaturesH3M features; + + /** List of templates loaded from the map, used on later stage to create + * objects but not needed for fully functional CMap */ + std::vector> templates; + + /** ptr to the map object which gets filled by data from the buffer */ + CMap * map; + + /** + * ptr to the map header object which gets filled by data from the buffer. + * (when loading a map then the mapHeader ptr points to the same object) + */ + std::unique_ptr mapHeader; + std::unique_ptr reader; + CInputStream * inputStream; + + std::string mapName; + std::string modName; + std::string fileEncoding; + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 3d59ae8b6..7f6059400 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1,1424 +1,1370 @@ -/* - * MapFormatJson.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 "MapFormatJson.h" - -#include "../filesystem/CInputStream.h" -#include "../filesystem/COutputStream.h" -#include "../JsonDetail.h" -#include "CMap.h" -#include "MapFormat.h" -#include "../ArtifactUtils.h" -#include "../CModHandler.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../VCMI_Lib.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../mapObjectConstructors/AObjectTypeHandler.h" -#include "../mapObjectConstructors/CObjectClassesHandler.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../mapObjects/CGTownInstance.h" -#include "../spells/CSpellHandler.h" -#include "../CSkillHandler.h" -#include "../StringConstants.h" -#include "../serializer/JsonDeserializer.h" -#include "../serializer/JsonSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class MapObjectResolver: public IInstanceResolver -{ -public: - MapObjectResolver(const CMapFormatJson * owner_); - - si32 decode (const std::string & identifier) const override; - std::string encode(si32 identifier) const override; - -private: - const CMapFormatJson * owner; -}; - -MapObjectResolver::MapObjectResolver(const CMapFormatJson * owner_): - owner(owner_) -{ - -} - -si32 MapObjectResolver::decode(const std::string & identifier) const -{ - //always decode as ObjectInstanceID - - auto it = owner->map->instanceNames.find(identifier); - - if(it != owner->map->instanceNames.end()) - { - return (*it).second->id.getNum(); - } - else - { - logGlobal->error("Object not found: %s", identifier); - return -1; - } -} - -std::string MapObjectResolver::encode(si32 identifier) const -{ - ObjectInstanceID id; - - //use h3m questIdentifiers if they are present - if(owner->map->questIdentifierToId.empty()) - { - id = ObjectInstanceID(identifier); - } - else - { - id = owner->map->questIdentifierToId[identifier]; - } - - si32 oid = id.getNum(); - if(oid < 0 || oid >= owner->map->objects.size()) - { - logGlobal->error("Cannot get object with id %d", oid); - return ""; - } - - return owner->map->objects[oid]->instanceName; -} - -namespace HeaderDetail -{ - static const std::vector difficultyMap = - { - "EASY", - "NORMAL", - "HARD", - "EXPERT", - "IMPOSSIBLE" - }; - - enum class ECanPlay - { - NONE = 0, - PLAYER_OR_AI = 1, - PLAYER_ONLY = 2, - AI_ONLY = 3 - }; - - static const std::vector canPlayMap = - { - "", - "PlayerOrAI", - "PlayerOnly", - "AIOnly" - }; -} - -namespace TriggeredEventsDetail -{ - static const std::array conditionNames = - { - "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", - "control", "destroy", "transport", "daysPassed", - "isHuman", "daysWithoutTown", "standardWin", "constValue", - - "have_0", "haveBuilding_0", "destroy_0" - }; - - static const std::array typeNames = { "victory", "defeat" }; - - static EMetaclass decodeMetaclass(const std::string & source) - { - if(source.empty()) - return EMetaclass::INVALID; - auto rawId = vstd::find_pos(NMetaclass::names, source); - - if(rawId >= 0) - return static_cast(rawId); - else - return EMetaclass::INVALID; - } - - static std::string encodeIdentifier(EMetaclass metaType, si32 type) - { - std::string metaclassName = NMetaclass::names[static_cast(metaType)]; - std::string identifier; - - switch(metaType) - { - case EMetaclass::ARTIFACT: - { - identifier = ArtifactID::encode(type); - } - break; - case EMetaclass::CREATURE: - { - identifier = CreatureID::encode(type); - } - break; - case EMetaclass::OBJECT: - { - //TODO - std::set subtypes = VLC->objtypeh->knownSubObjects(type); - if(!subtypes.empty()) - { - si32 subtype = *subtypes.begin(); - auto handler = VLC->objtypeh->getHandlerFor(type, subtype); - identifier = handler->getTypeName(); - } - } - break; - case EMetaclass::RESOURCE: - { - identifier = GameConstants::RESOURCE_NAMES[type]; - } - break; - default: - { - logGlobal->error("Unsupported metaclass %s for event condition", metaclassName); - return ""; - } - break; - } - - return CModHandler::makeFullIdentifier("", metaclassName, identifier); - } - - static EventCondition JsonToCondition(const JsonNode & node) - { - EventCondition event; - - const auto & conditionName = node.Vector()[0].String(); - - auto pos = vstd::find_pos(conditionNames, conditionName); - - event.condition = static_cast(pos); - - if (node.Vector().size() > 1) - { - const JsonNode & data = node.Vector()[1]; - - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - std::string fullIdentifier = data["type"].String(); - std::string metaTypeName; - std::string scope; - std::string identifier; - CModHandler::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - event.metaType = decodeMetaclass(metaTypeName); - - auto type = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), fullIdentifier, false); - - if(type) - event.objectType = type.value(); - event.objectInstanceName = data["object"].String(); - if(data["value"].isNumber()) - event.value = static_cast(data["value"].Integer()); - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) - { - auto identifier = VLC->modh->identifiers.getIdentifier(data["type"]); - if(identifier) - event.objectType = identifier.value(); - else - throw std::runtime_error("Identifier resolution failed in event condition"); - } - - if (data["type"].isNumber()) - event.objectType = static_cast(data["type"].Float()); - - if (!data["value"].isNull()) - event.value = static_cast(data["value"].Float()); - } - break; - } - - if (!data["position"].isNull()) - { - const auto & position = data["position"].Vector(); - event.position.x = static_cast(position.at(0).Float()); - event.position.y = static_cast(position.at(1).Float()); - event.position.z = static_cast(position.at(2).Float()); - } - } - return event; - } - - static JsonNode ConditionToJson(const EventCondition & event) - { - JsonNode json; - - JsonVector & asVector = json.Vector(); - - JsonNode condition; - condition.String() = conditionNames.at(event.condition); - asVector.push_back(condition); - - JsonNode data; - - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - if(event.metaType != EMetaclass::INVALID) - data["type"].String() = encodeIdentifier(event.metaType, event.objectType); - - if(event.value > 0) - data["value"].Integer() = event.value; - - if(!event.objectInstanceName.empty()) - data["object"].String() = event.objectInstanceName; - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if(event.objectType != -1) - data["type"].Integer() = event.objectType; - - if(event.value != -1) - data["value"].Integer() = event.value; - } - break; - } - - if(event.position != int3(-1, -1, -1)) - { - auto & position = data["position"].Vector(); - position.resize(3); - position[0].Float() = event.position.x; - position[1].Float() = event.position.y; - position[2].Float() = event.position.z; - } - - if(!data.isNull()) - asVector.push_back(data); - - return json; - } -}//namespace TriggeredEventsDetail - -namespace TerrainDetail -{ - static const std::array flipCodes = - { - '_', '-', '|', '+' - }; -} - -///CMapFormatJson -const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 1; - -const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; -const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; - -CMapFormatJson::CMapFormatJson(): - fileVersionMajor(0), fileVersionMinor(0), - mapObjectResolver(std::make_unique(this)), - map(nullptr), mapHeader(nullptr) -{ - -} - -TerrainType * CMapFormatJson::getTerrainByCode(const std::string & code) -{ - for(const auto & object : VLC->terrainTypeHandler->objects) - { - if(object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -RiverType * CMapFormatJson::getRiverByCode(const std::string & code) -{ - for(const auto & object : VLC->riverTypeHandler->objects) - { - if (object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -RoadType * CMapFormatJson::getRoadByCode(const std::string & code) -{ - for(const auto & object : VLC->roadTypeHandler->objects) - { - if (object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const -{ - //TODO: unify allowed factions with others - make them std::vector - - std::vector temp; - temp.resize(VLC->townh->size(), false); - auto standard = VLC->townh->getDefaultAllowed(); - - if(handler.saving) - { - for(auto faction : VLC->townh->objects) - if(faction->town && vstd::contains(value, faction->getIndex())) - temp[static_cast(faction->getIndex())] = true; - } - - handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, standard, temp); - - if(!handler.saving) - { - value.clear(); - for (std::size_t i=0; i(i)); - } -} - -void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) -{ - handler.serializeString("name", mapHeader->name); - handler.serializeString("description", mapHeader->description); - handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); - - //todo: support arbitrary percentage - handler.serializeEnum("difficulty", mapHeader->difficulty, HeaderDetail::difficultyMap); - - serializePlayerInfo(handler); - - handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes); - -// handler.serializeString("victoryString", mapHeader->victoryMessage); - handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); - -// handler.serializeString("defeatString", mapHeader->defeatMessage); - handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); -} - -void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) -{ - auto playersData = handler.enterStruct("players"); - - for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) - { - PlayerInfo & info = mapHeader->players[player]; - - if(handler.saving) - { - if(!info.canAnyonePlay()) - continue; - } - - auto playerData = handler.enterStruct(GameConstants::PLAYER_COLOR_NAMES[player]); - - if(!handler.saving) - { - if(handler.getCurrent().isNull()) - { - info.canComputerPlay = false; - info.canHumanPlay = false; - continue; - } - } - - serializeAllowedFactions(handler, info.allowedFactions); - - HeaderDetail::ECanPlay canPlay = HeaderDetail::ECanPlay::NONE; - - if(handler.saving) - { - if(info.canComputerPlay) - { - canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_OR_AI : HeaderDetail::ECanPlay::AI_ONLY; - } - else - { - canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_ONLY : HeaderDetail::ECanPlay::NONE; - } - } - - handler.serializeEnum("canPlay", canPlay, HeaderDetail::canPlayMap); - - if(!handler.saving) - { - switch(canPlay) - { - case HeaderDetail::ECanPlay::PLAYER_OR_AI: - info.canComputerPlay = true; - info.canHumanPlay = true; - break; - case HeaderDetail::ECanPlay::PLAYER_ONLY: - info.canComputerPlay = false; - info.canHumanPlay = true; - break; - case HeaderDetail::ECanPlay::AI_ONLY: - info.canComputerPlay = true; - info.canHumanPlay = false; - break; - default: - info.canComputerPlay = false; - info.canHumanPlay = false; - break; - } - } - - //saving whole structure only if position is valid - if(!handler.saving || info.posOfMainTown.valid()) - { - auto mainTown = handler.enterStruct("mainTown"); - handler.serializeBool("generateHero", info.generateHeroAtMainTown); - handler.serializeInt("x", info.posOfMainTown.x, -1); - handler.serializeInt("y", info.posOfMainTown.y, -1); - handler.serializeInt("l", info.posOfMainTown.z, -1); - } - if(!handler.saving) - { - info.hasMainTown = info.posOfMainTown.valid(); - } - - handler.serializeString("mainHero", info.mainHeroInstance);//must be before "heroes" - - //heroes - if(handler.saving) - { - //ignoring heroesNames and saving from actual map objects - //TODO: optimize - for(auto & obj : map->objects) - { - if((obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO) && obj->tempOwner == PlayerColor(player)) - { - auto * hero = dynamic_cast(obj.get()); - - auto heroes = handler.enterStruct("heroes"); - if(hero) - { - auto heroData = handler.enterStruct(hero->instanceName); - heroData->serializeString("name", hero->nameCustom); - - if(hero->ID == Obj::HERO) - { - std::string temp; - if(hero->type) - { - temp = hero->type->getJsonKey(); - } - else - { - temp = VLC->heroh->objects[hero->subID]->getJsonKey(); - } - handler.serializeString("type", temp); - } - } - } - } - } - else - { - info.heroesNames.clear(); - - auto heroes = handler.enterStruct("heroes"); - - for(const auto & hero : handler.getCurrent().Struct()) - { - const JsonNode & data = hero.second; - const std::string instanceName = hero.first; - - SHeroName hname; - hname.heroId = -1; - std::string rawId = data["type"].String(); - - if(!rawId.empty()) - hname.heroId = HeroTypeID::decode(rawId); - - hname.heroName = data["name"].String(); - - if(instanceName == info.mainHeroInstance) - { - //this is main hero - info.mainCustomHeroName = hname.heroName; - info.hasRandomHero = (hname.heroId == -1); - info.mainCustomHeroId = hname.heroId; - info.mainCustomHeroPortrait = -1; - //todo:mainHeroPortrait - } - - info.heroesNames.push_back(hname); - } - } - - handler.serializeBool("randomFaction", info.isFactionRandom); - } -} - -void CMapFormatJson::readTeams(JsonDeserializer & handler) -{ - auto teams = handler.enterArray("teams"); - const JsonNode & src = teams->getCurrent(); - - if(src.getType() != JsonNode::JsonType::DATA_VECTOR) - { - // No alliances - if(src.getType() != JsonNode::JsonType::DATA_NULL) - logGlobal->error("Invalid teams field type"); - - mapHeader->howManyTeams = 0; - for(auto & player : mapHeader->players) - if(player.canAnyonePlay()) - player.team = TeamID(mapHeader->howManyTeams++); - } - else - { - const JsonVector & srcVector = src.Vector(); - mapHeader->howManyTeams = static_cast(srcVector.size()); - - for(int team = 0; team < mapHeader->howManyTeams; team++) - for(const JsonNode & playerData : srcVector[team].Vector()) - { - PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); - if(player.isValidPlayer()) - if(mapHeader->players[player.getNum()].canAnyonePlay()) - mapHeader->players[player.getNum()].team = TeamID(team); - } - - for(PlayerInfo & player : mapHeader->players) - if(player.canAnyonePlay() && player.team == TeamID::NO_TEAM) - player.team = TeamID(mapHeader->howManyTeams++); - } -} - -void CMapFormatJson::writeTeams(JsonSerializer & handler) -{ - std::vector> teamsData; - - teamsData.resize(mapHeader->howManyTeams); - - //get raw data - for(int idx = 0; idx < mapHeader->players.size(); idx++) - { - const PlayerInfo & player = mapHeader->players.at(idx); - int team = player.team.getNum(); - if(vstd::iswithin(team, 0, mapHeader->howManyTeams-1) && player.canAnyonePlay()) - teamsData.at(team).insert(PlayerColor(idx)); - } - - //remove single-member teams - vstd::erase_if(teamsData, [](std::set & elem) -> bool - { - return elem.size() <= 1; - }); - - if(!teamsData.empty()) - { - JsonNode dest; - - //construct output - dest.setType(JsonNode::JsonType::DATA_VECTOR); - - for(const std::set & teamData : teamsData) - { - JsonNode team(JsonNode::JsonType::DATA_VECTOR); - for(const PlayerColor & player : teamData) - { - JsonNode member(JsonNode::JsonType::DATA_STRING); - member.String() = GameConstants::PLAYER_COLOR_NAMES[player.getNum()]; - team.Vector().push_back(std::move(member)); - } - dest.Vector().push_back(std::move(team)); - } - handler.serializeRaw("teams", dest, std::nullopt); - } -} - -void CMapFormatJson::readTriggeredEvents(JsonDeserializer & handler) -{ - const JsonNode & input = handler.getCurrent(); - - mapHeader->triggeredEvents.clear(); - - for(const auto & entry : input["triggeredEvents"].Struct()) - { - TriggeredEvent event; - event.identifier = entry.first; - readTriggeredEvent(event, entry.second); - mapHeader->triggeredEvents.push_back(event); - } -} - -void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const -{ - using namespace TriggeredEventsDetail; - - event.onFulfill.jsonDeserialize(source["message"]); - event.description.jsonDeserialize(source["description"]); - event.effect.type = vstd::find_pos(typeNames, source["effect"]["type"].String()); - event.effect.toOtherMessage.jsonDeserialize(source["effect"]["messageToSend"]); - event.trigger = EventExpression(source["condition"], JsonToCondition); // logical expression -} - -void CMapFormatJson::writeTriggeredEvents(JsonSerializer & handler) -{ - JsonNode triggeredEvents(JsonNode::JsonType::DATA_STRUCT); - - for(const auto & event : mapHeader->triggeredEvents) - writeTriggeredEvent(event, triggeredEvents[event.identifier]); - - handler.serializeRaw("triggeredEvents", triggeredEvents, std::nullopt); -} - -void CMapFormatJson::writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const -{ - using namespace TriggeredEventsDetail; - - if(!event.onFulfill.empty()) - event.onFulfill.jsonSerialize(dest["message"]); - - if(!event.description.empty()) - event.description.jsonSerialize(dest["description"]); - - dest["effect"]["type"].String() = typeNames.at(static_cast(event.effect.type)); - - if(!event.effect.toOtherMessage.empty()) - event.description.jsonSerialize(dest["effect"]["messageToSend"]); - - dest["condition"] = event.trigger.toJson(ConditionToJson); -} - -void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) -{ - auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format - - const JsonNode & data = handler.getCurrent(); - - for(const auto & entry : data.Struct()) - { - HeroTypeID type(HeroTypeID::decode(entry.first)); - - ui8 mask = 0; - - for(const JsonNode & playerData : entry.second["availableFor"].Vector()) - { - PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); - if(player.isValidPlayer()) - { - mask |= 1 << player.getNum(); - } - } - - if(mask != 0 && mask != GameConstants::ALL_PLAYERS && type.getNum() >= 0) - { - DisposedHero hero; - - hero.heroId = type.getNum(); - hero.players = mask; - //name and portrait are not used - - map->disposedHeroes.push_back(hero); - } - } -} - -void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) -{ - if(map->disposedHeroes.empty()) - return; - - auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format - - for(const DisposedHero & hero : map->disposedHeroes) - { - std::string type = HeroTypeID::encode(hero.heroId); - - auto definition = definitions->enterStruct(type); - - JsonNode players(JsonNode::JsonType::DATA_VECTOR); - - for(int playerNum = 0; playerNum < PlayerColor::PLAYER_LIMIT_I; playerNum++) - { - if((1 << playerNum) & hero.players) - { - JsonNode player(JsonNode::JsonType::DATA_STRING); - player.String() = GameConstants::PLAYER_COLOR_NAMES[playerNum]; - players.Vector().push_back(player); - } - } - definition->serializeRaw("availableFor", players, std::nullopt); - } -} - -void CMapFormatJson::serializeRumors(JsonSerializeFormat & handler) -{ - auto rumors = handler.enterArray("rumors"); - rumors.serializeStruct(map->rumors); -} - -void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) -{ - //todo:serializePredefinedHeroes - - if(handler.saving) - { - if(!map->predefinedHeroes.empty()) - { - auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - - for(auto & hero : map->predefinedHeroes) - { - auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); - - hero->serializeJsonDefinition(handler); - } - } - } - else - { - auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - - const JsonNode & data = handler.getCurrent(); - - for(const auto & p : data.Struct()) - { - auto predefinedHero = handler.enterStruct(p.first); - - auto * hero = new CGHeroInstance(); - hero->ID = Obj::HERO; - hero->setHeroTypeName(p.first); - hero->serializeJsonDefinition(handler); - - map->predefinedHeroes.emplace_back(hero); - } - } -} - -void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) -{ - serializeRumors(handler); - - serializePredefinedHeroes(handler); - - handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); - - handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); - - handler.serializeLIC("allowedSpells", &SpellID::decode, &SpellID::encode, VLC->spellh->getDefaultAllowed(), map->allowedSpells); - - //todo:events -} - -void CMapFormatJson::readOptions(JsonDeserializer & handler) -{ - readDisposedHeroes(handler); - serializeOptions(handler); -} - -void CMapFormatJson::writeOptions(JsonSerializer & handler) -{ - writeDisposedHeroes(handler); - serializeOptions(handler); -} - -///CMapPatcher -CMapPatcher::CMapPatcher(const JsonNode & stream): input(stream) -{ - //todo: update map patches and change this - fileVersionMajor = 0; - fileVersionMinor = 0; -} - -void CMapPatcher::patchMapHeader(std::unique_ptr & header) -{ - map = nullptr; - mapHeader = header.get(); - if (!input.isNull()) - readPatchData(); -} - -void CMapPatcher::readPatchData() -{ - JsonDeserializer handler(mapObjectResolver.get(), input); - readTriggeredEvents(handler); -} - -///CMapLoaderJson -CMapLoaderJson::CMapLoaderJson(CInputStream * stream) - : buffer(stream) - , ioApi(new CProxyROIOApi(buffer)) - , loader("", "_", ioApi) -{ -} - -std::unique_ptr CMapLoaderJson::loadMap() -{ - LOG_TRACE(logGlobal); - std::unique_ptr result = std::make_unique(); - map = result.get(); - mapHeader = map; - readMap(); - return result; -} - -std::unique_ptr CMapLoaderJson::loadMapHeader() -{ - LOG_TRACE(logGlobal); - map = nullptr; - std::unique_ptr result = std::make_unique(); - mapHeader = result.get(); - readHeader(false); - return result; -} - -JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) -{ - ResourceID resource(archiveFilename, EResType::TEXT); - - if(!loader.existsResource(resource)) - throw std::runtime_error(archiveFilename+" not found"); - - auto data = loader.load(resource)->readAll(); - - JsonNode res(reinterpret_cast(data.first.get()), data.second); - - return res; -} - -void CMapLoaderJson::readMap() -{ - LOG_TRACE(logGlobal); - readHeader(true); - map->initTerrain(); - readTerrain(); - readObjects(); - - map->calculateGuardingGreaturePositions(); -} - -void CMapLoaderJson::readHeader(const bool complete) -{ - //do not use map field here, use only mapHeader - JsonNode header = getFromArchive(HEADER_FILE_NAME); - - fileVersionMajor = static_cast(header["versionMajor"].Integer()); - - if(fileVersionMajor != VERSION_MAJOR) - { - logGlobal->error("Unsupported map format version: %d", fileVersionMajor); - throw std::runtime_error("Unsupported map format version"); - } - - fileVersionMinor = static_cast(header["versionMinor"].Integer()); - - if(fileVersionMinor > VERSION_MINOR) - { - logGlobal->warn("Too new map format revision: %d. This map should work but some of map features may be ignored.", fileVersionMinor); - } - - JsonDeserializer handler(mapObjectResolver.get(), header); - - mapHeader->version = EMapFormat::VCMI;//todo: new version field - - //loading mods - if(!header["mods"].isNull()) - { - for(auto & mod : header["mods"].Vector()) - mapHeader->mods[mod["name"].String()] = CModVersion::fromString(mod["version"].String()); - } - - //todo: multilevel map load support - { - auto levels = handler.enterStruct("mapLevels"); - - { - auto surface = handler.enterStruct("surface"); - handler.serializeInt("height", mapHeader->height); - handler.serializeInt("width", mapHeader->width); - } - { - auto underground = handler.enterStruct("underground"); - mapHeader->twoLevel = !underground->getCurrent().isNull(); - } - } - - serializeHeader(handler); - - readTriggeredEvents(handler); - - readTeams(handler); - //TODO: check mods - - if(complete) - readOptions(handler); -} - -void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile) -{ - try - { - using namespace TerrainDetail; - {//terrain type - const std::string typeCode = src.substr(0, 2); - tile.terType = getTerrainByCode(typeCode); - } - int startPos = 2; //0+typeCode fixed length - {//terrain view - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid terrain view in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.terView = atoi(rawCode.c_str()); - startPos += len; - } - {//terrain flip - int terrainFlip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (terrainFlip < 0) - throw std::runtime_error("Invalid terrain flip in " + src); - else - tile.extTileFlags = terrainFlip; - } - if (startPos >= src.size()) - return; - bool hasRoad = true; - {//road type - const std::string typeCode = src.substr(startPos, 2); - startPos += 2; - 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.riverType = getRiverByCode(typeCode); - hasRoad = false; - if(!tile.riverType) - { - throw std::runtime_error("Invalid river type in " + src); - } - } - } - if (hasRoad) - {//road dir - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid road dir in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.roadDir = atoi(rawCode.c_str()); - startPos += len; - } - if (hasRoad) - {//road flip - int flip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (flip < 0) - throw std::runtime_error("Invalid road flip in " + src); - else - tile.extTileFlags |= (flip << 4); - } - if (startPos >= src.size()) - return; - if (hasRoad) - {//river type - const std::string typeCode = src.substr(startPos, 2); - startPos += 2; - tile.riverType = getRiverByCode(typeCode); - } - {//river dir - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid river dir in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.riverDir = atoi(rawCode.c_str()); - startPos += len; - } - {//river flip - int flip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (flip < 0) - throw std::runtime_error("Invalid road flip in " + src); - else - tile.extTileFlags |= (flip << 2); - } - } - catch (const std::exception &) - { - logGlobal->error("Failed to read terrain tile: %s"); - } -} - -void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index) -{ - int3 pos(0, 0, index); - - const JsonVector & rows = src.Vector(); - - if(rows.size() != map->height) - throw std::runtime_error("Invalid terrain data"); - - for(pos.y = 0; pos.y < map->height; pos.y++) - { - const JsonVector & tiles = rows[pos.y].Vector(); - - if(tiles.size() != map->width) - throw std::runtime_error("Invalid terrain data"); - - for(pos.x = 0; pos.x < map->width; pos.x++) - readTerrainTile(tiles[pos.x].String(), map->getTile(pos)); - } -} - -void CMapLoaderJson::readTerrain() -{ - { - const JsonNode surface = getFromArchive("surface_terrain.json"); - readTerrainLevel(surface, 0); - } - if(map->twoLevel) - { - const JsonNode underground = getFromArchive("underground_terrain.json"); - readTerrainLevel(underground, 1); - } - - map->calculateWaterContent(); -} - -CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json): - owner(_owner), instance(nullptr), id(-1), jsonKey(json.first), configuration(json.second) -{ - -} - -void CMapLoaderJson::MapObjectLoader::construct() -{ - //TODO:consider move to ObjectTypeHandler - //find type handler - std::string typeName = configuration["type"].String(); - std::string subtypeName = configuration["subtype"].String(); - if(typeName.empty()) - { - logGlobal->error("Object type missing"); - logGlobal->debug(configuration.toJson()); - return; - } - - int3 pos; - pos.x = static_cast(configuration["x"].Float()); - pos.y = static_cast(configuration["y"].Float()); - pos.z = static_cast(configuration["l"].Float()); - - //special case for grail - if(typeName == "grail") - { - owner->map->grailPos = pos; - - owner->map->grailRadius = static_cast(configuration["options"]["grailRadius"].Float()); - return; - } - else if(subtypeName.empty()) - { - logGlobal->error("Object subtype missing"); - logGlobal->debug(configuration.toJson()); - return; - } - - auto handler = VLC->objtypeh->getHandlerFor( CModHandler::scopeMap(), typeName, subtypeName); - - auto * appearance = new ObjectTemplate; - - appearance->id = Obj(handler->getIndex()); - appearance->subid = handler->getSubIndex(); - appearance->readJson(configuration["template"], false); - - // Will be destroyed soon and replaced with shared template - instance = handler->create(std::shared_ptr(appearance)); - - instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); - instance->instanceName = jsonKey; - instance->pos = pos; - owner->map->addNewObject(instance); -} - -void CMapLoaderJson::MapObjectLoader::configure() -{ - if(nullptr == instance) - return; - - JsonDeserializer handler(owner->mapObjectResolver.get(), configuration); - - instance->serializeJson(handler); - - //artifact instance serialization requires access to Map object, handle it here for now - //todo: find better solution for artifact instance serialization - - if(auto * art = dynamic_cast(instance)) - { - auto artID = ArtifactID::NONE; - int spellID = -1; - - if(art->ID == Obj::SPELL_SCROLL) - { - auto spellIdentifier = configuration["options"]["spell"].String(); - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", spellIdentifier); - if(rawId) - spellID = rawId.value(); - else - spellID = 0; - artID = ArtifactID::SPELL_SCROLL; - } - else if(art->ID == Obj::ARTIFACT) - { - //specific artifact - artID = ArtifactID(art->subID); - } - - art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID); - } - - if(auto * hero = dynamic_cast(instance)) - { - auto o = handler.enterStruct("options"); - hero->serializeJsonArtifacts(handler, "artifacts", owner->map); - } -} - -void CMapLoaderJson::readObjects() -{ - LOG_TRACE(logGlobal); - - std::vector> loaders;//todo: optimize MapObjectLoader memory layout - - JsonNode data = getFromArchive(OBJECTS_FILE_NAME); - - //get raw data - for(auto & p : data.Struct()) - loaders.push_back(std::make_unique(this, p)); - - for(auto & ptr : loaders) - ptr->construct(); - - //configure objects after all objects are constructed so we may resolve internal IDs even to actual pointers OTF - for(auto & ptr : loaders) - ptr->configure(); - - std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) - { - return a->subID < b->subID; - }); -} - -///CMapSaverJson -CMapSaverJson::CMapSaverJson(CInputOutputStream * stream) - : buffer(stream) - , ioApi(new CProxyIOApi(buffer)) - , saver(ioApi, "_") -{ - fileVersionMajor = VERSION_MAJOR; - fileVersionMinor = VERSION_MINOR; -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CMapSaverJson::~CMapSaverJson() = default; - -void CMapSaverJson::addToArchive(const JsonNode & data, const std::string & filename) -{ - std::ostringstream out; - JsonWriter writer(out); - writer.writeNode(data); - out.flush(); - - { - auto s = out.str(); - std::unique_ptr stream = saver.addFile(filename); - - if(stream->write(reinterpret_cast(s.c_str()), s.size()) != s.size()) - throw std::runtime_error("CMapSaverJson::saveHeader() zip compression failed."); - } -} - -void CMapSaverJson::saveMap(const std::unique_ptr& map) -{ - this->map = map.get(); - this->mapHeader = this->map; - writeHeader(); - writeTerrain(); - writeObjects(); -} - -void CMapSaverJson::writeHeader() -{ - logGlobal->trace("Saving header"); - - JsonNode header; - JsonSerializer handler(mapObjectResolver.get(), header); - - header["versionMajor"].Float() = VERSION_MAJOR; - header["versionMinor"].Float() = VERSION_MINOR; - - //write mods - JsonNode & mods = header["mods"]; - for(const auto & mod : mapHeader->mods) - { - JsonNode modWriter; - modWriter["name"].String() = mod.first; - modWriter["version"].String() = mod.second.toString(); - mods.Vector().push_back(modWriter); - } - - //todo: multilevel map save support - JsonNode & levels = header["mapLevels"]; - levels["surface"]["height"].Float() = mapHeader->height; - levels["surface"]["width"].Float() = mapHeader->width; - levels["surface"]["index"].Float() = 0; - - if(mapHeader->twoLevel) - { - levels["underground"]["height"].Float() = mapHeader->height; - levels["underground"]["width"].Float() = mapHeader->width; - levels["underground"]["index"].Float() = 1; - } - - serializeHeader(handler); - - writeTriggeredEvents(handler); - - writeTeams(handler); - - writeOptions(handler); - - addToArchive(header, HEADER_FILE_NAME); -} - -std::string CMapSaverJson::writeTerrainTile(const TerrainTile & tile) -{ - using namespace TerrainDetail; - - std::ostringstream out; - 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]; - - if(tile.roadType->getId() != Road::NO_ROAD) - out << tile.roadType->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]; - - return out.str(); -} - -JsonNode CMapSaverJson::writeTerrainLevel(const int index) -{ - JsonNode data; - int3 pos(0,0,index); - - data.Vector().resize(map->height); - - for(pos.y = 0; pos.y < map->height; pos.y++) - { - JsonNode & row = data.Vector()[pos.y]; - - row.Vector().resize(map->width); - - for(pos.x = 0; pos.x < map->width; pos.x++) - row.Vector()[pos.x].String() = writeTerrainTile(map->getTile(pos)); - } - - return data; -} - -void CMapSaverJson::writeTerrain() -{ - logGlobal->trace("Saving terrain"); - //todo: multilevel map save support - - JsonNode surface = writeTerrainLevel(0); - addToArchive(surface, "surface_terrain.json"); - - if(map->twoLevel) - { - JsonNode underground = writeTerrainLevel(1); - addToArchive(underground, "underground_terrain.json"); - } -} - -void CMapSaverJson::writeObjects() -{ - logGlobal->trace("Saving objects"); - JsonNode data(JsonNode::JsonType::DATA_STRUCT); - - JsonSerializer handler(mapObjectResolver.get(), data); - - for(CGObjectInstance * obj : map->objects) - { - //logGlobal->trace("\t%s", obj->instanceName); - auto temp = handler.enterStruct(obj->instanceName); - - obj->serializeJson(handler); - } - - if(map->grailPos.valid()) - { - JsonNode grail(JsonNode::JsonType::DATA_STRUCT); - grail["type"].String() = "grail"; - - grail["x"].Float() = map->grailPos.x; - grail["y"].Float() = map->grailPos.y; - grail["l"].Float() = map->grailPos.z; - - grail["options"]["radius"].Float() = map->grailRadius; - - std::string grailId = boost::str(boost::format("grail_%d") % map->objects.size()); - - data[grailId] = grail; - } - - //cleanup empty options - for(auto & p : data.Struct()) - { - JsonNode & obj = p.second; - if(obj["options"].Struct().empty()) - obj.Struct().erase("options"); - } - - addToArchive(data, OBJECTS_FILE_NAME); -} - - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatJson.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 "MapFormatJson.h" + +#include "../filesystem/CInputStream.h" +#include "../filesystem/COutputStream.h" +#include "../JsonDetail.h" +#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 "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../modding/ModScope.h" +#include "../modding/ModUtility.h" +#include "../spells/CSpellHandler.h" +#include "../CSkillHandler.h" +#include "../constants/StringConstants.h" +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" +#include "../Languages.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class MapObjectResolver: public IInstanceResolver +{ +public: + MapObjectResolver(const CMapFormatJson * owner_); + + si32 decode (const std::string & identifier) const override; + std::string encode(si32 identifier) const override; + +private: + const CMapFormatJson * owner; +}; + +MapObjectResolver::MapObjectResolver(const CMapFormatJson * owner_): + owner(owner_) +{ + +} + +si32 MapObjectResolver::decode(const std::string & identifier) const +{ + //always decode as ObjectInstanceID + + auto it = owner->map->instanceNames.find(identifier); + + if(it != owner->map->instanceNames.end()) + { + return (*it).second->id.getNum(); + } + else + { + logGlobal->error("Object not found: %s", identifier); + return -1; + } +} + +std::string MapObjectResolver::encode(si32 identifier) const +{ + ObjectInstanceID id; + + //use h3m questIdentifiers if they are present + if(owner->map->questIdentifierToId.empty()) + { + id = ObjectInstanceID(identifier); + } + else + { + id = owner->map->questIdentifierToId[identifier]; + } + + si32 oid = id.getNum(); + if(oid < 0 || oid >= owner->map->objects.size()) + { + logGlobal->error("Cannot get object with id %d", oid); + return ""; + } + + return owner->map->objects[oid]->instanceName; +} + +namespace HeaderDetail +{ + static const std::vector difficultyMap = + { + "EASY", + "NORMAL", + "HARD", + "EXPERT", + "IMPOSSIBLE" + }; + + enum class ECanPlay + { + NONE = 0, + PLAYER_OR_AI = 1, + PLAYER_ONLY = 2, + AI_ONLY = 3 + }; + + static const std::vector canPlayMap = + { + "", + "PlayerOrAI", + "PlayerOnly", + "AIOnly" + }; +} + +namespace TriggeredEventsDetail +{ + static const std::array conditionNames = + { + "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", + "control", "destroy", "transport", "daysPassed", + "isHuman", "daysWithoutTown", "standardWin", "constValue" + }; + + static const std::array typeNames = { "victory", "defeat" }; + + static EventCondition JsonToCondition(const JsonNode & node) + { + EventCondition event; + + const auto & conditionName = node.Vector()[0].String(); + + auto pos = vstd::find_pos(conditionNames, conditionName); + + event.condition = static_cast(pos); + + if (node.Vector().size() > 1) + { + const JsonNode & data = node.Vector()[1]; + + event.objectInstanceName = data["object"].String(); + event.value = data["value"].Integer(); + + switch (event.condition) + { + case EventCondition::HAVE_ARTIFACT: + case EventCondition::TRANSPORT: + if (data["type"].isNumber()) // compatibility + event.objectType = ArtifactID(data["type"].Integer()); + else + event.objectType = ArtifactID(ArtifactID::decode(data["type"].String())); + break; + case EventCondition::HAVE_CREATURES: + if (data["type"].isNumber()) // compatibility + event.objectType = CreatureID(data["type"].Integer()); + else + event.objectType = CreatureID(CreatureID::decode(data["type"].String())); + break; + case EventCondition::HAVE_RESOURCES: + if (data["type"].isNumber()) // compatibility + event.objectType = GameResID(data["type"].Integer()); + else + event.objectType = GameResID(GameResID::decode(data["type"].String())); + break; + case EventCondition::HAVE_BUILDING: + if (data["type"].isNumber()) // compatibility + event.objectType = BuildingID(data["type"].Integer()); + else + event.objectType = BuildingID(BuildingID::decode(data["type"].String())); + break; + case EventCondition::CONTROL: + case EventCondition::DESTROY: + if (data["type"].isNumber()) // compatibility + event.objectType = MapObjectID(data["type"].Integer()); + else + event.objectType = MapObjectID(MapObjectID::decode(data["type"].String())); + break; + } + + if (!data["position"].isNull()) + { + const auto & position = data["position"].Vector(); + event.position.x = static_cast(position.at(0).Float()); + event.position.y = static_cast(position.at(1).Float()); + event.position.z = static_cast(position.at(2).Float()); + } + } + return event; + } + + static JsonNode ConditionToJson(const EventCondition & event) + { + JsonNode json; + + JsonVector & asVector = json.Vector(); + + JsonNode condition; + condition.String() = conditionNames.at(event.condition); + asVector.push_back(condition); + + JsonNode data; + + if(!event.objectInstanceName.empty()) + data["object"].String() = event.objectInstanceName; + + data["type"].String() = event.objectType.toString(); + data["value"].Integer() = event.value; + + if(event.position != int3(-1, -1, -1)) + { + auto & position = data["position"].Vector(); + position.resize(3); + position[0].Float() = event.position.x; + position[1].Float() = event.position.y; + position[2].Float() = event.position.z; + } + + if(!data.isNull()) + asVector.push_back(data); + + return json; + } +}//namespace TriggeredEventsDetail + +namespace TerrainDetail +{ + static const std::array flipCodes = + { + '_', '-', '|', '+' + }; +} + +///CMapFormatJson +const int CMapFormatJson::VERSION_MAJOR = 2; +const int CMapFormatJson::VERSION_MINOR = 0; + +const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; +const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; +const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"}; + +CMapFormatJson::CMapFormatJson(): + fileVersionMajor(0), fileVersionMinor(0), + mapObjectResolver(std::make_unique(this)), + map(nullptr), mapHeader(nullptr) +{ + +} + +TerrainType * CMapFormatJson::getTerrainByCode(const std::string & code) +{ + for(const auto & object : VLC->terrainTypeHandler->objects) + { + if(object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +RiverType * CMapFormatJson::getRiverByCode(const std::string & code) +{ + for(const auto & object : VLC->riverTypeHandler->objects) + { + if (object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +RoadType * CMapFormatJson::getRoadByCode(const std::string & code) +{ + for(const auto & object : VLC->roadTypeHandler->objects) + { + if (object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const +{ + std::set temp; + + if(handler.saving) + { + for(auto faction : VLC->townh->objects) + if(faction->town && vstd::contains(value, faction->getId())) + temp.insert(faction->getId()); + } + + handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, VLC->townh->getDefaultAllowed(), temp); + + if(!handler.saving) + value = temp; +} + +void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) +{ + handler.serializeStruct("name", mapHeader->name); + handler.serializeStruct("description", mapHeader->description); + handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); + + //todo: support arbitrary percentage + handler.serializeEnum("difficulty", mapHeader->difficulty, HeaderDetail::difficultyMap); + + serializePlayerInfo(handler); + + handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes); + + handler.serializeStruct("victoryMessage", mapHeader->victoryMessage); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + + handler.serializeStruct("defeatMessage", mapHeader->defeatMessage); + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); +} + +void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) +{ + auto playersData = handler.enterStruct("players"); + + for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) + { + PlayerInfo & info = mapHeader->players[player]; + + if(handler.saving) + { + if(!info.canAnyonePlay()) + continue; + } + + auto playerData = handler.enterStruct(GameConstants::PLAYER_COLOR_NAMES[player]); + + if(!handler.saving) + { + if(handler.getCurrent().isNull()) + { + info.canComputerPlay = false; + info.canHumanPlay = false; + continue; + } + } + + serializeAllowedFactions(handler, info.allowedFactions); + + HeaderDetail::ECanPlay canPlay = HeaderDetail::ECanPlay::NONE; + + if(handler.saving) + { + if(info.canComputerPlay) + { + canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_OR_AI : HeaderDetail::ECanPlay::AI_ONLY; + } + else + { + canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_ONLY : HeaderDetail::ECanPlay::NONE; + } + } + + handler.serializeEnum("canPlay", canPlay, HeaderDetail::canPlayMap); + + if(!handler.saving) + { + switch(canPlay) + { + case HeaderDetail::ECanPlay::PLAYER_OR_AI: + info.canComputerPlay = true; + info.canHumanPlay = true; + break; + case HeaderDetail::ECanPlay::PLAYER_ONLY: + info.canComputerPlay = false; + info.canHumanPlay = true; + break; + case HeaderDetail::ECanPlay::AI_ONLY: + info.canComputerPlay = true; + info.canHumanPlay = false; + break; + default: + info.canComputerPlay = false; + info.canHumanPlay = false; + break; + } + } + + //saving whole structure only if position is valid + if(!handler.saving || info.posOfMainTown.valid()) + { + auto mainTown = handler.enterStruct("mainTown"); + handler.serializeBool("generateHero", info.generateHeroAtMainTown); + handler.serializeInt("x", info.posOfMainTown.x, -1); + handler.serializeInt("y", info.posOfMainTown.y, -1); + handler.serializeInt("l", info.posOfMainTown.z, -1); + } + if(!handler.saving) + { + info.hasMainTown = info.posOfMainTown.valid(); + } + + handler.serializeString("mainHero", info.mainHeroInstance);//must be before "heroes" + + //heroes + if(handler.saving) + { + //ignoring heroesNames and saving from actual map objects + //TODO: optimize + for(auto & obj : map->objects) + { + if((obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO) && obj->tempOwner == PlayerColor(player)) + { + auto * hero = dynamic_cast(obj.get()); + + auto heroes = handler.enterStruct("heroes"); + if(hero) + { + auto heroData = handler.enterStruct(hero->instanceName); + heroData->serializeString("name", hero->nameCustomTextId); + + if(hero->ID == Obj::HERO) + { + std::string temp; + if(hero->type) + temp = hero->type->getJsonKey(); + else + temp = hero->getHeroType().toEntity(VLC)->getJsonKey(); + + handler.serializeString("type", temp); + } + } + } + } + } + else + { + info.heroesNames.clear(); + + auto heroes = handler.enterStruct("heroes"); + + for(const auto & hero : handler.getCurrent().Struct()) + { + const JsonNode & data = hero.second; + const std::string instanceName = hero.first; + + SHeroName hname; + hname.heroId = HeroTypeID::NONE; + std::string rawId = data["type"].String(); + + if(!rawId.empty()) + hname.heroId = HeroTypeID(HeroTypeID::decode(rawId)); + + hname.heroName = data["name"].String(); + + if(instanceName == info.mainHeroInstance) + { + //this is main hero + info.mainCustomHeroNameTextId = hname.heroName; + info.hasRandomHero = (hname.heroId == HeroTypeID::NONE); + info.mainCustomHeroId = hname.heroId; + info.mainCustomHeroPortrait = HeroTypeID::NONE; + //todo:mainHeroPortrait + } + + info.heroesNames.push_back(hname); + } + } + + handler.serializeBool("randomFaction", info.isFactionRandom); + } +} + +void CMapFormatJson::readTeams(JsonDeserializer & handler) +{ + auto teams = handler.enterArray("teams"); + const JsonNode & src = teams->getCurrent(); + + if(src.getType() != JsonNode::JsonType::DATA_VECTOR) + { + // No alliances + if(src.getType() != JsonNode::JsonType::DATA_NULL) + logGlobal->error("Invalid teams field type"); + + mapHeader->howManyTeams = 0; + for(auto & player : mapHeader->players) + if(player.canAnyonePlay()) + player.team = TeamID(mapHeader->howManyTeams++); + } + else + { + const JsonVector & srcVector = src.Vector(); + mapHeader->howManyTeams = static_cast(srcVector.size()); + + for(int team = 0; team < mapHeader->howManyTeams; team++) + for(const JsonNode & playerData : srcVector[team].Vector()) + { + PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); + if(player.isValidPlayer()) + if(mapHeader->players[player.getNum()].canAnyonePlay()) + mapHeader->players[player.getNum()].team = TeamID(team); + } + + for(PlayerInfo & player : mapHeader->players) + if(player.canAnyonePlay() && player.team == TeamID::NO_TEAM) + player.team = TeamID(mapHeader->howManyTeams++); + } +} + +void CMapFormatJson::writeTeams(JsonSerializer & handler) +{ + std::vector> teamsData; + + teamsData.resize(mapHeader->howManyTeams); + + //get raw data + for(int idx = 0; idx < mapHeader->players.size(); idx++) + { + const PlayerInfo & player = mapHeader->players.at(idx); + int team = player.team.getNum(); + if(vstd::iswithin(team, 0, mapHeader->howManyTeams-1) && player.canAnyonePlay()) + teamsData.at(team).insert(PlayerColor(idx)); + } + + //remove single-member teams + vstd::erase_if(teamsData, [](std::set & elem) -> bool + { + return elem.size() <= 1; + }); + + if(!teamsData.empty()) + { + JsonNode dest; + + //construct output + dest.setType(JsonNode::JsonType::DATA_VECTOR); + + for(const std::set & teamData : teamsData) + { + JsonNode team(JsonNode::JsonType::DATA_VECTOR); + for(const PlayerColor & player : teamData) + { + JsonNode member(JsonNode::JsonType::DATA_STRING); + member.String() = GameConstants::PLAYER_COLOR_NAMES[player.getNum()]; + team.Vector().push_back(std::move(member)); + } + dest.Vector().push_back(std::move(team)); + } + handler.serializeRaw("teams", dest, std::nullopt); + } +} + +void CMapFormatJson::readTriggeredEvents(JsonDeserializer & handler) +{ + const JsonNode & input = handler.getCurrent(); + + mapHeader->triggeredEvents.clear(); + + for(const auto & entry : input["triggeredEvents"].Struct()) + { + TriggeredEvent event; + event.identifier = entry.first; + readTriggeredEvent(event, entry.second); + mapHeader->triggeredEvents.push_back(event); + } +} + +void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const +{ + using namespace TriggeredEventsDetail; + + event.onFulfill.jsonDeserialize(source["message"]); + event.description.jsonDeserialize(source["description"]); + event.effect.type = vstd::find_pos(typeNames, source["effect"]["type"].String()); + event.effect.toOtherMessage.jsonDeserialize(source["effect"]["messageToSend"]); + event.trigger = EventExpression(source["condition"], JsonToCondition); // logical expression +} + +void CMapFormatJson::writeTriggeredEvents(JsonSerializer & handler) +{ + JsonNode triggeredEvents(JsonNode::JsonType::DATA_STRUCT); + + for(const auto & event : mapHeader->triggeredEvents) + writeTriggeredEvent(event, triggeredEvents[event.identifier]); + + handler.serializeRaw("triggeredEvents", triggeredEvents, std::nullopt); +} + +void CMapFormatJson::writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const +{ + using namespace TriggeredEventsDetail; + + if(!event.onFulfill.empty()) + event.onFulfill.jsonSerialize(dest["message"]); + + if(!event.description.empty()) + event.description.jsonSerialize(dest["description"]); + + dest["effect"]["type"].String() = typeNames.at(static_cast(event.effect.type)); + + if(!event.effect.toOtherMessage.empty()) + event.description.jsonSerialize(dest["effect"]["messageToSend"]); + + dest["condition"] = event.trigger.toJson(ConditionToJson); +} + +void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) +{ + auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format + + const JsonNode & data = handler.getCurrent(); + + for(const auto & entry : data.Struct()) + { + HeroTypeID type(HeroTypeID::decode(entry.first)); + + std::set mask; + + for(const JsonNode & playerData : entry.second["availableFor"].Vector()) + { + PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); + if(player.isValidPlayer()) + mask.insert(player); + } + + if(!mask.empty() && mask.size() != PlayerColor::PLAYER_LIMIT_I && type.getNum() >= 0) + { + DisposedHero hero; + + hero.heroId = type; + hero.players = mask; + //name and portrait are not used + + map->disposedHeroes.push_back(hero); + } + } +} + +void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) +{ + if(map->disposedHeroes.empty()) + return; + + auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format + + for(DisposedHero & hero : map->disposedHeroes) + { + std::string type = HeroTypeID::encode(hero.heroId.getNum()); + + auto definition = definitions->enterStruct(type); + + JsonNode players(JsonNode::JsonType::DATA_VECTOR); + definition->serializeIdArray("availableFor", hero.players); + } +} + +void CMapFormatJson::serializeRumors(JsonSerializeFormat & handler) +{ + auto rumors = handler.enterArray("rumors"); + rumors.serializeStruct(map->rumors); +} + +void CMapFormatJson::serializeTimedEvents(JsonSerializeFormat & handler) +{ + auto events = handler.enterArray("events"); + std::vector temp(map->events.begin(), map->events.end()); + events.serializeStruct(temp); + map->events.assign(temp.begin(), temp.end()); +} + +void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) +{ + //todo:serializePredefinedHeroes + + if(handler.saving) + { + if(!map->predefinedHeroes.empty()) + { + auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); + + for(auto & hero : map->predefinedHeroes) + { + auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); + + hero->serializeJsonDefinition(handler); + } + } + } + else + { + auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); + + const JsonNode & data = handler.getCurrent(); + + for(const auto & p : data.Struct()) + { + auto predefinedHero = handler.enterStruct(p.first); + + auto * hero = new CGHeroInstance(); + hero->ID = Obj::HERO; + hero->setHeroTypeName(p.first); + hero->serializeJsonDefinition(handler); + + map->predefinedHeroes.emplace_back(hero); + } + } +} + +void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) +{ + serializeRumors(handler); + + serializeTimedEvents(handler); + + serializePredefinedHeroes(handler); + + handler.serializeLIC("allowedAbilities", &SecondarySkill::decode, &SecondarySkill::encode, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); + + handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); + + handler.serializeLIC("allowedSpells", &SpellID::decode, &SpellID::encode, VLC->spellh->getDefaultAllowed(), map->allowedSpells); + + //todo:events +} + +void CMapFormatJson::readOptions(JsonDeserializer & handler) +{ + readDisposedHeroes(handler); + serializeOptions(handler); +} + +void CMapFormatJson::writeOptions(JsonSerializer & handler) +{ + writeDisposedHeroes(handler); + serializeOptions(handler); +} + +///CMapPatcher +CMapPatcher::CMapPatcher(const JsonNode & stream): input(stream) +{ + //todo: update map patches and change this + fileVersionMajor = 0; + fileVersionMinor = 0; +} + +void CMapPatcher::patchMapHeader(std::unique_ptr & header) +{ + map = nullptr; + mapHeader = header.get(); + if (!input.isNull()) + readPatchData(); +} + +void CMapPatcher::readPatchData() +{ + JsonDeserializer handler(mapObjectResolver.get(), input); + readTriggeredEvents(handler); + + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + handler.serializeStruct("victoryString", mapHeader->victoryMessage); + handler.serializeStruct("defeatString", mapHeader->defeatMessage); +} + +///CMapLoaderJson +CMapLoaderJson::CMapLoaderJson(CInputStream * stream) + : buffer(stream) + , ioApi(new CProxyROIOApi(buffer)) + , loader("", "_", ioApi) +{ +} + +std::unique_ptr CMapLoaderJson::loadMap() +{ + LOG_TRACE(logGlobal); + std::unique_ptr result = std::make_unique(); + map = result.get(); + mapHeader = map; + readMap(); + return result; +} + +std::unique_ptr CMapLoaderJson::loadMapHeader() +{ + LOG_TRACE(logGlobal); + map = nullptr; + std::unique_ptr result = std::make_unique(); + mapHeader = result.get(); + readHeader(false); + return result; +} + +bool CMapLoaderJson::isExistArchive(const std::string & archiveFilename) +{ + return loader.existsResource(JsonPath::builtin(archiveFilename)); +} + +JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) +{ + JsonPath resource = JsonPath::builtin(archiveFilename); + + if(!loader.existsResource(resource)) + throw std::runtime_error(archiveFilename+" not found"); + + auto data = loader.load(resource)->readAll(); + + JsonNode res(reinterpret_cast(data.first.get()), data.second); + + return res; +} + +void CMapLoaderJson::readMap() +{ + LOG_TRACE(logGlobal); + readHeader(true); + map->initTerrain(); + readTerrain(); + readObjects(); + + map->calculateGuardingGreaturePositions(); +} + +void CMapLoaderJson::readHeader(const bool complete) +{ + //do not use map field here, use only mapHeader + JsonNode header = getFromArchive(HEADER_FILE_NAME); + + fileVersionMajor = static_cast(header["versionMajor"].Integer()); + + if(fileVersionMajor > VERSION_MAJOR) + { + logGlobal->error("Unsupported map format version: %d", fileVersionMajor); + throw std::runtime_error("Unsupported map format version"); + } + + fileVersionMinor = static_cast(header["versionMinor"].Integer()); + + if(fileVersionMinor > VERSION_MINOR) + { + logGlobal->warn("Too new map format revision: %d. This map should work but some of map features may be ignored.", fileVersionMinor); + } + + JsonDeserializer handler(mapObjectResolver.get(), header); + + mapHeader->version = EMapFormat::VCMI;//todo: new version field + + //loading mods + if(!header["mods"].isNull()) + { + for(auto & mod : header["mods"].Vector()) + { + ModVerificationInfo info; + info.version = CModVersion::fromString(mod["version"].String()); + info.checksum = mod["checksum"].Integer(); + info.name = mod["name"].String(); + info.parent = mod["parent"].String(); + info.impactsGameplay = true; + + if(!mod["modId"].isNull()) + mapHeader->mods[mod["modId"].String()] = info; + else + mapHeader->mods[mod["name"].String()] = info; + } + } + + //todo: multilevel map load support + { + auto levels = handler.enterStruct("mapLevels"); + + { + auto surface = handler.enterStruct("surface"); + handler.serializeInt("height", mapHeader->height); + handler.serializeInt("width", mapHeader->width); + } + { + auto underground = handler.enterStruct("underground"); + mapHeader->twoLevel = !underground->getCurrent().isNull(); + } + } + + serializeHeader(handler); + + readTriggeredEvents(handler); + + readTeams(handler); + //TODO: check mods + + if(complete) + readOptions(handler); + + readTranslations(); +} + +void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile) +{ + try + { + using namespace TerrainDetail; + {//terrain type + const std::string typeCode = src.substr(0, 2); + tile.terType = getTerrainByCode(typeCode); + } + int startPos = 2; //0+typeCode fixed length + {//terrain view + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid terrain view in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.terView = atoi(rawCode.c_str()); + startPos += len; + } + {//terrain flip + int terrainFlip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (terrainFlip < 0) + throw std::runtime_error("Invalid terrain flip in " + src); + else + tile.extTileFlags = terrainFlip; + } + if (startPos >= src.size()) + return; + bool hasRoad = true; + {//road type + const std::string typeCode = src.substr(startPos, 2); + startPos += 2; + 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.riverType = getRiverByCode(typeCode); + hasRoad = false; + if(!tile.riverType) + { + throw std::runtime_error("Invalid river type in " + src); + } + } + } + if (hasRoad) + {//road dir + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid road dir in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.roadDir = atoi(rawCode.c_str()); + startPos += len; + } + if (hasRoad) + {//road flip + int flip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (flip < 0) + throw std::runtime_error("Invalid road flip in " + src); + else + tile.extTileFlags |= (flip << 4); + } + if (startPos >= src.size()) + return; + if (hasRoad) + {//river type + const std::string typeCode = src.substr(startPos, 2); + startPos += 2; + tile.riverType = getRiverByCode(typeCode); + } + {//river dir + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid river dir in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.riverDir = atoi(rawCode.c_str()); + startPos += len; + } + {//river flip + int flip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (flip < 0) + throw std::runtime_error("Invalid road flip in " + src); + else + tile.extTileFlags |= (flip << 2); + } + } + catch (const std::exception &) + { + logGlobal->error("Failed to read terrain tile: %s"); + } +} + +void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index) +{ + int3 pos(0, 0, index); + + const JsonVector & rows = src.Vector(); + + if(rows.size() != map->height) + throw std::runtime_error("Invalid terrain data"); + + for(pos.y = 0; pos.y < map->height; pos.y++) + { + const JsonVector & tiles = rows[pos.y].Vector(); + + if(tiles.size() != map->width) + throw std::runtime_error("Invalid terrain data"); + + for(pos.x = 0; pos.x < map->width; pos.x++) + readTerrainTile(tiles[pos.x].String(), map->getTile(pos)); + } +} + +void CMapLoaderJson::readTerrain() +{ + { + const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]); + readTerrainLevel(surface, 0); + } + if(map->twoLevel) + { + const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]); + readTerrainLevel(underground, 1); + } + + map->calculateWaterContent(); +} + +CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json): + owner(_owner), instance(nullptr), id(-1), jsonKey(json.first), configuration(json.second) +{ + +} + +void CMapLoaderJson::MapObjectLoader::construct() +{ + //TODO:consider move to ObjectTypeHandler + //find type handler + std::string typeName = configuration["type"].String(); + std::string subtypeName = configuration["subtype"].String(); + if(typeName.empty()) + { + logGlobal->error("Object type missing"); + logGlobal->debug(configuration.toJson()); + return; + } + + int3 pos; + pos.x = static_cast(configuration["x"].Float()); + pos.y = static_cast(configuration["y"].Float()); + pos.z = static_cast(configuration["l"].Float()); + + //special case for grail + if(typeName == "grail") + { + owner->map->grailPos = pos; + + owner->map->grailRadius = static_cast(configuration["options"]["grailRadius"].Float()); + return; + } + else if(subtypeName.empty()) + { + logGlobal->error("Object subtype missing"); + logGlobal->debug(configuration.toJson()); + return; + } + + auto handler = VLC->objtypeh->getHandlerFor( ModScope::scopeMap(), typeName, subtypeName); + + auto * appearance = new ObjectTemplate; + + appearance->id = Obj(handler->getIndex()); + appearance->subid = handler->getSubIndex(); + appearance->readJson(configuration["template"], false); + + // Will be destroyed soon and replaced with shared template + instance = handler->create(std::shared_ptr(appearance)); + + instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); + instance->instanceName = jsonKey; + instance->pos = pos; + owner->map->addNewObject(instance); +} + +void CMapLoaderJson::MapObjectLoader::configure() +{ + if(nullptr == instance) + return; + + JsonDeserializer handler(owner->mapObjectResolver.get(), configuration); + + instance->serializeJson(handler); + + //artifact instance serialization requires access to Map object, handle it here for now + //todo: find better solution for artifact instance serialization + + if(auto * art = dynamic_cast(instance)) + { + ArtifactID artID = ArtifactID::NONE; + SpellID spellID = SpellID::NONE; + + if(art->ID == Obj::SPELL_SCROLL) + { + auto spellIdentifier = configuration["options"]["spell"].String(); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", spellIdentifier); + if(rawId) + spellID = rawId.value(); + else + spellID = 0; + artID = ArtifactID::SPELL_SCROLL; + } + else if(art->ID == Obj::ARTIFACT) + { + //specific artifact + artID = art->getArtifact(); + } + + art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); + } + + if(auto * hero = dynamic_cast(instance)) + { + auto o = handler.enterStruct("options"); + hero->serializeJsonArtifacts(handler, "artifacts", owner->map); + } +} + +void CMapLoaderJson::readObjects() +{ + LOG_TRACE(logGlobal); + + std::vector> loaders;//todo: optimize MapObjectLoader memory layout + + JsonNode data = getFromArchive(OBJECTS_FILE_NAME); + + //get raw data + for(auto & p : data.Struct()) + loaders.push_back(std::make_unique(this, p)); + + for(auto & ptr : loaders) + ptr->construct(); + + //configure objects after all objects are constructed so we may resolve internal IDs even to actual pointers OTF + for(auto & ptr : loaders) + ptr->configure(); + + std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) + { + return a->getObjTypeIndex() < b->getObjTypeIndex(); + }); +} + +void CMapLoaderJson::readTranslations() +{ + std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; + for(auto & language : Languages::getLanguageList()) + { + if(isExistArchive(language.identifier + ".json")) + mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json"); + } + mapHeader->registerMapStrings(); +} + + +///CMapSaverJson +CMapSaverJson::CMapSaverJson(CInputOutputStream * stream) + : buffer(stream) + , ioApi(new CProxyIOApi(buffer)) + , saver(ioApi, "_") +{ + fileVersionMajor = VERSION_MAJOR; + fileVersionMinor = VERSION_MINOR; +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CMapSaverJson::~CMapSaverJson() = default; + +void CMapSaverJson::addToArchive(const JsonNode & data, const std::string & filename) +{ + std::ostringstream out; + JsonWriter writer(out); + writer.writeNode(data); + out.flush(); + + { + auto s = out.str(); + std::unique_ptr stream = saver.addFile(filename); + + if(stream->write(reinterpret_cast(s.c_str()), s.size()) != s.size()) + throw std::runtime_error("CMapSaverJson::saveHeader() zip compression failed."); + } +} + +void CMapSaverJson::saveMap(const std::unique_ptr& map) +{ + this->map = map.get(); + this->mapHeader = this->map; + writeHeader(); + writeTerrain(); + writeObjects(); +} + +void CMapSaverJson::writeHeader() +{ + logGlobal->trace("Saving header"); + + JsonNode header; + JsonSerializer handler(mapObjectResolver.get(), header); + + header["versionMajor"].Float() = VERSION_MAJOR; + header["versionMinor"].Float() = VERSION_MINOR; + + //write mods + JsonNode & mods = header["mods"]; + for(const auto & mod : mapHeader->mods) + { + JsonNode modWriter; + modWriter["modId"].String() = mod.first; + modWriter["name"].String() = mod.second.name; + modWriter["parent"].String() = mod.second.parent; + modWriter["version"].String() = mod.second.version.toString(); + modWriter["checksum"].Integer() = mod.second.checksum; + mods.Vector().push_back(modWriter); + } + + //todo: multilevel map save support + JsonNode & levels = header["mapLevels"]; + levels["surface"]["height"].Float() = mapHeader->height; + levels["surface"]["width"].Float() = mapHeader->width; + levels["surface"]["index"].Float() = 0; + + if(mapHeader->twoLevel) + { + levels["underground"]["height"].Float() = mapHeader->height; + levels["underground"]["width"].Float() = mapHeader->width; + levels["underground"]["index"].Float() = 1; + } + + serializeHeader(handler); + + writeTriggeredEvents(handler); + + writeTeams(handler); + + writeOptions(handler); + + writeTranslations(); + + addToArchive(header, HEADER_FILE_NAME); +} + +std::string CMapSaverJson::writeTerrainTile(const TerrainTile & tile) +{ + using namespace TerrainDetail; + + std::ostringstream out; + 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]; + + if(tile.roadType->getId() != Road::NO_ROAD) + out << tile.roadType->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]; + + return out.str(); +} + +JsonNode CMapSaverJson::writeTerrainLevel(const int index) +{ + JsonNode data; + int3 pos(0,0,index); + + data.Vector().resize(map->height); + + for(pos.y = 0; pos.y < map->height; pos.y++) + { + JsonNode & row = data.Vector()[pos.y]; + + row.Vector().resize(map->width); + + for(pos.x = 0; pos.x < map->width; pos.x++) + row.Vector()[pos.x].String() = writeTerrainTile(map->getTile(pos)); + } + + return data; +} + +void CMapSaverJson::writeTerrain() +{ + logGlobal->trace("Saving terrain"); + //todo: multilevel map save support + + JsonNode surface = writeTerrainLevel(0); + addToArchive(surface, TERRAIN_FILE_NAMES[0]); + + if(map->twoLevel) + { + JsonNode underground = writeTerrainLevel(1); + addToArchive(underground, TERRAIN_FILE_NAMES[1]); + } +} + +void CMapSaverJson::writeObjects() +{ + logGlobal->trace("Saving objects"); + JsonNode data(JsonNode::JsonType::DATA_STRUCT); + + JsonSerializer handler(mapObjectResolver.get(), data); + + for(CGObjectInstance * obj : map->objects) + { + //logGlobal->trace("\t%s", obj->instanceName); + auto temp = handler.enterStruct(obj->instanceName); + + obj->serializeJson(handler); + } + + if(map->grailPos.valid()) + { + JsonNode grail(JsonNode::JsonType::DATA_STRUCT); + grail["type"].String() = "grail"; + + grail["x"].Float() = map->grailPos.x; + grail["y"].Float() = map->grailPos.y; + grail["l"].Float() = map->grailPos.z; + + grail["options"]["radius"].Float() = map->grailRadius; + + std::string grailId = boost::str(boost::format("grail_%d") % map->objects.size()); + + data[grailId] = grail; + } + + //cleanup empty options + for(auto & p : data.Struct()) + { + JsonNode & obj = p.second; + if(obj["options"].Struct().empty()) + obj.Struct().erase("options"); + } + + addToArchive(data, OBJECTS_FILE_NAME); +} + +void CMapSaverJson::writeTranslations() +{ + for(auto & s : mapHeader->translations.Struct()) + { + auto & language = s.first; + if(Languages::getLanguageOptions(language).identifier.empty()) + { + logGlobal->error("Serializing of unsupported language %s is not permitted", language); + continue;; + } + logGlobal->trace("Saving translations, language: %s", language); + addToArchive(s.second, language + ".json"); + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 143810a25..6efe186b4 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -1,278 +1,292 @@ -/* - * MapFormatJson.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 "CMapService.h" -#include "../JsonNode.h" - -#include "../filesystem/CZipSaver.h" -#include "../filesystem/CZipLoader.h" -#include "../GameConstants.h" - -#include "../serializer/JsonSerializeFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct TriggeredEvent; -struct TerrainTile; -struct PlayerInfo; -class CGObjectInstance; -class AObjectTypeHandler; -class TerrainType; -class RoadType; -class RiverType; - -class JsonSerializeFormat; -class JsonDeserializer; -class JsonSerializer; - -class DLL_LINKAGE CMapFormatJson -{ -public: - static const int VERSION_MAJOR; - static const int VERSION_MINOR; - - static const std::string HEADER_FILE_NAME; - static const std::string OBJECTS_FILE_NAME; - - int fileVersionMajor; - int fileVersionMinor; -protected: - friend class MapObjectResolver; - std::unique_ptr mapObjectResolver; - - /** ptr to the map object which gets filled by data from the buffer or written to buffer */ - CMap * map; - - /** - * ptr to the map header object which gets filled by data from the buffer. - * (when loading map and mapHeader point to the same object) - */ - CMapHeader * mapHeader; - - CMapFormatJson(); - - static TerrainType * getTerrainByCode(const std::string & code); - static RiverType * getRiverByCode(const std::string & code); - static RoadType * getRoadByCode(const std::string & code); - - void serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const; - - ///common part of header saving/loading - void serializeHeader(JsonSerializeFormat & handler); - - ///player information saving/loading - void serializePlayerInfo(JsonSerializeFormat & handler); - - /** - * Reads team settings to header - */ - void readTeams(JsonDeserializer & handler); - - /** - * Saves team settings to header - */ - void writeTeams(JsonSerializer & handler); - - /** - * Reads triggered events, including victory/loss conditions - */ - void readTriggeredEvents(JsonDeserializer & handler); - - /** - * Writes triggered events, including victory/loss conditions - */ - void writeTriggeredEvents(JsonSerializer & handler); - - /** - * Reads one of triggered events - */ - void readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const; - - /** - * Writes one of triggered events - */ - void writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const; - - void writeDisposedHeroes(JsonSerializeFormat & handler); - - void readDisposedHeroes(JsonSerializeFormat & handler); - - void serializePredefinedHeroes(JsonSerializeFormat & handler); - - void serializeRumors(JsonSerializeFormat & handler); - - ///common part of map attributes saving/loading - void serializeOptions(JsonSerializeFormat & handler); - - /** - * Loads map attributes except header ones - */ - void readOptions(JsonDeserializer & handler); - - /** - * Saves map attributes except header ones - */ - void writeOptions(JsonSerializer & handler); -}; - -class DLL_LINKAGE CMapPatcher : public CMapFormatJson, public IMapPatcher -{ -public: - /** - * Default constructor. - * - * @param stream. A stream containing the map data. - */ - CMapPatcher(const JsonNode & stream); - -public: //IMapPatcher - /** - * Modifies supplied map header using Json data - * - */ - void patchMapHeader(std::unique_ptr & header) override; - -private: - /** - * Reads subset of header that can be replaced by patching. - */ - void readPatchData(); - - JsonNode input; -}; - -class DLL_LINKAGE CMapLoaderJson : public CMapFormatJson, public IMapLoader -{ -public: - /** - * Constructor. - * - * @param stream a stream containing the map data - */ - CMapLoaderJson(CInputStream * stream); - - /** - * Loads the VCMI/Json map file. - * - * @return a unique ptr of the loaded map class - */ - std::unique_ptr loadMap() override; - - /** - * Loads the VCMI/Json map header. - * - * @return a unique ptr of the loaded map header class - */ - std::unique_ptr loadMapHeader() override; - - struct MapObjectLoader - { - MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json); - CMapLoaderJson * owner; - CGObjectInstance * instance; - ObjectInstanceID id; - std::string jsonKey;//full id defined by map creator - JsonNode & configuration; - - ///constructs object (without configuration) - void construct(); - - ///configures object - void configure(); - }; - - /** - * Reads the map header. - */ - void readHeader(const bool complete); - - /** - * Reads complete map. - */ - void readMap(); - - static void readTerrainTile(const std::string & src, TerrainTile & tile); - - void readTerrainLevel(const JsonNode & src, const int index); - - void readTerrain(); - - /** - * Loads all map objects from zip archive - */ - void readObjects(); - - JsonNode getFromArchive(const std::string & archiveFilename); - -private: - CInputStream * buffer; - std::shared_ptr ioApi; - - CZipLoader loader;///< object to handle zip archive operations -}; - -class DLL_LINKAGE CMapSaverJson : public CMapFormatJson, public IMapSaver -{ -public: - /** - * Constructor. - * - * @param stream a stream to save the map to, will contain zip archive - */ - CMapSaverJson(CInputOutputStream * stream); - - ~CMapSaverJson(); - - /** - * Actually saves the VCMI/Json map into stream. - */ - void saveMap(const std::unique_ptr & map) override; - - /** - * Saves @data as json file with specified @filename - */ - void addToArchive(const JsonNode & data, const std::string & filename); - - /** - * Saves header to zip archive - */ - void writeHeader(); - - /** - * Encodes one tile into string - * @param tile tile to serialize - */ - static std::string writeTerrainTile(const TerrainTile & tile); - - /** - * Saves map level into json - * @param index z coordinate - */ - JsonNode writeTerrainLevel(const int index); - - /** - * Saves all terrain into zip archive - */ - void writeTerrain(); - - /** - * Saves all map objects into zip archive - */ - void writeObjects(); - -private: - CInputOutputStream * buffer; - std::shared_ptr ioApi; - CZipSaver saver;///< object to handle zip archive operations -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatJson.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 "CMapService.h" +#include "../JsonNode.h" + +#include "../filesystem/CZipSaver.h" +#include "../filesystem/CZipLoader.h" +#include "../GameConstants.h" + +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct TriggeredEvent; +struct TerrainTile; +struct PlayerInfo; +class CGObjectInstance; +class AObjectTypeHandler; +class TerrainType; +class RoadType; +class RiverType; + +class JsonSerializeFormat; +class JsonDeserializer; +class JsonSerializer; + +class DLL_LINKAGE CMapFormatJson +{ +public: + static const int VERSION_MAJOR; + static const int VERSION_MINOR; + + static const std::string HEADER_FILE_NAME; + static const std::string OBJECTS_FILE_NAME; + static const std::string TERRAIN_FILE_NAMES[2]; + + int fileVersionMajor; + int fileVersionMinor; +protected: + friend class MapObjectResolver; + std::unique_ptr mapObjectResolver; + + /** ptr to the map object which gets filled by data from the buffer or written to buffer */ + CMap * map; + + /** + * ptr to the map header object which gets filled by data from the buffer. + * (when loading map and mapHeader point to the same object) + */ + CMapHeader * mapHeader; + + CMapFormatJson(); + + static TerrainType * getTerrainByCode(const std::string & code); + static RiverType * getRiverByCode(const std::string & code); + static RoadType * getRoadByCode(const std::string & code); + + void serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const; + + ///common part of header saving/loading + void serializeHeader(JsonSerializeFormat & handler); + + ///player information saving/loading + void serializePlayerInfo(JsonSerializeFormat & handler); + + /** + * Reads team settings to header + */ + void readTeams(JsonDeserializer & handler); + + /** + * Saves team settings to header + */ + void writeTeams(JsonSerializer & handler); + + /** + * Reads triggered events, including victory/loss conditions + */ + void readTriggeredEvents(JsonDeserializer & handler); + + /** + * Writes triggered events, including victory/loss conditions + */ + void writeTriggeredEvents(JsonSerializer & handler); + + /** + * Reads one of triggered events + */ + void readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const; + + /** + * Writes one of triggered events + */ + void writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const; + + void writeDisposedHeroes(JsonSerializeFormat & handler); + + void readDisposedHeroes(JsonSerializeFormat & handler); + + void serializePredefinedHeroes(JsonSerializeFormat & handler); + + void serializeRumors(JsonSerializeFormat & handler); + + void serializeTimedEvents(JsonSerializeFormat & handler); + + ///common part of map attributes saving/loading + void serializeOptions(JsonSerializeFormat & handler); + + /** + * Loads map attributes except header ones + */ + void readOptions(JsonDeserializer & handler); + + /** + * Saves map attributes except header ones + */ + void writeOptions(JsonSerializer & handler); +}; + +class DLL_LINKAGE CMapPatcher : public CMapFormatJson, public IMapPatcher +{ +public: + /** + * Default constructor. + * + * @param stream. A stream containing the map data. + */ + CMapPatcher(const JsonNode & stream); + +public: //IMapPatcher + /** + * Modifies supplied map header using Json data + * + */ + void patchMapHeader(std::unique_ptr & header) override; + +private: + /** + * Reads subset of header that can be replaced by patching. + */ + void readPatchData(); + + JsonNode input; +}; + +class DLL_LINKAGE CMapLoaderJson : public CMapFormatJson, public IMapLoader +{ +public: + /** + * Constructor. + * + * @param stream a stream containing the map data + */ + CMapLoaderJson(CInputStream * stream); + + /** + * Loads the VCMI/Json map file. + * + * @return a unique ptr of the loaded map class + */ + std::unique_ptr loadMap() override; + + /** + * Loads the VCMI/Json map header. + * + * @return a unique ptr of the loaded map header class + */ + std::unique_ptr loadMapHeader() override; + + struct MapObjectLoader + { + MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json); + CMapLoaderJson * owner; + CGObjectInstance * instance; + ObjectInstanceID id; + std::string jsonKey;//full id defined by map creator + JsonNode & configuration; + + ///constructs object (without configuration) + void construct(); + + ///configures object + void configure(); + }; + + /** + * Reads the map header. + */ + void readHeader(const bool complete); + + /** + * Reads complete map. + */ + void readMap(); + + /** + * Reads texts and translations + */ + void readTranslations(); + + static void readTerrainTile(const std::string & src, TerrainTile & tile); + + void readTerrainLevel(const JsonNode & src, const int index); + + void readTerrain(); + + /** + * Loads all map objects from zip archive + */ + void readObjects(); + + bool isExistArchive(const std::string & archiveFilename); + JsonNode getFromArchive(const std::string & archiveFilename); + +private: + CInputStream * buffer; + std::shared_ptr ioApi; + + CZipLoader loader;///< object to handle zip archive operations +}; + +class DLL_LINKAGE CMapSaverJson : public CMapFormatJson, public IMapSaver +{ +public: + /** + * Constructor. + * + * @param stream a stream to save the map to, will contain zip archive + */ + CMapSaverJson(CInputOutputStream * stream); + + ~CMapSaverJson(); + + /** + * Actually saves the VCMI/Json map into stream. + */ + void saveMap(const std::unique_ptr & map) override; + + /** + * Saves @data as json file with specified @filename + */ + void addToArchive(const JsonNode & data, const std::string & filename); + + /** + * Saves header to zip archive + */ + void writeHeader(); + + /** + * Saves texts and translations to zip archive + */ + void writeTranslations(); + + /** + * Encodes one tile into string + * @param tile tile to serialize + */ + static std::string writeTerrainTile(const TerrainTile & tile); + + /** + * Saves map level into json + * @param index z coordinate + */ + JsonNode writeTerrainLevel(const int index); + + /** + * Saves all terrain into zip archive + */ + void writeTerrain(); + + /** + * Saves all map objects into zip archive + */ + void writeObjects(); + +private: + CInputOutputStream * buffer; + std::shared_ptr ioApi; + CZipSaver saver;///< object to handle zip archive operations +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 2168848fa..0f7207e0c 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -13,13 +13,13 @@ #include "../JsonNode.h" #include "../VCMI_Lib.h" -#include "../CModHandler.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -29,7 +29,7 @@ void MapIdentifiersH3M::loadMapping(std::map & resul for (auto entry : mapping.Struct()) { IdentifierID sourceID (entry.second.Integer()); - IdentifierID targetID (*VLC->modh->identifiers.getIdentifier(entry.second.meta, identifierName, entry.first)); + IdentifierID targetID (*VLC->identifiers()->getIdentifier(entry.second.meta, identifierName, entry.first)); result[sourceID] = targetID; } @@ -37,15 +37,18 @@ void MapIdentifiersH3M::loadMapping(std::map & resul void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) { + if (!mapping["supported"].Bool()) + throw std::runtime_error("Unsupported map format!"); + for (auto entryFaction : mapping["buildings"].Struct()) { - FactionID factionID (*VLC->modh->identifiers.getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); + FactionID factionID (*VLC->identifiers()->getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); auto buildingMap = entryFaction.second; for (auto entryBuilding : buildingMap.Struct()) { BuildingID sourceID (entryBuilding.second.Integer()); - BuildingID targetID (*VLC->modh->identifiers.getIdentifier(entryBuilding.second.meta, "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); + BuildingID targetID (*VLC->identifiers()->getIdentifier(entryBuilding.second.meta, "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); mappingFactionBuilding[factionID][sourceID] = targetID; } @@ -53,11 +56,11 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) for (auto entryTemplate : mapping["templates"].Struct()) { - std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); - std::string vcmiName = boost::to_lower_copy(entryTemplate.first); + AnimationPath h3mName = AnimationPath::builtinTODO(entryTemplate.second.String()); + AnimationPath vcmiName = AnimationPath::builtinTODO(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(ResourceID( "SPRITES/" + vcmiName, EResType::ANIMATION))) - logMod->warn("Template animation file %s was not found!", vcmiName); + if (!CResourceHandler::get()->existsResource(vcmiName.addPrefix("SPRITES/"))) + logMod->warn("Template animation file %s was not found!", vcmiName.getOriginalName()); mappingObjectTemplate[h3mName] = vcmiName; } @@ -87,15 +90,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) } } - for (auto entry : mapping["portraits"].Struct()) - { - int32_t sourceID = entry.second.Integer(); - int32_t targetID = *VLC->modh->identifiers.getIdentifier(entry.second.meta, "hero", entry.first); - int32_t iconID = VLC->heroTypes()->getByIndex(targetID)->getIconIndex(); - - mappingHeroPortrait[sourceID] = iconID; - } - + loadMapping(mappingHeroPortrait, mapping["portraits"], "hero"); loadMapping(mappingBuilding, mapping["buildingsCommon"], "building.core:random"); loadMapping(mappingFaction, mapping["factions"], "faction"); loadMapping(mappingCreature, mapping["creatures"], "creature"); @@ -108,7 +103,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) void MapIdentifiersH3M::remapTemplate(ObjectTemplate & objectTemplate) { - std::string name = boost::to_lower_copy(objectTemplate.animationFile); + auto name = objectTemplate.animationFile; if (mappingObjectTemplate.count(name)) objectTemplate.animationFile = mappingObjectTemplate.at(name); @@ -168,7 +163,7 @@ HeroTypeID MapIdentifiersH3M::remap(HeroTypeID input) const return input; } -int32_t MapIdentifiersH3M::remapPortrrait(int32_t input) const +HeroTypeID MapIdentifiersH3M::remapPortrait(HeroTypeID input) const { if (mappingHeroPortrait.count(input)) return mappingHeroPortrait.at(input); diff --git a/lib/mapping/MapIdentifiersH3M.h b/lib/mapping/MapIdentifiersH3M.h index 71cea67dc..9b694f158 100644 --- a/lib/mapping/MapIdentifiersH3M.h +++ b/lib/mapping/MapIdentifiersH3M.h @@ -11,6 +11,7 @@ #pragma once #include "../GameConstants.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -37,13 +38,13 @@ class MapIdentifiersH3M std::map mappingFaction; std::map mappingCreature; std::map mappingHeroType; - std::map mappingHeroPortrait; + std::map mappingHeroPortrait; std::map mappingHeroClass; std::map mappingTerrain; std::map mappingArtifact; std::map mappingSecondarySkill; - std::map mappingObjectTemplate; + std::map mappingObjectTemplate; std::map mappingObjectIndex; template @@ -54,7 +55,7 @@ public: void remapTemplate(ObjectTemplate & objectTemplate); BuildingID remapBuilding(std::optional owner, BuildingID input) const; - int32_t remapPortrrait(int32_t input) const; + HeroTypeID remapPortrait(HeroTypeID input) const; FactionID remap(FactionID input) const; CreatureID remap(CreatureID input) const; HeroTypeID remap(HeroTypeID input) const; diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index feba9e023..4a1c75e49 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -1,410 +1,443 @@ -/* - * MapReaderH3M.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 "MapReaderH3M.h" - -#include "../filesystem/CBinaryReader.h" -#include "../int3.h" -#include "../mapObjects/ObjectTemplate.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template<> -BuildingID MapReaderH3M::remapIdentifier(const BuildingID & identifier) -{ - return identifier; -} - -template<> -GameResID MapReaderH3M::remapIdentifier(const GameResID & identifier) -{ - return identifier; -} - -template<> -SpellID MapReaderH3M::remapIdentifier(const SpellID & identifier) -{ - return identifier; -} - -template -Identifier MapReaderH3M::remapIdentifier(const Identifier & identifier) -{ - return remapper.remap(identifier); -} - -MapReaderH3M::MapReaderH3M(CInputStream * stream) - : reader(std::make_unique(stream)) -{ -} - -void MapReaderH3M::setFormatLevel(const MapFormatFeaturesH3M & newFeatures) -{ - features = newFeatures; -} - -void MapReaderH3M::setIdentifierRemapper(const MapIdentifiersH3M & newRemapper) -{ - remapper = newRemapper; -} - -ArtifactID MapReaderH3M::readArtifact() -{ - ArtifactID result; - - if(features.levelAB) - result = ArtifactID(reader->readUInt16()); - else - result = ArtifactID(reader->readUInt8()); - - if(result == features.artifactIdentifierInvalid) - return ArtifactID::NONE; - - if (result < features.artifactsCount) - return remapIdentifier(result); - - logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); - return ArtifactID::NONE; -} - -ArtifactID MapReaderH3M::readArtifact32() -{ - ArtifactID result(reader->readInt32()); - - if(result == ArtifactID::NONE) - return ArtifactID::NONE; - - if (result < features.artifactsCount) - return remapIdentifier(result); - - logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); - return ArtifactID::NONE; -} - -HeroTypeID MapReaderH3M::readHero() -{ - HeroTypeID result(reader->readUInt8()); - - if(result.getNum() == features.heroIdentifierInvalid) - return HeroTypeID(-1); - - assert(result.getNum() < features.heroesCount); - return remapIdentifier(result); -} - -int32_t MapReaderH3M::readHeroPortrait() -{ - HeroTypeID result(reader->readUInt8()); - - if(result.getNum() == features.heroIdentifierInvalid) - return int32_t(-1); - - assert(result.getNum() < features.heroesPortraitsCount); - return remapper.remapPortrrait(result); -} - -CreatureID MapReaderH3M::readCreature() -{ - CreatureID result; - - if(features.levelAB) - result = CreatureID(reader->readUInt16()); - else - result = CreatureID(reader->readUInt8()); - - if(result == features.creatureIdentifierInvalid) - return CreatureID::NONE; - - if(result < features.creaturesCount) - return remapIdentifier(result);; - - // this may be random creature in army/town, to be randomized later - CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); - assert(randomIndex < CreatureID::NONE); - - if (randomIndex > -16) - return randomIndex; - - logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); - return CreatureID::NONE; -} - -TerrainId MapReaderH3M::readTerrain() -{ - TerrainId result(readUInt8()); - assert(result.getNum() < features.terrainsCount); - return remapIdentifier(result);; -} - -RoadId MapReaderH3M::readRoad() -{ - RoadId result(readInt8()); - assert(result < Road::ORIGINAL_ROAD_COUNT); - return result; -} - -RiverId MapReaderH3M::readRiver() -{ - RiverId result(readInt8()); - assert(result < River::ORIGINAL_RIVER_COUNT); - return result; -} - -SecondarySkill MapReaderH3M::readSkill() -{ - SecondarySkill result(readUInt8()); - assert(result < features.skillsCount); - return remapIdentifier(result);; -} - -SpellID MapReaderH3M::readSpell() -{ - SpellID result(readUInt8()); - if(result == features.spellIdentifierInvalid) - return SpellID::NONE; - if(result == features.spellIdentifierInvalid - 1) - return SpellID::PRESET; - - assert(result < features.spellsCount); - return remapIdentifier(result);; -} - -SpellID MapReaderH3M::readSpell32() -{ - SpellID result(readInt32()); - if(result == features.spellIdentifierInvalid) - return SpellID::NONE; - assert(result < features.spellsCount); - return result; -} - -PlayerColor MapReaderH3M::readPlayer() -{ - PlayerColor result(readUInt8()); - assert(result < PlayerColor::PLAYER_LIMIT || result == PlayerColor::NEUTRAL); - return result; -} - -PlayerColor MapReaderH3M::readPlayer32() -{ - PlayerColor result(readInt32()); - - assert(result < PlayerColor::PLAYER_LIMIT || result == PlayerColor::NEUTRAL); - return result; -} - -void MapReaderH3M::readBitmaskBuildings(std::set & dest, std::optional faction) -{ - std::set h3m; - readBitmask(h3m, features.buildingsBytes, features.buildingsCount, false); - - for (auto const & h3mEntry : h3m) - { - BuildingID mapped = remapper.remapBuilding(faction, h3mEntry); - - if (mapped != BuildingID::NONE) // artifact merchant may be set in random town, but not present in actual town - dest.insert(mapped); - } -} - -void MapReaderH3M::readBitmaskFactions(std::set & dest, bool invert) -{ - readBitmask(dest, features.factionsBytes, features.factionsCount, invert); -} - -void MapReaderH3M::readBitmaskResources(std::set & dest, bool invert) -{ - readBitmask(dest, features.resourcesBytes, features.resourcesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, bool invert) -{ - uint32_t classesCount = reader->readUInt32(); - uint32_t classesBytes = (classesCount + 7) / 8; - - readBitmask(dest, classesBytes, classesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroes(std::vector & dest, bool invert) -{ - readBitmask(dest, features.heroesBytes, features.heroesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) -{ - uint32_t heroesCount = readUInt32(); - uint32_t heroesBytes = (heroesCount + 7) / 8; - assert(heroesCount <= features.heroesCount); - - readBitmask(dest, heroesBytes, heroesCount, invert); -} - -void MapReaderH3M::readBitmaskArtifacts(std::vector &dest, bool invert) -{ - readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); -} - -void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool invert) -{ - uint32_t artifactsCount = reader->readUInt32(); - uint32_t artifactsBytes = (artifactsCount + 7) / 8; - assert(artifactsCount <= features.artifactsCount); - - readBitmask(dest, artifactsBytes, artifactsCount, invert); -} - -void MapReaderH3M::readBitmaskSpells(std::vector & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - -void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - -void MapReaderH3M::readBitmaskSkills(std::vector & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - -void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - -template -void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, const int objectsToRead, bool invert) -{ - for(int byte = 0; byte < bytesToRead; ++byte) - { - const ui8 mask = reader->readUInt8(); - for(int bit = 0; bit < 8; ++bit) - { - if(byte * 8 + bit < objectsToRead) - { - const size_t index = byte * 8 + bit; - const bool flag = mask & (1 << bit); - const bool result = (flag != invert); - - Identifier h3mID(index); - Identifier vcmiID = remapIdentifier(h3mID); - - if (vcmiID.getNum() >= dest.size()) - dest.resize(vcmiID.getNum() + 1); - dest[vcmiID.getNum()] = result; - } - } - } -} - -template -void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) -{ - std::vector bitmap; - bitmap.resize(objectsToRead, false); - readBitmask(bitmap, bytesToRead, objectsToRead, invert); - - for(int i = 0; i < bitmap.size(); i++) - if(bitmap[i]) - dest.insert(static_cast(i)); -} - -int3 MapReaderH3M::readInt3() -{ - int3 p; - p.x = reader->readUInt8(); - p.y = reader->readUInt8(); - p.z = reader->readUInt8(); - return p; -} - -std::shared_ptr MapReaderH3M::readObjectTemplate() -{ - auto tmpl = std::make_shared(); - tmpl->readMap(*reader); - remapper.remapTemplate(*tmpl); - return tmpl; -} - -void MapReaderH3M::skipUnused(size_t amount) -{ - reader->skip(amount); -} - -void MapReaderH3M::skipZero(size_t amount) -{ -#ifdef NDEBUG - skipUnused(amount); -#else - for(size_t i = 0; i < amount; ++i) - { - uint8_t value = reader->readUInt8(); - assert(value == 0); - } -#endif -} - -void MapReaderH3M::readResourses(TResources & resources) -{ - for(int x = 0; x < features.resourcesCount; ++x) - resources[x] = reader->readInt32(); -} - -bool MapReaderH3M::readBool() -{ - uint8_t result = readUInt8(); - assert(result == 0 || result == 1); - - return result != 0; -} - -ui8 MapReaderH3M::readUInt8() -{ - return reader->readUInt8(); -} - -si8 MapReaderH3M::readInt8() -{ - return reader->readInt8(); -} - -ui16 MapReaderH3M::readUInt16() -{ - return reader->readUInt16(); -} - -si16 MapReaderH3M::readInt16() -{ - return reader->readInt16(); -} - -ui32 MapReaderH3M::readUInt32() -{ - return reader->readUInt32(); -} - -si32 MapReaderH3M::readInt32() -{ - return reader->readInt32(); -} - -std::string MapReaderH3M::readBaseString() -{ - return reader->readBaseString(); -} - -CBinaryReader & MapReaderH3M::getInternalReader() -{ - return *reader; -} - -VCMI_LIB_NAMESPACE_END +/* + * MapReaderH3M.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 "MapReaderH3M.h" + +#include "../filesystem/CBinaryReader.h" +#include "../int3.h" +#include "../mapObjects/ObjectTemplate.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template<> +BuildingID MapReaderH3M::remapIdentifier(const BuildingID & identifier) +{ + return identifier; +} + +template<> +GameResID MapReaderH3M::remapIdentifier(const GameResID & identifier) +{ + return identifier; +} + +template<> +SpellID MapReaderH3M::remapIdentifier(const SpellID & identifier) +{ + return identifier; +} + +template<> +PlayerColor MapReaderH3M::remapIdentifier(const PlayerColor & identifier) +{ + return identifier; +} + +template +Identifier MapReaderH3M::remapIdentifier(const Identifier & identifier) +{ + return remapper.remap(identifier); +} + +MapReaderH3M::MapReaderH3M(CInputStream * stream) + : reader(std::make_unique(stream)) +{ +} + +void MapReaderH3M::setFormatLevel(const MapFormatFeaturesH3M & newFeatures) +{ + features = newFeatures; +} + +void MapReaderH3M::setIdentifierRemapper(const MapIdentifiersH3M & newRemapper) +{ + remapper = newRemapper; +} + +ArtifactID MapReaderH3M::readArtifact() +{ + ArtifactID result; + + if(features.levelAB) + result = ArtifactID(reader->readUInt16()); + else + result = ArtifactID(reader->readUInt8()); + + if(result.getNum() == features.artifactIdentifierInvalid) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + +ArtifactID MapReaderH3M::readArtifact8() +{ + ArtifactID result(reader->readUInt8()); + + if(result.getNum() == 0xff) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + +ArtifactID MapReaderH3M::readArtifact32() +{ + ArtifactID result(reader->readInt32()); + + if(result == ArtifactID::NONE) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + +HeroTypeID MapReaderH3M::readHero() +{ + HeroTypeID result(reader->readUInt8()); + + if(result.getNum() == features.heroIdentifierInvalid) + return HeroTypeID(-1); + + assert(result.getNum() < features.heroesCount); + return remapIdentifier(result); +} + +HeroTypeID MapReaderH3M::readHeroPortrait() +{ + HeroTypeID result(reader->readUInt8()); + + if(result.getNum() == features.heroIdentifierInvalid) + return HeroTypeID::NONE; + + if (result.getNum() >= features.heroesPortraitsCount) + { + logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); + return HeroTypeID::NONE; + } + + return remapper.remapPortrait(result); +} + +CreatureID MapReaderH3M::readCreature() +{ + CreatureID result; + + if(features.levelAB) + result = CreatureID(reader->readUInt16()); + else + result = CreatureID(reader->readUInt8()); + + if(result.getNum() == features.creatureIdentifierInvalid) + return CreatureID::NONE; + + if(result.getNum() < features.creaturesCount) + return remapIdentifier(result);; + + // this may be random creature in army/town, to be randomized later + CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); + assert(randomIndex < CreatureID::NONE); + + if (randomIndex.getNum() > -16) + return randomIndex; + + logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); + return CreatureID::NONE; +} + +TerrainId MapReaderH3M::readTerrain() +{ + TerrainId result(readUInt8()); + assert(result.getNum() < features.terrainsCount); + return remapIdentifier(result);; +} + +RoadId MapReaderH3M::readRoad() +{ + RoadId result(readInt8()); + assert(result.getNum() <= features.roadsCount); + return result; +} + +RiverId MapReaderH3M::readRiver() +{ + RiverId result(readInt8()); + assert(result.getNum() <= features.riversCount); + return result; +} + +SecondarySkill MapReaderH3M::readSkill() +{ + SecondarySkill result(readUInt8()); + assert(result.getNum() < features.skillsCount); + return remapIdentifier(result);; +} + +SpellID MapReaderH3M::readSpell() +{ + SpellID result(readUInt8()); + if(result.getNum() == features.spellIdentifierInvalid) + return SpellID::NONE; + if(result.getNum() == features.spellIdentifierInvalid - 1) + return SpellID::PRESET; + + assert(result.getNum() < features.spellsCount); + return remapIdentifier(result);; +} + +SpellID MapReaderH3M::readSpell32() +{ + SpellID result(readInt32()); + if(result.getNum() == features.spellIdentifierInvalid) + return SpellID::NONE; + assert(result.getNum() < features.spellsCount); + return result; +} + +GameResID MapReaderH3M::readGameResID() +{ + GameResID result(readInt8()); + assert(result.getNum() < features.resourcesCount); + return result; +} + +PlayerColor MapReaderH3M::readPlayer() +{ + uint8_t value = readUInt8(); + + if (value == 255) + return PlayerColor::NEUTRAL; + + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + + return PlayerColor(value); +} + +PlayerColor MapReaderH3M::readPlayer32() +{ + uint32_t value = readUInt32(); + + if (value == 255) + return PlayerColor::NEUTRAL; + + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + + return PlayerColor(value); +} + +void MapReaderH3M::readBitmaskBuildings(std::set & dest, std::optional faction) +{ + std::set h3m; + readBitmask(h3m, features.buildingsBytes, features.buildingsCount, false); + + for (auto const & h3mEntry : h3m) + { + BuildingID mapped = remapper.remapBuilding(faction, h3mEntry); + + if (mapped != BuildingID::NONE) // artifact merchant may be set in random town, but not present in actual town + dest.insert(mapped); + } +} + +void MapReaderH3M::readBitmaskFactions(std::set & dest, bool invert) +{ + readBitmask(dest, features.factionsBytes, features.factionsCount, invert); +} + +void MapReaderH3M::readBitmaskPlayers(std::set & dest, bool invert) +{ + readBitmask(dest, 1, 8, invert); +} + +void MapReaderH3M::readBitmaskResources(std::set & dest, bool invert) +{ + readBitmask(dest, features.resourcesBytes, features.resourcesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, bool invert) +{ + uint32_t classesCount = reader->readUInt32(); + uint32_t classesBytes = (classesCount + 7) / 8; + + readBitmask(dest, classesBytes, classesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroes(std::set & dest, bool invert) +{ + readBitmask(dest, features.heroesBytes, features.heroesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroesSized(std::set & dest, bool invert) +{ + uint32_t heroesCount = readUInt32(); + uint32_t heroesBytes = (heroesCount + 7) / 8; + assert(heroesCount <= features.heroesCount); + + readBitmask(dest, heroesBytes, heroesCount, invert); +} + +void MapReaderH3M::readBitmaskArtifacts(std::set &dest, bool invert) +{ + readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); +} + +void MapReaderH3M::readBitmaskArtifactsSized(std::set &dest, bool invert) +{ + uint32_t artifactsCount = reader->readUInt32(); + uint32_t artifactsBytes = (artifactsCount + 7) / 8; + assert(artifactsCount <= features.artifactsCount); + + readBitmask(dest, artifactsBytes, artifactsCount, invert); +} + +void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) +{ + readBitmask(dest, features.spellsBytes, features.spellsCount, invert); +} + +void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) +{ + readBitmask(dest, features.skillsBytes, features.skillsCount, invert); +} + +template +void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) +{ + for(int byte = 0; byte < bytesToRead; ++byte) + { + const ui8 mask = reader->readUInt8(); + for(int bit = 0; bit < 8; ++bit) + { + if(byte * 8 + bit < objectsToRead) + { + const size_t index = byte * 8 + bit; + const bool flag = mask & (1 << bit); + const bool result = (flag != invert); + + Identifier h3mID(index); + Identifier vcmiID = remapIdentifier(h3mID); + + if (result) + dest.insert(vcmiID); + else + dest.erase(vcmiID); + } + } + } +} + +int3 MapReaderH3M::readInt3() +{ + int3 p; + p.x = reader->readUInt8(); + p.y = reader->readUInt8(); + p.z = reader->readUInt8(); + return p; +} + +std::shared_ptr MapReaderH3M::readObjectTemplate() +{ + auto tmpl = std::make_shared(); + tmpl->readMap(*reader); + remapper.remapTemplate(*tmpl); + return tmpl; +} + +void MapReaderH3M::skipUnused(size_t amount) +{ + reader->skip(amount); +} + +void MapReaderH3M::skipZero(size_t amount) +{ +#ifdef NDEBUG + skipUnused(amount); +#else + for(size_t i = 0; i < amount; ++i) + { + uint8_t value = reader->readUInt8(); + assert(value == 0); + } +#endif +} + +void MapReaderH3M::readResourses(TResources & resources) +{ + for(int x = 0; x < features.resourcesCount; ++x) + resources[x] = reader->readInt32(); +} + +bool MapReaderH3M::readBool() +{ + uint8_t result = readUInt8(); + assert(result == 0 || result == 1); + + return result != 0; +} + +ui8 MapReaderH3M::readUInt8() +{ + return reader->readUInt8(); +} + +si8 MapReaderH3M::readInt8() +{ + return reader->readInt8(); +} + +ui16 MapReaderH3M::readUInt16() +{ + return reader->readUInt16(); +} + +si16 MapReaderH3M::readInt16() +{ + return reader->readInt16(); +} + +ui32 MapReaderH3M::readUInt32() +{ + return reader->readUInt32(); +} + +si32 MapReaderH3M::readInt32() +{ + return reader->readInt32(); +} + +std::string MapReaderH3M::readBaseString() +{ + return reader->readBaseString(); +} + +CBinaryReader & MapReaderH3M::getInternalReader() +{ + return *reader; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index c8e6bf6ff..e2b5e01b8 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -1,99 +1,97 @@ -/* - * MapReaderH3M.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" -#include "MapFeaturesH3M.h" -#include "MapIdentifiersH3M.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBinaryReader; -class CInputStream; -struct MapFormatFeaturesH3M; -class int3; -enum class EMapFormat : uint8_t; - -class MapReaderH3M -{ -public: - explicit MapReaderH3M(CInputStream * stream); - - void setFormatLevel(const MapFormatFeaturesH3M & features); - void setIdentifierRemapper(const MapIdentifiersH3M & remapper); - - ArtifactID readArtifact(); - ArtifactID readArtifact32(); - CreatureID readCreature(); - HeroTypeID readHero(); - int32_t readHeroPortrait(); - TerrainId readTerrain(); - RoadId readRoad(); - RiverId readRiver(); - SecondarySkill readSkill(); - SpellID readSpell(); - SpellID readSpell32(); - PlayerColor readPlayer(); - PlayerColor readPlayer32(); - - void readBitmaskBuildings(std::set & dest, std::optional faction); - void readBitmaskFactions(std::set & dest, bool invert); - void readBitmaskResources(std::set & dest, bool invert); - void readBitmaskHeroClassesSized(std::set & dest, bool invert); - void readBitmaskHeroes(std::vector & dest, bool invert); - void readBitmaskHeroesSized(std::vector & dest, bool invert); - void readBitmaskArtifacts(std::vector & dest, bool invert); - void readBitmaskArtifactsSized(std::vector & dest, bool invert); - void readBitmaskSpells(std::vector & dest, bool invert); - void readBitmaskSpells(std::set & dest, bool invert); - void readBitmaskSkills(std::vector & dest, bool invert); - void readBitmaskSkills(std::set & dest, bool invert); - - int3 readInt3(); - - std::shared_ptr readObjectTemplate(); - - void skipUnused(size_t amount); - void skipZero(size_t amount); - - void readResourses(TResources & resources); - - bool readBool(); - - ui8 readUInt8(); - si8 readInt8(); - ui16 readUInt16(); - si16 readInt16(); - ui32 readUInt32(); - si32 readInt32(); - - std::string readBaseString(); - - CBinaryReader & getInternalReader(); - -private: - template - Identifier remapIdentifier(const Identifier & identifier); - - template - void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); - - template - void readBitmask(std::vector & dest, int bytesToRead, int objectsToRead, bool invert); - - MapFormatFeaturesH3M features; - MapIdentifiersH3M remapper; - - std::unique_ptr reader; -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapReaderH3M.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" +#include "MapFeaturesH3M.h" +#include "MapIdentifiersH3M.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBinaryReader; +class CInputStream; +struct MapFormatFeaturesH3M; +class int3; +enum class EMapFormat : uint8_t; + +class MapReaderH3M +{ +public: + explicit MapReaderH3M(CInputStream * stream); + + void setFormatLevel(const MapFormatFeaturesH3M & features); + void setIdentifierRemapper(const MapIdentifiersH3M & remapper); + + ArtifactID readArtifact(); + ArtifactID readArtifact8(); + ArtifactID readArtifact32(); + CreatureID readCreature(); + HeroTypeID readHero(); + HeroTypeID readHeroPortrait(); + TerrainId readTerrain(); + RoadId readRoad(); + RiverId readRiver(); + SecondarySkill readSkill(); + SpellID readSpell(); + SpellID readSpell32(); + GameResID readGameResID(); + PlayerColor readPlayer(); + PlayerColor readPlayer32(); + + void readBitmaskBuildings(std::set & dest, std::optional faction); + void readBitmaskFactions(std::set & dest, bool invert); + void readBitmaskPlayers(std::set & dest, bool invert); + void readBitmaskResources(std::set & dest, bool invert); + void readBitmaskHeroClassesSized(std::set & dest, bool invert); + void readBitmaskHeroes(std::set & dest, bool invert); + void readBitmaskHeroesSized(std::set & dest, bool invert); + void readBitmaskArtifacts(std::set & dest, bool invert); + void readBitmaskArtifactsSized(std::set & dest, bool invert); + void readBitmaskSpells(std::set & dest, bool invert); + void readBitmaskSkills(std::set & dest, bool invert); + + int3 readInt3(); + + std::shared_ptr readObjectTemplate(); + + void skipUnused(size_t amount); + void skipZero(size_t amount); + + void readResourses(TResources & resources); + + bool readBool(); + + ui8 readUInt8(); + si8 readInt8(); + ui16 readUInt16(); + si16 readInt16(); + ui32 readUInt32(); + si32 readInt32(); + + std::string readBaseString(); + + CBinaryReader & getInternalReader(); + +private: + template + Identifier remapIdentifier(const Identifier & identifier); + + template + void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); + + MapFormatFeaturesH3M features; + MapIdentifiersH3M remapper; + + std::unique_ptr reader; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/ObstacleProxy.cpp b/lib/mapping/ObstacleProxy.cpp index d6a30de33..fd295572b 100644 --- a/lib/mapping/ObstacleProxy.cpp +++ b/lib/mapping/ObstacleProxy.cpp @@ -208,10 +208,11 @@ bool EditorObstaclePlacer::isInTheMap(const int3& tile) return map->isInTheMap(tile); } -void EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) +std::set EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) { auto obstacles = createObstacles(rand); finalInsertion(map->getEditManager(), obstacles); + return obstacles; } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/ObstacleProxy.h b/lib/mapping/ObstacleProxy.h index ef30e4bd0..70f1f46df 100644 --- a/lib/mapping/ObstacleProxy.h +++ b/lib/mapping/ObstacleProxy.h @@ -67,10 +67,10 @@ public: bool isInTheMap(const int3& tile) override; - void placeObstacles(CRandomGenerator& rand); + std::set placeObstacles(CRandomGenerator& rand); private: CMap* map; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ActiveModsInSaveList.cpp b/lib/modding/ActiveModsInSaveList.cpp new file mode 100644 index 000000000..264e122ae --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.cpp @@ -0,0 +1,112 @@ +/* + * ActiveModsInSaveList.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 "ActiveModsInSaveList.h" + +#include "../VCMI_Lib.h" +#include "CModInfo.h" +#include "CModHandler.h" +#include "ModIncompatibility.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::vector ActiveModsInSaveList::getActiveMods() +{ + return VLC->modh->getActiveMods(); +} + +const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod) +{ + return VLC->modh->getModInfo(mod).getVerificationInfo(); +} + +void ActiveModsInSaveList::verifyActiveMods(const std::vector> & modList) +{ + auto searchVerificationInfo = [&modList](const TModID & m) -> const ModVerificationInfo* + { + for(auto & i : modList) + if(i.first == m) + return &i.second; + return nullptr; + }; + + std::vector missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + + for(const auto & m : VLC->modh->getActiveMods()) + { + if(searchVerificationInfo(m)) + continue; + + //TODO: support actual disabling of these mods + if(VLC->modh->getModInfo(m).checkModGameplayAffecting()) + excessiveMods.push_back(m); + } + + for(const auto & infoPair : modList) + { + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + + if(!vstd::contains(VLC->modh->getAllMods(), remoteModId)) + { + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed + continue; + } + + auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo(); + modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(VLC->modh->getActiveMods(), remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + continue; + + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //incompatible mod impacts gameplay + } + + //filter mods + for(auto & m : missingMods) + { + if(auto * vInfo = searchVerificationInfo(m)) + { + assert(vInfo->parent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + for(auto & m : excessiveMods) + { + auto & vInfo = VLC->modh->getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } + + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); + + //TODO: support actual enabling of required mods +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ActiveModsInSaveList.h b/lib/modding/ActiveModsInSaveList.h new file mode 100644 index 000000000..846b51af7 --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.h @@ -0,0 +1,51 @@ +/* + * ActiveModsInSaveList.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 "ModVerificationInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ActiveModsInSaveList +{ + std::vector getActiveMods(); + const ModVerificationInfo & getVerificationInfo(TModID mod); + + /// Checks whether provided mod list is compatible with current VLC and throws on failure + void verifyActiveMods(const std::vector> & modList); +public: + template void serialize(Handler &h, const int version) + { + if(h.saving) + { + std::vector activeMods = getActiveMods(); + h & activeMods; + for(const auto & m : activeMods) + h & getVerificationInfo(m); + } + else + { + std::vector saveActiveMods; + h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } + + verifyActiveMods(saveModInfos); + } + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp new file mode 100644 index 000000000..88e3f1851 --- /dev/null +++ b/lib/modding/CModHandler.cpp @@ -0,0 +1,520 @@ +/* + * CModHandler.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 "CModHandler.h" + +#include "CModInfo.h" +#include "ModScope.h" +#include "ContentTypeHandler.h" +#include "IdentifierStorage.h" +#include "ModIncompatibility.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 "../filesystem/Filesystem.h" +#include "../spells/CSpellHandler.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()) +{ +} + +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(bool onlyEssential) +{ + JsonNode modConfig; + + if(onlyEssential) + { + loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods + } + else + { + 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; +} + +std::vector CModHandler::getActiveMods() const +{ + return activeMods; +} + +std::string CModHandler::getModLoadErrors() const +{ + return modLoadErrors->toString(); +} + +const CModInfo & CModHandler::getModInfo(const TModID & modId) const +{ + return allMods.at(modId); +} + +static JsonNode genDefaultFS() +{ + // default FS config for mods: directory "Content" that acts as H3 root directory + JsonNode defaultFS; + defaultFS[""].Vector().resize(2); + defaultFS[""].Vector()[0]["type"].String() = "zip"; + defaultFS[""].Vector()[0]["path"].String() = "/Content.zip"; + defaultFS[""].Vector()[1]["type"].String() = "dir"; + defaultFS[""].Vector()[1]["path"].String() = "/Content"; + return defaultFS; +} + +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"]); + 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(); +} + +void CModHandler::loadModFilesystems() +{ + CGeneralTextHandler::detectInstallParameters(); + + activeMods = validateAndSortDependencies(activeMods); + + coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); + + for(std::string & modName : activeMods) + { + CModInfo & mod = allMods[modName]; + CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); + } +} + +TModID CModHandler::findResourceOrigin(const ResourcePath & name) const +{ + for(const auto & modID : boost::adaptors::reverse(activeMods)) + { + if(CResourceHandler::get(modID)->existsResource(name)) + return modID; + } + + if(CResourceHandler::get("core")->existsResource(name)) + return "core"; + + if(CResourceHandler::get("mapEditor")->existsResource(name)) + return "core"; // Workaround for loading maps via map editor + + assert(0); + return ""; +} + +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; +} + +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; + + logMod->error("Mod not found: '%s'", modId); + return {}; +} + +void CModHandler::initializeConfig() +{ + VLC->settingsHandler->load(coreMod->config["settings"]); + + for(const TModID & modName : activeMods) + { + const auto & mod = allMods[modName]; + if (!mod.config["settings"].isNull()) + VLC->settingsHandler->load(mod.config["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 (!language.hasTranslation) + continue; + + 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]; + + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string modBaseLanguage = allMods[modName].baseLanguage; + + auto baseTranslationList = mod.config["translations"].convertTo >(); + auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo >(); + + JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList); + JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList); + + VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation); + VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation); +} + +void CModHandler::load() +{ + CStopWatch totalTime; + CStopWatch timer; + + logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); + + content->init(); + + for(const TModID & modName : activeMods) + { + logMod->trace("Generating checksum for %s", modName); + allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(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()); + + content->load(*coreMod); + for(const TModID & modName : activeMods) + content->load(allMods[modName]); + +#if SCRIPTING_ENABLED + VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load +#endif + + content->loadCustom(); + + for(const TModID & modName : activeMods) + loadTranslation(modName); + + for(const TModID & modName : activeMods) + if (!validateTranslations(modName)) + allMods[modName].validation = CModInfo::FAILED; + + logMod->info("\tLoading mod data: %d ms", timer.getDiff()); + VLC->creh->loadCrExpMod(); + VLC->identifiersHandler->finalize(); + logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); + + content->afterLoadFinalization(); + logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); + logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); +} + +void CModHandler::afterLoad(bool onlyEssential) +{ + JsonNode modSettings; + for (auto & modEntry : allMods) + { + std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); + + modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); + } + 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.toJson(); + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h new file mode 100644 index 000000000..4028ce6c2 --- /dev/null +++ b/lib/modding/CModHandler.h @@ -0,0 +1,88 @@ +/* + * CModHandler.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 CModHandler; +class CModIndentifier; +class CModInfo; +struct CModVersion; +class JsonNode; +class IHandlerBase; +class CIdentifierStorage; +class CContentHandler; +struct ModVerificationInfo; +class ResourcePath; +class MetaString; + +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; + + 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); + + bool validateTranslations(TModID modName) const; + + CModVersion getModVersion(TModID modName) const; + +public: + std::shared_ptr content; //(!)Do not serialize FIXME: make private + + /// receives list of available mods and trying to load mod.json from all of them + void initializeConfig(); + void loadMods(bool onlyEssential = false); + void loadModFilesystems(); + + /// returns ID of mod that provides selected file resource + TModID findResourceOrigin(const ResourcePath & name) const; + + std::string getModLanguage(const TModID & modId) const; + + std::set getModDependencies(const TModID & modId, bool & isModFound) const; + + /// returns list of all (active) mods + std::vector getAllMods() 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; + + /// load content from all available mods + void load(); + void afterLoad(bool onlyEssential); + + CModHandler(); + ~CModHandler(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp new file mode 100644 index 000000000..ca7d0a97d --- /dev/null +++ b/lib/modding/CModInfo.cpp @@ -0,0 +1,200 @@ +/* + * 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.setMeta(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)) +{ + verificationInfo.name = config["name"].String(); + 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 != VLC->generaltexth->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 +{ + 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 new file mode 100644 index 000000000..b89fdae02 --- /dev/null +++ b/lib/modding/CModInfo.h @@ -0,0 +1,82 @@ +/* + * 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 "../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/CModVersion.cpp b/lib/modding/CModVersion.cpp similarity index 54% rename from lib/CModVersion.cpp rename to lib/modding/CModVersion.cpp index 0624dc1e7..3ed3640a8 100644 --- a/lib/CModVersion.cpp +++ b/lib/modding/CModVersion.cpp @@ -20,9 +20,9 @@ CModVersion CModVersion::GameVersion() CModVersion CModVersion::fromString(std::string from) { - int major = 0; - int minor = 0; - int patch = 0; + int major = Any; + int minor = Any; + int patch = Any; try { auto pointPos = from.find('.'); @@ -45,19 +45,47 @@ CModVersion CModVersion::fromString(std::string from) std::string CModVersion::toString() const { - return std::to_string(major) + '.' + std::to_string(minor) + '.' + std::to_string(patch); + std::string res; + if(major != Any) + { + res += std::to_string(major); + if(minor != Any) + { + res += '.' + std::to_string(minor); + if(patch != Any) + res += '.' + std::to_string(patch); + } + } + return res; } bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool checkPatch) const { + bool doCheckMinor = checkMinor && minor != Any && other.minor != Any; + bool doCheckPatch = checkPatch && patch != Any && other.patch != Any; + + assert(!doCheckPatch || (doCheckPatch && doCheckMinor)); + return (major == other.major && - (!checkMinor || minor >= other.minor) && - (!checkPatch || minor > other.minor || (minor == other.minor && patch >= other.patch))); + (!doCheckMinor || minor >= other.minor) && + (!doCheckPatch || minor > other.minor || (minor == other.minor && patch >= other.patch))); } bool CModVersion::isNull() const { - return major == 0 && minor == 0 && patch == 0; + return major == Any; +} + +bool operator < (const CModVersion & lesser, const CModVersion & greater) +{ + //specific is "greater" than non-specific, that's why do not check for Any value + if(lesser.major == greater.major) + { + if(lesser.minor == greater.minor) + return lesser.patch < greater.patch; + return lesser.minor < greater.minor; + } + return lesser.major < greater.major; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CModVersion.h b/lib/modding/CModVersion.h similarity index 74% rename from lib/CModVersion.h rename to lib/modding/CModVersion.h index 877615f27..e0ae7c8be 100644 --- a/lib/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -10,13 +10,23 @@ #pragma once +#ifdef __UCLIBC__ +#undef major +#undef minor +#undef patch +#endif + VCMI_LIB_NAMESPACE_BEGIN +using TModID = std::string; + struct DLL_LINKAGE CModVersion { - int major = 0; - int minor = 0; - int patch = 0; + static const int Any = -1; + + int major = Any; + int minor = Any; + int patch = Any; CModVersion() = default; CModVersion(int mj, int mi, int p): major(mj), minor(mi), patch(p) {} @@ -36,4 +46,6 @@ struct DLL_LINKAGE CModVersion } }; +DLL_LINKAGE bool operator < (const CModVersion & lesser, const CModVersion & greater); + VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp new file mode 100644 index 000000000..977ad172b --- /dev/null +++ b/lib/modding/ContentTypeHandler.cpp @@ -0,0 +1,259 @@ +/* + * ContentTypeHandler.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 "ContentTypeHandler.h" + +#include "CModHandler.h" +#include "CModInfo.h" +#include "ModScope.h" + +#include "../BattleFieldHandler.h" +#include "../CArtHandler.h" +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CHeroHandler.h" +#include "../CSkillHandler.h" +#include "../CStopWatch.h" +#include "../CTownHandler.h" +#include "../GameSettings.h" +#include "../IHandlerBase.h" +#include "../Languages.h" +#include "../ObstacleHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../ScriptHandler.h" +#include "../constants/StringConstants.h" +#include "../TerrainHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../rmg/CRmgTemplateStorage.h" +#include "../spells/CSpellHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): + handler(handler), + objectName(objectName), + originalData(handler->loadLegacyData()) +{ + for(auto & node : originalData) + { + node.setMeta(ModScope::scopeBuiltin()); + } +} + +bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector & fileList, bool validate) +{ + bool result = false; + JsonNode data = JsonUtils::assembleFromFiles(fileList, result); + data.setMeta(modName); + + ModInfo & modInfo = modData[modName]; + + for(auto entry : data.Struct()) + { + size_t colon = entry.first.find(':'); + + if (colon == std::string::npos) + { + // normal object, local to this mod + std::swap(modInfo.modData[entry.first], entry.second); + } + else + { + std::string remoteName = entry.first.substr(0, colon); + std::string objectName = entry.first.substr(colon + 1); + + // patching this mod? Send warning and continue - this situation can be handled normally + if (remoteName == modName) + logMod->warn("Redundant namespace definition for %s", objectName); + + logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); + JsonNode & remoteConf = modData[remoteName].patches[objectName]; + + JsonUtils::merge(remoteConf, entry.second); + } + } + return result; +} + +bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) +{ + ModInfo & modInfo = modData[modName]; + bool result = true; + + auto performValidate = [&,this](JsonNode & data, const std::string & name){ + handler->beforeValidate(data); + if (validate) + result &= JsonUtils::validate(data, "vcmi:" + objectName, name); + }; + + // apply patches + if (!modInfo.patches.isNull()) + JsonUtils::merge(modInfo.modData, modInfo.patches); + + for(auto & entry : modInfo.modData.Struct()) + { + const std::string & name = entry.first; + JsonNode & data = entry.second; + + if (data.meta != modName) + { + // in this scenario, entire object record comes from another mod + // normally, this is used to "patch" object from another mod (which is legal) + // however in this case there is no object to patch. This might happen in such cases: + // - 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 from mod %s but no such object exist!", data.meta, name, modName); + continue; + } + + if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) + { + if (modName != "core") + logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName); + + // try to add H3 object data + size_t index = static_cast(data["index"].Float()); + + if(originalData.size() > index) + { + logMod->trace("found original data in loadMod(%s) at index %d", name, index); + JsonUtils::merge(originalData[index], data); + std::swap(originalData[index], data); + originalData[index].clear(); // do not use same data twice (same ID) + } + else + { + logMod->trace("no original data in loadMod(%s) at index %d", name, index); + } + performValidate(data, name); + handler->loadObject(modName, name, data, index); + } + else + { + // normal new object + logMod->trace("no index in loadMod(%s)", name); + performValidate(data,name); + handler->loadObject(modName, name, data); + } + } + return result; +} + +void ContentTypeHandler::loadCustom() +{ + handler->loadCustom(); +} + +void ContentTypeHandler::afterLoadFinalization() +{ + handler->afterLoadFinalization(); +} + +void CContentHandler::init() +{ + handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(&VLC->heroh->classes, "heroClass"))); + handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth, "artifact"))); + handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh, "creature"))); + handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh, "faction"))); + handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh, "object"))); + handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh, "hero"))); + handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell"))); + handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill"))); + handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh, "template"))); +#if SCRIPTING_ENABLED + handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script"))); +#endif + handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield"))); + handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler, "terrain"))); + handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler, "river"))); + handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler, "road"))); + handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle"))); + //TODO: any other types of moddables? +} + +bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) +{ + bool result = true; + for(auto & handler : handlers) + { + result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo >(), validate); + } + return result; +} + +bool CContentHandler::loadMod(const std::string & modName, bool validate) +{ + bool result = true; + for(auto & handler : handlers) + { + result &= handler.second.loadMod(modName, validate); + } + return result; +} + +void CContentHandler::loadCustom() +{ + for(auto & handler : handlers) + { + handler.second.loadCustom(); + } +} + +void CContentHandler::afterLoadFinalization() +{ + for(auto & handler : handlers) + { + handler.second.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); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h new file mode 100644 index 000000000..e1224013e --- /dev/null +++ b/lib/modding/ContentTypeHandler.h @@ -0,0 +1,77 @@ +/* + * ContentTypeHandler.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 "../JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IHandlerBase; +class CModInfo; + +/// internal type to handle loading of one data type (e.g. artifacts, creatures) +class DLL_LINKAGE ContentTypeHandler +{ +public: + struct ModInfo + { + /// mod data from this mod and for this mod + JsonNode modData; + /// mod data for this mod from other mods (patches) + JsonNode patches; + }; + /// handler to which all data will be loaded + IHandlerBase * handler; + std::string objectName; + + /// contains all loaded H3 data + std::vector originalData; + std::map modData; + + ContentTypeHandler(IHandlerBase * handler, const std::string & objectName); + + /// 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 loadMod(const std::string & modName, bool validate); + void loadCustom(); + void afterLoadFinalization(); +}; + +/// 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); + + /// actually loads data in mod + void load(CModInfo & mod); + + void loadCustom(); + + /// all data was loaded, time for final validation / integration + void afterLoadFinalization(); + + const ContentTypeHandler & operator[] (const std::string & name) const; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp new file mode 100644 index 000000000..2a360db36 --- /dev/null +++ b/lib/modding/IdentifierStorage.cpp @@ -0,0 +1,437 @@ +/* + * IdentifierStorage.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 "IdentifierStorage.h" + +#include "CModHandler.h" +#include "ModScope.h" + +#include "../JsonNode.h" +#include "../VCMI_Lib.h" +#include "../constants/StringConstants.h" +#include "../spells/CSpellHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +CIdentifierStorage::CIdentifierStorage() +{ + //TODO: moddable spell schools + for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) + registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id.getNum()); + + registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool::ANY.getNum()); + + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); + + for (int i = 0; i < std::size(GameConstants::PLAYER_COLOR_NAMES); ++i) + registerObject(ModScope::scopeBuiltin(), "playerColor", GameConstants::PLAYER_COLOR_NAMES[i], i); + + + for(int i=0; iwarn("BIG WARNING: identifier %s seems to be broken!", ID); + else + { + size_t pos = 0; + do + { + if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase + { + logMod->warn("Warning: identifier %s is not in camelCase!", ID); + ID[pos] = std::tolower(ID[pos]);// Try to fix the ID + } + pos = ID.find('.', pos); + } + while(pos++ != std::string::npos); + } +} + +void CIdentifierStorage::requestIdentifier(ObjectCallback callback) const +{ + checkIdentifier(callback.type); + checkIdentifier(callback.name); + + assert(!callback.localScope.empty()); + + if (state != ELoadingState::FINISHED) // enqueue request if loading is still in progress + scheduledRequests.push_back(callback); + else // execute immediately for "late" requests + resolveIdentifier(callback); +} + +CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional) +{ + assert(!scope.empty()); + + auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); + auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); + + if (scope == scopeAndFullName.first) + logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); + + ObjectCallback result; + result.localScope = scope; + result.remoteScope = scopeAndFullName.first; + result.type = typeAndName.first; + result.name = typeAndName.second; + result.callback = callback; + result.optional = optional; + return result; +} + +CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional) +{ + assert(!scope.empty()); + + auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); + auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); + + if(!typeAndName.first.empty()) + { + if (typeAndName.first != type) + logMod->warn("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); + else + logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope); + } + + if (scope == scopeAndFullName.first) + logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); + + ObjectCallback result; + result.localScope = scope; + result.remoteScope = scopeAndFullName.first; + result.type = type; + result.name = typeAndName.second; + result.callback = callback; + result.optional = optional; + return result; +} + +void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameWithType(scope, fullName, callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameWithType(name.meta, name.String(), callback, false)); +} + +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)); +} + +void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, true)); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const +{ + assert(state != ELoadingState::LOADING); + + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s of type %s from mod %s", name , type ,scope); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) const +{ + assert(state != ELoadingState::LOADING); + + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s of type %s from mod %s", name.String(), type, name.meta); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) const +{ + assert(state != ELoadingState::LOADING); + + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s from mod %s", name.String(), name.meta); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) const +{ + assert(state != ELoadingState::LOADING); + + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); + + return std::optional(); +} + +void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) +{ + assert(state != ELoadingState::FINISHED); + + ObjectData data; + data.scope = scope; + data.id = identifier; + + std::string fullID = type + '.' + name; + checkIdentifier(fullID); + + std::pair mapping = std::make_pair(fullID, data); + if(!vstd::containsMapping(registeredObjects, mapping)) + { + logMod->trace("registered %s as %s:%s", fullID, scope, identifier); + registeredObjects.insert(mapping); + } +} + +std::vector CIdentifierStorage::getPossibleIdentifiers(const ObjectCallback & request) const +{ + std::set allowedScopes; + bool isValidScope = true; + + // called have not specified destination mod explicitly + if (request.remoteScope.empty()) + { + // special scope that should have access to all in-game objects + if (request.localScope == ModScope::scopeGame()) + { + for(const auto & modName : VLC->modh->getActiveMods()) + allowedScopes.insert(modName); + } + + // normally ID's from all required mods, own mod and virtual built-in mod are allowed + else if(request.localScope != ModScope::scopeBuiltin() && !request.localScope.empty()) + { + allowedScopes = VLC->modh->getModDependencies(request.localScope, isValidScope); + + if(!isValidScope) + return std::vector(); + + allowedScopes.insert(request.localScope); + } + + // all mods can access built-in mod + allowedScopes.insert(ModScope::scopeBuiltin()); + } + else + { + //if destination mod was specified explicitly, restrict lookup to this mod + if(request.remoteScope == ModScope::scopeBuiltin() ) + { + //built-in mod is an implicit dependency for all mods, allow access into it + allowedScopes.insert(request.remoteScope); + } + else if ( request.localScope == ModScope::scopeGame() ) + { + // allow access, this is special scope that should have access to all in-game objects + allowedScopes.insert(request.remoteScope); + } + else if(request.remoteScope == request.localScope ) + { + // allow self-access + allowedScopes.insert(request.remoteScope); + } + else + { + // allow access only if mod is in our dependencies + auto myDeps = VLC->modh->getModDependencies(request.localScope, isValidScope); + + if(!isValidScope) + return std::vector(); + + if(myDeps.count(request.remoteScope)) + allowedScopes.insert(request.remoteScope); + } + } + + std::string fullID = request.type + '.' + request.name; + + auto entries = registeredObjects.equal_range(fullID); + if (entries.first != entries.second) + { + std::vector locatedIDs; + + for (auto it = entries.first; it != entries.second; it++) + { + if (vstd::contains(allowedScopes, it->second.scope)) + { + locatedIDs.push_back(it->second); + } + } + return locatedIDs; + } + return std::vector(); +} + +bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const +{ + auto identifiers = getPossibleIdentifiers(request); + if (identifiers.size() == 1) // normally resolved ID + { + request.callback(identifiers.front().id); + return true; + } + + if (request.optional && identifiers.empty()) // failed to resolve optinal ID + { + return true; + } + + // error found. Try to generate some debug info + if(identifiers.empty()) + logMod->error("Unknown identifier!"); + else + logMod->error("Ambiguous identifier request!"); + + logMod->error("Request for %s.%s from mod %s", request.type, request.name, request.localScope); + + for(const auto & id : identifiers) + { + logMod->error("\tID is available in mod %s", id.scope); + } + return false; +} + +void CIdentifierStorage::finalize() +{ + assert(state == ELoadingState::LOADING); + + state = ELoadingState::FINALIZING; + bool errorsFound = false; + + while ( !scheduledRequests.empty() ) + { + // Use local copy since new requests may appear during resolving, invalidating any iterators + auto request = scheduledRequests.back(); + scheduledRequests.pop_back(); + + if (!resolveIdentifier(request)) + errorsFound = true; + } + + debugDumpIdentifiers(); + + if (errorsFound) + logMod->error("All known identifiers were dumped into log file"); + + assert(errorsFound == false); + state = ELoadingState::FINISHED; + +} + +void CIdentifierStorage::debugDumpIdentifiers() +{ + logMod->trace("List of all registered objects:"); + + std::map> objectList; + + for(const auto & object : registeredObjects) + { + size_t categoryLength = object.first.find('.'); + assert(categoryLength != std::string::npos); + + std::string objectCategory = object.first.substr(0, categoryLength); + std::string objectName = object.first.substr(categoryLength + 1); + + objectList[objectCategory].push_back("[" + object.second.scope + "] " + objectName); + } + + for(auto & category : objectList) + boost::range::sort(category.second); + + for(const auto & category : objectList) + { + logMod->trace(""); + logMod->trace("### %s", category.first); + logMod->trace(""); + + for(const auto & entry : category.second) + logMod->trace("- " + entry); + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h new file mode 100644 index 000000000..31857c447 --- /dev/null +++ b/lib/modding/IdentifierStorage.h @@ -0,0 +1,101 @@ +/* + * IdentifierStorage.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; + +/// class that stores all object identifiers strings and maps them to numeric ID's +/// if possible, objects ID's should be in format ., camelCase e.g. "creature.grandElf" +class DLL_LINKAGE CIdentifierStorage +{ + enum class ELoadingState + { + LOADING, + FINALIZING, + FINISHED + }; + + struct ObjectCallback // entry created on ID request + { + std::string localScope; /// scope from which this ID was requested + std::string remoteScope; /// scope in which this object must be found + std::string type; /// type, e.g. creature, faction, hero, etc + std::string name; /// string ID + std::function callback; + bool optional; + + /// Builds callback from identifier in form "targetMod:type.name" + static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional); + + /// Builds callback from identifier in form "targetMod:name" + static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional); + + private: + ObjectCallback() = default; + }; + + struct ObjectData // entry created on ID registration + { + si32 id; + std::string scope; /// scope in which this ID located + + bool operator==(const ObjectData & other) const + { + return id == other.id && scope == other.scope; + } + }; + + std::multimap registeredObjects; + mutable std::vector scheduledRequests; + + ELoadingState state = ELoadingState::LOADING; + + /// Helper method that dumps all registered identifier into log file + void debugDumpIdentifiers(); + + /// Check if identifier can be valid (camelCase, point as separator) + static void checkIdentifier(std::string & ID); + + void requestIdentifier(ObjectCallback callback) const; + bool resolveIdentifier(const ObjectCallback & callback) const; + std::vector getPossibleIdentifiers(const ObjectCallback & callback) const; + +public: + CIdentifierStorage(); + virtual ~CIdentifierStorage() = default; + + /// request identifier for specific object name. + /// Function callback will be called during ID resolution phase of loading + void requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const; + ///fullName = [remoteScope:]type.name + void requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) const; + void requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const; + void requestIdentifier(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; + + /// get identifier immediately. If identifier is not know and not silent call will result in error message + std::optional getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent = false) const; + std::optional getIdentifier(const std::string & type, const JsonNode & name, bool silent = false) const; + std::optional getIdentifier(const JsonNode & name, bool silent = false) const; + std::optional getIdentifier(const std::string & scope, const std::string & fullName, bool silent = false) const; + + /// registers new object + void registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier); + + /// called at the very end of loading to check for any missing ID's + void finalize(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h new file mode 100644 index 000000000..23af7deaf --- /dev/null +++ b/lib/modding/ModIncompatibility.h @@ -0,0 +1,57 @@ +/* + * ModIncompatibility.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 DLL_LINKAGE ModIncompatibility: public std::exception +{ +public: + using ModListWithVersion = std::vector>; + using ModList = std::vector; + + ModIncompatibility(const ModListWithVersion & _missingMods) + { + std::ostringstream _ss; + for(const auto & m : _missingMods) + _ss << m.first << ' ' << m.second << std::endl; + messageMissingMods = _ss.str(); + } + + ModIncompatibility(const ModListWithVersion & _missingMods, ModList & _excessiveMods) + : ModIncompatibility(_missingMods) + { + std::ostringstream _ss; + for(const auto & m : _excessiveMods) + _ss << m << std::endl; + messageExcessiveMods = _ss.str(); + } + + const char * what() const noexcept override + { + static const std::string w("Mod incompatibility exception"); + return w.c_str(); + } + + const std::string & whatMissing() const noexcept + { + return messageMissingMods; + } + + const std::string & whatExcessive() const noexcept + { + return messageExcessiveMods; + } + +private: + std::string messageMissingMods, messageExcessiveMods; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModScope.h b/lib/modding/ModScope.h new file mode 100644 index 000000000..d6a65448d --- /dev/null +++ b/lib/modding/ModScope.h @@ -0,0 +1,53 @@ +/* + * ModScope.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 + +namespace ModScope +{ + +/// returns true if scope is reserved for internal use and can not be used by mods +inline bool isScopeReserved(const std::string & scope) +{ + //following scopes are reserved - either in use by mod system or by filesystem + static const std::array reservedScopes = { + "core", "map", "game", "root", "saves", "config", "local", "initial", "mapEditor" + }; + + return std::find(reservedScopes.begin(), reservedScopes.end(), scope) != reservedScopes.end(); +} + +/// reserved scope name for referencing built-in (e.g. H3) objects +inline const std::string & scopeBuiltin() +{ + static const std::string scope = "core"; + return scope; +} + +/// reserved scope name for accessing objects from any loaded mod +inline const std::string & scopeGame() +{ + static const std::string scope = "game"; + return scope; +} + +/// reserved scope name for accessing object for map loading +inline const std::string & scopeMap() +{ + //TODO: implement accessing map dependencies for both H3 and VCMI maps + // for now, allow access to any identifiers + static const std::string scope = "game"; + return scope; +} + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModUtility.cpp b/lib/modding/ModUtility.cpp new file mode 100644 index 000000000..afa7ef35e --- /dev/null +++ b/lib/modding/ModUtility.cpp @@ -0,0 +1,77 @@ +/* + * CModHandler.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 "ModUtility.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +std::string ModUtility::normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier) +{ + auto p = vstd::splitStringToPair(identifier, ':'); + + if(p.first.empty()) + p.first = scope; + + if(p.first == remoteScope) + p.first.clear(); + + return p.first.empty() ? p.second : p.first + ":" + p.second; +} + +void ModUtility::parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier) +{ + auto p = vstd::splitStringToPair(fullIdentifier, ':'); + + scope = p.first; + + auto p2 = vstd::splitStringToPair(p.second, '.'); + + if(!p2.first.empty()) + { + type = p2.first; + identifier = p2.second; + } + else + { + type = p.second; + identifier.clear(); + } +} + +std::string ModUtility::makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier) +{ + if(type.empty()) + logGlobal->error("Full identifier (%s %s) requires type name", scope, identifier); + + std::string actualScope = scope; + std::string actualName = identifier; + + //ignore scope if identifier is scoped + auto scopeAndName = vstd::splitStringToPair(identifier, ':'); + + if(!scopeAndName.first.empty()) + { + actualScope = scopeAndName.first; + actualName = scopeAndName.second; + } + + if(actualScope.empty()) + { + return actualName.empty() ? type : type + "." + actualName; + } + else + { + return actualName.empty() ? actualScope+ ":" + type : actualScope + ":" + type + "." + actualName; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModUtility.h b/lib/modding/ModUtility.h new file mode 100644 index 000000000..edf6ae456 --- /dev/null +++ b/lib/modding/ModUtility.h @@ -0,0 +1,25 @@ +/* + * ModUtility.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 + +// NOTE: all methods in this namespace should be considered internal to modding system and should not be used outside of this module +namespace ModUtility +{ + DLL_LINKAGE std::string normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier); + + DLL_LINKAGE void parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier); + + DLL_LINKAGE std::string makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier); + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModVerificationInfo.h b/lib/modding/ModVerificationInfo.h new file mode 100644 index 000000000..e6652e40d --- /dev/null +++ b/lib/modding/ModVerificationInfo.h @@ -0,0 +1,44 @@ +/* + * ModVerificationInfo.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 "CModVersion.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct ModVerificationInfo +{ + /// human-readable mod name + std::string name; + + /// version of the mod + CModVersion version; + + /// CRC-32 checksum of the mod + ui32 checksum = 0; + + /// parent mod ID, empty if root-level mod + TModID parent; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h, const int v) + { + h & name; + h & version; + h & checksum; + h & parent; + h & impactsGameplay; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h new file mode 100644 index 000000000..d3acb2cdd --- /dev/null +++ b/lib/networkPacks/ArtifactLocation.h @@ -0,0 +1,49 @@ +/* + * ArtifactLocation.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 ArtifactLocation +{ + ObjectInstanceID artHolder; + ArtifactPosition slot; + std::optional creature; + + ArtifactLocation() + : artHolder(ObjectInstanceID::NONE) + , slot(ArtifactPosition::PRE_FIRST) + , creature(std::nullopt) + { + } + ArtifactLocation(const ObjectInstanceID id, const ArtifactPosition & slot = ArtifactPosition::PRE_FIRST) + : artHolder(id) + , slot(slot) + , creature(std::nullopt) + { + } + ArtifactLocation(const ObjectInstanceID id, const std::optional creatureSlot) + : artHolder(id) + , slot(ArtifactPosition::PRE_FIRST) + , creature(creatureSlot) + { + } + + template void serialize(Handler & h, const int version) + { + h & artHolder; + h & slot; + h & creature; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/BattleChanges.h b/lib/networkPacks/BattleChanges.h new file mode 100644 index 000000000..510505e23 --- /dev/null +++ b/lib/networkPacks/BattleChanges.h @@ -0,0 +1,81 @@ +/* + * BattleChanges.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 "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleChanges +{ +public: + enum class EOperation : si8 + { + ADD, + RESET_STATE, + UPDATE, + REMOVE, + }; + + JsonNode data; + EOperation operation = EOperation::RESET_STATE; + + BattleChanges() = default; + explicit BattleChanges(EOperation operation_) + : operation(operation_) + { + } +}; + +class UnitChanges : public BattleChanges +{ +public: + uint32_t id = 0; + int64_t healthDelta = 0; + + UnitChanges() = default; + UnitChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_) + , id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & healthDelta; + h & data; + h & operation; + } +}; + +class ObstacleChanges : public BattleChanges +{ +public: + uint32_t id = 0; + + ObstacleChanges() = default; + + ObstacleChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_), + id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & data; + h & operation; + } +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/networkPacks/Component.h b/lib/networkPacks/Component.h new file mode 100644 index 000000000..43ec61277 --- /dev/null +++ b/lib/networkPacks/Component.h @@ -0,0 +1,77 @@ +/* + * Component.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/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +enum class ComponentType : int8_t +{ + NONE = -1, + PRIM_SKILL, + SEC_SKILL, + RESOURCE, + RESOURCE_PER_DAY, + CREATURE, + ARTIFACT, + SPELL_SCROLL, + MANA, + EXPERIENCE, + LEVEL, + SPELL, + MORALE, + LUCK, + BUILDING, + HERO_PORTRAIT, + FLAG +}; + +using ComponentSubType = VariantIdentifier; + +struct Component +{ + ComponentType type = ComponentType::NONE; + ComponentSubType subType; + std::optional value; // + give; - take + + template void serialize(Handler &h, const int version) + { + h & type; + h & subType; + h & value; + } + + Component() = default; + + template, bool> = true> + Component(ComponentType type, Numeric value) + : type(type) + , value(value) + { + } + + template, bool> = true> + Component(ComponentType type, IdentifierType subType) + : type(type) + , subType(subType) + { + } + + Component(ComponentType type, ComponentSubType subType, int32_t value) + : type(type) + , subType(subType) + , value(value) + { + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/EInfoWindowMode.h b/lib/networkPacks/EInfoWindowMode.h new file mode 100644 index 000000000..3559ce9ae --- /dev/null +++ b/lib/networkPacks/EInfoWindowMode.h @@ -0,0 +1,22 @@ +/* + * EInfoWindowMode.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 EInfoWindowMode : uint8_t +{ + AUTO, + MODAL, + INFO +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/networkPacks/EOpenWindowMode.h b/lib/networkPacks/EOpenWindowMode.h new file mode 100644 index 000000000..655137ac7 --- /dev/null +++ b/lib/networkPacks/EOpenWindowMode.h @@ -0,0 +1,28 @@ +/* + * EOpenWindowMode.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 EOpenWindowMode : uint8_t +{ + EXCHANGE_WINDOW, + RECRUITMENT_FIRST, + RECRUITMENT_ALL, + SHIPYARD_WINDOW, + THIEVES_GUILD, + UNIVERSITY_WINDOW, + HILL_FORT_WINDOW, + MARKET_WINDOW, + PUZZLE_MAP, + TAVERN_WINDOW +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/EntityChanges.h b/lib/networkPacks/EntityChanges.h new file mode 100644 index 000000000..296148297 --- /dev/null +++ b/lib/networkPacks/EntityChanges.h @@ -0,0 +1,33 @@ +/* + * EInfoWindowMode.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 "../JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class EntityChanges +{ +public: + Metatype metatype = Metatype::UNKNOWN; + int32_t entityIndex = 0; + JsonNode data; + template void serialize(Handler & h, const int version) + { + h & metatype; + h & entityIndex; + h & data; + } +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h similarity index 93% rename from lib/NetPackVisitor.h rename to lib/networkPacks/NetPackVisitor.h index da1c6e2e6..05df2897a 100644 --- a/lib/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -9,8 +9,11 @@ */ #pragma once -#include "NetPacks.h" -#include "NetPacksLobby.h" +#include "PacksForClient.h" +#include "PacksForClientBattle.h" +#include "PacksForServer.h" +#include "PacksForLobby.h" +#include "SetStackEffect.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,7 +29,10 @@ public: virtual void visitSystemMessage(SystemMessage & pack) {} virtual void visitPlayerBlocked(PlayerBlocked & pack) {} virtual void visitPlayerCheated(PlayerCheated & pack) {} - virtual void visitYourTurn(YourTurn & pack) {} + virtual void visitPlayerStartsTurn(PlayerStartsTurn & pack) {} + virtual void visitDaysWithoutTown(DaysWithoutTown & pack) {} + virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} + virtual void visitGamePause(GamePause & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} virtual void visitSetResources(SetResources & pack) {} virtual void visitSetPrimSkill(SetPrimSkill & pack) {} @@ -39,6 +45,7 @@ public: virtual void visitSetAvailableHeroes(SetAvailableHero & pack) {} virtual void visitGiveBonus(GiveBonus & pack) {} virtual void visitChangeObjPos(ChangeObjPos & pack) {} + virtual void visitPlayerEndsTurn(PlayerEndsTurn & pack) {}; virtual void visitPlayerEndsGame(PlayerEndsGame & pack) {} virtual void visitPlayerReinitInterface(PlayerReinitInterface & pack) {} virtual void visitRemoveBonus(RemoveBonus & pack) {} @@ -80,7 +87,6 @@ public: virtual void visitInfoWindow(InfoWindow & pack) {} virtual void visitSetObjectProperty(SetObjectProperty & pack) {} virtual void visitChangeObjectVisitors(ChangeObjectVisitors & pack) {} - virtual void visitPrepareHeroLevelUp(PrepareHeroLevelUp & pack) {} virtual void visitHeroLevelUp(HeroLevelUp & pack) {} virtual void visitCommanderLevelUp(CommanderLevelUp & pack) {} virtual void visitBlockingDialog(BlockingDialog & pack) {} @@ -134,7 +140,6 @@ public: virtual void visitBuildBoat(BuildBoat & pack) {} virtual void visitQueryReply(QueryReply & pack) {} virtual void visitMakeAction(MakeAction & pack) {} - virtual void visitMakeCustomAction(MakeCustomAction & pack) {} virtual void visitDigWithHero(DigWithHero & pack) {} virtual void visitCastAdvSpell(CastAdvSpell & pack) {} virtual void visitSaveGame(SaveGame & pack) {} @@ -145,6 +150,7 @@ public: virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) {} virtual void visitLobbyChatMessage(LobbyChatMessage & pack) {} virtual void visitLobbyGuiAction(LobbyGuiAction & pack) {} + virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) {} virtual void visitLobbyEndGame(LobbyEndGame & pack) {} virtual void visitLobbyStartGame(LobbyStartGame & pack) {} virtual void visitLobbyChangeHost(LobbyChangeHost & pack) {} @@ -155,6 +161,8 @@ public: virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) {} virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {} virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {} + virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {} + virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {} virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {} virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) {} virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {} diff --git a/lib/networkPacks/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h new file mode 100644 index 000000000..b77e92e94 --- /dev/null +++ b/lib/networkPacks/NetPacksBase.h @@ -0,0 +1,87 @@ +/* + * NetPacksBase.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 + +class CGameState; +class CConnection; + +class ICPackVisitor; + +struct DLL_LINKAGE CPack +{ + /// Pointer to connection that pack received from + /// Only set & used on server + std::shared_ptr c; + + CPack() = default; + virtual ~CPack() = default; + + template void serialize(Handler &h, const int version) + { + logNetwork->error("CPack serialized... this should not happen!"); + assert(false && "CPack serialized"); + } + + void applyGs(CGameState * gs) + {} + + void visit(ICPackVisitor & cpackVisitor); + +protected: + /// + /// For basic types of netpacks hierarchy like CPackForClient. Called first. + /// + virtual void visitBasic(ICPackVisitor & cpackVisitor); + + /// + /// For leaf types of netpacks hierarchy. Called after visitBasic. + /// + virtual void visitTyped(ICPackVisitor & cpackVisitor); +}; + +struct DLL_LINKAGE CPackForClient : public CPack +{ +protected: + void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +struct DLL_LINKAGE Query : public CPackForClient +{ + QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) +}; + +struct DLL_LINKAGE CPackForServer : public CPack +{ + mutable PlayerColor player = PlayerColor::NEUTRAL; + mutable si32 requestID; + + template void serialize(Handler &h, const int version) + { + h & player; + h & requestID; + } + +protected: + void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +struct DLL_LINKAGE CPackForLobby : public CPack +{ + virtual bool isForServer() const; + +protected: + void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp similarity index 78% rename from lib/NetPacksLib.cpp rename to lib/networkPacks/NetPacksLib.cpp index 855c5bbd1..8868859a5 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1,2550 +1,2532 @@ -/* - * NetPacksLib.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 "ArtifactUtils.h" -#include "NetPacks.h" -#include "NetPackVisitor.h" -#include "CGeneralTextHandler.h" -#include "CArtHandler.h" -#include "CHeroHandler.h" -#include "CModHandler.h" -#include "VCMI_Lib.h" -#include "mapping/CMap.h" -#include "spells/CSpellHandler.h" -#include "CCreatureHandler.h" -#include "gameState/CGameState.h" -#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 "mapObjects/CGCreature.h" -#include "mapObjects/CGMarket.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "campaign/CampaignState.h" -#include "GameSettings.h" - - -VCMI_LIB_NAMESPACE_BEGIN - -#define THROW_IF_NO_BATTLE if (!gs->curB) throw std::runtime_error("Trying to apply pack when no battle!"); - -void CPack::visit(ICPackVisitor & visitor) -{ - visitBasic(visitor); - - // visitBasic may destroy this and in such cases we do not want to call visitTyped - if(visitor.callTyped()) - { - visitTyped(visitor); - } -} - -void CPack::visitBasic(ICPackVisitor & visitor) -{ -} - -void CPack::visitTyped(ICPackVisitor & visitor) -{ -} - -void CPackForClient::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForClient(*this); -} - -void CPackForServer::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForServer(*this); -} - -void CPackForLobby::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForLobby(*this); -} - -bool CPackForLobby::isForServer() const -{ - return false; -} - -bool CLobbyPackToServer::isForServer() const -{ - return true; -} - -void PackageApplied::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPackageApplied(*this); -} - -void SystemMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSystemMessage(*this); -} - -void PlayerBlocked::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerBlocked(*this); -} - -void PlayerCheated::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerCheated(*this); -} - -void YourTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitYourTurn(*this); -} - -void EntitiesChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEntitiesChanged(*this); -} - -void SetResources::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetResources(*this); -} - -void SetPrimSkill::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetPrimSkill(*this); -} - -void SetSecSkill::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetSecSkill(*this); -} - -void HeroVisitCastle::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroVisitCastle(*this); -} - -void ChangeSpells::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeSpells(*this); -} - -void SetMana::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetMana(*this); -} - -void SetMovePoints::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetMovePoints(*this); -} - -void FoWChange::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitFoWChange(*this); -} - -void SetAvailableHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableHeroes(*this); -} - -void GiveBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGiveBonus(*this); -} - -void ChangeObjPos::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeObjPos(*this); -} - -void PlayerEndsGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerEndsGame(*this); -} - -void PlayerReinitInterface::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerReinitInterface(*this); -} - -void RemoveBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRemoveBonus(*this); -} - -void SetCommanderProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetCommanderProperty(*this); -} - -void AddQuest::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAddQuest(*this); -} - -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); -} - -void RemoveObject::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRemoveObject(*this); -} - -void TryMoveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTryMoveHero(*this); -} - -void NewStructures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewStructures(*this); -} - -void RazeStructures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRazeStructures(*this); -} - -void SetAvailableCreatures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableCreatures(*this); -} - -void SetHeroesInTown::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetHeroesInTown(*this); -} - -void HeroRecruited::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroRecruited(*this); -} - -void GiveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGiveHero(*this); -} - -void OpenWindow::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitOpenWindow(*this); -} - -void NewObject::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewObject(*this); -} - -void SetAvailableArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableArtifacts(*this); -} - -void NewArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewArtifact(*this); -} - -void ChangeStackCount::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeStackCount(*this); -} - -void SetStackType::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetStackType(*this); -} - -void EraseStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseStack(*this); -} - -void SwapStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSwapStacks(*this); -} - -void InsertNewStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitInsertNewStack(*this); -} - -void RebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRebalanceStacks(*this); -} - -void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkRebalanceStacks(*this); -} - -void BulkSmartRebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSmartRebalanceStacks(*this); -} - -void PutArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPutArtifact(*this); -} - -void EraseArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseArtifact(*this); -} - -void MoveArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMoveArtifact(*this); -} - -void BulkMoveArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMoveArtifacts(*this); -} - -void AssembledArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAssembledArtifact(*this); -} - -void DisassembledArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDisassembledArtifact(*this); -} - -void HeroVisit::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroVisit(*this); -} - -void NewTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewTurn(*this); -} - -void InfoWindow::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitInfoWindow(*this); -} - -void SetObjectProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetObjectProperty(*this); -} - -void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeObjectVisitors(*this); -} - -void PrepareHeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPrepareHeroLevelUp(*this); -} - -void HeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroLevelUp(*this); -} - -void CommanderLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCommanderLevelUp(*this); -} - -void BlockingDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBlockingDialog(*this); -} - -void GarrisonDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGarrisonDialog(*this); -} - -void ExchangeDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitExchangeDialog(*this); -} - -void TeleportDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTeleportDialog(*this); -} - -void MapObjectSelectDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMapObjectSelectDialog(*this); -} - -void BattleStart::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleStart(*this); -} - -void BattleNextRound::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleNextRound(*this); -} - -void BattleSetActiveStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSetActiveStack(*this); -} - -void BattleResult::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleResult(*this); -} - -void BattleLogMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleLogMessage(*this); -} - -void BattleStackMoved::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleStackMoved(*this); -} - -void BattleUnitsChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleUnitsChanged(*this); -} - -void BattleAttack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleAttack(*this); -} - -void StartAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitStartAction(*this); -} - -void EndAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEndAction(*this); -} - -void BattleSpellCast::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSpellCast(*this); -} - -void SetStackEffect::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetStackEffect(*this); -} - -void StacksInjured::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitStacksInjured(*this); -} - -void BattleResultsApplied::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleResultsApplied(*this); -} - -void BattleObstaclesChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleObstaclesChanged(*this); -} - -void BattleSetStackProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSetStackProperty(*this); -} - -void BattleTriggerEffect::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleTriggerEffect(*this); -} - -void BattleUpdateGateState::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleUpdateGateState(*this); -} - -void AdvmapSpellCast::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAdvmapSpellCast(*this); -} - -void ShowWorldViewEx::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitShowWorldViewEx(*this); -} - -void EndTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEndTurn(*this); -} - -void DismissHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDismissHero(*this); -} - -void MoveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMoveHero(*this); -} - -void CastleTeleportHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCastleTeleportHero(*this); -} - -void ArrangeStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitArrangeStacks(*this); -} - -void BulkMoveArmy::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMoveArmy(*this); -} - -void BulkSplitStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSplitStack(*this); -} - -void BulkMergeStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMergeStacks(*this); -} - -void BulkSmartSplitStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSmartSplitStack(*this); -} - -void DisbandCreature::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDisbandCreature(*this); -} - -void BuildStructure::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuildStructure(*this); -} - -void RazeStructure::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRazeStructure(*this); -} - -void RecruitCreatures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRecruitCreatures(*this); -} - -void UpgradeCreature::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpgradeCreature(*this); -} - -void GarrisonHeroSwap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGarrisonHeroSwap(*this); -} - -void ExchangeArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitExchangeArtifacts(*this); -} - -void BulkExchangeArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkExchangeArtifacts(*this); -} - -void AssembleArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAssembleArtifacts(*this); -} - -void EraseArtifactByClient::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseArtifactByClient(*this); -} - -void BuyArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuyArtifact(*this); -} - -void TradeOnMarketplace::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTradeOnMarketplace(*this); -} - -void SetFormation::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetFormation(*this); -} - -void HireHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHireHero(*this); -} - -void BuildBoat::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuildBoat(*this); -} - -void QueryReply::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitQueryReply(*this); -} - -void MakeAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMakeAction(*this); -} - -void MakeCustomAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMakeCustomAction(*this); -} - -void DigWithHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDigWithHero(*this); -} - -void CastAdvSpell::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCastAdvSpell(*this); -} - -void SaveGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSaveGame(*this); -} - -void PlayerMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerMessage(*this); -} - -void PlayerMessageClient::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerMessageClient(*this); -} - -void CenterView::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCenterView(*this); -} - -void LobbyClientConnected::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyClientConnected(*this); -} - -void LobbyClientDisconnected::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyClientDisconnected(*this); -} - -void LobbyChatMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChatMessage(*this); -} - -void LobbyGuiAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyGuiAction(*this); -} - -void LobbyEndGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyEndGame(*this); -} - -void LobbyStartGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyStartGame(*this); -} - -void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChangeHost(*this); -} - -void LobbyUpdateState::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyUpdateState(*this); -} - -void LobbySetMap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetMap(*this); -} - -void LobbySetCampaign::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaign(*this); -} - -void LobbySetCampaignMap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaignMap(*this); -} - -void LobbySetCampaignBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaignBonus(*this); -} - -void LobbyChangePlayerOption::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChangePlayerOption(*this); -} - -void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetPlayer(*this); -} - -void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetTurnTime(*this); -} - -void LobbySetDifficulty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetDifficulty(*this); -} - -void LobbyForceSetPlayer::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyForceSetPlayer(*this); -} - -void LobbyShowMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyShowMessage(*this); -} - -void SetResources::applyGs(CGameState * gs) const -{ - assert(player < PlayerColor::PLAYER_LIMIT); - if(abs) - gs->getPlayerState(player)->resources = res; - else - gs->getPlayerState(player)->resources += res; - - //just ensure that player resources are not negative - //server is responsible to check if player can afford deal - //but events on server side are allowed to take more than player have - gs->getPlayerState(player)->resources.positive(); -} - -void SetPrimSkill::applyGs(CGameState * gs) const -{ - CGHeroInstance * hero = gs->getHero(id); - assert(hero); - hero->setPrimarySkill(which, val, abs); -} - -void SetSecSkill::applyGs(CGameState * gs) const -{ - CGHeroInstance *hero = gs->getHero(id); - hero->setSecSkillLevel(which, val, abs); -} - -void SetCommanderProperty::applyGs(CGameState *gs) -{ - CCommanderInstance * commander = gs->getHero(heroid)->commander; - assert (commander); - - switch (which) - { - case BONUS: - commander->accumulateBonus (std::make_shared(accumulatedBonus)); - break; - case SPECIAL_SKILL: - commander->accumulateBonus (std::make_shared(accumulatedBonus)); - commander->specialSkills.insert (additionalInfo); - break; - case SECONDARY_SKILL: - commander->secondarySkills[additionalInfo] = static_cast(amount); - break; - case ALIVE: - if (amount) - commander->setAlive(true); - else - commander->setAlive(false); - break; - case EXPERIENCE: - commander->giveStackExp(amount); //TODO: allow setting exp for stacks via netpacks - break; - } -} - -void AddQuest::applyGs(CGameState * gs) const -{ - assert (vstd::contains(gs->players, player)); - auto * vec = &gs->players[player].quests; - if (!vstd::contains(*vec, quest)) - vec->push_back (quest); - else - logNetwork->warn("Warning! Attempt to add duplicated quest"); -} - -void UpdateArtHandlerLists::applyGs(CGameState * gs) const -{ - VLC->arth->minors = minors; - VLC->arth->majors = majors; - VLC->arth->treasures = treasures; - VLC->arth->relics = relics; -} - -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 -{ - gs->getHero(hid)->setFormation(formation); -} - -void HeroVisitCastle::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->getHero(hid); - CGTownInstance *t = gs->getTown(tid); - - assert(h); - assert(t); - - if(start()) - t->setVisitingHero(h); - else - t->setVisitingHero(nullptr); -} - -void ChangeSpells::applyGs(CGameState *gs) -{ - CGHeroInstance *hero = gs->getHero(hid); - - if(learn) - for(const auto & sid : spells) - hero->addSpellToSpellbook(sid); - else - for(const auto & sid : spells) - hero->removeSpellFromSpellbook(sid); -} - -void SetMana::applyGs(CGameState * gs) const -{ - CGHeroInstance * hero = gs->getHero(hid); - - assert(hero); - - if(absolute) - hero->mana = val; - else - hero->mana += val; - - vstd::amax(hero->mana, 0); //not less than 0 -} - -void SetMovePoints::applyGs(CGameState * gs) const -{ - CGHeroInstance *hero = gs->getHero(hid); - - assert(hero); - - if(absolute) - hero->setMovementPoints(val); - else - hero->setMovementPoints(hero->movementPointsRemaining() + val); -} - -void FoWChange::applyGs(CGameState *gs) -{ - TeamState * team = gs->getPlayerTeam(player); - auto fogOfWarMap = team->fogOfWarMap; - for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode; - if (mode == 0) //do not hide too much - { - std::unordered_set tilesRevealed; - for (auto & elem : gs->map->objects) - { - const CGObjectInstance *o = elem; - if (o) - { - switch(o->ID) - { - case Obj::HERO: - case Obj::MINE: - case Obj::TOWN: - case Obj::ABANDONED_MINE: - if(vstd::contains(team->players, o->tempOwner)) //check owned observators - gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1); - break; - } - } - } - for(const int3 & t : tilesRevealed) //probably not the most optimal solution ever - (*fogOfWarMap)[t.z][t.x][t.y] = 1; - } -} - -void SetAvailableHero::applyGs(CGameState *gs) -{ - gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); -} - -void GiveBonus::applyGs(CGameState *gs) -{ - CBonusSystemNode *cbsn = nullptr; - switch(who) - { - case ETarget::HERO: - cbsn = gs->getHero(ObjectInstanceID(id)); - break; - case ETarget::PLAYER: - cbsn = gs->getPlayerState(PlayerColor(id)); - break; - case ETarget::TOWN: - cbsn = gs->getTown(ObjectInstanceID(id)); - break; - case ETarget::BATTLE: - assert(Bonus::OneBattle(&bonus)); - cbsn = dynamic_cast(gs->curB.get()); - break; - } - - assert(cbsn); - - if(Bonus::OneWeek(&bonus)) - bonus.turnsRemain = 8 - gs->getDate(Date::DAY_OF_WEEK); // set correct number of days before adding bonus - - auto b = std::make_shared(bonus); - cbsn->addNewBonus(b); - - std::string &descr = b->description; - - if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) - { - if (bonus.source == BonusSource::OBJECT) - { - descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" - } - else if(bonus.source == BonusSource::TOWN_STRUCTURE) - { - descr = bonus.description; - return; - } - else - { - descr = bdescr.toString(); - } - } - else - { - descr = bdescr.toString(); - } - // Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them - boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); - boost::replace_first(descr, "%s", std::to_string(std::abs(bonus.val))); -} - -void ChangeObjPos::applyGs(CGameState *gs) -{ - CGObjectInstance *obj = gs->getObjInstance(objid); - if(!obj) - { - logNetwork->error("Wrong ChangeObjPos: object %d doesn't exist!", objid.getNum()); - return; - } - gs->map->removeBlockVisTiles(obj); - obj->pos = nPos + obj->getVisitableOffset(); - gs->map->addBlockVisTiles(obj); -} - -void ChangeObjectVisitors::applyGs(CGameState * gs) const -{ - switch (mode) { - case VISITOR_ADD: - 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); - } - } - break; - case VISITOR_CLEAR: - 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 - } - } - - for(auto &elem : gs->players) - { - elem.second.visitedObjects.erase(object); - } - - break; - case VISITOR_REMOVE: - gs->getHero(hero)->visitedObjects.erase(object); - break; - } -} - -void PlayerEndsGame::applyGs(CGameState * gs) const -{ - PlayerState *p = gs->getPlayerState(player); - if(victoryLossCheckResult.victory()) - { - p->status = EPlayerStatus::WINNER; - - // TODO: Campaign-specific code might as well go somewhere else - // keep all heroes from the winning player - if(p->human && gs->scenarioOps->campState) - { - std::vector crossoverHeroes; - for (CGHeroInstance * hero : gs->map->heroesOnMap) - if (hero->tempOwner == player) - crossoverHeroes.push_back(hero); - - gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); - } - } - else - { - p->status = EPlayerStatus::LOSER; - } -} - -void PlayerReinitInterface::applyGs(CGameState *gs) -{ - if(!gs || !gs->scenarioOps) - return; - - //TODO: what does mean if more that one player connected? - if(playerConnectionId == PlayerSettings::PLAYER_AI) - { - for(const auto & player : players) - gs->scenarioOps->getIthPlayersSettings(player).connectedPlayerIDs.clear(); - } -} - -void RemoveBonus::applyGs(CGameState *gs) -{ - CBonusSystemNode * node = nullptr; - if (who == GiveBonus::ETarget::HERO) - node = gs->getHero(ObjectInstanceID(whoID)); - else - node = gs->getPlayerState(PlayerColor(whoID)); - - BonusList &bonuses = node->getExportedBonusList(); - - for(const auto & b : bonuses) - { - if(vstd::to_underlying(b->source) == source && b->sid == id) - { - bonus = *b; //backup bonus (to show to interfaces later) - node->removeBonus(b); - break; - } - } -} - -void RemoveObject::applyGs(CGameState *gs) -{ - - CGObjectInstance *obj = gs->getObjInstance(id); - logGlobal->debug("removing object id=%d; address=%x; name=%s", id, (intptr_t)obj, obj->getObjectName()); - //unblock tiles - gs->map->removeBlockVisTiles(obj); - - 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); - - // FIXME: workaround: - // hero should be attached to siegeNode after battle - // however this code might also be called on dismissing hero while in town - if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode)) - beatenHero->detachFrom(*siegeNode); - - beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero - vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) - { - return asi.artifact->artType->getId() == ArtifactID::GRAIL; - }); - - if(beatenHero->visitedTown) - { - if(beatenHero->visitedTown->garrisonHero == beatenHero) - beatenHero->visitedTown->garrisonHero = nullptr; - else - beatenHero->visitedTown->visitingHero = nullptr; - - beatenHero->visitedTown = nullptr; - beatenHero->inTownGarrison = false; - } - //return hero to the pool, so he may reappear in tavern - - gs->heroesPool->addHeroToPool(beatenHero); - gs->map->objects[id.getNum()] = nullptr; - - //If hero on Boat is removed, the Boat disappears - if(beatenHero->boat) - { - beatenHero->detachFrom(const_cast(*beatenHero->boat)); - gs->map->instanceNames.erase(beatenHero->boat->instanceName); - gs->map->objects[beatenHero->boat->id.getNum()].dellNull(); - beatenHero->boat = nullptr; - } - return; - } - - const auto * quest = dynamic_cast(obj); - if (quest) - { - gs->map->quests[quest->quest->qid] = nullptr; - for (auto &player : gs->players) - { - for (auto &q : player.second.quests) - { - if (q.obj == obj) - { - q.obj = nullptr; - } - } - } - } - - for (TriggeredEvent & event : gs->map->triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - if (cond.object == obj) - { - if (cond.condition == EventCondition::DESTROY || cond.condition == EventCondition::DESTROY_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 1; // destroyed object, from now on always fulfilled - } - else if (cond.condition == EventCondition::CONTROL || cond.condition == EventCondition::HAVE_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 0; // destroyed object, from now on can not be fulfilled - } - } - return cond; - }; - event.trigger = event.trigger.morph(patcher); - } - gs->map->instanceNames.erase(obj->instanceName); - gs->map->objects[id.getNum()].dellNull(); - gs->map->calculateGuardingGreaturePositions(); -} - -static int getDir(const int3 & src, const int3 & dst) -{ - int ret = -1; - if(dst.x+1 == src.x && dst.y+1 == src.y) //tl - { - ret = 1; - } - else if(dst.x == src.x && dst.y+1 == src.y) //t - { - ret = 2; - } - else if(dst.x-1 == src.x && dst.y+1 == src.y) //tr - { - ret = 3; - } - else if(dst.x-1 == src.x && dst.y == src.y) //r - { - ret = 4; - } - else if(dst.x-1 == src.x && dst.y-1 == src.y) //br - { - ret = 5; - } - else if(dst.x == src.x && dst.y-1 == src.y) //b - { - ret = 6; - } - else if(dst.x+1 == src.x && dst.y-1 == src.y) //bl - { - ret = 7; - } - else if(dst.x+1 == src.x && dst.y == src.y) //l - { - ret = 8; - } - return ret; -} - -void TryMoveHero::applyGs(CGameState *gs) -{ - CGHeroInstance *h = gs->getHero(id); - if (!h) - { - logGlobal->error("Attempt ot move unavailable hero %d", id.getNum()); - return; - } - - h->setMovementPoints(movePoints); - - if((result == SUCCESS || result == BLOCKING_VISIT || result == EMBARK || result == DISEMBARK) && start != end) - { - auto dir = getDir(start,end); - if(dir > 0 && dir <= 8) - h->moveDir = dir; - //else don`t change move direction - hero might have traversed the subterranean gate, direction should be kept - } - - if(result == EMBARK) //hero enters boat at destination tile - { - const TerrainTile &tt = gs->map->getTile(h->convertToVisitablePos(end)); - assert(tt.visitableObjects.size() >= 1 && tt.visitableObjects.back()->ID == Obj::BOAT); //the only visitable object at destination is Boat - auto * boat = dynamic_cast(tt.visitableObjects.back()); - assert(boat); - - gs->map->removeBlockVisTiles(boat); //hero blockvis mask will be used, we don't need to duplicate it with boat - h->boat = boat; - h->attachTo(*boat); - boat->hero = h; - } - else if(result == DISEMBARK) //hero leaves boat to destination tile - { - auto * b = const_cast(h->boat); - b->direction = h->moveDir; - b->pos = start; - b->hero = nullptr; - gs->map->addBlockVisTiles(b); - h->detachFrom(*b); - h->boat = nullptr; - } - - if(start!=end && (result == SUCCESS || result == TELEPORTATION || result == EMBARK || result == DISEMBARK)) - { - gs->map->removeBlockVisTiles(h); - h->pos = end; - if(auto * b = const_cast(h->boat)) - b->pos = end; - gs->map->addBlockVisTiles(h); - } - - auto fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; - for(const int3 & t : fowRevealed) - (*fogOfWarMap)[t.z][t.x][t.y] = 1; -} - -void NewStructures::applyGs(CGameState *gs) -{ - CGTownInstance *t = gs->getTown(tid); - - 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); - } - } - t->builded = builded; - t->recreateBuildingsBonuses(); -} - -void RazeStructures::applyGs(CGameState *gs) -{ - CGTownInstance *t = gs->getTown(tid); - for(const auto & id : bid) - { - t->builtBuildings.erase(id); - - t->updateAppearance(); - } - t->destroyed = destroyed; //yeaha - t->recreateBuildingsBonuses(); -} - -void SetAvailableCreatures::applyGs(CGameState * gs) const -{ - auto * dw = dynamic_cast(gs->getObjInstance(tid)); - assert(dw); - dw->creatures = creatures; -} - -void SetHeroesInTown::applyGs(CGameState * gs) const -{ - CGTownInstance *t = gs->getTown(tid); - - CGHeroInstance * v = gs->getHero(visiting); - CGHeroInstance * g = gs->getHero(garrison); - - bool newVisitorComesFromGarrison = v && v == t->garrisonHero; - bool newGarrisonComesFromVisiting = g && g == t->visitingHero; - - if(newVisitorComesFromGarrison) - t->setGarrisonedHero(nullptr); - if(newGarrisonComesFromVisiting) - t->setVisitingHero(nullptr); - if(!newGarrisonComesFromVisiting || v) - t->setVisitingHero(v); - if(!newVisitorComesFromGarrison || g) - t->setGarrisonedHero(g); - - if(v) - { - gs->map->addBlockVisTiles(v); - } - if(g) - { - gs->map->removeBlockVisTiles(g); - } -} - -void HeroRecruited::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); - CGTownInstance *t = gs->getTown(tid); - PlayerState *p = gs->getPlayerState(player); - - if (boatId >= 0) - { - CGObjectInstance *obj = gs->getObjInstance(boatId); - auto * boat = dynamic_cast(obj); - if (boat) - { - gs->map->removeBlockVisTiles(boat); - h->attachToBoat(boat); - } - } - - h->setOwner(player); - h->pos = tile; - h->initObj(gs->getRandomGenerator()); - - if(h->id == ObjectInstanceID()) - { - h->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - gs->map->objects.emplace_back(h); - } - else - gs->map->objects[h->id.getNum()] = h; - - gs->map->heroesOnMap.emplace_back(h); - p->heroes.emplace_back(h); - h->attachTo(*p); - gs->map->addBlockVisTiles(h); - - if(t) - t->setVisitingHero(h); -} - -void GiveHero::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->getHero(id); - - if (boatId >= 0) - { - CGObjectInstance *obj = gs->getObjInstance(boatId); - auto * boat = dynamic_cast(obj); - if (boat) - { - gs->map->removeBlockVisTiles(boat); - h->attachToBoat(boat); - } - } - - //bonus system - h->detachFrom(gs->globalEffects); - h->attachTo(*gs->getPlayerState(player)); - - auto oldVisitablePos = h->visitablePos(); - gs->map->removeBlockVisTiles(h,true); - h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); - - h->setOwner(player); - h->setMovementPoints(h->movementPointsLimit(true)); - h->pos = h->convertFromVisitablePos(oldVisitablePos); - gs->map->heroesOnMap.emplace_back(h); - gs->getPlayerState(h->getOwner())->heroes.emplace_back(h); - - gs->map->addBlockVisTiles(h); - h->inTownGarrison = false; -} - -void NewObject::applyGs(CGameState *gs) -{ - TerrainId terrainType = ETerrainId::NONE; - - 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(); - handler->configureObject(o, gs->getRandomGenerator()); - - 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(CreatureID(subID), -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); - 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->ID = ID; - o->subID = subID; - o->pos = targetPos + o->getVisitableOffset(); - - gs->map->objects.emplace_back(o); - gs->map->addBlockVisTiles(o); - o->initObj(gs->getRandomGenerator()); - gs->map->calculateGuardingGreaturePositions(); - - createdObjectID = o->id; - - logGlobal->debug("Added object id=%d; address=%x; name=%s", o->id, (intptr_t)o, o->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); - } - gs->map->addNewArtifactInstance(art); -} - -const CStackInstance * StackLocation::getStack() -{ - if(!army->hasStackAtSlot(slot)) - { - logNetwork->warn("%s don't have a stack at slot %d", army->nodeName(), slot.getNum()); - return nullptr; - } - return &army->getStack(slot); -} - -struct ObjectRetriever -{ - const CArmedInstance * operator()(const ConstTransitivePtr &h) const - { - return h; - } - const CArmedInstance * operator()(const ConstTransitivePtr &s) const - { - return s->armyObj; - } -}; -template -struct GetBase -{ - template - T * operator()(TArg &arg) const - { - return arg; - } -}; - - -void ArtifactLocation::removeArtifact() -{ - CArtifactInstance *a = getArt(); - assert(a); - a->removeFrom(*this); -} - -const CArmedInstance * ArtifactLocation::relatedObj() const -{ - return std::visit(ObjectRetriever(), artHolder); -} - -PlayerColor ArtifactLocation::owningPlayer() const -{ - const auto * obj = relatedObj(); - return obj ? obj->tempOwner : PlayerColor::NEUTRAL; -} - -CArtifactSet *ArtifactLocation::getHolderArtSet() -{ - return std::visit(GetBase(), artHolder); -} - -CBonusSystemNode *ArtifactLocation::getHolderNode() -{ - return std::visit(GetBase(), artHolder); -} - -const CArtifactInstance *ArtifactLocation::getArt() const -{ - const auto * s = getSlot(); - if(s) - return s->getArt(); - else - return nullptr; -} - -CArtifactSet * ArtifactLocation::getHolderArtSet() const -{ - auto * t = const_cast(this); - return t->getHolderArtSet(); -} - -const CBonusSystemNode * ArtifactLocation::getHolderNode() const -{ - auto * t = const_cast(this); - return t->getHolderNode(); -} - -CArtifactInstance *ArtifactLocation::getArt() -{ - const ArtifactLocation *t = this; - return const_cast(t->getArt()); -} - -const ArtSlotInfo *ArtifactLocation::getSlot() const -{ - return getHolderArtSet()->getSlot(slot); -} - -void ChangeStackCount::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] ChangeStackCount: invalid army object %d, possible game state corruption.", army.getNum()); - - if(absoluteValue) - srcObj->setStackCount(slot, count); - else - srcObj->changeStackCount(slot, count); -} - -void SetStackType::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] SetStackType: invalid army object %d, possible game state corruption.", army.getNum()); - - srcObj->setStackType(slot, type); -} - -void EraseStack::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] EraseStack: invalid army object %d, possible game state corruption.", army.getNum()); - - srcObj->eraseStack(slot); -} - -void SwapStacks::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(srcArmy); - if(!srcObj) - logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); - - auto * dstObj = gs->getArmyInstance(dstArmy); - if(!dstObj) - logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); - - CStackInstance * s1 = srcObj->detachStack(srcSlot); - CStackInstance * s2 = dstObj->detachStack(dstSlot); - - srcObj->putStack(srcSlot, s2); - dstObj->putStack(dstSlot, s1); -} - -void InsertNewStack::applyGs(CGameState *gs) -{ - if(auto * obj = gs->getArmyInstance(army)) - obj->putStack(slot, new CStackInstance(type, count)); - else - logNetwork->error("[CRITICAL] InsertNewStack: invalid army object %d, possible game state corruption.", army.getNum()); -} - -void RebalanceStacks::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(srcArmy); - if(!srcObj) - logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); - - auto * dstObj = gs->getArmyInstance(dstArmy); - if(!dstObj) - logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); - - StackLocation src(srcObj, srcSlot); - StackLocation dst(dstObj, dstSlot); - - const CCreature * srcType = src.army->getCreature(src.slot); - TQuantity srcCount = src.army->getStackCount(src.slot); - bool stackExp = VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); - - if(srcCount == count) //moving whole stack - { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); - - if(c) //stack at dest -> merge - { - assert(c == srcType); - auto alHere = ArtifactLocation (src.getStack(), ArtifactPosition::CREATURE_SLOT); - auto alDest = ArtifactLocation (dst.getStack(), ArtifactPosition::CREATURE_SLOT); - auto * artHere = alHere.getArt(); - auto * artDest = alDest.getArt(); - if (artHere) - { - if (alDest.getArt()) - { - auto * hero = dynamic_cast(src.army.get()); - auto dstSlot = ArtifactUtils::getArtBackpackPosition(hero, alDest.getArt()->getTypeId()); - if(hero && dstSlot != ArtifactPosition::PRE_FIRST) - { - artDest->move (alDest, ArtifactLocation (hero, dstSlot)); - } - //else - artifact cna be lost :/ - else - { - EraseArtifact ea; - ea.al = alDest; - ea.applyGs(gs); - logNetwork->warn("Cannot move artifact! No free slots"); - } - artHere->move (alHere, alDest); - //TODO: choose from dialog - } - else //just move to the other slot before stack gets erased - { - artHere->move (alHere, alDest); - } - } - if (stackExp) - { - ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); - src.army->eraseStack(src.slot); - dst.army->changeStackCount(dst.slot, count); - dst.army->setStackExp(dst.slot, totalExp /(dst.army->getStackCount(dst.slot))); //mean - } - else - { - src.army->eraseStack(src.slot); - dst.army->changeStackCount(dst.slot, count); - } - } - else //move stack to an empty slot, no exp change needed - { - CStackInstance *stackDetached = src.army->detachStack(src.slot); - dst.army->putStack(dst.slot, stackDetached); - } - } - else - { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); - if(c) //stack at dest -> rebalance - { - assert(c == srcType); - if (stackExp) - { - ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); - src.army->changeStackCount(src.slot, -count); - dst.army->changeStackCount(dst.slot, count); - dst.army->setStackExp(dst.slot, totalExp /(src.army->getStackCount(src.slot) + dst.army->getStackCount(dst.slot))); //mean - } - else - { - src.army->changeStackCount(src.slot, -count); - dst.army->changeStackCount(dst.slot, count); - } - } - else //split stack to an empty slot - { - src.army->changeStackCount(src.slot, -count); - dst.army->addToSlot(dst.slot, srcType->getId(), count, false); - if (stackExp) - dst.army->setStackExp(dst.slot, src.army->getStackExperience(src.slot)); - } - } - - CBonusSystemNode::treeHasChanged(); -} - -void BulkRebalanceStacks::applyGs(CGameState * gs) -{ - for(auto & move : moves) - move.applyGs(gs); -} - -void BulkSmartRebalanceStacks::applyGs(CGameState * gs) -{ - for(auto & move : moves) - move.applyGs(gs); - - for(auto & change : changes) - change.applyGs(gs); -} - -void PutArtifact::applyGs(CGameState *gs) -{ - assert(art->canBePutAt(al)); - // Ensure that artifact has been correctly added via NewArtifact pack - assert(vstd::contains(gs->map->artInstances, art)); - assert(!art->getParentNodes().empty()); - art->putAt(al); -} - -void EraseArtifact::applyGs(CGameState *gs) -{ - const auto * slot = al.getSlot(); - if(slot->locked) - { - logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); - DisassembledArtifact dis; - dis.al.artHolder = al.artHolder; - auto * aset = al.getHolderArtSet(); - #ifndef NDEBUG - bool found = false; - #endif - for(auto& p : aset->artifactsWorn) - { - auto art = p.second.artifact; - if(art->isCombined() && art->isPart(slot->artifact)) - { - dis.al.slot = aset->getArtPos(art); - #ifndef NDEBUG - found = true; - #endif - break; - } - } - assert(found && "Failed to determine the assembly this locked artifact belongs to"); - logGlobal->debug("Found the corresponding assembly: %s", dis.al.getSlot()->artifact->artType->getNameTranslated()); - dis.applyGs(gs); - } - else - { - logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); - } - al.removeArtifact(); -} - -void MoveArtifact::applyGs(CGameState * gs) -{ - CArtifactInstance * art = src.getArt(); - assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); - art->move(src, dst); -} - -void BulkMoveArtifacts::applyGs(CGameState * gs) -{ - enum class EBulkArtsOp - { - BULK_MOVE, - BULK_REMOVE, - BULK_PUT - }; - - auto bulkArtsOperation = [this](std::vector & artsPack, - CArtifactSet * artSet, EBulkArtsOp operation) -> void - { - int numBackpackArtifactsMoved = 0; - for(auto & slot : artsPack) - { - // When an object gets removed from the backpack, the backpack shrinks - // so all the following indices will be affected. Thus, we need to update - // the subsequent artifact slots to account for that - auto srcPos = slot.srcPos; - if(ArtifactUtils::isSlotBackpack(srcPos) && (operation != EBulkArtsOp::BULK_PUT)) - { - srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); - } - const auto * slotInfo = artSet->getSlot(srcPos); - assert(slotInfo); - auto * art = const_cast(slotInfo->getArt()); - assert(art); - switch(operation) - { - case EBulkArtsOp::BULK_MOVE: - const_cast(art)->move( - ArtifactLocation(srcArtHolder, srcPos), ArtifactLocation(dstArtHolder, slot.dstPos)); - break; - case EBulkArtsOp::BULK_REMOVE: - art->removeFrom(ArtifactLocation(dstArtHolder, srcPos)); - break; - case EBulkArtsOp::BULK_PUT: - art->putAt(ArtifactLocation(srcArtHolder, slot.dstPos)); - break; - default: - break; - } - - if(srcPos >= GameConstants::BACKPACK_START) - { - numBackpackArtifactsMoved++; - } - } - }; - - if(swap) - { - // Swap - auto * leftSet = getSrcHolderArtSet(); - auto * rightSet = getDstHolderArtSet(); - CArtifactFittingSet artFittingSet(leftSet->bearerType()); - - artFittingSet.artifactsWorn = rightSet->artifactsWorn; - artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; - - bulkArtsOperation(artsPack1, rightSet, EBulkArtsOp::BULK_REMOVE); - bulkArtsOperation(artsPack0, leftSet, EBulkArtsOp::BULK_MOVE); - bulkArtsOperation(artsPack1, &artFittingSet, EBulkArtsOp::BULK_PUT); - } - else - { - bulkArtsOperation(artsPack0, getSrcHolderArtSet(), EBulkArtsOp::BULK_MOVE); - } -} - -void AssembledArtifact::applyGs(CGameState *gs) -{ - CArtifactSet * artSet = al.getHolderArtSet(); - [[maybe_unused]] const CArtifactInstance *transformedArt = al.getArt(); - assert(transformedArt); - bool combineEquipped = !ArtifactUtils::isSlotBackpack(al.slot); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->artType->getId(), combineEquipped), [=](const CArtifact * art)->bool - { - return art->getId() == builtArt->getId(); - })); - - auto * combinedArt = new CArtifactInstance(builtArt); - gs->map->addNewArtifactInstance(combinedArt); - // Retrieve all constituents - for(const CArtifact * constituent : builtArt->getConstituents()) - { - ArtifactPosition pos = combineEquipped ? artSet->getArtPos(constituent->getId(), true, false) : - artSet->getArtBackpackPos(constituent->getId()); - assert(pos >= 0); - CArtifactInstance * constituentInstance = artSet->getArt(pos); - - //move constituent from hero to be part of new, combined artifact - constituentInstance->removeFrom(ArtifactLocation(al.artHolder, pos)); - if(combineEquipped) - { - if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), pos)) - al.slot = pos; - if(al.slot == pos) - pos = ArtifactPosition::PRE_FIRST; - } - else - { - al.slot = std::min(al.slot, pos); - pos = ArtifactPosition::PRE_FIRST; - } - combinedArt->addPart(constituentInstance, pos); - } - - //put new combined artifacts - combinedArt->putAt(al); -} - -void DisassembledArtifact::applyGs(CGameState *gs) -{ - auto * disassembled = al.getArt(); - assert(disassembled); - - auto parts = disassembled->getPartsInfo(); - disassembled->removeFrom(al); - for(auto & part : parts) - { - ArtifactLocation partLoc = al; - // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos - partLoc.slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); - disassembled->detachFrom(*part.art); - part.art->putAt(partLoc); - } - gs->map->eraseArtifactInstance(disassembled); -} - -void HeroVisit::applyGs(CGameState *gs) -{ -} - -void SetAvailableArtifacts::applyGs(CGameState * gs) const -{ - if(id >= 0) - { - if(auto * bm = dynamic_cast(gs->map->objects[id].get())) - { - bm->artifacts = arts; - } - else - { - logNetwork->error("Wrong black market id!"); - } - } - else - { - CGTownInstance::merchantArtifacts = arts; - } -} - -void NewTurn::applyGs(CGameState *gs) -{ - gs->day = day; - - // Update bonuses before doing anything else so hero don't get more MP than needed - gs->globalEffects.removeBonusesRecursive(Bonus::OneDay); //works for children -> all game objs - gs->globalEffects.reduceBonusDurations(Bonus::NDays); - 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; - } - - hero->setMovementPoints(h.move); - hero->mana = h.mana; - } - - gs->heroesPool->onNewDay(); - - for(const auto & re : res) - { - assert(re.first < PlayerColor::PLAYER_LIMIT); - gs->getPlayerState(re.first)->resources = re.second; - } - - for(const auto & creatureSet : cres) //set available creatures in towns - creatureSet.second.applyGs(gs); - - for(CGTownInstance* t : gs->map->towns) - t->builded = 0; - - if(gs->getDate(Date::DAY_OF_WEEK) == 1) - gs->updateRumor(); - - //count days without town for all players, regardless of their turn order - for (auto &p : gs->players) - { - PlayerState & playerState = p.second; - if (playerState.status == EPlayerStatus::INGAME) - { - if (playerState.towns.empty()) - { - if (playerState.daysWithoutCastle) - ++(*playerState.daysWithoutCastle); - else - playerState.daysWithoutCastle = std::make_optional(0); - } - else - { - playerState.daysWithoutCastle = std::nullopt; - } - } - } -} - -void SetObjectProperty::applyGs(CGameState * gs) const -{ - CGObjectInstance *obj = gs->getObjInstance(id); - if(!obj) - { - logNetwork->error("Wrong object ID - property cannot be set!"); - return; - } - - auto * cai = dynamic_cast(obj); - if(what == ObjProperty::OWNER && cai) - { - if(obj->ID == Obj::TOWN) - { - auto * t = dynamic_cast(obj); - assert(t); - if(t->tempOwner < PlayerColor::PLAYER_LIMIT) - gs->getPlayerState(t->tempOwner)->towns -= t; - if(val < PlayerColor::PLAYER_LIMIT_I) - { - PlayerState * p = gs->getPlayerState(PlayerColor(val)); - p->towns.emplace_back(t); - - //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured - if(p->daysWithoutCastle) - p->daysWithoutCastle = std::nullopt; - } - } - - CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); - nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); - obj->setProperty(what,val); - nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); - } - else //not an armed instance - { - obj->setProperty(what,val); - } -} - -void PrepareHeroLevelUp::applyGs(CGameState * gs) -{ - auto * hero = gs->getHero(heroId); - assert(hero); - - auto proposedSkills = hero->getLevelUpProposedSecondarySkills(); - - if(skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - skills.push_back(*RandomGeneratorUtil::nextItem(proposedSkills, hero->skillsInfo.rand)); - } - else - { - skills = proposedSkills; - } -} - -void HeroLevelUp::applyGs(CGameState * gs) const -{ - auto * hero = gs->getHero(heroId); - assert(hero); - hero->levelUp(skills); -} - -void CommanderLevelUp::applyGs(CGameState * gs) const -{ - auto * hero = gs->getHero(heroId); - assert(hero); - auto commander = hero->commander; - assert(commander); - commander->levelUp(); -} - -void BattleStart::applyGs(CGameState * gs) const -{ - gs->curB = info; - gs->curB->localInit(); -} - -void BattleNextRound::applyGs(CGameState * gs) const -{ - THROW_IF_NO_BATTLE - gs->curB->nextRound(round); -} - -void BattleSetActiveStack::applyGs(CGameState * gs) const -{ - THROW_IF_NO_BATTLE - gs->curB->nextTurn(stack); -} - -void BattleTriggerEffect::applyGs(CGameState * gs) const -{ - THROW_IF_NO_BATTLE - CStack * st = gs->curB->getStack(stackID); - assert(st); - switch(static_cast(effect)) - { - case BonusType::HP_REGENERATION: - { - int64_t toHeal = val; - st->heal(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); - break; - } - case BonusType::MANA_DRAIN: - { - CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); - st->drainedMana = true; - h->mana -= val; - vstd::amax(h->mana, 0); - break; - } - case BonusType::POISON: - { - auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON) - .And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) - b->val = val; - break; - } - case BonusType::ENCHANTER: - case BonusType::MORALE: - break; - case BonusType::FEAR: - st->fear = true; - break; - default: - logNetwork->error("Unrecognized trigger effect type %d", effect); - } -} - -void BattleUpdateGateState::applyGs(CGameState * gs) const -{ - if(gs->curB) - gs->curB->si.gateState = state; -} - -void BattleResultAccepted::applyGs(CGameState * gs) const -{ - // Remove any "until next battle" bonuses - for(auto & res : heroResult) - { - if(res.hero) - res.hero->removeBonusesRecursive(Bonus::OneBattle); - } - - if(winnerSide != 2) - { - // Grow up growing artifacts - const auto hero = heroResult[winnerSide].hero; - - if (hero) - { - if(hero->commander && hero->commander->alive) - { - for(auto & art : hero->commander->artifactsWorn) - art.second.artifact->growingUp(); - } - for(auto & art : hero->artifactsWorn) - { - art.second.artifact->growingUp(); - } - } - } - - if(VLC->settings()->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); - CBonusSystemNode::treeHasChanged(); - } - - gs->curB.dellNull(); -} - -void BattleLogMessage::applyGs(CGameState *gs) -{ - //nothing -} - -void BattleLogMessage::applyBattle(IBattleState * battleState) -{ - //nothing -} - -void BattleStackMoved::applyGs(CGameState *gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void BattleStackMoved::applyBattle(IBattleState * battleState) -{ - battleState->moveUnit(stack, tilesToMove.back()); -} - -void BattleStackAttacked::applyGs(CGameState * gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void BattleStackAttacked::applyBattle(IBattleState * battleState) -{ - battleState->setUnitState(newState.id, newState.data, newState.healthDelta); -} - -void BattleAttack::applyGs(CGameState * gs) -{ - THROW_IF_NO_BATTLE - CStack * attacker = gs->curB->getStack(stackAttacking); - assert(attacker); - - attackerChanges.applyGs(gs); - - for(BattleStackAttacked & stackAttacked : bsa) - stackAttacked.applyGs(gs); - - attacker->removeBonusesRecursive(Bonus::UntilAttack); -} - -void StartAction::applyGs(CGameState *gs) -{ - THROW_IF_NO_BATTLE - - CStack *st = gs->curB->getStack(ba.stackNumber); - - if(ba.actionType == EActionType::END_TACTIC_PHASE) - { - gs->curB->tacticDistance = 0; - return; - } - - if(gs->curB->tacticDistance) - { - // moves in tactics phase do not affect creature status - // (tactics stack queue is managed by client) - return; - } - - if(ba.actionType != EActionType::HERO_SPELL) //don't check for stack if it's custom action by hero - { - assert(st); - } - else - { - gs->curB->sides[ba.side].usedSpellsHistory.emplace_back(ba.actionSubtype); - } - - switch(ba.actionType) - { - case EActionType::DEFEND: - st->waiting = false; - st->defending = true; - st->defendingAnim = true; - break; - case EActionType::WAIT: - st->defendingAnim = false; - st->waiting = true; - st->waitedThisTurn = true; - break; - case EActionType::HERO_SPELL: //no change in current stack state - break; - default: //any active stack action - attack, catapult, heal, spell... - st->waiting = false; - st->defendingAnim = false; - st->movedThisRound = true; - break; - } -} - -void BattleSpellCast::applyGs(CGameState * gs) const -{ - THROW_IF_NO_BATTLE - - if(castByHero) - { - if(side < 2) - { - gs->curB->sides[side].castSpellsCount++; - } - } -} - -void SetStackEffect::applyGs(CGameState *gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void SetStackEffect::applyBattle(IBattleState * battleState) -{ - for(const auto & stackData : toRemove) - battleState->removeUnitBonus(stackData.first, stackData.second); - - for(const auto & stackData : toUpdate) - battleState->updateUnitBonus(stackData.first, stackData.second); - - for(const auto & stackData : toAdd) - battleState->addUnitBonus(stackData.first, stackData.second); -} - - -void StacksInjured::applyGs(CGameState *gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void StacksInjured::applyBattle(IBattleState * battleState) -{ - for(BattleStackAttacked stackAttacked : stacks) - stackAttacked.applyBattle(battleState); -} - -void BattleUnitsChanged::applyGs(CGameState *gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void BattleUnitsChanged::applyBattle(IBattleState * battleState) -{ - for(auto & elem : changedStacks) - { - switch(elem.operation) - { - case BattleChanges::EOperation::RESET_STATE: - battleState->setUnitState(elem.id, elem.data, elem.healthDelta); - break; - case BattleChanges::EOperation::REMOVE: - battleState->removeUnit(elem.id); - break; - case BattleChanges::EOperation::ADD: - battleState->addUnit(elem.id, elem.data); - break; - case BattleChanges::EOperation::UPDATE: - battleState->updateUnit(elem.id, elem.data); - break; - default: - logNetwork->error("Unknown unit operation %d", static_cast(elem.operation)); - break; - } - } -} - -void BattleObstaclesChanged::applyGs(CGameState * gs) -{ - THROW_IF_NO_BATTLE; - applyBattle(gs->curB); -} - -void BattleObstaclesChanged::applyBattle(IBattleState * battleState) -{ - for(const auto & change : changes) - { - switch(change.operation) - { - case BattleChanges::EOperation::REMOVE: - battleState->removeObstacle(change.id); - break; - case BattleChanges::EOperation::ADD: - battleState->addObstacle(change); - break; - case BattleChanges::EOperation::UPDATE: - battleState->updateObstacle(change); - break; - default: - logNetwork->error("Unknown obstacle operation %d", static_cast(change.operation)); - break; - } - } -} - -CatapultAttack::CatapultAttack() = default; - -CatapultAttack::~CatapultAttack() = default; - -void CatapultAttack::applyGs(CGameState * gs) -{ - THROW_IF_NO_BATTLE - applyBattle(gs->curB); -} - -void CatapultAttack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCatapultAttack(*this); -} - -void CatapultAttack::applyBattle(IBattleState * battleState) -{ - const auto * town = battleState->getDefendedTown(); - if(!town) - return; - - if(town->fortLevel() == CGTownInstance::NONE) - return; - - for(const auto & part : attackedParts) - { - auto newWallState = SiegeInfo::applyDamage(battleState->getWallState(part.attackedPart), part.damageDealt); - battleState->setWallState(part.attackedPart, newWallState); - } -} - -void BattleSetStackProperty::applyGs(CGameState * gs) const -{ - THROW_IF_NO_BATTLE - CStack * stack = gs->curB->getStack(stackID); - switch(which) - { - case CASTS: - { - if(absolute) - logNetwork->error("Can not change casts in absolute mode"); - else - stack->casts.use(-val); - break; - } - case ENCHANTER_COUNTER: - { - auto & counter = gs->curB->sides[gs->curB->whatSide(stack->unitOwner())].enchanterCounter; - if(absolute) - counter = val; - else - counter += val; - vstd::amax(counter, 0); - break; - } - case UNBIND: - { - stack->removeBonusesRecursive(Selector::type()(BonusType::BIND_EFFECT)); - break; - } - case CLONED: - { - stack->cloned = true; - break; - } - case HAS_CLONE: - { - stack->cloneID = val; - break; - } - } -} - -void PlayerCheated::applyGs(CGameState * gs) const -{ - if(!player.isValidPlayer()) - return; - - gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; - gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; -} - -void YourTurn::applyGs(CGameState * gs) const -{ - gs->currentPlayer = player; - - auto & playerState = gs->players[player]; - playerState.daysWithoutCastle = daysWithoutCastle; -} - -Component::Component(const CStackBasicDescriptor & stack) - : id(EComponentType::CREATURE) - , subtype(stack.type->getId()) - , val(stack.count) -{ -} - -void EntitiesChanged::applyGs(CGameState * gs) -{ - for(const auto & change : changes) - gs->updateEntity(change.metatype, change.entityIndex, change.data); -} - -const CArtifactInstance * ArtSlotInfo::getArt() const -{ - if(locked) - { - logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); - return nullptr; - } - return artifact; -} - -CArtifactSet * BulkMoveArtifacts::getSrcHolderArtSet() -{ - return std::visit(GetBase(), srcArtHolder); -} - -CArtifactSet * BulkMoveArtifacts::getDstHolderArtSet() -{ - return std::visit(GetBase(), dstArtHolder); -} - -VCMI_LIB_NAMESPACE_END +/* + * NetPacksLib.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 "ArtifactUtils.h" +#include "PacksForClient.h" +#include "PacksForClientBattle.h" +#include "PacksForServer.h" +#include "StackLocation.h" +#include "PacksForLobby.h" +#include "SetStackEffect.h" +#include "NetPackVisitor.h" +#include "CGeneralTextHandler.h" +#include "CArtHandler.h" +#include "CHeroHandler.h" +#include "VCMI_Lib.h" +#include "mapping/CMap.h" +#include "spells/CSpellHandler.h" +#include "CCreatureHandler.h" +#include "gameState/CGameState.h" +#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 "mapObjects/CGCreature.h" +#include "mapObjects/CGMarket.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "campaign/CampaignState.h" +#include "GameSettings.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CPack::visit(ICPackVisitor & visitor) +{ + visitBasic(visitor); + + // visitBasic may destroy this and in such cases we do not want to call visitTyped + if(visitor.callTyped()) + { + visitTyped(visitor); + } +} + +void CPack::visitBasic(ICPackVisitor & visitor) +{ +} + +void CPack::visitTyped(ICPackVisitor & visitor) +{ +} + +void CPackForClient::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForClient(*this); +} + +void CPackForServer::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForServer(*this); +} + +void CPackForLobby::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForLobby(*this); +} + +bool CPackForLobby::isForServer() const +{ + return false; +} + +bool CLobbyPackToServer::isForServer() const +{ + return true; +} + +void PackageApplied::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPackageApplied(*this); +} + +void SystemMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSystemMessage(*this); +} + +void PlayerBlocked::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerBlocked(*this); +} + +void PlayerCheated::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerCheated(*this); +} + +void PlayerStartsTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerStartsTurn(*this); +} + +void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDaysWithoutTown(*this); +} + +void EntitiesChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEntitiesChanged(*this); +} + +void SetResources::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetResources(*this); +} + +void SetPrimSkill::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetPrimSkill(*this); +} + +void SetSecSkill::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetSecSkill(*this); +} + +void HeroVisitCastle::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroVisitCastle(*this); +} + +void ChangeSpells::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeSpells(*this); +} + +void SetMana::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetMana(*this); +} + +void SetMovePoints::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetMovePoints(*this); +} + +void FoWChange::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitFoWChange(*this); +} + +void SetAvailableHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableHeroes(*this); +} + +void GiveBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGiveBonus(*this); +} + +void ChangeObjPos::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeObjPos(*this); +} + +void PlayerEndsTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerEndsTurn(*this); +} + +void PlayerEndsGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerEndsGame(*this); +} + +void PlayerReinitInterface::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerReinitInterface(*this); +} + +void RemoveBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRemoveBonus(*this); +} + +void SetCommanderProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetCommanderProperty(*this); +} + +void AddQuest::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAddQuest(*this); +} + +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); +} + +void RemoveObject::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRemoveObject(*this); +} + +void TryMoveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTryMoveHero(*this); +} + +void NewStructures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewStructures(*this); +} + +void RazeStructures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRazeStructures(*this); +} + +void SetAvailableCreatures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableCreatures(*this); +} + +void SetHeroesInTown::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetHeroesInTown(*this); +} + +void HeroRecruited::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroRecruited(*this); +} + +void GiveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGiveHero(*this); +} + +void OpenWindow::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitOpenWindow(*this); +} + +void NewObject::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewObject(*this); +} + +void SetAvailableArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableArtifacts(*this); +} + +void NewArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewArtifact(*this); +} + +void ChangeStackCount::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeStackCount(*this); +} + +void SetStackType::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetStackType(*this); +} + +void EraseStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseStack(*this); +} + +void SwapStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSwapStacks(*this); +} + +void InsertNewStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitInsertNewStack(*this); +} + +void RebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRebalanceStacks(*this); +} + +void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkRebalanceStacks(*this); +} + +void BulkSmartRebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSmartRebalanceStacks(*this); +} + +void PutArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPutArtifact(*this); +} + +void EraseArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseArtifact(*this); +} + +void MoveArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMoveArtifact(*this); +} + +void BulkMoveArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMoveArtifacts(*this); +} + +void AssembledArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAssembledArtifact(*this); +} + +void DisassembledArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDisassembledArtifact(*this); +} + +void HeroVisit::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroVisit(*this); +} + +void NewTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewTurn(*this); +} + +void InfoWindow::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitInfoWindow(*this); +} + +void SetObjectProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetObjectProperty(*this); +} + +void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeObjectVisitors(*this); +} + +void HeroLevelUp::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroLevelUp(*this); +} + +void CommanderLevelUp::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCommanderLevelUp(*this); +} + +void BlockingDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBlockingDialog(*this); +} + +void GarrisonDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGarrisonDialog(*this); +} + +void ExchangeDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitExchangeDialog(*this); +} + +void TeleportDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTeleportDialog(*this); +} + +void MapObjectSelectDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMapObjectSelectDialog(*this); +} + +void BattleStart::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleStart(*this); +} + +void BattleNextRound::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleNextRound(*this); +} + +void BattleSetActiveStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSetActiveStack(*this); +} + +void BattleResult::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleResult(*this); +} + +void BattleLogMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleLogMessage(*this); +} + +void BattleStackMoved::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleStackMoved(*this); +} + +void BattleUnitsChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleUnitsChanged(*this); +} + +void BattleAttack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleAttack(*this); +} + +void StartAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitStartAction(*this); +} + +void EndAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEndAction(*this); +} + +void BattleSpellCast::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSpellCast(*this); +} + +void SetStackEffect::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetStackEffect(*this); +} + +void StacksInjured::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitStacksInjured(*this); +} + +void BattleResultsApplied::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleResultsApplied(*this); +} + +void BattleObstaclesChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleObstaclesChanged(*this); +} + +void BattleSetStackProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSetStackProperty(*this); +} + +void BattleTriggerEffect::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleTriggerEffect(*this); +} + +void BattleUpdateGateState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleUpdateGateState(*this); +} + +void AdvmapSpellCast::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAdvmapSpellCast(*this); +} + +void ShowWorldViewEx::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitShowWorldViewEx(*this); +} + +void EndTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEndTurn(*this); +} + +void GamePause::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGamePause(*this); +} + +void DismissHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDismissHero(*this); +} + +void MoveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMoveHero(*this); +} + +void CastleTeleportHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCastleTeleportHero(*this); +} + +void ArrangeStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitArrangeStacks(*this); +} + +void BulkMoveArmy::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMoveArmy(*this); +} + +void BulkSplitStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSplitStack(*this); +} + +void BulkMergeStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMergeStacks(*this); +} + +void BulkSmartSplitStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSmartSplitStack(*this); +} + +void DisbandCreature::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDisbandCreature(*this); +} + +void BuildStructure::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuildStructure(*this); +} + +void RazeStructure::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRazeStructure(*this); +} + +void RecruitCreatures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRecruitCreatures(*this); +} + +void UpgradeCreature::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitUpgradeCreature(*this); +} + +void GarrisonHeroSwap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGarrisonHeroSwap(*this); +} + +void ExchangeArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitExchangeArtifacts(*this); +} + +void BulkExchangeArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkExchangeArtifacts(*this); +} + +void AssembleArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAssembleArtifacts(*this); +} + +void EraseArtifactByClient::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseArtifactByClient(*this); +} + +void BuyArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuyArtifact(*this); +} + +void TradeOnMarketplace::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTradeOnMarketplace(*this); +} + +void SetFormation::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetFormation(*this); +} + +void HireHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHireHero(*this); +} + +void BuildBoat::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuildBoat(*this); +} + +void QueryReply::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitQueryReply(*this); +} + +void MakeAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMakeAction(*this); +} + +void DigWithHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDigWithHero(*this); +} + +void CastAdvSpell::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCastAdvSpell(*this); +} + +void SaveGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSaveGame(*this); +} + +void PlayerMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerMessage(*this); +} + +void PlayerMessageClient::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerMessageClient(*this); +} + +void CenterView::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCenterView(*this); +} + +void LobbyClientConnected::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyClientConnected(*this); +} + +void LobbyClientDisconnected::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyClientDisconnected(*this); +} + +void LobbyChatMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChatMessage(*this); +} + +void LobbyGuiAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyGuiAction(*this); +} + +void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyLoadProgress(*this); +} + +void LobbyEndGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyEndGame(*this); +} + +void LobbyStartGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyStartGame(*this); +} + +void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChangeHost(*this); +} + +void LobbyUpdateState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyUpdateState(*this); +} + +void LobbySetMap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetMap(*this); +} + +void LobbySetCampaign::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaign(*this); +} + +void LobbySetCampaignMap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaignMap(*this); +} + +void LobbySetCampaignBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaignBonus(*this); +} + +void LobbyChangePlayerOption::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChangePlayerOption(*this); +} + +void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayer(*this); +} + +void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayerName(*this); +} + +void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetSimturns(*this); +} + +void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetTurnTime(*this); +} + +void LobbySetDifficulty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetDifficulty(*this); +} + +void LobbyForceSetPlayer::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyForceSetPlayer(*this); +} + +void LobbyShowMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyShowMessage(*this); +} + +void SetResources::applyGs(CGameState * gs) const +{ + assert(player.isValidPlayer()); + if(abs) + gs->getPlayerState(player)->resources = res; + else + gs->getPlayerState(player)->resources += res; + gs->getPlayerState(player)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); + + //just ensure that player resources are not negative + //server is responsible to check if player can afford deal + //but events on server side are allowed to take more than player have + gs->getPlayerState(player)->resources.positive(); +} + +void SetPrimSkill::applyGs(CGameState * gs) const +{ + CGHeroInstance * hero = gs->getHero(id); + assert(hero); + hero->setPrimarySkill(which, val, abs); +} + +void SetSecSkill::applyGs(CGameState * gs) const +{ + CGHeroInstance *hero = gs->getHero(id); + hero->setSecSkillLevel(which, val, abs); +} + +void SetCommanderProperty::applyGs(CGameState *gs) +{ + CCommanderInstance * commander = gs->getHero(heroid)->commander; + assert (commander); + + switch (which) + { + case BONUS: + commander->accumulateBonus (std::make_shared(accumulatedBonus)); + break; + case SPECIAL_SKILL: + commander->accumulateBonus (std::make_shared(accumulatedBonus)); + commander->specialSkills.insert (additionalInfo); + break; + case SECONDARY_SKILL: + commander->secondarySkills[additionalInfo] = static_cast(amount); + break; + case ALIVE: + if (amount) + commander->setAlive(true); + else + commander->setAlive(false); + break; + case EXPERIENCE: + commander->giveStackExp(amount); //TODO: allow setting exp for stacks via netpacks + break; + } +} + +void AddQuest::applyGs(CGameState * gs) const +{ + assert (vstd::contains(gs->players, player)); + auto * vec = &gs->players[player].quests; + if (!vstd::contains(*vec, quest)) + vec->push_back (quest); + else + logNetwork->warn("Warning! Attempt to add duplicated quest"); +} + +void UpdateArtHandlerLists::applyGs(CGameState * gs) const +{ + 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 +{ + gs->getHero(hid)->setFormation(formation); +} + +void HeroVisitCastle::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->getHero(hid); + CGTownInstance *t = gs->getTown(tid); + + assert(h); + assert(t); + + if(start()) + t->setVisitingHero(h); + else + t->setVisitingHero(nullptr); +} + +void ChangeSpells::applyGs(CGameState *gs) +{ + CGHeroInstance *hero = gs->getHero(hid); + + if(learn) + for(const auto & sid : spells) + hero->addSpellToSpellbook(sid); + else + for(const auto & sid : spells) + hero->removeSpellFromSpellbook(sid); +} + +void SetMana::applyGs(CGameState * gs) const +{ + CGHeroInstance * hero = gs->getHero(hid); + + assert(hero); + + if(absolute) + hero->mana = val; + else + hero->mana += val; + + vstd::amax(hero->mana, 0); //not less than 0 +} + +void SetMovePoints::applyGs(CGameState * gs) const +{ + CGHeroInstance *hero = gs->getHero(hid); + + assert(hero); + + if(absolute) + hero->setMovementPoints(val); + else + hero->setMovementPoints(hero->movementPointsRemaining() + val); +} + +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; + + if (mode == ETileVisibility::HIDDEN) //do not hide too much + { + std::unordered_set tilesRevealed; + for (auto & elem : gs->map->objects) + { + const CGObjectInstance *o = elem; + if (o) + { + switch(o->ID.toEnum()) + { + case Obj::HERO: + case Obj::MINE: + case Obj::TOWN: + case Obj::ABANDONED_MINE: + if(vstd::contains(team->players, o->tempOwner)) //check owned observators + gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), ETileVisibility::HIDDEN, o->tempOwner); + break; + } + } + } + for(const int3 & t : tilesRevealed) //probably not the most optimal solution ever + (*fogOfWarMap)[t.z][t.x][t.y] = 1; + } +} + +void SetAvailableHero::applyGs(CGameState *gs) +{ + gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); +} + +void GiveBonus::applyGs(CGameState *gs) +{ + CBonusSystemNode *cbsn = nullptr; + switch(who) + { + case ETarget::OBJECT: + cbsn = dynamic_cast(gs->getObjInstance(id.as())); + break; + case ETarget::PLAYER: + cbsn = gs->getPlayerState(id.as()); + break; + case ETarget::BATTLE: + assert(Bonus::OneBattle(&bonus)); + cbsn = dynamic_cast(gs->getBattle(id.as())); + break; + } + + assert(cbsn); + + if(Bonus::OneWeek(&bonus)) + bonus.turnsRemain = 8 - gs->getDate(Date::DAY_OF_WEEK); // set correct number of days before adding bonus + + auto b = std::make_shared(bonus); + cbsn->addNewBonus(b); + + std::string &descr = b->description; + + if(!bdescr.empty()) + { + descr = bdescr.toString(); + } + else if(!descr.empty()) + { + //use preseet description + } + else if((bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE) + && (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE)) + { + //no description, use generic + //?could use allways when Type == BonusDuration::Type::ONE_BATTLE + descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" + } + else + { + logGlobal->debug("Empty bonus decription. Type=%d", (int) bonus.type); + } + // Some of(?) versions of H3 use " %s" here instead of %d. Try to replace both of them + boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); // " +/-%d Temporary until next battle + boost::replace_first(descr, " %s", boost::str(boost::format(" %+d") % bonus.val)); // " %s" in arraytxt.69, fountian of fortune +} + +void ChangeObjPos::applyGs(CGameState *gs) +{ + CGObjectInstance *obj = gs->getObjInstance(objid); + if(!obj) + { + logNetwork->error("Wrong ChangeObjPos: object %d doesn't exist!", objid.getNum()); + return; + } + gs->map->removeBlockVisTiles(obj); + obj->pos = nPos + obj->getVisitableOffset(); + gs->map->addBlockVisTiles(obj); +} + +void ChangeObjectVisitors::applyGs(CGameState * gs) const +{ + switch (mode) { + case VISITOR_ADD: + 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); + } + } + break; + case VISITOR_CLEAR: + 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 + } + } + + for(auto &elem : gs->players) + { + elem.second.visitedObjects.erase(object); + } + + break; + case VISITOR_GLOBAL: + { + CGObjectInstance * objectPtr = gs->getObjInstance(object); + gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID}); + break; + } + case VISITOR_REMOVE: + gs->getHero(hero)->visitedObjects.erase(object); + break; + } +} + +void PlayerEndsGame::applyGs(CGameState * gs) const +{ + PlayerState *p = gs->getPlayerState(player); + if(victoryLossCheckResult.victory()) + { + p->status = EPlayerStatus::WINNER; + + // TODO: Campaign-specific code might as well go somewhere else + // keep all heroes from the winning player + if(p->human && gs->scenarioOps->campState) + { + std::vector crossoverHeroes; + for (CGHeroInstance * hero : gs->map->heroesOnMap) + if (hero->tempOwner == player) + crossoverHeroes.push_back(hero); + + gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); + } + } + else + { + p->status = EPlayerStatus::LOSER; + } + + // defeated player may be making turn right now + gs->actingPlayers.erase(player); +} + +void PlayerReinitInterface::applyGs(CGameState *gs) +{ + if(!gs || !gs->scenarioOps) + return; + + //TODO: what does mean if more that one player connected? + if(playerConnectionId == PlayerSettings::PLAYER_AI) + { + for(const auto & player : players) + gs->scenarioOps->getIthPlayersSettings(player).connectedPlayerIDs.clear(); + } +} + +void RemoveBonus::applyGs(CGameState *gs) +{ + CBonusSystemNode *node = nullptr; + switch(who) + { + case GiveBonus::ETarget::OBJECT: + node = dynamic_cast(gs->getObjInstance(whoID.as())); + break; + case GiveBonus::ETarget::PLAYER: + node = gs->getPlayerState(whoID.as()); + break; + case GiveBonus::ETarget::BATTLE: + assert(Bonus::OneBattle(&bonus)); + node = dynamic_cast(gs->getBattle(whoID.as())); + break; + } + + BonusList &bonuses = node->getExportedBonusList(); + + for(const auto & b : bonuses) + { + if(b->source == source && b->sid == id) + { + bonus = *b; //backup bonus (to show to interfaces later) + node->removeBonus(b); + break; + } + } +} + +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 + gs->map->removeBlockVisTiles(obj); + + 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); + + // FIXME: workaround: + // hero should be attached to siegeNode after battle + // however this code might also be called on dismissing hero while in town + if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode)) + beatenHero->detachFrom(*siegeNode); + + beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero + vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) + { + return asi.artifact->artType->getId() == ArtifactID::GRAIL; + }); + + if(beatenHero->visitedTown) + { + if(beatenHero->visitedTown->garrisonHero == beatenHero) + beatenHero->visitedTown->garrisonHero = nullptr; + else + beatenHero->visitedTown->visitingHero = nullptr; + + beatenHero->visitedTown = nullptr; + beatenHero->inTownGarrison = false; + } + //return hero to the pool, so he may reappear in tavern + + gs->heroesPool->addHeroToPool(beatenHero); + gs->map->objects[objectID.getNum()] = nullptr; + + //If hero on Boat is removed, the Boat disappears + if(beatenHero->boat) + { + beatenHero->detachFrom(const_cast(*beatenHero->boat)); + gs->map->instanceNames.erase(beatenHero->boat->instanceName); + gs->map->objects[beatenHero->boat->id.getNum()].dellNull(); + beatenHero->boat = nullptr; + } + return; + } + + const auto * quest = dynamic_cast(obj); + if (quest) + { + gs->map->quests[quest->quest->qid] = nullptr; + for (auto &player : gs->players) + { + for (auto &q : player.second.quests) + { + if (q.obj == obj) + { + q.obj = nullptr; + } + } + } + } + + for (TriggeredEvent & event : gs->map->triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + if (cond.objectID == obj->id) + { + if (cond.condition == EventCondition::DESTROY) + { + cond.condition = EventCondition::CONST_VALUE; + cond.value = 1; // destroyed object, from now on always fulfilled + } + else if (cond.condition == EventCondition::CONTROL) + { + cond.condition = EventCondition::CONST_VALUE; + cond.value = 0; // destroyed object, from now on can not be fulfilled + } + } + return cond; + }; + event.trigger = event.trigger.morph(patcher); + } + gs->map->instanceNames.erase(obj->instanceName); + gs->map->objects[objectID.getNum()].dellNull(); + gs->map->calculateGuardingGreaturePositions(); +} + +static int getDir(const int3 & src, const int3 & dst) +{ + int ret = -1; + if(dst.x+1 == src.x && dst.y+1 == src.y) //tl + { + ret = 1; + } + else if(dst.x == src.x && dst.y+1 == src.y) //t + { + ret = 2; + } + else if(dst.x-1 == src.x && dst.y+1 == src.y) //tr + { + ret = 3; + } + else if(dst.x-1 == src.x && dst.y == src.y) //r + { + ret = 4; + } + else if(dst.x-1 == src.x && dst.y-1 == src.y) //br + { + ret = 5; + } + else if(dst.x == src.x && dst.y-1 == src.y) //b + { + ret = 6; + } + else if(dst.x+1 == src.x && dst.y-1 == src.y) //bl + { + ret = 7; + } + else if(dst.x+1 == src.x && dst.y == src.y) //l + { + ret = 8; + } + return ret; +} + +void TryMoveHero::applyGs(CGameState *gs) +{ + CGHeroInstance *h = gs->getHero(id); + if (!h) + { + logGlobal->error("Attempt ot move unavailable hero %d", id.getNum()); + return; + } + + h->setMovementPoints(movePoints); + + if((result == SUCCESS || result == BLOCKING_VISIT || result == EMBARK || result == DISEMBARK) && start != end) + { + auto dir = getDir(start,end); + if(dir > 0 && dir <= 8) + h->moveDir = dir; + //else don`t change move direction - hero might have traversed the subterranean gate, direction should be kept + } + + if(result == EMBARK) //hero enters boat at destination tile + { + const TerrainTile &tt = gs->map->getTile(h->convertToVisitablePos(end)); + assert(tt.visitableObjects.size() >= 1 && tt.visitableObjects.back()->ID == Obj::BOAT); //the only visitable object at destination is Boat + auto * boat = dynamic_cast(tt.visitableObjects.back()); + assert(boat); + + gs->map->removeBlockVisTiles(boat); //hero blockvis mask will be used, we don't need to duplicate it with boat + h->boat = boat; + h->attachTo(*boat); + boat->hero = h; + } + else if(result == DISEMBARK) //hero leaves boat to destination tile + { + auto * b = const_cast(h->boat); + b->direction = h->moveDir; + b->pos = start; + b->hero = nullptr; + gs->map->addBlockVisTiles(b); + h->detachFrom(*b); + h->boat = nullptr; + } + + if(start!=end && (result == SUCCESS || result == TELEPORTATION || result == EMBARK || result == DISEMBARK)) + { + gs->map->removeBlockVisTiles(h); + h->pos = end; + if(auto * b = const_cast(h->boat)) + b->pos = end; + gs->map->addBlockVisTiles(h); + } + + auto & fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; + for(const int3 & t : fowRevealed) + (*fogOfWarMap)[t.z][t.x][t.y] = 1; +} + +void NewStructures::applyGs(CGameState *gs) +{ + CGTownInstance *t = gs->getTown(tid); + + 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); + } + } + t->builded = builded; + t->recreateBuildingsBonuses(); +} + +void RazeStructures::applyGs(CGameState *gs) +{ + CGTownInstance *t = gs->getTown(tid); + for(const auto & id : bid) + { + t->builtBuildings.erase(id); + + t->updateAppearance(); + } + t->destroyed = destroyed; //yeaha + t->recreateBuildingsBonuses(); +} + +void SetAvailableCreatures::applyGs(CGameState * gs) const +{ + auto * dw = dynamic_cast(gs->getObjInstance(tid)); + assert(dw); + dw->creatures = creatures; +} + +void SetHeroesInTown::applyGs(CGameState * gs) const +{ + CGTownInstance *t = gs->getTown(tid); + + CGHeroInstance * v = gs->getHero(visiting); + CGHeroInstance * g = gs->getHero(garrison); + + bool newVisitorComesFromGarrison = v && v == t->garrisonHero; + bool newGarrisonComesFromVisiting = g && g == t->visitingHero; + + if(newVisitorComesFromGarrison) + t->setGarrisonedHero(nullptr); + if(newGarrisonComesFromVisiting) + t->setVisitingHero(nullptr); + if(!newGarrisonComesFromVisiting || v) + t->setVisitingHero(v); + if(!newVisitorComesFromGarrison || g) + t->setGarrisonedHero(g); + + if(v) + { + gs->map->addBlockVisTiles(v); + } + if(g) + { + gs->map->removeBlockVisTiles(g); + } +} + +void HeroRecruited::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); + CGTownInstance *t = gs->getTown(tid); + PlayerState *p = gs->getPlayerState(player); + + if (boatId != ObjectInstanceID::NONE) + { + CGObjectInstance *obj = gs->getObjInstance(boatId); + auto * boat = dynamic_cast(obj); + if (boat) + { + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); + } + } + + h->setOwner(player); + h->pos = tile; + h->initObj(gs->getRandomGenerator()); + + if(h->id == ObjectInstanceID()) + { + h->id = ObjectInstanceID(static_cast(gs->map->objects.size())); + gs->map->objects.emplace_back(h); + } + else + gs->map->objects[h->id.getNum()] = h; + + gs->map->heroesOnMap.emplace_back(h); + p->heroes.emplace_back(h); + h->attachTo(*p); + gs->map->addBlockVisTiles(h); + + if(t) + t->setVisitingHero(h); +} + +void GiveHero::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->getHero(id); + + if (boatId != ObjectInstanceID::NONE) + { + CGObjectInstance *obj = gs->getObjInstance(boatId); + auto * boat = dynamic_cast(obj); + if (boat) + { + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); + } + } + + //bonus system + h->detachFrom(gs->globalEffects); + h->attachTo(*gs->getPlayerState(player)); + + auto oldVisitablePos = h->visitablePos(); + gs->map->removeBlockVisTiles(h,true); + h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); + + h->setOwner(player); + h->setMovementPoints(h->movementPointsLimit(true)); + h->pos = h->convertFromVisitablePos(oldVisitablePos); + gs->map->heroesOnMap.emplace_back(h); + gs->getPlayerState(h->getOwner())->heroes.emplace_back(h); + + gs->map->addBlockVisTiles(h); + h->inTownGarrison = false; +} + +void NewObject::applyGs(CGameState *gs) +{ + TerrainId terrainType = ETerrainId::NONE; + + 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(); + 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->calculateGuardingGreaturePositions(); + + createdObjectID = o->id; + + logGlobal->debug("Added object id=%d; address=%x; name=%s", o->id, (intptr_t)o, o->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); + } + gs->map->addNewArtifactInstance(art); +} + +const CStackInstance * StackLocation::getStack() +{ + if(!army->hasStackAtSlot(slot)) + { + logNetwork->warn("%s don't have a stack at slot %d", army->nodeName(), slot.getNum()); + return nullptr; + } + return &army->getStack(slot); +} + +struct ObjectRetriever +{ + const CArmedInstance * operator()(const ConstTransitivePtr &h) const + { + return h; + } + const CArmedInstance * operator()(const ConstTransitivePtr &s) const + { + return s->armyObj; + } +}; +template +struct GetBase +{ + template + T * operator()(TArg &arg) const + { + return arg; + } +}; + +void ChangeStackCount::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] ChangeStackCount: invalid army object %d, possible game state corruption.", army.getNum()); + + if(absoluteValue) + srcObj->setStackCount(slot, count); + else + srcObj->changeStackCount(slot, count); +} + +void SetStackType::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] SetStackType: invalid army object %d, possible game state corruption.", army.getNum()); + + srcObj->setStackType(slot, type); +} + +void EraseStack::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] EraseStack: invalid army object %d, possible game state corruption.", army.getNum()); + + srcObj->eraseStack(slot); +} + +void SwapStacks::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(srcArmy); + if(!srcObj) + logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); + + auto * dstObj = gs->getArmyInstance(dstArmy); + if(!dstObj) + logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); + + CStackInstance * s1 = srcObj->detachStack(srcSlot); + CStackInstance * s2 = dstObj->detachStack(dstSlot); + + srcObj->putStack(srcSlot, s2); + dstObj->putStack(dstSlot, s1); +} + +void InsertNewStack::applyGs(CGameState *gs) +{ + if(auto * obj = gs->getArmyInstance(army)) + obj->putStack(slot, new CStackInstance(type, count)); + else + logNetwork->error("[CRITICAL] InsertNewStack: invalid army object %d, possible game state corruption.", army.getNum()); +} + +void RebalanceStacks::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(srcArmy); + if(!srcObj) + logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); + + auto * dstObj = gs->getArmyInstance(dstArmy); + if(!dstObj) + logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); + + StackLocation src(srcObj, srcSlot); + StackLocation dst(dstObj, dstSlot); + + const CCreature * srcType = src.army->getCreature(src.slot); + TQuantity srcCount = src.army->getStackCount(src.slot); + bool stackExp = VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); + + if(srcCount == count) //moving whole stack + { + const auto c = dst.army->getCreature(dst.slot); + + if(c) //stack at dest -> merge + { + assert(c == srcType); + + const auto srcHero = dynamic_cast(src.army.get()); + 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(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); + } + //else - artifact cna be lost :/ + else + { + EraseArtifact ea; + ea.al = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); + ea.al.creature = dst.slot; + ea.applyGs(gs); + logNetwork->warn("Cannot move artifact! No free slots"); + } + srcArt->move(*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); + } + } + if (stackExp) + { + ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); + src.army->eraseStack(src.slot); + dst.army->changeStackCount(dst.slot, count); + dst.army->setStackExp(dst.slot, totalExp /(dst.army->getStackCount(dst.slot))); //mean + } + else + { + src.army->eraseStack(src.slot); + dst.army->changeStackCount(dst.slot, count); + } + } + else //move stack to an empty slot, no exp change needed + { + CStackInstance *stackDetached = src.army->detachStack(src.slot); + dst.army->putStack(dst.slot, stackDetached); + } + } + else + { + [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); + if(c) //stack at dest -> rebalance + { + assert(c == srcType); + if (stackExp) + { + ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); + src.army->changeStackCount(src.slot, -count); + dst.army->changeStackCount(dst.slot, count); + dst.army->setStackExp(dst.slot, totalExp /(src.army->getStackCount(src.slot) + dst.army->getStackCount(dst.slot))); //mean + } + else + { + src.army->changeStackCount(src.slot, -count); + dst.army->changeStackCount(dst.slot, count); + } + } + else //split stack to an empty slot + { + src.army->changeStackCount(src.slot, -count); + dst.army->addToSlot(dst.slot, srcType->getId(), count, false); + if (stackExp) + dst.army->setStackExp(dst.slot, src.army->getStackExperience(src.slot)); + } + } + + CBonusSystemNode::treeHasChanged(); +} + +void BulkRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); +} + +void BulkSmartRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); + + for(auto & change : changes) + change.applyGs(gs); +} + +void PutArtifact::applyGs(CGameState *gs) +{ + // Ensure that artifact has been correctly added via NewArtifact pack + assert(vstd::contains(gs->map->artInstances, art)); + assert(!art->getParentNodes().empty()); + auto hero = gs->getHero(al.artHolder); + assert(hero); + assert(art && art->canBePutAt(hero, al.slot)); + art->putAt(*hero, al.slot); +} + +void EraseArtifact::applyGs(CGameState *gs) +{ + const auto hero = gs->getHero(al.artHolder); + assert(hero); + const auto slot = hero->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 : hero->artifactsWorn) + { + auto art = slotInfo.second.artifact; + if(art->isCombined() && art->isPart(slot->artifact)) + { + dis.al.slot = hero->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", hero->getArt(dis.al.slot)->artType->getNameTranslated()); + dis.applyGs(gs); + } + else + { + logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); + } + auto art = hero->getArt(al.slot); + assert(art); + art->removeFrom(*hero, al.slot); +} + +void MoveArtifact::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) +{ + enum class EBulkArtsOp + { + BULK_MOVE, + BULK_REMOVE, + BULK_PUT + }; + + auto bulkArtsOperation = [this, gs](std::vector & artsPack, + CArtifactSet & artSet, EBulkArtsOp operation) -> void + { + int numBackpackArtifactsMoved = 0; + for(auto & slot : artsPack) + { + // When an object gets removed from the backpack, the backpack shrinks + // so all the following indices will be affected. Thus, we need to update + // the subsequent artifact slots to account for that + auto srcPos = slot.srcPos; + if(ArtifactUtils::isSlotBackpack(srcPos) && (operation != EBulkArtsOp::BULK_PUT)) + { + srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); + } + auto * art = artSet.getArt(srcPos); + assert(art); + switch(operation) + { + case EBulkArtsOp::BULK_MOVE: + art->move(artSet, srcPos, *gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)), slot.dstPos); + break; + case EBulkArtsOp::BULK_REMOVE: + art->removeFrom(artSet, srcPos); + break; + case EBulkArtsOp::BULK_PUT: + art->putAt(*gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)), slot.dstPos); + break; + default: + break; + } + + if(srcPos >= ArtifactPosition::BACKPACK_START) + { + numBackpackArtifactsMoved++; + } + } + }; + + auto * leftSet = gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)); + if(swap) + { + // Swap + auto * rightSet = gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)); + CArtifactFittingSet artFittingSet(leftSet->bearerType()); + + artFittingSet.artifactsWorn = rightSet->artifactsWorn; + artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; + + bulkArtsOperation(artsPack1, *rightSet, EBulkArtsOp::BULK_REMOVE); + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack1, artFittingSet, EBulkArtsOp::BULK_PUT); + } + else + { + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); + } +} + +void AssembledArtifact::applyGs(CGameState *gs) +{ + auto hero = gs->getHero(al.artHolder); + assert(hero); + const auto transformedArt = hero->getArt(al.slot); + assert(transformedArt); + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(hero, 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()) + { + ArtifactPosition slot; + if(transformedArt->getTypeId() == constituent->getId()) + slot = transformedArtSlot; + else + slot = hero->getArtPos(constituent->getId(), false, false); + + assert(slot != ArtifactPosition::PRE_FIRST); + slotsInvolved.emplace_back(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(transformedArtSlot)) + { + + if(ArtifactUtils::isSlotBackpack(slot)) + { + al.slot = ArtifactPosition::BACKPACK_START; + 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) + { + const auto constituentInstance = hero->getArt(slot); + constituentInstance->removeFrom(*hero, slot); + + 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); +} + +void DisassembledArtifact::applyGs(CGameState *gs) +{ + auto hero = gs->getHero(al.artHolder); + assert(hero); + auto disassembledArt = hero->getArt(al.slot); + assert(disassembledArt); + + auto parts = disassembledArt->getPartsInfo(); + disassembledArt->removeFrom(*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->eraseArtifactInstance(disassembledArt); +} + +void HeroVisit::applyGs(CGameState *gs) +{ +} + +void SetAvailableArtifacts::applyGs(CGameState * gs) const +{ + if(id != ObjectInstanceID::NONE) + { + if(auto * bm = dynamic_cast(gs->getObjInstance(id))) + { + bm->artifacts = arts; + } + else + { + logNetwork->error("Wrong black market id!"); + } + } + else + { + CGTownInstance::merchantArtifacts = arts; + } +} + +void NewTurn::applyGs(CGameState *gs) +{ + gs->day = day; + + // Update bonuses before doing anything else so hero don't get more MP than needed + gs->globalEffects.removeBonusesRecursive(Bonus::OneDay); //works for children -> all game objs + gs->globalEffects.reduceBonusDurations(Bonus::NDays); + 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; + } + + hero->setMovementPoints(h.move); + hero->mana = h.mana; + } + + gs->heroesPool->onNewDay(); + + for(const auto & re : res) + { + assert(re.first.isValidPlayer()); + gs->getPlayerState(re.first)->resources = re.second; + gs->getPlayerState(re.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); + } + + for(const auto & creatureSet : cres) //set available creatures in towns + creatureSet.second.applyGs(gs); + + for(CGTownInstance* t : gs->map->towns) + t->builded = 0; + + if(gs->getDate(Date::DAY_OF_WEEK) == 1) + gs->updateRumor(); +} + +void SetObjectProperty::applyGs(CGameState * gs) const +{ + CGObjectInstance *obj = gs->getObjInstance(id); + if(!obj) + { + logNetwork->error("Wrong object ID - property cannot be set!"); + return; + } + + auto * cai = dynamic_cast(obj); + if(what == ObjProperty::OWNER && cai) + { + if(obj->ID == Obj::TOWN) + { + auto * t = dynamic_cast(obj); + assert(t); + + PlayerColor oldOwner = t->tempOwner; + if(oldOwner.isValidPlayer()) + { + auto * state = gs->getPlayerState(oldOwner); + state->towns -= t; + + if(state->towns.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 + if(p->daysWithoutCastle) + p->daysWithoutCastle = std::nullopt; + } + } + + CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); + nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); + obj->setProperty(what, identifier); + nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); + } + else //not an armed instance + { + obj->setProperty(what, identifier); + } +} + +void HeroLevelUp::applyGs(CGameState * gs) const +{ + auto * hero = gs->getHero(heroId); + assert(hero); + hero->levelUp(skills); +} + +void CommanderLevelUp::applyGs(CGameState * gs) const +{ + auto * hero = gs->getHero(heroId); + assert(hero); + auto commander = hero->commander; + assert(commander); + commander->levelUp(); +} + +void BattleStart::applyGs(CGameState * gs) const +{ + assert(battleID == gs->nextBattleID); + + gs->currentBattles.emplace_back(info); + + info->battleID = gs->nextBattleID; + info->localInit(); + + gs->nextBattleID = BattleID(gs->nextBattleID.getNum() + 1); +} + +void BattleNextRound::applyGs(CGameState * gs) const +{ + gs->getBattle(battleID)->nextRound(); +} + +void BattleSetActiveStack::applyGs(CGameState * gs) const +{ + gs->getBattle(battleID)->nextTurn(stack); +} + +void BattleTriggerEffect::applyGs(CGameState * gs) const +{ + CStack * st = gs->getBattle(battleID)->getStack(stackID); + assert(st); + switch(static_cast(effect)) + { + case BonusType::HP_REGENERATION: + { + int64_t toHeal = val; + st->heal(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); + break; + } + case BonusType::MANA_DRAIN: + { + CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); + st->drainedMana = true; + h->mana -= val; + vstd::amax(h->mana, 0); + break; + } + case BonusType::POISON: + { + auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID(SpellID::POISON)) + .And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) + b->val = val; + break; + } + case BonusType::ENCHANTER: + case BonusType::MORALE: + break; + case BonusType::FEAR: + st->fear = true; + break; + default: + logNetwork->error("Unrecognized trigger effect type %d", effect); + } +} + +void BattleUpdateGateState::applyGs(CGameState * gs) const +{ + if(gs->getBattle(battleID)) + gs->getBattle(battleID)->si.gateState = state; +} + +void BattleCancelled::applyGs(CGameState * gs) const +{ + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); +} + +void BattleResultAccepted::applyGs(CGameState * gs) const +{ + // Remove any "until next battle" bonuses + for(auto & res : heroResult) + { + if(res.hero) + res.hero->removeBonusesRecursive(Bonus::OneBattle); + } + + if(winnerSide != 2) + { + // Grow up growing artifacts + const auto hero = heroResult[winnerSide].hero; + + if (hero) + { + if(hero->commander && hero->commander->alive) + { + for(auto & art : hero->commander->artifactsWorn) + art.second.artifact->growingUp(); + } + for(auto & art : hero->artifactsWorn) + { + art.second.artifact->growingUp(); + } + } + } + + if(VLC->settings()->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); + CBonusSystemNode::treeHasChanged(); + } + + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); +} + +void BattleLogMessage::applyGs(CGameState *gs) +{ + //nothing +} + +void BattleLogMessage::applyBattle(IBattleState * battleState) +{ + //nothing +} + +void BattleStackMoved::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleStackMoved::applyBattle(IBattleState * battleState) +{ + battleState->moveUnit(stack, tilesToMove.back()); +} + +void BattleStackAttacked::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleStackAttacked::applyBattle(IBattleState * battleState) +{ + battleState->setUnitState(newState.id, newState.data, newState.healthDelta); +} + +void BattleAttack::applyGs(CGameState * gs) +{ + CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking); + assert(attacker); + + attackerChanges.applyGs(gs); + + for(BattleStackAttacked & stackAttacked : bsa) + stackAttacked.applyGs(gs); + + attacker->removeBonusesRecursive(Bonus::UntilAttack); +} + +void StartAction::applyGs(CGameState *gs) +{ + CStack *st = gs->getBattle(battleID)->getStack(ba.stackNumber); + + if(ba.actionType == EActionType::END_TACTIC_PHASE) + { + gs->getBattle(battleID)->tacticDistance = 0; + return; + } + + if(gs->getBattle(battleID)->tacticDistance) + { + // moves in tactics phase do not affect creature status + // (tactics stack queue is managed by client) + return; + } + + if (ba.isUnitAction()) + { + assert(st); // stack must exists for all non-hero actions + + switch(ba.actionType) + { + case EActionType::DEFEND: + st->waiting = false; + st->defending = true; + st->defendingAnim = true; + break; + case EActionType::WAIT: + st->defendingAnim = false; + st->waiting = true; + st->waitedThisTurn = true; + break; + case EActionType::HERO_SPELL: //no change in current stack state + break; + default: //any active stack action - attack, catapult, heal, spell... + st->waiting = false; + st->defendingAnim = false; + st->movedThisRound = true; + break; + } + } + else + { + if(ba.actionType == EActionType::HERO_SPELL) + gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell); + } +} + +void BattleSpellCast::applyGs(CGameState * gs) const +{ + if(castByHero) + { + if(side < 2) + { + gs->getBattle(battleID)->sides[side].castSpellsCount++; + } + } +} + +void SetStackEffect::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void SetStackEffect::applyBattle(IBattleState * battleState) +{ + for(const auto & stackData : toRemove) + battleState->removeUnitBonus(stackData.first, stackData.second); + + for(const auto & stackData : toUpdate) + battleState->updateUnitBonus(stackData.first, stackData.second); + + for(const auto & stackData : toAdd) + battleState->addUnitBonus(stackData.first, stackData.second); +} + + +void StacksInjured::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void StacksInjured::applyBattle(IBattleState * battleState) +{ + for(BattleStackAttacked stackAttacked : stacks) + stackAttacked.applyBattle(battleState); +} + +void BattleUnitsChanged::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleUnitsChanged::applyBattle(IBattleState * battleState) +{ + for(auto & elem : changedStacks) + { + switch(elem.operation) + { + case BattleChanges::EOperation::RESET_STATE: + battleState->setUnitState(elem.id, elem.data, elem.healthDelta); + break; + case BattleChanges::EOperation::REMOVE: + battleState->removeUnit(elem.id); + break; + case BattleChanges::EOperation::ADD: + battleState->addUnit(elem.id, elem.data); + break; + case BattleChanges::EOperation::UPDATE: + battleState->updateUnit(elem.id, elem.data); + break; + default: + logNetwork->error("Unknown unit operation %d", static_cast(elem.operation)); + break; + } + } +} + +void BattleObstaclesChanged::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleObstaclesChanged::applyBattle(IBattleState * battleState) +{ + for(const auto & change : changes) + { + switch(change.operation) + { + case BattleChanges::EOperation::REMOVE: + battleState->removeObstacle(change.id); + break; + case BattleChanges::EOperation::ADD: + battleState->addObstacle(change); + break; + case BattleChanges::EOperation::UPDATE: + battleState->updateObstacle(change); + break; + default: + logNetwork->error("Unknown obstacle operation %d", static_cast(change.operation)); + break; + } + } +} + +CatapultAttack::CatapultAttack() = default; + +CatapultAttack::~CatapultAttack() = default; + +void CatapultAttack::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void CatapultAttack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCatapultAttack(*this); +} + +void CatapultAttack::applyBattle(IBattleState * battleState) +{ + const auto * town = battleState->getDefendedTown(); + if(!town) + return; + + if(town->fortLevel() == CGTownInstance::NONE) + return; + + for(const auto & part : attackedParts) + { + auto newWallState = SiegeInfo::applyDamage(battleState->getWallState(part.attackedPart), part.damageDealt); + battleState->setWallState(part.attackedPart, newWallState); + } +} + +void BattleSetStackProperty::applyGs(CGameState * gs) const +{ + CStack * stack = gs->getBattle(battleID)->getStack(stackID); + switch(which) + { + case CASTS: + { + if(absolute) + logNetwork->error("Can not change casts in absolute mode"); + else + stack->casts.use(-val); + break; + } + case ENCHANTER_COUNTER: + { + auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter; + if(absolute) + counter = val; + else + counter += val; + vstd::amax(counter, 0); + break; + } + case UNBIND: + { + stack->removeBonusesRecursive(Selector::type()(BonusType::BIND_EFFECT)); + break; + } + case CLONED: + { + stack->cloned = true; + break; + } + case HAS_CLONE: + { + stack->cloneID = val; + break; + } + } +} + +void PlayerCheated::applyGs(CGameState * gs) const +{ + if(!player.isValidPlayer()) + return; + + gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; + gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; + gs->getPlayerState(player)->cheated = true; +} + +void PlayerStartsTurn::applyGs(CGameState * gs) const +{ + //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 +{ + assert(gs->actingPlayers.count(player) == 1); + gs->actingPlayers.erase(player); +} + +void DaysWithoutTown::applyGs(CGameState * gs) const +{ + auto & playerState = gs->players[player]; + playerState.daysWithoutCastle = daysWithoutCastle; +} + +void TurnTimeUpdate::applyGs(CGameState *gs) const +{ + auto & playerState = gs->players[player]; + playerState.turnTimer = turnTimer; +} + +void EntitiesChanged::applyGs(CGameState * gs) +{ + for(const auto & change : changes) + gs->updateEntity(change.metatype, change.entityIndex, change.data); +} + +const CArtifactInstance * ArtSlotInfo::getArt() const +{ + if(locked) + { + logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); + return nullptr; + } + return artifact; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h new file mode 100644 index 000000000..67029d375 --- /dev/null +++ b/lib/networkPacks/ObjProperty.h @@ -0,0 +1,72 @@ +/* + * ObjProperty.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/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +enum class ObjProperty : int8_t +{ + INVALID, + OWNER, + BLOCKVIS, + PRIMARY_STACK_COUNT, + VISITORS, + VISITED, + ID, + AVAILABLE_CREATURE, + MONSTER_COUNT, + MONSTER_POWER, + MONSTER_EXP, + MONSTER_RESTORE_TYPE, + MONSTER_REFUSED_JOIN, + + //town-specific + STRUCTURE_ADD_VISITING_HERO, + STRUCTURE_CLEAR_VISITORS, + STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state + BONUS_VALUE_FIRST, + BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) + + SEERHUT_VISITED, + SEERHUT_COMPLETE, + OBELISK_VISITED, + + //creature-bank specific + BANK_DAYCOUNTER, + BANK_RESET, + BANK_CLEAR, + + //object with reward + REWARD_RANDOMIZE, + REWARD_SELECT, + REWARD_CLEARED +}; + +class NumericID : public StaticIdentifier +{ +public: + using StaticIdentifier::StaticIdentifier; + + static si32 decode(const std::string & identifier) + { + return std::stoi(identifier); + } + static std::string encode(const si32 index) + { + return std::to_string(index); + } +}; + +using ObjPropertyID = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h new file mode 100644 index 000000000..9462179f6 --- /dev/null +++ b/lib/networkPacks/PacksForClient.h @@ -0,0 +1,1511 @@ +/* + * PacksForClient.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 "ArtifactLocation.h" +#include "Component.h" +#include "EInfoWindowMode.h" +#include "EOpenWindowMode.h" +#include "EntityChanges.h" +#include "NetPacksBase.h" +#include "ObjProperty.h" + +#include "../CCreatureSet.h" +#include "../MetaString.h" +#include "../ResourceSet.h" +#include "../TurnTimerInfo.h" +#include "../gameState/EVictoryLossCheckResult.h" +#include "../gameState/QuestInfo.h" +#include "../gameState/TavernSlot.h" +#include "../int3.h" +#include "../mapping/CMapDefines.h" +#include "../spells/ViewSpellInt.h" + +class CClient; +class CGameHandler; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGameState; +class CArtifact; +class CGObjectInstance; +class CArtifactInstance; +struct StackLocation; +struct ArtSlotInfo; +struct QuestInfo; +class IBattleState; +class BattleInfo; + +// This one teleport-specific, but has to be available everywhere in callbacks and netpacks +// For now it's will be there till teleports code refactored and moved into own file +using TTeleportExitsList = std::vector>; + +/***********************************************************************************************************/ +struct DLL_LINKAGE PackageApplied : public CPackForClient +{ + PackageApplied() = default; + explicit PackageApplied(ui8 Result) + : result(Result) + { + } + void visitTyped(ICPackVisitor & visitor) override; + + ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK + ui32 packType = 0; //type id of applied package + ui32 requestID = 0; //an ID given by client to the request that was applied + PlayerColor player; + + template void serialize(Handler & h, const int version) + { + h & result; + h & packType; + h & requestID; + h & player; + } +}; + +struct DLL_LINKAGE SystemMessage : public CPackForClient +{ + explicit SystemMessage(std::string Text) + : text(std::move(Text)) + { + } + SystemMessage() = default; + + void visitTyped(ICPackVisitor & visitor) override; + + std::string text; + + template void serialize(Handler & h, const int version) + { + h & text; + } +}; + +struct DLL_LINKAGE PlayerBlocked : public CPackForClient +{ + enum EReason { UPCOMING_BATTLE, ONGOING_MOVEMENT }; + enum EMode { BLOCKADE_STARTED, BLOCKADE_ENDED }; + + EReason reason = UPCOMING_BATTLE; + EMode startOrEnd = BLOCKADE_STARTED; + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & reason; + h & startOrEnd; + h & player; + } +}; + +struct DLL_LINKAGE PlayerCheated : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + bool losingCheatCode = false; + bool winningCheatCode = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & losingCheatCode; + h & winningCheatCode; + } +}; + +struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + TurnTimerInfo turnTimer; + + template void serialize(Handler & h, const int version) + { + h & player; + h & turnTimer; + } +}; + +struct DLL_LINKAGE PlayerStartsTurn : public Query +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + } +}; + +struct DLL_LINKAGE DaysWithoutTown : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + std::optional daysWithoutCastle; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & daysWithoutCastle; + } +}; + +struct DLL_LINKAGE EntitiesChanged : public CPackForClient +{ + std::vector changes; + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & changes; + } +}; + +struct DLL_LINKAGE SetResources : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + bool abs = true; //false - changes by value; 1 - sets to value + PlayerColor player; + ResourceSet res; //res[resid] => res amount + + template void serialize(Handler & h, const int version) + { + h & abs; + h & player; + h & res; + } +}; + +struct DLL_LINKAGE SetPrimSkill : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + ui8 abs = 0; //0 - changes by value; 1 - sets to value + ObjectInstanceID id; + PrimarySkill which = PrimarySkill::ATTACK; + si64 val = 0; + + template void serialize(Handler & h, const int version) + { + h & abs; + h & id; + h & which; + h & val; + } +}; + +struct DLL_LINKAGE SetSecSkill : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + ui8 abs = 0; //0 - changes by value; 1 - sets to value + ObjectInstanceID id; + SecondarySkill which; + ui16 val = 0; + + template void serialize(Handler & h, const int version) + { + h & abs; + h & id; + h & which; + h & val; + } +}; + +struct DLL_LINKAGE HeroVisitCastle : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + ui8 flags = 0; //1 - start + ObjectInstanceID tid, hid; + + bool start() const //if hero is entering castle (if false - leaving) + { + return flags & 1; + } + + template void serialize(Handler & h, const int version) + { + h & flags; + h & tid; + h & hid; + } +}; + +struct DLL_LINKAGE ChangeSpells : public CPackForClient +{ + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + ui8 learn = 1; //1 - gives spell, 0 - takes + ObjectInstanceID hid; + std::set spells; + + template void serialize(Handler & h, const int version) + { + h & learn; + h & hid; + h & spells; + } +}; + +struct DLL_LINKAGE SetMana : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + ObjectInstanceID hid; + si32 val = 0; + bool absolute = true; + + template void serialize(Handler & h, const int version) + { + h & val; + h & hid; + h & absolute; + } +}; + +struct DLL_LINKAGE SetMovePoints : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID hid; + si32 val = 0; + bool absolute = true; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & val; + h & hid; + h & absolute; + } +}; + +struct DLL_LINKAGE FoWChange : public CPackForClient +{ + void applyGs(CGameState * gs); + + std::unordered_set tiles; + PlayerColor player; + ETileVisibility mode; + bool waitForDialogs = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tiles; + h & player; + h & mode; + h & waitForDialogs; + } +}; + +struct DLL_LINKAGE SetAvailableHero : public CPackForClient +{ + SetAvailableHero() + { + army.clearSlots(); + } + void applyGs(CGameState * gs); + + TavernHeroSlot slotID; + TavernSlotRole roleID; + PlayerColor player; + HeroTypeID hid; //HeroTypeID::NONE if no hero + CSimpleArmy army; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & slotID; + h & roleID; + h & player; + h & hid; + h & army; + } +}; + +struct DLL_LINKAGE GiveBonus : public CPackForClient +{ + enum class ETarget : int8_t { OBJECT, PLAYER, BATTLE }; + + explicit GiveBonus(ETarget Who = ETarget::OBJECT) + :who(Who) + { + } + + void applyGs(CGameState * gs); + + ETarget who = ETarget::OBJECT; + VariantIdentifier id; + Bonus bonus; + MetaString bdescr; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & bonus; + h & id; + h & bdescr; + h & who; + assert(id.getNum() != -1); + } +}; + +struct DLL_LINKAGE ChangeObjPos : public CPackForClient +{ + void applyGs(CGameState * gs); + + /// Object to move + ObjectInstanceID objid; + /// New position of visitable tile of an object + int3 nPos; + /// Player that initiated this action, if any + PlayerColor initiator; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & objid; + h & nPos; + h & initiator; + } +}; + +struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + } +}; + +struct DLL_LINKAGE PlayerEndsGame : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + EVictoryLossCheckResult victoryLossCheckResult; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & victoryLossCheckResult; + } +}; + +struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient +{ + void applyGs(CGameState * gs); + + std::vector players; + ui8 playerConnectionId; //PLAYER_AI for AI player + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & players; + h & playerConnectionId; + } +}; + +struct DLL_LINKAGE RemoveBonus : public CPackForClient +{ + explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::OBJECT) + :who(Who) + { + } + + void applyGs(CGameState * gs); + + GiveBonus::ETarget who; //who receives bonus + VariantIdentifier whoID; + + //vars to identify bonus: its source + BonusSource source; + BonusSourceID id; //source id + + //used locally: copy of removed bonus + Bonus bonus; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & source; + h & id; + h & who; + h & whoID; + } +}; + +struct DLL_LINKAGE SetCommanderProperty : public CPackForClient +{ + enum ECommanderProperty { ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL }; + + void applyGs(CGameState * gs); + + ObjectInstanceID heroid; + + ECommanderProperty which = ALIVE; + TExpType amount = 0; //0 for dead, >0 for alive + si32 additionalInfo = 0; //for secondary skills choice + Bonus accumulatedBonus; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & heroid; + h & which; + h & amount; + h & additionalInfo; + h & accumulatedBonus; + } +}; + +struct DLL_LINKAGE AddQuest : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + QuestInfo quest; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & quest; + } +}; + +struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient +{ + std::map allocatedArtifacts; + + void applyGs(CGameState * gs) const; + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & allocatedArtifacts; + } +}; + +struct DLL_LINKAGE UpdateMapEvents : public CPackForClient +{ + std::list events; + + void applyGs(CGameState * gs) const; + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + 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, const int version) + { + h & town; + h & events; + } +}; + +struct DLL_LINKAGE ChangeFormation : public CPackForClient +{ + ObjectInstanceID hid; + EArmyFormation formation{}; + + void applyGs(CGameState * gs) const; + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & hid; + h & formation; + } +}; + +struct DLL_LINKAGE RemoveObject : public CPackForClient +{ + RemoveObject() = default; + RemoveObject(const ObjectInstanceID & objectID, const PlayerColor & initiator) + : objectID(objectID) + , initiator(initiator) + { + } + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + /// ID of removed object + ObjectInstanceID objectID; + + /// Player that initiated this action, if any + PlayerColor initiator; + + template void serialize(Handler & h, const int version) + { + h & objectID; + h & initiator; + } +}; + +struct DLL_LINKAGE TryMoveHero : public CPackForClient +{ + void applyGs(CGameState * gs); + + enum EResult + { + FAILED, + SUCCESS, + TELEPORTATION, + BLOCKING_VISIT, + EMBARK, + DISEMBARK + }; + + ObjectInstanceID id; + ui32 movePoints = 0; + EResult result = FAILED; //uses EResult + int3 start, end; //h3m format + std::unordered_set fowRevealed; //revealed tiles + std::optional attackedFrom; // Set when stepping into endangered tile. + + void visitTyped(ICPackVisitor & visitor) override; + + bool stopMovement() const + { + return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & result; + h & start; + h & end; + h & movePoints; + h & fowRevealed; + h & attackedFrom; + } +}; + +struct DLL_LINKAGE NewStructures : public CPackForClient +{ + void applyGs(CGameState * gs); + + ObjectInstanceID tid; + std::set bid; + si16 builded = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & bid; + h & builded; + } +}; + +struct DLL_LINKAGE RazeStructures : public CPackForClient +{ + void applyGs(CGameState * gs); + + ObjectInstanceID tid; + std::set bid; + si16 destroyed = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & bid; + h & destroyed; + } +}; + +struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID tid; + std::vector > > creatures; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & creatures; + } +}; + +struct DLL_LINKAGE SetHeroesInTown : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & visiting; + h & garrison; + } +}; + +struct DLL_LINKAGE HeroRecruited : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + HeroTypeID hid; //subID of hero + ObjectInstanceID tid; + ObjectInstanceID boatId; + int3 tile; + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & hid; + h & tid; + h & boatId; + h & tile; + h & player; + } +}; + +struct DLL_LINKAGE GiveHero : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID id; //object id + ObjectInstanceID boatId; + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & boatId; + h & player; + } +}; + +struct DLL_LINKAGE OpenWindow : public Query +{ + EOpenWindowMode window; + ObjectInstanceID object; + ObjectInstanceID visitor; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & window; + h & object; + h & visitor; + } +}; + +struct DLL_LINKAGE NewObject : public CPackForClient +{ + void applyGs(CGameState * gs); + + /// Object ID to create + MapObjectID ID; + /// Object secondary ID to create + MapObjectSubID subID; + /// Position of visitable tile of created object + int3 targetPos; + /// 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, const int version) + { + h & ID; + subID.serializeIdentifier(h, ID, version); + h & targetPos; + h & initiator; + } +}; + +struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + //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; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & arts; + } +}; + +struct DLL_LINKAGE CGarrisonOperationPack : CPackForClient +{ +}; + +struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + TQuantity count; + bool absoluteValue; //if not -> count will be added (or subtracted if negative) + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & count; + h & absoluteValue; + } +}; + +struct DLL_LINKAGE SetStackType : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + CreatureID type; + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & type; + } +}; + +struct DLL_LINKAGE EraseStack : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + } +}; + +struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack +{ + ObjectInstanceID srcArmy; + ObjectInstanceID dstArmy; + SlotID srcSlot; + SlotID dstSlot; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & srcArmy; + h & dstArmy; + h & srcSlot; + h & dstSlot; + } +}; + +struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + CreatureID type; + TQuantity count = 0; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & type; + h & count; + } +}; + +///moves creatures from src stack to dst slot, may be used for merging/splittint/moving stacks +struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack +{ + ObjectInstanceID srcArmy; + ObjectInstanceID dstArmy; + SlotID srcSlot; + SlotID dstSlot; + + TQuantity count; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & srcArmy; + h & dstArmy; + h & srcSlot; + h & dstSlot; + h & count; + } +}; + +struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & moves; + } +}; + +struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + std::vector changes; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & moves; + h & changes; + } +}; + +struct DLL_LINKAGE CArtifactOperationPack : CPackForClient +{ +}; + +struct DLL_LINKAGE PutArtifact : CArtifactOperationPack +{ + PutArtifact() = default; + explicit PutArtifact(ArtifactLocation & dst, bool askAssemble = true) + : al(dst), askAssemble(askAssemble) + { + } + + ArtifactLocation al; + bool askAssemble; + ConstTransitivePtr art; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + h & askAssemble; + h & art; + } +}; + +struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack +{ + ConstTransitivePtr art; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & art; + } +}; + +struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack +{ + ArtifactLocation al; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + } +}; + +struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack +{ + MoveArtifact() = default; + MoveArtifact(ArtifactLocation * src, ArtifactLocation * dst, bool askAssemble = true) + : src(*src), dst(*dst), askAssemble(askAssemble) + { + } + ArtifactLocation src, dst; + bool askAssemble = true; + + void applyGs(CGameState * gs); + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & src; + h & dst; + h & askAssemble; + } +}; + +struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack +{ + struct LinkedSlots + { + ArtifactPosition srcPos; + ArtifactPosition dstPos; + + LinkedSlots() = default; + LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos) + : srcPos(srcPos) + , dstPos(dstPos) + { + } + template void serialize(Handler & h, const int version) + { + h & srcPos; + h & dstPos; + } + }; + + ObjectInstanceID srcArtHolder; + ObjectInstanceID dstArtHolder; + std::optional srcCreature; + std::optional dstCreature; + + BulkMoveArtifacts() + : srcArtHolder(ObjectInstanceID::NONE) + , dstArtHolder(ObjectInstanceID::NONE) + , swap(false) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) + { + } + BulkMoveArtifacts(const ObjectInstanceID srcArtHolder, const ObjectInstanceID dstArtHolder, bool swap) + : srcArtHolder(std::move(srcArtHolder)) + , dstArtHolder(std::move(dstArtHolder)) + , swap(swap) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) + { + } + + void applyGs(CGameState * gs); + + std::vector artsPack0; + std::vector artsPack1; + bool swap; + bool askAssemble; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & artsPack0; + h & artsPack1; + h & srcArtHolder; + h & dstArtHolder; + h & srcCreature; + h & dstCreature; + h & swap; + h & askAssemble; + } +}; + +struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack +{ + ArtifactLocation al; //where assembly will be put + CArtifact * builtArt; + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + h & builtArt; + } +}; + +struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack +{ + ArtifactLocation al; + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + } +}; + +struct DLL_LINKAGE HeroVisit : public CPackForClient +{ + PlayerColor player; + ObjectInstanceID heroId; + ObjectInstanceID objId; + + bool starting; //false -> ending + + void applyGs(CGameState * gs); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & heroId; + h & objId; + h & starting; + } +}; + +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; + ui32 move, mana; //id is a general serial id + template void serialize(Handler & h, const int version) + { + 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, const int version) + { + 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; + MetaString text; + std::vector components; + PlayerColor player; + ui16 soundID = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & type; + h & text; + h & components; + h & player; + h & soundID; + } + InfoWindow() = default; +}; + +struct DLL_LINKAGE SetObjectProperty : public CPackForClient +{ + void applyGs(CGameState * gs) const; + ObjectInstanceID id; + ObjProperty what{}; + + ObjPropertyID identifier; + + SetObjectProperty() = default; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & what; + h & identifier; + } +}; + +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) + }; + 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 visitTyped(ICPackVisitor & visitor) override; + + ChangeObjectVisitors() = default; + + ChangeObjectVisitors(VisitMode mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) + : mode(mode) + , object(object) + , hero(heroID) + { + } + + template void serialize(Handler & h, const int version) + { + h & object; + h & hero; + h & mode; + } +}; + +struct DLL_LINKAGE HeroLevelUp : public Query +{ + PlayerColor player; + ObjectInstanceID heroId; + + PrimarySkill primskill = PrimarySkill::ATTACK; + std::vector skills; + + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & heroId; + h & primskill; + h & skills; + } +}; + +struct DLL_LINKAGE CommanderLevelUp : public Query +{ + PlayerColor player; + ObjectInstanceID heroId; + + std::vector skills; //0-5 - secondary skills, val-100 - special skill + + void applyGs(CGameState * gs) const; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & heroId; + h & skills; + } +}; + +//A dialog that requires making decision by player - it may contain components to choose between or has yes/no options +//Client responds with QueryReply, where answer: 0 - cancel pressed, choice doesn't matter; 1/2/... - first/second/... component selected and OK pressed +//Until sending reply player won't be allowed to take any actions +struct DLL_LINKAGE BlockingDialog : public Query +{ + enum { ALLOW_CANCEL = 1, SELECTION = 2 }; + MetaString text; + std::vector components; + PlayerColor player; + ui8 flags = 0; + ui16 soundID = 0; + + bool cancel() const + { + return flags & ALLOW_CANCEL; + } + bool selection() const + { + return flags & SELECTION; + } + + BlockingDialog(bool yesno, bool Selection) + { + if(yesno) flags |= ALLOW_CANCEL; + if(Selection) flags |= SELECTION; + } + BlockingDialog() = default; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & text; + h & components; + h & player; + h & flags; + h & soundID; + } +}; + +struct DLL_LINKAGE GarrisonDialog : public Query +{ + ObjectInstanceID objid, hid; + bool removableUnits = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & objid; + h & hid; + h & removableUnits; + } +}; + +struct DLL_LINKAGE ExchangeDialog : public Query +{ + PlayerColor player; + + ObjectInstanceID hero1; + ObjectInstanceID hero2; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & hero1; + h & hero2; + } +}; + +struct DLL_LINKAGE TeleportDialog : public Query +{ + TeleportDialog() = default; + + TeleportDialog(const ObjectInstanceID & hero, const TeleportChannelID & Channel) + : hero(hero) + , channel(Channel) + { + } + ObjectInstanceID hero; + TeleportChannelID channel; + TTeleportExitsList exits; + bool impassable = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & hero; + h & channel; + h & exits; + h & impassable; + } +}; + +struct DLL_LINKAGE MapObjectSelectDialog : public Query +{ + PlayerColor player; + Component icon; + MetaString title; + MetaString description; + std::vector objects; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & icon; + h & title; + h & description; + h & objects; + } +}; + +struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient +{ + ObjectInstanceID casterID; + SpellID spellID; + template void serialize(Handler & h, const int version) + { + h & casterID; + h & spellID; + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient +{ + PlayerColor player; + bool showTerrain; // TODO: send terrain state + + std::vector objectPositions; + + template void serialize(Handler & h, const int version) + { + h & player; + h & showTerrain; + h & objectPositions; + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE PlayerMessageClient : public CPackForClient +{ + PlayerMessageClient() = default; + PlayerMessageClient(const PlayerColor & Player, std::string Text) + : player(Player) + , text(std::move(Text)) + { + } + void visitTyped(ICPackVisitor & visitor) override; + + PlayerColor player; + std::string text; + + template void serialize(Handler & h, const int version) + { + h & player; + h & text; + } +}; + +struct DLL_LINKAGE CenterView : public CPackForClient +{ + PlayerColor player; + int3 pos; + ui32 focusTime = 0; //ms + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & pos; + h & player; + h & focusTime; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h new file mode 100644 index 000000000..ad0878fe9 --- /dev/null +++ b/lib/networkPacks/PacksForClientBattle.h @@ -0,0 +1,557 @@ +/* + * PacksForClientBattle.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 "BattleChanges.h" +#include "../MetaString.h" +#include "../battle/BattleAction.h" + +class CClient; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CArmedInstance; +class IBattleState; +class BattleInfo; + +struct DLL_LINKAGE BattleStart : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + BattleInfo * info = nullptr; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & info; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleNextRound : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + ui8 askPlayerInterface = true; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & askPlayerInterface; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleCancelled: public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultAccepted : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + struct HeroBattleResults + { + HeroBattleResults() + : hero(nullptr), army(nullptr), exp(0) {} + + CGHeroInstance * hero; + CArmedInstance * army; + TExpType exp; + + template void serialize(Handler & h, const int version) + { + h & hero; + h & army; + h & exp; + } + }; + + BattleID battleID = BattleID::NONE; + std::array heroResult; + ui8 winnerSide; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & heroResult; + h & winnerSide; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResult : public Query +{ + void applyFirstCl(CClient * cl); + + 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 + std::set artifacts; //artifacts taken from loser to winner - currently unused + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & queryID; + h & result; + h & winner; + h & casualties[0]; + h & casualties[1]; + h & exp; + h & artifacts; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleLogMessage : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + std::vector lines; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & lines; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleStackMoved : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + std::vector tilesToMove; + int distance = 0; + bool teleporting = false; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & tilesToMove; + h & distance; + h & teleporting; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changedStacks; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changedStacks; + assert(battleID != BattleID::NONE); + } +}; + +struct BattleStackAttacked +{ + DLL_LINKAGE void applyGs(CGameState * gs); + DLL_LINKAGE void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + ui32 stackAttacked = 0, attackerID = 0; + ui32 killedAmount = 0; + int64_t damageAmount = 0; + UnitChanges newState; + enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; + ui32 flags = 0; //uses EFlags (above) + SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set + + bool killed() const//if target stack was killed + { + return flags & KILLED || flags & CLONE_KILLED; + } + bool cloneKilled() const + { + return flags & CLONE_KILLED; + } + bool isSecondary() const//if stack was not a primary target (receives no spell effects) + { + return flags & SECONDARY; + } + ///Attacked with spell (SPELL_LIKE_ATTACK) + bool isSpell() const + { + return flags & SPELL_EFFECT; + } + bool willRebirth() const//resurrection, e.g. Phoenix + { + return flags & REBIRTH; + } + bool fireShield() const + { + return flags & FIRE_SHIELD; + } + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackAttacked; + h & attackerID; + h & newState; + h & flags; + h & killedAmount; + h & damageAmount; + h & spellID; + assert(battleID != BattleID::NONE); + } + bool operator<(const BattleStackAttacked & b) const + { + return stackAttacked < b.stackAttacked; + } +}; + +struct DLL_LINKAGE BattleAttack : public CPackForClient +{ + void applyGs(CGameState * gs); + BattleUnitsChanged attackerChanges; + + BattleID battleID = BattleID::NONE; + std::vector bsa; + ui32 stackAttacking = 0; + ui32 flags = 0; //uses Eflags (below) + enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; + + BattleHex tile; + SpellID spellID = SpellID::NONE; //for SPELL_LIKE + + bool shot() const//distance attack - decrease number of shots + { + return flags & SHOT; + } + bool counter() const//is it counterattack? + { + return flags & COUNTER; + } + bool lucky() const + { + return flags & LUCKY; + } + bool unlucky() const + { + return flags & UNLUCKY; + } + bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg + { + return flags & BALLISTA_DOUBLE_DMG; + } + bool deathBlow() const + { + return flags & DEATH_BLOW; + } + bool spellLike() const + { + return flags & SPELL_LIKE; + } + bool lifeDrain() const + { + return flags & LIFE_DRAIN; + } + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & bsa; + h & stackAttacking; + h & flags; + h & tile; + h & spellID; + h & attackerChanges; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StartAction : public CPackForClient +{ + StartAction() = default; + explicit StartAction(BattleAction act) + : ba(std::move(act)) + { + } + void applyFirstCl(CClient * cl); + void applyGs(CGameState * gs); + + BattleID battleID = BattleID::NONE; + BattleAction ba; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & ba; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE EndAction : public CPackForClient +{ + void visitTyped(ICPackVisitor & visitor) override; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + } +}; + +struct DLL_LINKAGE BattleSpellCast : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + bool activeCast = true; + ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender + SpellID spellID; //id of spell + ui8 manaGained = 0; //mana channeling ability + BattleHex tile; //destination tile (may not be set in some global/mass spells + std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) + std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) + std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) + si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID + bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & side; + h & spellID; + h & manaGained; + h & tile; + h & affectedCres; + h & resistedCres; + h & reflectedCres; + h & casterStack; + h & castByHero; + h & activeCast; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StacksInjured : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector stacks; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stacks; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultsApplied : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + PlayerColor player1, player2; + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & player1; + h & player2; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changes; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changes; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE CatapultAttack : public CPackForClient +{ + struct AttackInfo + { + si16 destinationTile; + EWallPart attackedPart; + ui8 damageDealt; + + template void serialize(Handler & h, const int version) + { + h & destinationTile; + h & attackedPart; + h & damageDealt; + } + }; + + CatapultAttack(); + ~CatapultAttack() override; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector< AttackInfo > attackedParts; + int attacker = -1; //if -1, then a spell caused this + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & attackedParts; + h & attacker; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient +{ + enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; + + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + int stackID = 0; + BattleStackProperty which = CASTS; + int val = 0; + int absolute = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & which; + h & val; + h & absolute; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +///activated at the beginning of turn +struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient +{ + void applyGs(CGameState * gs) const; //effect + + BattleID battleID = BattleID::NONE; + int stackID = 0; + int effect = 0; //use corresponding Bonus type + int val = 0; + int additionalInfo = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & effect; + h & val; + h & additionalInfo; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + EGateState state = EGateState::NONE; + template void serialize(Handler & h, const int version) + { + h & battleID; + h & state; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacksLobby.h b/lib/networkPacks/PacksForLobby.h similarity index 73% rename from lib/NetPacksLobby.h rename to lib/networkPacks/PacksForLobby.h index 47cad0e7f..ef55fe054 100644 --- a/lib/NetPacksLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -1,5 +1,5 @@ /* - * NetPacksLobby.h, part of VCMI engine + * PacksForLobby.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,9 +9,8 @@ */ #pragma once -#include "NetPacksBase.h" - #include "StartInfo.h" +#include "NetPacksBase.h" class CServerHandler; class CVCMIServer; @@ -43,7 +42,7 @@ struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate int clientId = -1; int hostClientId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -62,7 +61,7 @@ struct DLL_LINKAGE LobbyClientDisconnected : public CLobbyPackToPropagate bool shutdownServer = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -75,7 +74,7 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate { std::string playerName, message; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -91,7 +90,7 @@ struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate } action = NONE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -99,11 +98,23 @@ struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate } }; +struct DLL_LINKAGE LobbyLoadProgress : public CLobbyPackToPropagate +{ + unsigned char progress; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & progress; + } +}; + struct DLL_LINKAGE LobbyEndGame : public CLobbyPackToPropagate { bool closeConnection = false, restart = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -119,7 +130,7 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate CGameState * initializedGameState = nullptr; int clientId = -1; //-1 means to all clients - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -136,7 +147,7 @@ struct DLL_LINKAGE LobbyChangeHost : public CLobbyPackToPropagate { int newHostConnectionId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -149,7 +160,7 @@ struct DLL_LINKAGE LobbyUpdateState : public CLobbyPackToPropagate LobbyState state; bool hostChanged = false; // Used on client-side only - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -164,7 +175,7 @@ struct DLL_LINKAGE LobbySetMap : public CLobbyPackToServer LobbySetMap() : mapInfo(nullptr), mapGenOpts(nullptr) {} - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -177,7 +188,7 @@ struct DLL_LINKAGE LobbySetCampaign : public CLobbyPackToServer { std::shared_ptr ourCampaign; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -189,7 +200,7 @@ struct DLL_LINKAGE LobbySetCampaignMap : public CLobbyPackToServer { CampaignScenarioID mapId = CampaignScenarioID::NONE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -201,7 +212,7 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer { int bonusId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -211,17 +222,17 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer { - enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS}; + enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS, TOWN_ID, HERO_ID, BONUS_ID}; ui8 what = UNKNOWN; - si8 direction = 0; //-1 or +1 + int32_t value = 0; PlayerColor color = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { h & what; - h & direction; + h & value; h & color; } }; @@ -230,7 +241,7 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer { PlayerColor clickedColor = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -238,15 +249,41 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer } }; -struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer +struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer { - ui8 turnTime = 0; + PlayerColor color = PlayerColor::CANNOT_DETERMINE; + std::string name = ""; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { - h & turnTime; + h & color; + h & name; + } +}; + +struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer +{ + SimturnsInfo simturnsInfo; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & simturnsInfo; + } +}; + +struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer +{ + TurnTimerInfo turnTimerInfo; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & turnTimerInfo; } }; @@ -254,7 +291,7 @@ struct DLL_LINKAGE LobbySetDifficulty : public CLobbyPackToServer { ui8 difficulty = 0; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -267,7 +304,7 @@ struct DLL_LINKAGE LobbyForceSetPlayer : public CLobbyPackToServer ui8 targetConnectedPlayer = -1; PlayerColor targetPlayerColor = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -280,7 +317,7 @@ struct DLL_LINKAGE LobbyShowMessage : public CLobbyPackToPropagate { std::string message; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h new file mode 100644 index 000000000..de1d7d613 --- /dev/null +++ b/lib/networkPacks/PacksForServer.h @@ -0,0 +1,671 @@ +/* + * PacksForServer.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 "ArtifactLocation.h" +#include "NetPacksBase.h" +#include "TradeItem.h" + +#include "../int3.h" +#include "../battle/BattleAction.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE GamePause : public CPackForServer +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE EndTurn : public CPackForServer +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE DismissHero : public CPackForServer +{ + DismissHero() = default; + DismissHero(const ObjectInstanceID & HID) + : hid(HID) + { + } + ObjectInstanceID hid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + } +}; + +struct DLL_LINKAGE MoveHero : public CPackForServer +{ + MoveHero() = default; + MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) + : dest(Dest) + , hid(HID) + , transit(Transit) + { + } + int3 dest; + ObjectInstanceID hid; + bool transit = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + h & transit; + } +}; + +struct DLL_LINKAGE CastleTeleportHero : public CPackForServer +{ + CastleTeleportHero() = default; + CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) + : dest(Dest) + , hid(HID) + , source(Source) + { + } + ObjectInstanceID dest; + ObjectInstanceID hid; + si8 source = 0; //who give teleporting, 1=castle gate + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + } +}; + +struct DLL_LINKAGE ArrangeStacks : public CPackForServer +{ + ArrangeStacks() = default; + ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) + : what(W) + , p1(P1) + , p2(P2) + , id1(ID1) + , id2(ID2) + , val(VAL) + { + } + + ui8 what = 0; //1 - swap; 2 - merge; 3 - split + SlotID p1, p2; //positions of first and second stack + ObjectInstanceID id1, id2; //ids of objects with garrison + si32 val = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & what; + h & p1; + h & p2; + h & id1; + h & id2; + h & val; + } +}; + +struct DLL_LINKAGE BulkMoveArmy : public CPackForServer +{ + SlotID srcSlot; + ObjectInstanceID srcArmy; + ObjectInstanceID destArmy; + + BulkMoveArmy() = default; + + BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) + : srcArmy(srcArmy) + , destArmy(destArmy) + , srcSlot(srcSlot) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcSlot; + h & srcArmy; + h & destArmy; + } +}; + +struct DLL_LINKAGE BulkSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + si32 amount = 0; + + BulkSplitStack() = default; + + BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) + : src(src) + , srcOwner(srcOwner) + , amount(howMany) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + h & amount; + } +}; + +struct DLL_LINKAGE BulkMergeStacks : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkMergeStacks() = default; + + BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkSmartSplitStack() = default; + + BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE DisbandCreature : public CPackForServer +{ + DisbandCreature() = default; + DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) + : pos(Pos) + , id(ID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + } +}; + +struct DLL_LINKAGE BuildStructure : public CPackForServer +{ + BuildStructure() = default; + BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) + : tid(TID) + , bid(BID) + { + } + ObjectInstanceID tid; //town id + BuildingID bid; //structure id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & bid; + } +}; + +struct DLL_LINKAGE RazeStructure : public BuildStructure +{ + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE RecruitCreatures : public CPackForServer +{ + RecruitCreatures() = default; + RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) + : tid(TID) + , dst(DST) + , crid(CRID) + , amount(Amount) + , level(Level) + { + } + ObjectInstanceID tid; //dwelling id, or town + ObjectInstanceID dst; //destination ID, e.g. hero + CreatureID crid; + ui32 amount = 0; //creature amount + si32 level = 0; //dwelling level to buy from, -1 if any + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & dst; + h & crid; + h & amount; + h & level; + } +}; + +struct DLL_LINKAGE UpgradeCreature : public CPackForServer +{ + UpgradeCreature() = default; + UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) + : pos(Pos) + , id(ID) + , cid(CRID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + CreatureID cid; //id of type to which we want make upgrade + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + h & cid; + } +}; + +struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer +{ + GarrisonHeroSwap() = default; + GarrisonHeroSwap(const ObjectInstanceID & TID) + : tid(TID) + { + } + ObjectInstanceID tid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + } +}; + +struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer +{ + ArtifactLocation src, dst; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & dst; + } +}; + +struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer +{ + ObjectInstanceID srcHero; + ObjectInstanceID dstHero; + bool swap = false; + bool equipped = true; + bool backpack = true; + + BulkExchangeArtifacts() = default; + BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) + : srcHero(srcHero) + , dstHero(dstHero) + , swap(swap) + , equipped(equipped) + , backpack(backpack) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcHero; + h & dstHero; + h & swap; + h & equipped; + h & backpack; + } +}; + +struct DLL_LINKAGE AssembleArtifacts : public CPackForServer +{ + AssembleArtifacts() = default; + AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) + : heroID(_heroID) + , artifactSlot(_artifactSlot) + , assemble(_assemble) + , assembleTo(_assembleTo) + { + } + ObjectInstanceID heroID; + ArtifactPosition artifactSlot; + bool assemble = false; // True to assemble artifact, false to disassemble. + ArtifactID assembleTo; // Artifact to assemble into. + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & heroID; + h & artifactSlot; + h & assemble; + h & assembleTo; + } +}; + +struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer +{ + EraseArtifactByClient() = default; + EraseArtifactByClient(const ArtifactLocation & al) + : al(al) + { + } + ArtifactLocation al; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & al; + } +}; + +struct DLL_LINKAGE BuyArtifact : public CPackForServer +{ + BuyArtifact() = default; + BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) + : hid(HID) + , aid(AID) + { + } + ObjectInstanceID hid; + ArtifactID aid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & aid; + } +}; + +struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer +{ + ObjectInstanceID marketId; + ObjectInstanceID heroId; + + EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; + std::vector r1; + std::vector r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] + std::vector val; //units of sold resource + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & marketId; + h & heroId; + h & mode; + h & r1; + h & r2; + h & val; + } +}; + +struct DLL_LINKAGE SetFormation : public CPackForServer +{ + SetFormation() = default; + ; + SetFormation(const ObjectInstanceID & HID, EArmyFormation Formation) + : hid(HID) + , formation(Formation) + { + } + ObjectInstanceID hid; + EArmyFormation formation{}; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & formation; + } +}; + +struct DLL_LINKAGE HireHero : public CPackForServer +{ + HireHero() = default; + HireHero(HeroTypeID HID, const ObjectInstanceID & TID) + : hid(HID) + , tid(TID) + { + } + HeroTypeID hid; //available hero serial + ObjectInstanceID tid; //town (tavern) id + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & tid; + h & player; + } +}; + +struct DLL_LINKAGE BuildBoat : public CPackForServer +{ + ObjectInstanceID objid; //where player wants to buy a boat + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & objid; + } +}; + +struct DLL_LINKAGE QueryReply : public CPackForServer +{ + QueryReply() = default; + QueryReply(const QueryID & QID, std::optional Reply) + : qid(QID) + , reply(Reply) + { + } + QueryID qid; + PlayerColor player; + std::optional reply; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & qid; + h & player; + h & reply; + } +}; + +struct DLL_LINKAGE MakeAction : public CPackForServer +{ + MakeAction() = default; + MakeAction(BattleAction BA) + : ba(std::move(BA)) + { + } + BattleAction ba; + BattleID battleID; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & ba; + h & battleID; + } +}; + +struct DLL_LINKAGE DigWithHero : public CPackForServer +{ + ObjectInstanceID id; //digging hero id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & id; + } +}; + +struct DLL_LINKAGE CastAdvSpell : public CPackForServer +{ + ObjectInstanceID hid; //hero id + SpellID sid; //spell id + int3 pos; //selected tile (not always used) + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & sid; + h & pos; + } +}; + +/***********************************************************************************************************/ + +struct DLL_LINKAGE SaveGame : public CPackForServer +{ + SaveGame() = default; + SaveGame(std::string Fname) + : fname(std::move(Fname)) + { + } + std::string fname; + + void applyGs(CGameState * gs) {}; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & fname; + } +}; + +struct DLL_LINKAGE PlayerMessage : public CPackForServer +{ + PlayerMessage() = default; + PlayerMessage(std::string Text, const ObjectInstanceID & obj) + : text(std::move(Text)) + , currObj(obj) + { + } + + void applyGs(CGameState * gs) {}; + + void visitTyped(ICPackVisitor & visitor) override; + + std::string text; + ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & text; + h & currObj; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/SetStackEffect.h b/lib/networkPacks/SetStackEffect.h new file mode 100644 index 000000000..f53b4cfe2 --- /dev/null +++ b/lib/networkPacks/SetStackEffect.h @@ -0,0 +1,42 @@ +/* + * SetStackEffect.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 "../bonuses/Bonus.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IBattleState; + +struct DLL_LINKAGE SetStackEffect : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector>> toAdd; + std::vector>> toUpdate; + std::vector>> toRemove; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & toAdd; + h & toUpdate; + h & toRemove; + assert(battleID != BattleID::NONE); + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/StackLocation.h b/lib/networkPacks/StackLocation.h new file mode 100644 index 000000000..abb922d0f --- /dev/null +++ b/lib/networkPacks/StackLocation.h @@ -0,0 +1,40 @@ +/* + * StackLocation.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 "../ConstTransitivePtr.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArmedInstance; +class CStackInstance; + +struct StackLocation +{ + ConstTransitivePtr army; + SlotID slot; + + StackLocation() = default; + StackLocation(const CArmedInstance * Army, const SlotID & Slot) + : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! + , slot(Slot) + { + } + + DLL_LINKAGE const CStackInstance * getStack(); + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/TradeItem.h b/lib/networkPacks/TradeItem.h new file mode 100644 index 000000000..43846d1d1 --- /dev/null +++ b/lib/networkPacks/TradeItem.h @@ -0,0 +1,20 @@ +/* + * TradeItem.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/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TradeItemSell = VariantIdentifier; +using TradeItemBuy = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/CGPathNode.cpp b/lib/pathfinder/CGPathNode.cpp index cbcd949d5..057161295 100644 --- a/lib/pathfinder/CGPathNode.cpp +++ b/lib/pathfinder/CGPathNode.cpp @@ -24,6 +24,24 @@ static bool canSeeObj(const CGObjectInstance * obj) return obj != nullptr && obj->ID != Obj::EVENT; } +const CGPathNode & CGPath::currNode() const +{ + assert(nodes.size() > 1); + return nodes[nodes.size()-1]; +} + +const CGPathNode & CGPath::nextNode() const +{ + assert(nodes.size() > 1); + return nodes[nodes.size()-2]; +} + +const CGPathNode & CGPath::lastNode() const +{ + assert(nodes.size() > 1); + return nodes[0]; +} + int3 CGPath::startPos() const { return nodes[nodes.size()-1].coord; diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index 77effdfcb..15363f419 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -143,6 +143,18 @@ struct DLL_LINKAGE CGPathNode return turns < 255; } + bool isTeleportAction() const + { + if (action != EPathNodeAction::TELEPORT_NORMAL && + action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && + action != EPathNodeAction::TELEPORT_BATTLE) + { + return false; + } + + return true; + } + using TFibHeap = boost::heap::fibonacci_heap>>; TFibHeap::handle_type pqHandle; @@ -156,6 +168,13 @@ struct DLL_LINKAGE CGPath { std::vector nodes; //just get node by node + /// Starting position of path, matches location of hero + const CGPathNode & currNode() const; + /// First node in path, this is where hero will move next + const CGPathNode & nextNode() const; + /// Last node in path, this is what hero wants to reach in the end + const CGPathNode & lastNode() const; + int3 startPos() const; // start point int3 endPos() const; //destination point }; @@ -178,7 +197,7 @@ struct DLL_LINKAGE CPathsInfo STRONG_INLINE CGPathNode * getNode(const int3 & coord, const ELayer layer) { - return &nodes[layer][coord.z][coord.x][coord.y]; + return &nodes[layer.getNum()][coord.z][coord.x][coord.y]; } }; @@ -190,8 +209,8 @@ struct DLL_LINKAGE PathNodeInfo const TerrainTile * tile; int3 coord; bool guarded; - PlayerRelations::PlayerRelations objectRelations; - PlayerRelations::PlayerRelations heroRelations; + PlayerRelations objectRelations; + PlayerRelations heroRelations; bool isInitialPosition; PathNodeInfo(); diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 78a9b307a..46d4f564b 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -20,14 +20,41 @@ #include "../TerrainHandler.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapping/CMap.h" +#include "spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN +bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const +{ + // we can always make the first step, even when standing on object + if(source.node->theNodeBefore == nullptr) + return true; + + if (!source.nodeObject) + return true; + + if (!source.isNodeObjectVisitable()) + return true; + + // we can always move from visitable object if hero has teleported here (e.g. went through monolith) + if (source.node->isTeleportAction()) + return true; + + // we can not go through teleporters since moving onto a teleport will teleport hero and may invalidate path (e.g. one-way teleport or enemy hero on other side) + if (dynamic_cast(source.nodeObject) != nullptr) + return false; + + return true; +} + std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const { std::vector neighbourTiles; - neighbourTiles.reserve(8); + if (!canMoveFromNode(source)) + return neighbourTiles; + + neighbourTiles.reserve(8); getNeighbours( *source.tile, source.node->coord, @@ -37,7 +64,7 @@ std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & sour if(source.isNodeObjectVisitable()) { - vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool + vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool { return !canMoveBetween(tile, source.nodeObject->visitablePos()); }); @@ -121,6 +148,8 @@ void CPathfinder::calculatePaths() movement = hlp->getMaxMovePoints(source.node->layer); if(!hlp->passOneTurnLimitCheck(source)) continue; + if(turn >= hlp->options.turnLimit) + continue; } source.isInitialPosition = source.nodeHero == hlp->hero; @@ -133,6 +162,9 @@ void CPathfinder::calculatePaths() if(neighbour->locked) continue; + if (source.node->theNodeBefore && source.node->theNodeBefore->coord == neighbour->coord ) + continue; // block U-turns + if(!hlp->isLayerAvailable(neighbour->layer)) continue; @@ -257,15 +289,18 @@ std::vector CPathfinderHelper::getTeleportExits(const PathNodeInfo & sourc teleportationExits.push_back(exit); } } - else if(options.useCastleGate - && (source.nodeObject->ID == Obj::TOWN && source.nodeObject->subID == ETownType::INFERNO - && source.objectRelations != PlayerRelations::ENEMIES)) + else if(options.useCastleGate && source.nodeObject->ID == Obj::TOWN && source.objectRelations != PlayerRelations::ENEMIES) { - /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo - /// This may be handy if we allow to use teleportation to friendly towns - for(const auto & exit : getCastleGates(source)) + auto * town = dynamic_cast(source.nodeObject); + assert(town); + if (town && town->getFaction() == FactionID::INFERNO) { - teleportationExits.push_back(exit); + /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo + /// This may be handy if we allow to use teleportation to friendly towns + for(const auto & exit : getCastleGates(source)) + { + teleportationExits.push_back(exit); + } } } @@ -296,7 +331,7 @@ bool CPathfinder::isLayerTransitionPossible() const if(source.node->action == EPathNodeAction::BATTLE) return false; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case ELayer::LAND: if(destLayer == ELayer::AIR) @@ -365,7 +400,7 @@ void CPathfinderHelper::initializePatrol() if(hero->patrol.patrolRadius) { state = PATROL_RADIUS; - gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, std::optional(), 0, int3::DIST_MANHATTAN); + gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, ETileVisibility::REVEALED, std::optional(), int3::DIST_MANHATTAN); } else state = PATROL_LOCKED; @@ -455,8 +490,13 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const return true; } +int CPathfinderHelper::getGuardiansCount(int3 tile) const +{ + return getGuardingCreatures(tile).size(); +} + CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options): - CGameInfoCallback(gs, std::optional()), + CGameInfoCallback(gs), turn(-1), hero(Hero), options(Options), @@ -465,6 +505,12 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her turnsInfo.reserve(16); updateTurnInfo(); initializePatrol(); + + SpellID flySpell = SpellID::FLY; + canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); + + SpellID waterWalk = SpellID::WATER_WALK; + canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell()); } CPathfinderHelper::~CPathfinderHelper() @@ -488,18 +534,24 @@ void CPathfinderHelper::updateTurnInfo(const int Turn) bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(!options.useFlying) return false; + if(canCastFly && options.canUseCast) + return true; + break; case EPathfindingLayer::WATER: if(!options.useWaterWalking) return false; + if(canCastWaterWalk && options.canUseCast) + return true; + break; } @@ -511,9 +563,9 @@ const TurnInfo * CPathfinderHelper::getTurnInfo() const return turnsInfo[turn]; } -bool CPathfinderHelper::hasBonusOfType(const BonusType type, const int subtype) const +bool CPathfinderHelper::hasBonusOfType(const BonusType type) const { - return turnsInfo[turn]->hasBonusOfType(type, subtype); + return turnsInfo[turn]->hasBonusOfType(type); } int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 4c196ee3b..4203c8ba1 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -19,7 +19,7 @@ class CGWhirlpool; struct TurnInfo; struct PathfinderOptions; -class CPathfinder +class DLL_LINKAGE CPathfinder { public: friend class CPathfinderHelper; @@ -72,16 +72,19 @@ public: const CGHeroInstance * hero; std::vector turnsInfo; const PathfinderOptions & options; + bool canCastFly; + bool canCastWaterWalk; CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options); virtual ~CPathfinderHelper(); void initializePatrol(); bool isHeroPatrolLocked() const; + bool canMoveFromNode(const PathNodeInfo & source) const; bool isPatrolMovementAllowed(const int3 & dst) const; void updateTurnInfo(const int turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; const TurnInfo * getTurnInfo() const; - bool hasBonusOfType(const BonusType type, const int subtype = -1) const; + bool hasBonusOfType(BonusType type) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; std::vector getCastleGates(const PathNodeInfo & source) const; @@ -121,6 +124,8 @@ public: int movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const; bool passOneTurnLimitCheck(const PathNodeInfo & source) const; + + int getGuardiansCount(int3 tile) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/NodeStorage.cpp b/lib/pathfinder/NodeStorage.cpp index 38936dcf5..67c27fe68 100644 --- a/lib/pathfinder/NodeStorage.cpp +++ b/lib/pathfinder/NodeStorage.cpp @@ -27,7 +27,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState int3 pos; const PlayerColor player = out.hero->tempOwner; const int3 sizes = gs->getMapSize(); - const auto fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; //make 200% sure that these are loop invariants (also a bit shorter code), let compiler do the rest(loop unswitching) const bool useFlying = options.useFlying; diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 02b5d8b6a..4c83acafc 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -30,6 +30,8 @@ PathfinderOptions::PathfinderOptions() , lightweightFlyingMode(false) , oneTurnSpecialLayersLimit(true) , originalMovementRules(false) + , turnLimit(std::numeric_limits::max()) + , canUseCast(false) { } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 32859ac82..96d75cb2a 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -68,6 +68,14 @@ struct DLL_LINKAGE PathfinderOptions /// I find it's reasonable limitation, but it's will make some movements more expensive than in H3. bool originalMovementRules; + /// Max number of turns to compute. Default = infinite + uint8_t turnLimit; + + /// + /// For AI. Allows water walk and fly layers if hero can cast appropriate spells + /// + bool canUseCast; + PathfinderOptions(); }; diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index 0121c8448..749cc513e 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -19,11 +19,11 @@ VCMI_LIB_NAMESPACE_BEGIN namespace PathfinderUtil { - using FoW = std::shared_ptr>; + using FoW = std::unique_ptr>; using ELayer = EPathfindingLayer; - template - EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, FoW fow, const PlayerColor player, const CGameState * gs) + 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]) return EPathAccessibility::BLOCKED; diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 44dc9b330..d041aff85 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -123,7 +123,7 @@ void DestinationActionRule::process( EPathNodeAction action = EPathNodeAction::NORMAL; const auto * hero = pathfinderHelper->hero; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(source.node->layer == EPathfindingLayer::SAIL) @@ -290,7 +290,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(destination.node->accessible == EPathAccessibility::BLOCKED) return BlockingReason::DESTINATION_BLOCKED; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) @@ -298,8 +298,8 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(source.guarded) { - if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) && - !destination.isGuardianTile) // Can step into tile of guard + if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) + && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { return BlockingReason::SOURCE_GUARDED; } @@ -359,7 +359,7 @@ void LayerTransitionRule::process( if(source.node->layer == destination.node->layer) return; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(destination.node->layer == EPathfindingLayer::SAIL) diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 6442a54f8..1f32139a6 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -22,8 +22,9 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) { for(const auto & terrain : VLC->terrainTypeHandler->objects) { - noTerrainPenalty.push_back(static_cast( - bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(terrain->getIndex()))))); + auto selector = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(terrain->getId())); + if (bl->getFirst(selector)) + noTerrainPenalty.insert(terrain->getId()); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); @@ -47,7 +48,7 @@ TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn): bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::AIR) @@ -71,7 +72,12 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const return true; } -bool TurnInfo::hasBonusOfType(BonusType type, int subtype) const +bool TurnInfo::hasBonusOfType(BonusType type) const +{ + return hasBonusOfType(type, BonusSubtypeID()); +} + +bool TurnInfo::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const { switch(type) { @@ -82,14 +88,19 @@ bool TurnInfo::hasBonusOfType(BonusType type, int subtype) const case BonusType::WATER_WALKING: return bonusCache->waterWalking; case BonusType::NO_TERRAIN_PENALTY: - return bonusCache->noTerrainPenalty[subtype]; + return bonusCache->noTerrainPenalty.count(subtype.as()); } return static_cast( bonuses->getFirst(Selector::type()(type).And(Selector::subtype()(subtype)))); } -int TurnInfo::valOfBonuses(BonusType type, int subtype) const +int TurnInfo::valOfBonuses(BonusType type) const +{ + return valOfBonuses(type, BonusSubtypeID()); +} + +int TurnInfo::valOfBonuses(BonusType type, BonusSubtypeID subtype) const { switch(type) { diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index aa7d77b40..6fff27e6f 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -21,7 +21,7 @@ struct DLL_LINKAGE TurnInfo /// This is certainly not the best design ever and certainly can be improved /// Unfortunately for pathfinder that do hundreds of thousands calls onus system add too big overhead struct BonusCache { - std::vector noTerrainPenalty; + std::set noTerrainPenalty; bool freeShipBoarding; bool flyingMovement; int flyingMovementVal; @@ -42,8 +42,10 @@ struct DLL_LINKAGE TurnInfo TurnInfo(const CGHeroInstance * Hero, const int Turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; - bool hasBonusOfType(const BonusType type, const int subtype = -1) const; - int valOfBonuses(const BonusType type, const int subtype = -1) const; + bool hasBonusOfType(const BonusType type) const; + bool hasBonusOfType(const BonusType type, const BonusSubtypeID subtype) const; + int valOfBonuses(const BonusType type) const; + int valOfBonuses(const BonusType type, const BonusSubtypeID subtype) const; void updateHeroBonuses(BonusType type, const CSelector& sel) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; }; diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp deleted file mode 100644 index 73c1dc5ff..000000000 --- a/lib/registerTypes/RegisterTypes.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/* - * RegisterTypes.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" -#define INSTANTIATE_REGISTER_TYPES_HERE -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../spells/CSpellHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -// For reference: peak memory usage by gcc during compilation of register type templates -// registerTypesMapObjects: 1.9 Gb -// registerTypes2: 2.2 Gb -// registerTypesClientPacks1 1.6 Gb -// registerTypesClientPacks2 1.6 Gb -// registerTypesServerPacks: 1.3 Gb -// registerTypes4: 1.3 Gb - - -#define DEFINE_EXTERNAL_METHOD(METHODNAME) \ -extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ -extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); \ -extern template DLL_LINKAGE void METHODNAME(CTypeList & s); \ - -//DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) -DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) -DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) - -template void registerTypes(BinaryDeserializer & s); -template void registerTypes(BinarySerializer & s); -template void registerTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 985fb927d..2c2284452 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -1,424 +1,28 @@ -/* - * 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 "../NetPacks.h" -#include "../NetPacksLobby.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CCreatureSet.h" -#include "../CPlayerState.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../CModHandler.h" //needed? -#include "../mapObjectConstructors/CRewardableConstructor.h" -#include "../mapObjectConstructors/CommonConstructors.h" -#include "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjectConstructors/DwellingInstanceConstructor.h" -#include "../mapObjectConstructors/HillFortInstanceConstructor.h" -#include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGTownBuilding.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../battle/CObstacleInstance.h" -#include "../bonuses/CBonusSystemNode.h" -#include "../bonuses/Limiters.h" -#include "../bonuses/Updaters.h" -#include "../bonuses/Propagators.h" -#include "../CStack.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BinarySerializer; -class BinaryDeserializer; -class CTypeList; - -template -void registerTypesMapObjects1(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(); 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(); -} - -template -void registerTypesMapObjectTypes(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -#define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() - - REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CCartographer); - REGISTER_GENERIC_HANDLER(CGArtifact); - REGISTER_GENERIC_HANDLER(CGBlackMarket); - REGISTER_GENERIC_HANDLER(CGBoat); - REGISTER_GENERIC_HANDLER(CGBorderGate); - REGISTER_GENERIC_HANDLER(CGBorderGuard); - REGISTER_GENERIC_HANDLER(CGCreature); - REGISTER_GENERIC_HANDLER(CGDenOfthieves); - REGISTER_GENERIC_HANDLER(CGDwelling); - REGISTER_GENERIC_HANDLER(CGEvent); - REGISTER_GENERIC_HANDLER(CGGarrison); - REGISTER_GENERIC_HANDLER(CGHeroPlaceholder); - REGISTER_GENERIC_HANDLER(CGHeroInstance); - REGISTER_GENERIC_HANDLER(CGKeymasterTent); - REGISTER_GENERIC_HANDLER(CGLighthouse); - REGISTER_GENERIC_HANDLER(CGTerrainPatch); - REGISTER_GENERIC_HANDLER(CGMagi); - REGISTER_GENERIC_HANDLER(CGMarket); - REGISTER_GENERIC_HANDLER(CGMine); - REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGObservatory); - REGISTER_GENERIC_HANDLER(CGPandoraBox); - REGISTER_GENERIC_HANDLER(CGQuestGuard); - REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGScholar); - REGISTER_GENERIC_HANDLER(CGSeerHut); - REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGShrine); - REGISTER_GENERIC_HANDLER(CGSignBottle); - REGISTER_GENERIC_HANDLER(CGSirens); - REGISTER_GENERIC_HANDLER(CGMonolith); - REGISTER_GENERIC_HANDLER(CGSubterraneanGate); - REGISTER_GENERIC_HANDLER(CGWhirlpool); - REGISTER_GENERIC_HANDLER(CGTownInstance); - REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(CGWitchHut); - REGISTER_GENERIC_HANDLER(HillFort); - -#undef REGISTER_GENERIC_HANDLER - - 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 -} - -template -void registerTypesMapObjects2(Serializer &s) -{ - //Other object-related - 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(); - - //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(); -} -template -void registerTypesClientPacks1(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(); -} - -template -void registerTypesClientPacks2(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(); -} - -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(); -} - -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(); - // Only host client send - 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(); -} - -template -void registerTypes(Serializer &s) -{ - registerTypesMapObjects1(s); - registerTypesMapObjects2(s); - registerTypesMapObjectTypes(s); - registerTypesClientPacks1(s); - registerTypesClientPacks2(s); - registerTypesServerPacks(s); - registerTypesLobbyPacks(s); -} - -#ifndef INSTANTIATE_REGISTER_TYPES_HERE - -extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); -extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); -extern template DLL_LINKAGE void registerTypes(CTypeList & s); - -#endif - - -VCMI_LIB_NAMESPACE_END +/* + * 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 new file mode 100644 index 000000000..5ba7c3165 --- /dev/null +++ b/lib/registerTypes/RegisterTypesClientPacks.h @@ -0,0 +1,124 @@ +/* + * 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(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesLobbyPacks.h b/lib/registerTypes/RegisterTypesLobbyPacks.h new file mode 100644 index 000000000..6e20ee244 --- /dev/null +++ b/lib/registerTypes/RegisterTypesLobbyPacks.h @@ -0,0 +1,62 @@ +/* + * 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(); + // Only host client send + 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(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesMapObjects.h b/lib/registerTypes/RegisterTypesMapObjects.h new file mode 100644 index 000000000..73f87e5e3 --- /dev/null +++ b/lib/registerTypes/RegisterTypesMapObjects.h @@ -0,0 +1,137 @@ +/* + * 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(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesServerPacks.h b/lib/registerTypes/RegisterTypesServerPacks.h new file mode 100644 index 000000000..f5eed0156 --- /dev/null +++ b/lib/registerTypes/RegisterTypesServerPacks.h @@ -0,0 +1,56 @@ +/* + * 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(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp deleted file mode 100644 index 794489ec0..000000000 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ /dev/null @@ -1,35 +0,0 @@ -/* - * TypesClientPacks1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks1(BinaryDeserializer & s); -template void registerTypesClientPacks1(BinarySerializer & s); -template void registerTypesClientPacks1(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp deleted file mode 100644 index 29fb941fc..000000000 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* - * TypesClientPacks2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks2(BinaryDeserializer & s); -template void registerTypesClientPacks2(BinarySerializer & s); -template void registerTypesClientPacks2(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp deleted file mode 100644 index 8ac2734a7..000000000 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ /dev/null @@ -1,45 +0,0 @@ -/* - * TypesLobbyPacks.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 "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../gameState/CGameState.h" -#include "../gameState/CGameStateCampaign.h" -#include "../gameState/TavernHeroesPool.h" -#include "../mapping/CMap.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../RoadHandler.h" -#include "../RiverHandler.h" -#include "../TerrainHandler.h" -#include "../campaign/CampaignState.h" -#include "../NetPacks.h" -#include "../rmg/CMapGenOptions.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesLobbyPacks(BinaryDeserializer & s); -template void registerTypesLobbyPacks(BinarySerializer & s); -template void registerTypesLobbyPacks(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp deleted file mode 100644 index e0c7ab158..000000000 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * TypesMapObjects1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjects1(BinaryDeserializer & s); -template void registerTypesMapObjects1(BinarySerializer & s); -template void registerTypesMapObjects1(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp deleted file mode 100644 index f712e52fe..000000000 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* - * TypesMapObjects2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesMapObjects2(BinaryDeserializer & s); -template void registerTypesMapObjects2(BinarySerializer & s); -template void registerTypesMapObjects2(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects3.cpp b/lib/registerTypes/TypesMapObjects3.cpp deleted file mode 100644 index 56b3dce18..000000000 --- a/lib/registerTypes/TypesMapObjects3.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * TypesMapObjects3.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjectTypes(BinaryDeserializer & s); -template void registerTypesMapObjectTypes(BinarySerializer & s); -template void registerTypesMapObjectTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp deleted file mode 100644 index e5d2938a1..000000000 --- a/lib/registerTypes/TypesServerPacks.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * TypesServerPacks.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CModHandler.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesServerPacks(BinaryDeserializer & s); -template void registerTypesServerPacks(BinarySerializer & s); -template void registerTypesServerPacks(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 386506e59..4cab55235 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "Configuration.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,4 +24,84 @@ ui16 Rewardable::Configuration::getResetDuration() const return resetParameters.period; } +std::optional Rewardable::Configuration::getVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.values.count(variableID)) + return variables.values.at(variableID); + + return std::nullopt; +} + +JsonNode Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.preset.count(variableID)) + return variables.preset.at(variableID); + else + return JsonNode(); +} + +void Rewardable::Configuration::presetVariable(const std::string & category, const std::string & name, const JsonNode & value) +{ + std::string variableID = category + '@' + name; + variables.preset[variableID] = value; +} + +void Rewardable::Configuration::initVariable(const std::string & category, const std::string & name, int value) +{ + std::string variableID = category + '@' + name; + variables.values[variableID] = value; +} + +void Rewardable::ResetInfo::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("period", period); + handler.serializeBool("visitors", visitors); + handler.serializeBool("rewards", rewards); +} + +void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeStruct("limiter", limiter); + handler.serializeStruct("reward", reward); + handler.serializeStruct("message", message); + handler.serializeInt("visitType", visitType); +} + +void Rewardable::Variables::serializeJson(JsonSerializeFormat & handler) +{ + if (handler.saving) + { + JsonNode presetNode; + for (auto const & entry : preset) + presetNode[entry.first] = entry.second; + + handler.serializeRaw("preset", presetNode, {}); + } + else + { + preset.clear(); + JsonNode presetNode; + handler.serializeRaw("preset", presetNode, {}); + + for (auto const & entry : presetNode.Struct()) + preset[entry.first] = entry.second; + } +} + +void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeStruct("onSelect", onSelect); + handler.enterArray("info").serializeStruct(info); + handler.serializeEnum("selectMode", selectMode, std::vector{SelectModeString.begin(), SelectModeString.end()}); + handler.serializeEnum("visitMode", visitMode, std::vector{VisitModeString.begin(), VisitModeString.end()}); + handler.serializeStruct("resetParameters", resetParameters); + handler.serializeBool("canRefuse", canRefuse); + handler.serializeBool("showScoutedPreview", showScoutedPreview); + handler.serializeInt("infoWindowType", infoWindowType); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index a2069fc71..6ea12df6a 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -12,8 +12,8 @@ #include "Limiter.h" #include "MetaString.h" -#include "NetPacksBase.h" #include "Reward.h" +#include "../networkPacks/EInfoWindowMode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,6 +26,7 @@ enum EVisitMode VISIT_ONCE, // only once, first to visit get all the rewards VISIT_HERO, // every hero can visit object once VISIT_BONUS, // can be visited by any hero that don't have bonus from this object + VISIT_LIMITER, // can be visited by heroes that don't fulfill provided limiter VISIT_PLAYER // every player can visit object once }; @@ -46,7 +47,7 @@ enum class EEventType }; const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom"}; -const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "player"}; +const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "limiter", "player"}; struct DLL_LINKAGE ResetInfo { @@ -62,10 +63,11 @@ struct DLL_LINKAGE ResetInfo /// if true - reset list of visitors (heroes & players) on reset bool visitors; - /// if true - re-randomize rewards on a new week bool rewards; - + + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & period; @@ -82,24 +84,58 @@ struct DLL_LINKAGE VisitInfo /// Message that will be displayed on granting of this reward, if not empty MetaString message; + /// Object description that will be shown on right-click, after object name + /// Used only after player have "scouted" object and knows internal state of an object + MetaString description; + /// Event to which this reward is assigned EEventType visitType; + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & limiter; h & reward; h & message; + h & description; h & visitType; } }; +struct DLL_LINKAGE Variables +{ + /// List of variables used by this object in their current values + std::map values; + + /// List of per-instance preconfigured variables, e.g. from map + std::map preset; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & values; + h & preset; + } +}; + /// Base class that can handle granting rewards to visiting heroes. struct DLL_LINKAGE Configuration { /// Message that will be shown if player needs to select one of multiple rewards MetaString onSelect; + /// Object description that will be shown on right-click, after object name + /// Used only if player is not aware of object internal state, e.g. have never visited it + MetaString description; + + /// Text that will be shown if hero has not visited this object + MetaString notVisitedTooltip; + + /// Text that will be shown after hero has visited this object + MetaString visitedTooltip; + /// Rewards that can be applied by an object std::vector info; @@ -112,23 +148,45 @@ struct DLL_LINKAGE Configuration /// how and when should the object be reset Rewardable::ResetInfo resetParameters; + /// List of variables shoread between all limiters and rewards + Rewardable::Variables variables; + + /// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter" + Rewardable::Limiter visitLimiter; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; + /// if true - right-clicking object will show preview of object rewards + bool showScoutedPreview = false; + /// if true - object info will shown in infobox (like resource pickup) EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO; EVisitMode getVisitMode() const; ui16 getResetDuration() const; + + std::optional getVariable(const std::string & category, const std::string & name) const; + JsonNode getPresetVariable(const std::string & category, const std::string & name) const; + void presetVariable(const std::string & category, const std::string & name, const JsonNode & value); + void initVariable(const std::string & category, const std::string & name, int value); + + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler &h, const int version) { - h & info; - h & canRefuse; - h & resetParameters; h & onSelect; - h & visitMode; + h & description; + h & notVisitedTooltip; + h & visitedTooltip; + h & info; h & selectMode; + h & visitMode; + h & resetParameters; + h & variables; + h & visitLimiter; + h & canRefuse; + h & showScoutedPreview; h & infoWindowType; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index e68e44ba3..135f97c8c 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -16,27 +16,44 @@ #include "Reward.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../IGameCallback.h" #include "../JsonRandom.h" #include "../mapObjects/IObjectInterface.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN namespace { - MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier ) + MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier, EMetaText textSource = EMetaText::ADVOB_TXT ) { MetaString ret; + + if (value.isVector()) + { + for(const auto & entry : value.Vector()) + { + if (entry.isNumber()) + ret.appendLocalString(textSource, static_cast(entry.Float())); + if (entry.isString()) + ret.appendRawString(entry.String()); + } + return ret; + } + if (value.isNumber()) { - ret.appendLocalString(EMetaText::ADVOB_TXT, static_cast(value.Float())); + ret.appendLocalString(textSource, static_cast(value.Float())); return ret; } if (value.String().empty()) return ret; - ret.appendTextID(textIdentifier.get()); + if (value.String()[0] == '@') + ret.appendTextID(value.String().substr(1)); + else + ret.appendTextID(textIdentifier.get()); + return ret; } @@ -56,7 +73,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o objectTextID = objectName; auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){ - if (entry.isString() && !entry.String().empty()) + if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@') VLC->generaltexth->registerString(entry.meta, textID, entry.String()); }; @@ -81,6 +98,9 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o } loadString(parameters["onSelectMessage"], TextIdentifier(objectName, "onSelect")); + loadString(parameters["description"], TextIdentifier(objectName, "description")); + loadString(parameters["notVisitedTooltip"], TextIdentifier(objectName, "notVisitedText")); + loadString(parameters["visitedTooltip"], TextIdentifier(objectName, "visitedTooltip")); loadString(parameters["onVisitedMessage"], TextIdentifier(objectName, "onVisited")); loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty")); } @@ -102,26 +122,29 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const { - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + auto const & variables = object.variables.values; + limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng, variables); + limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables); + limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); + limiter.canLearnSkills = source["canLearnSkills"].Bool(); - limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng); - limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng); - limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng); - limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables); + limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables); - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); - limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); + limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables); - limiter.resources = JsonRandom::loadResources(source["resources"], rng); - - limiter.primary = JsonRandom::loadPrimary(source["primary"], rng); - limiter.secondary = JsonRandom::loadSecondary(source["secondary"], rng); - limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - limiter.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + limiter.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); + limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); + limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + limiter.canLearnSpells = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables); + limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); + + limiter.players = JsonRandom::loadColors(source["colors"], rng, variables); + limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); + limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); limiter.allOf = configureSublimiters(object, rng, source["allOf"] ); limiter.anyOf = configureSublimiters(object, rng, source["anyOf"] ); @@ -130,45 +153,55 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & reward, const JsonNode & source) const { - reward.resources = JsonRandom::loadResources(source["resources"], rng); + auto const & variables = object.variables.values; - reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng) - + JsonRandom::loadValue(source["gainedExp"], rng); // VCMI 1.1 compatibilty + reward.resources = JsonRandom::loadResources(source["resources"], rng, variables); - reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["gainedLevels"], rng); // VCMI 1.1 compatibilty + reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); - reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng); - reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng); - reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); + reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng, variables); + reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng, variables); + reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables, -1); - reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng); - reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, -1); + reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng, variables); + reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, variables, -1); reward.removeObject = source["removeObject"].Bool(); reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]); - reward.primary = JsonRandom::loadPrimary(source["primary"], rng); - reward.secondary = JsonRandom::loadSecondary(source["secondary"], rng); + reward.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); + reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); - - reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + reward.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); if(!source["spellCast"].isNull() && source["spellCast"].isStruct()) { - reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng); + reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng, variables); reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer(); } + if (!source["revealTiles"].isNull()) + { + auto const & entry = source["revealTiles"]; + + reward.revealTiles = RewardRevealTiles(); + reward.revealTiles->radius = JsonRandom::loadValue(entry["radius"], rng, variables); + reward.revealTiles->hide = entry["hide"].Bool(); + + reward.revealTiles->scoreSurface = JsonRandom::loadValue(entry["surface"], rng, variables); + reward.revealTiles->scoreSubterra = JsonRandom::loadValue(entry["subterra"], rng, variables); + reward.revealTiles->scoreWater = JsonRandom::loadValue(entry["water"], rng, variables); + reward.revealTiles->scoreRock = JsonRandom::loadValue(entry["rock"], rng, variables); + } + for ( auto node : source["changeCreatures"].Struct() ) { - CreatureID from(VLC->modh->identifiers.getIdentifier(node.second.meta, "creature", node.first).value()); - CreatureID dest(VLC->modh->identifiers.getIdentifier(node.second.meta, "creature", node.second.String()).value()); + CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); + CreatureID dest(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.second.String()).value()); - reward.extraComponents.emplace_back(Component::EComponentType::CREATURE, dest.getNum(), 0, 0); + reward.extraComponents.emplace_back(ComponentType::CREATURE, dest); reward.creaturesChange[from] = dest; } @@ -181,11 +214,66 @@ void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CR resetParameters.rewards = source["rewards"].Bool(); } +void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const +{ + for(const auto & category : source.Struct()) + { + for(const auto & entry : category.second.Struct()) + { + JsonNode preset = object.getPresetVariable(category.first, entry.first); + const JsonNode & input = preset.isNull() ? entry.second : preset; + int32_t value = -1; + + if (category.first == "number") + value = JsonRandom::loadValue(input, rng, object.variables.values); + + if (category.first == "artifact") + value = JsonRandom::loadArtifact(input, rng, object.variables.values).getNum(); + + if (category.first == "spell") + value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); + + if (category.first == "primarySkill") + value = JsonRandom::loadPrimary(input, rng, object.variables.values).getNum(); + + if (category.first == "secondarySkill") + value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); + + object.initVariable(category.first, entry.first, value); + } + } +} + +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables) const +{ + for (const auto & variable : variables.values ) + { + if( boost::algorithm::starts_with(variable.first, "spell")) + target.replaceName(SpellID(variable.second)); + + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + target.replaceName(SecondarySkill(variable.second)); + } +} + +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const +{ + 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, const JsonNode & source, - std::map & thrownDice, Rewardable::EEventType event, const std::string & modeName) const { @@ -196,21 +284,32 @@ void Rewardable::Info::configureRewards( if (!reward["appearChance"].isNull()) { JsonNode chance = reward["appearChance"]; - si32 diceID = static_cast(chance["dice"].Float()); + std::string diceID = std::to_string(chance["dice"].Integer()); - if (thrownDice.count(diceID) == 0) - thrownDice[diceID] = rng.getIntRange(0, 99)(); + auto diceValue = object.getVariable("dice", diceID); + + if (!diceValue.has_value()) + { + const JsonNode & preset = object.getPresetVariable("dice", diceID); + if (preset.isNull()) + object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + else + object.initVariable("dice", diceID, preset.Integer()); + + diceValue = object.getVariable("dice", diceID); + } + assert(diceValue.has_value()); if (!chance["min"].isNull()) { int min = static_cast(chance["min"].Float()); - if (min > thrownDice[diceID]) + if (min > *diceValue) continue; } if (!chance["max"].isNull()) { int max = static_cast(chance["max"].Float()); - if (max <= thrownDice[diceID]) + if (max <= *diceValue) continue; } } @@ -221,12 +320,10 @@ void Rewardable::Info::configureRewards( info.visitType = event; info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); + info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i), EMetaText::GENERAL_TXT); - for (const auto & artifact : info.reward.artifacts ) - info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); - - for (const auto & artifact : info.reward.spells ) - info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + replaceTextPlaceholders(info.message, object.variables, info); + replaceTextPlaceholders(info.description, object.variables, info); object.info.push_back(info); } @@ -236,19 +333,30 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand { object.info.clear(); - std::map thrownDice; + configureVariables(object, rng, parameters["variables"]); - configureRewards(object, rng, parameters["rewards"], thrownDice, Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); - configureRewards(object, rng, parameters["onVisited"], thrownDice, Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); - configureRewards(object, rng, parameters["onEmpty"], thrownDice, Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); + configureRewards(object, rng, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); + configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); + configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); - object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.description = loadMessage(parameters["description"], TextIdentifier(objectTextID, "description")); + object.notVisitedTooltip = loadMessage(parameters["notVisitedTooltip"], TextIdentifier(objectTextID, "notVisitedTooltip"), EMetaText::GENERAL_TXT); + object.visitedTooltip = loadMessage(parameters["visitedTooltip"], TextIdentifier(objectTextID, "visitedTooltip"), EMetaText::GENERAL_TXT); + + if (object.notVisitedTooltip.empty()) + object.notVisitedTooltip.appendTextID("core.genrltxt.353"); + + if (object.visitedTooltip.empty()) + object.visitedTooltip.appendTextID("core.genrltxt.352"); if (!parameters["onVisitedMessage"].isNull()) { Rewardable::VisitInfo onVisited; onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED; onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited")); + replaceTextPlaceholders(onVisited.message, object.variables); + object.info.push_back(onVisited); } @@ -257,12 +365,15 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onEmpty; onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE; onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty")); + replaceTextPlaceholders(onEmpty.message, object.variables); + object.info.push_back(onEmpty); } configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]); object.canRefuse = parameters["canRefuse"].Bool(); + object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); if(parameters["showInInfobox"].isNull()) object.infoWindowType = EInfoWindowMode::AUTO; @@ -288,6 +399,10 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand break; } } + + if (object.visitMode == Rewardable::VISIT_LIMITER) + configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); + } bool Rewardable::Info::givesResources() const diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index c9c826fae..5ad96ca40 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; +class MetaString; namespace Rewardable { @@ -24,6 +25,8 @@ struct Limiter; using LimitersList = std::vector>; struct Reward; struct Configuration; +struct Variables; +struct VisitInfo; struct ResetInfo; enum class EEventType; @@ -32,7 +35,11 @@ class DLL_LINKAGE Info : public IObjectInfo JsonNode parameters; std::string objectTextID; - void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, std::map & thrownDice, Rewardable::EEventType mode, const std::string & textPrefix) const; + void replaceTextPlaceholders(MetaString & target, const Variables & variables) const; + void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const; + + void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; + void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const; Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index d8521dbea..534641f37 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -12,11 +12,17 @@ #include "Interface.h" #include "../CHeroHandler.h" +#include "../TerrainHandler.h" +#include "../CPlayerState.h" #include "../CSoundBase.h" -#include "../NetPacks.h" +#include "../gameState/CGameState.h" #include "../spells/CSpellHandler.h" #include "../spells/ISpellMechanics.h" +#include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/MiscObjects.h" +#include "../mapping/CMapDefines.h" +#include "../networkPacks/StackLocation.h" +#include "../networkPacks/PacksForClient.h" #include "../IGameCallback.h" VCMI_LIB_NAMESPACE_BEGIN @@ -29,7 +35,7 @@ std::vector Rewardable::Interface::getAvailableRewards(const CGHeroInstanc { const Rewardable::VisitInfo & visit = configuration.info[i]; - if(event == visit.visitType && visit.limiter.heroAllowed(hero)) + if(event == visit.visitType && (!hero || visit.limiter.heroAllowed(hero))) { logGlobal->trace("Reward %d is allowed", i); ret.push_back(static_cast(i)); @@ -46,6 +52,58 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R cb->giveResources(hero->tempOwner, info.reward.resources); + if (info.reward.revealTiles) + { + const auto & props = *info.reward.revealTiles; + + const auto functor = [&props](const TerrainTile * tile) + { + int score = 0; + if (tile->terType->isSurface()) + score += props.scoreSurface; + + if (tile->terType->isUnderground()) + score += props.scoreSubterra; + + if (tile->terType->isWater()) + score += props.scoreWater; + + if (tile->terType->isRock()) + score += props.scoreRock; + + return score > 0; + }; + + std::unordered_set tiles; + if (props.radius > 0) + { + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->getOwner()); + if (props.hide) + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::REVEALED, hero->getOwner()); + + vstd::erase_if(tiles, [&](const int3 & coord){ + return !functor(cb->getTile(coord)); + }); + } + else + { + cb->getAllTiles(tiles, hero->tempOwner, -1, functor); + } + + if (props.hide) + { + for (auto & player : cb->gameState()->players) + { + if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && cb->getPlayerRelations(player.first, hero->getOwner()) == PlayerRelations::ENEMIES) + cb->changeFogOfWar(tiles, player.first, ETileVisibility::HIDDEN); + } + } + else + { + cb->changeFogOfWar(tiles, hero->getOwner(), ETileVisibility::REVEALED); + } + } + for(const auto & entry : info.reward.secondary) { int current = hero->getSecSkillLevel(entry.first); @@ -57,7 +115,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R } for(int i=0; i< info.reward.primary.size(); i++) - cb->changePrimSkill(hero, static_cast(i), info.reward.primary[i], false); + cb->changePrimSkill(hero, static_cast(i), info.reward.primary[i], false); si64 expToGive = 0; @@ -92,14 +150,14 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re for(const Bonus & bonus : info.reward.bonuses) { GiveBonus gb; - gb.who = GiveBonus::ETarget::HERO; + gb.who = GiveBonus::ETarget::OBJECT; gb.bonus = bonus; - gb.id = hero->id.getNum(); + gb.id = hero->id; cb->giveHeroBonus(&gb); } for(const ArtifactID & art : info.reward.artifacts) - cb->giveHeroNewArtifact(hero, VLC->arth->objects[art],ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, art.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); if(!info.reward.spells.empty()) { @@ -147,4 +205,9 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re cb->removeAfterVisit(instance); } +void Rewardable::Interface::serializeJson(JsonSerializeFormat & handler) +{ + configuration.serializeJson(handler); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index 74b798d69..ac85e6b72 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -10,8 +10,6 @@ #pragma once -#include "../CCreatureSet.h" -#include "../ResourceSet.h" #include "../spells/ExternalCaster.h" #include "Configuration.h" @@ -44,6 +42,8 @@ public: Rewardable::Configuration configuration; + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & configuration; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 2f5e25049..e6fd3b361 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -14,6 +14,12 @@ #include "../IGameCallback.h" #include "../CPlayerState.h" #include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/Component.h" +#include "../serializer/JsonSerializeFormat.h" +#include "../constants/StringConstants.h" +#include "../CHeroHandler.h" +#include "../CSkillHandler.h" +#include "../ArtifactUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -21,7 +27,7 @@ Rewardable::Limiter::Limiter() : dayOfWeek(0) , daysPassed(0) , heroExperience(0) - , heroLevel(0) + , heroLevel(-1) , manaPercentage(0) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) @@ -30,6 +36,33 @@ Rewardable::Limiter::Limiter() Rewardable::Limiter::~Limiter() = default; +bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) +{ + return l.dayOfWeek == r.dayOfWeek + && l.daysPassed == r.daysPassed + && l.heroLevel == r.heroLevel + && l.heroExperience == r.heroExperience + && l.manaPoints == r.manaPoints + && l.manaPercentage == r.manaPercentage + && l.secondary == r.secondary + && l.creatures == r.creatures + && l.spells == r.spells + && l.artifacts == r.artifacts + && l.players == r.players + && l.heroes == r.heroes + && l.heroClasses == r.heroClasses + && l.resources == r.resources + && l.primary == r.primary + && l.noneOf == r.noneOf + && l.allOf == r.allOf + && l.anyOf == r.anyOf; +} + +bool operator!=(const Rewardable::Limiter & l, const Rewardable::Limiter & r) +{ + return !(l == r); +} + bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) @@ -69,12 +102,15 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(manaPoints > hero->mana) return false; + if (canLearnSkills && !hero->canLearnSkill()) + return false; + if(manaPercentage > 100 * hero->mana / hero->manaLimit()) return false; for(size_t i=0; i hero->getPrimSkillLevel(static_cast(i))) + if(primary[i] > hero->getPrimSkillLevel(static_cast(i))) return false; } @@ -90,12 +126,40 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } - for(const auto & art : artifacts) + for(const auto & spell : canLearnSpells) { - if (!hero->hasArt(art)) + if (!hero->canLearnSpell(spell.toEntity(VLC), true)) return false; } + { + std::unordered_map artifactsRequirements; // artifact ID -> required count + for(const auto & art : artifacts) + ++artifactsRequirements[art]; + + size_t reqSlots = 0; + for(const auto & elem : artifactsRequirements) + { + // check required amount of artifacts + if(hero->getArtPosCount(elem.first, false, true, true) < elem.second) + return false; + if(!hero->hasArt(elem.first)) + reqSlots += hero->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; + } + if(!ArtifactUtils::isBackpackFreeSlots(hero, reqSlots)) + return false; + } + + if(!players.empty() && !vstd::contains(players, hero->getOwner())) + return false; + + if(!heroes.empty() && !vstd::contains(heroes, hero->type->getId())) + return false; + + if(!heroClasses.empty() && !vstd::contains(heroClasses, hero->type->heroClass->getId())) + return false; + + for(const auto & sublimiter : noneOf) { if (sublimiter->heroAllowed(hero)) @@ -119,4 +183,94 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } +void Rewardable::Limiter::loadComponents(std::vector & comps, + const CGHeroInstance * h) const +{ + if (heroExperience) + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); + + if (heroLevel > 0) + comps.emplace_back(ComponentType::EXPERIENCE, heroLevel); + + if (manaPoints || manaPercentage > 0) + { + int absoluteMana = (h && h->manaLimit()) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + comps.emplace_back(ComponentType::MANA, absoluteMana + manaPoints); + } + + for (size_t i=0; igetId(), entry.count); + + for(const auto & entry : players) + comps.emplace_back(ComponentType::FLAG, entry); + + for(const auto & entry : heroes) + comps.emplace_back(ComponentType::HERO_PORTRAIT, entry); + + for (size_t i=0; i> fieldValue(secondary.begin(), secondary.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) + { + h.serializeId("skill", e.first, SecondarySkill(SecondarySkill::NONE)); + h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); + }); + a.syncSize(fieldValue); + secondary = std::map(fieldValue.begin(), fieldValue.end()); + } + //sublimiters + auto serializeSublimitersList = [&handler](const std::string & field, LimitersList & container) + { + auto a = handler.enterArray(field); + a.syncSize(container); + for(int i = 0; i < container.size(); ++i) + { + if(!handler.saving) + container[i] = std::make_shared(); + auto e = a.enterStruct(i); + container[i]->serializeJson(handler); + } + }; + serializeSublimitersList("allOf", allOf); + serializeSublimitersList("anyOf", anyOf); + serializeSublimitersList("noneOf", noneOf); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index a443d1e2c..407db0f24 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CStackBasicDescriptor; +struct Component; namespace Rewardable { @@ -25,8 +26,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) -/// NOTE: in future should (partially) replace seer hut/quest guard quests checks -struct DLL_LINKAGE Limiter +struct DLL_LINKAGE Limiter final { /// day of week, unused if 0, 1-7 will test for current day of week si32 dayOfWeek; @@ -44,6 +44,9 @@ struct DLL_LINKAGE Limiter /// percentage of mana points that hero needs to have si32 manaPercentage; + /// Number of free secondary slots that hero needs to have + bool canLearnSkills; + /// resources player needs to have in order to trigger reward TResources resources; @@ -52,14 +55,24 @@ struct DLL_LINKAGE Limiter std::map secondary; /// artifacts that hero needs to have (equipped or in backpack) to trigger this - /// Note: does not checks for multiple copies of the same arts + /// checks for artifacts copies if same artifact id is included multiple times std::vector artifacts; /// Spells that hero must have in the spellbook std::vector spells; + /// Spells that hero must be able to learn + std::vector canLearnSpells; + /// creatures that hero needs to have std::vector creatures; + + /// only heroes/hero classes from list could pass limiter + std::vector heroes; + std::vector heroClasses; + + /// only player colors can pass limiter + std::vector players; /// sub-limiters, all must pass for this limiter to pass LimitersList allOf; @@ -74,6 +87,10 @@ struct DLL_LINKAGE Limiter ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; + + /// Generates list of components that describes reward for a specific hero + void loadComponents(std::vector & comps, + const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) { @@ -83,17 +100,28 @@ struct DLL_LINKAGE Limiter h & heroLevel; h & manaPoints; h & manaPercentage; + h & canLearnSkills; h & resources; h & primary; h & secondary; h & artifacts; + h & spells; + h & canLearnSpells; h & creatures; + h & heroes; + h & heroClasses; + h & players; h & allOf; h & anyOf; h & noneOf; } + + void serializeJson(JsonSerializeFormat & handler); }; } +bool DLL_LINKAGE operator== (const Rewardable::Limiter & l, const Rewardable::Limiter & r); +bool DLL_LINKAGE operator!= (const Rewardable::Limiter & l, const Rewardable::Limiter & r); + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 48caa35e5..c56837e57 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -12,9 +12,22 @@ #include "Reward.h" #include "../mapObjects/CGHeroInstance.h" +#include "../serializer/JsonSerializeFormat.h" +#include "../constants/StringConstants.h" +#include "../CSkillHandler.h" VCMI_LIB_NAMESPACE_BEGIN +void Rewardable::RewardRevealTiles::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeBool("hide", hide); + handler.serializeInt("scoreSurface", scoreSurface); + handler.serializeInt("scoreSubterra", scoreSubterra); + handler.serializeInt("scoreWater", scoreWater); + handler.serializeInt("scoreRock", scoreRock); + handler.serializeInt("radius", radius); +} + Rewardable::Reward::Reward() : heroExperience(0) , heroLevel(0) @@ -24,7 +37,7 @@ Rewardable::Reward::Reward() , movePercentage(-1) , primary(4, 0) , removeObject(false) - , spellCast(SpellID::NONE, SecSkillLevel::NONE) + , spellCast(SpellID::NONE, MasteryLevel::NONE) { } @@ -53,44 +66,95 @@ Component Rewardable::Reward::getDisplayedComponent(const CGHeroInstance * h) co return comps.front(); } -void Rewardable::Reward::loadComponents(std::vector & comps, - const CGHeroInstance * h) const +void Rewardable::Reward::loadComponents(std::vector & comps, const CGHeroInstance * h) const { for (auto comp : extraComponents) comps.push_back(comp); - - if (heroExperience) + + for (auto & bonus : bonuses) { - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); + if (bonus.type == BonusType::MORALE) + comps.emplace_back(ComponentType::MORALE, bonus.val); + if (bonus.type == BonusType::LUCK) + comps.emplace_back(ComponentType::LUCK, bonus.val); } + + if (heroExperience) + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); + if (heroLevel) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + comps.emplace_back(ComponentType::LEVEL, heroLevel); if (manaDiff || manaPercentage >= 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, calculateManaPoints(h) - h->mana, 0); + comps.emplace_back(ComponentType::MANA, h ? (calculateManaPoints(h) - h->mana) : manaDiff); for (size_t i=0; i(i), primary[i], 0); + comps.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill(i), primary[i]); } for(const auto & entry : secondary) - comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + comps.emplace_back(ComponentType::SEC_SKILL, entry.first, entry.second); for(const auto & entry : artifacts) - comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); for (size_t i=0; i(i), resources[i], 0); + comps.emplace_back(ComponentType::RESOURCE, GameResID(i), resources[i]); + } +} + +void Rewardable::Reward::serializeJson(JsonSerializeFormat & handler) +{ + resources.serializeJson(handler, "resources"); + handler.serializeBool("removeObject", removeObject); + handler.serializeInt("manaPercentage", manaPercentage); + handler.serializeInt("movePercentage", movePercentage); + handler.serializeInt("heroExperience", heroExperience); + handler.serializeInt("heroLevel", heroLevel); + handler.serializeInt("manaDiff", manaDiff); + handler.serializeInt("manaOverflowFactor", manaOverflowFactor); + handler.serializeInt("movePoints", movePoints); + handler.serializeIdArray("artifacts", artifacts); + handler.serializeIdArray("spells", spells); + handler.enterArray("creatures").serializeStruct(creatures); + handler.enterArray("primary").serializeArray(primary); + { + auto a = handler.enterArray("secondary"); + std::vector> fieldValue(secondary.begin(), secondary.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) + { + h.serializeId("skill", e.first); + h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); + }); + a.syncSize(fieldValue); + secondary = std::map(fieldValue.begin(), fieldValue.end()); + } + + { + auto a = handler.enterArray("creaturesChange"); + std::vector> fieldValue(creaturesChange.begin(), creaturesChange.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) + { + h.serializeId("creature", e.first, CreatureID{}); + h.serializeId("amount", e.second, CreatureID{}); + }); + creaturesChange = std::map(fieldValue.begin(), fieldValue.end()); + } + + { + auto a = handler.enterStruct("spellCast"); + a->serializeId("spell", spellCast.first, SpellID{}); + a->serializeInt("level", spellCast.second); } } diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 16f93bb8d..5c4fd84a2 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -13,6 +13,7 @@ #include "../ResourceSet.h" #include "../bonuses/Bonus.h" #include "../CCreatureSet.h" +#include "../networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,9 +28,37 @@ namespace Rewardable struct Reward; using RewardsList = std::vector>; +struct RewardRevealTiles +{ + /// Reveal distance, if not positive - reveal entire map + int radius; + /// Reveal score of terrains with "surface" flag set + int scoreSurface; + /// Reveal score of terrains with "subterra" flag set + int scoreSubterra; + /// Reveal score of terrains with "water" flag set + int scoreWater; + /// Reveal score of terrains with "rock" flag set + int scoreRock; + /// If set, then terrain will be instead hidden for all enemies (Cover of Darkness) + bool hide; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & radius; + h & scoreSurface; + h & scoreSubterra; + h & scoreWater; + h & scoreRock; + h & hide; + } +}; + /// Reward that can be granted to a hero /// NOTE: eventually should replace seer hut rewards and events/pandoras -struct DLL_LINKAGE Reward +struct DLL_LINKAGE Reward final { /// resources that will be given to player TResources resources; @@ -74,12 +103,14 @@ struct DLL_LINKAGE Reward /// list of components that will be added to reward description. First entry in list will override displayed component std::vector extraComponents; + std::optional revealTiles; + /// if set to true, object will be removed after granting reward bool removeObject; /// Generates list of components that describes reward for a specific hero - virtual void loadComponents(std::vector & comps, - const CGHeroInstance * h) const; + /// If hero is nullptr, then rewards will be generated without accounting for hero + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; Component getDisplayedComponent(const CGHeroInstance * h) const; @@ -107,9 +138,11 @@ struct DLL_LINKAGE Reward h & spells; h & creatures; h & creaturesChange; - if(version >= 821) - h & spellCast; + h & revealTiles; + h & spellCast; } + + void serializeJson(JsonSerializeFormat & handler); }; } diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 3201943eb..72d9e786c 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -22,10 +22,11 @@ VCMI_LIB_NAMESPACE_BEGIN CMapGenOptions::CMapGenOptions() : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true), - playerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), - waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr) + humanOrCpuPlayerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), + waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr), + customizedPlayers(false) { - resetPlayersMap(); + initPlayersMap(); setRoadEnabled(RoadId(Road::DIRT_ROAD), true); setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); @@ -63,23 +64,91 @@ void CMapGenOptions::setHasTwoLevels(bool value) hasTwoLevels = value; } -si8 CMapGenOptions::getPlayerCount() const +si8 CMapGenOptions::getHumanOrCpuPlayerCount() const { - return playerCount; + return humanOrCpuPlayerCount; } -void CMapGenOptions::setPlayerCount(si8 value) +void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) { assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); - playerCount = value; + humanOrCpuPlayerCount = value; - auto possibleCompPlayersCount = PlayerColor::PLAYER_LIMIT_I - value; + // Use template player limit, if any + auto playerLimit = getPlayerLimit(); + auto possibleCompPlayersCount = playerLimit - std::max(0, humanOrCpuPlayerCount); if (compOnlyPlayerCount > possibleCompPlayersCount) + { setCompOnlyPlayerCount(possibleCompPlayersCount); + } resetPlayersMap(); } +si8 CMapGenOptions::getMinPlayersCount(bool withTemplateLimit) const +{ + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); + + if (humans == RANDOM_SIZE && cpus == RANDOM_SIZE) + { + totalPlayers = 2; + } + else if (humans == RANDOM_SIZE) + { + totalPlayers = cpus + 1; // Must add at least 1 player + } + else if (cpus == RANDOM_SIZE) + { + totalPlayers = humans; + } + else + { + totalPlayers = humans + cpus; + } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amax(totalPlayers, playersRange.minValue()); + } + + // Can't play without at least 2 players + vstd::amax(totalPlayers, 2); + return totalPlayers; +} + +si8 CMapGenOptions::getMaxPlayersCount(bool withTemplateLimit) const +{ + // Max number of players possible with current settings + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); + if (humans == RANDOM_SIZE || cpus == RANDOM_SIZE) + { + totalPlayers = PlayerColor::PLAYER_LIMIT_I; + } + else + { + totalPlayers = humans + cpus; + } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amin(totalPlayers, playersRange.maxValue()); + } + + assert (totalPlayers <= PlayerColor::PLAYER_LIMIT_I); + assert (totalPlayers >= 2); + return totalPlayers; +} + si8 CMapGenOptions::getTeamCount() const { return teamCount; @@ -87,7 +156,7 @@ si8 CMapGenOptions::getTeamCount() const void CMapGenOptions::setTeamCount(si8 value) { - assert(getPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getPlayerCount()) || value == RANDOM_SIZE); + assert(getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getHumanOrCpuPlayerCount()) || value == RANDOM_SIZE); teamCount = value; } @@ -96,9 +165,20 @@ si8 CMapGenOptions::getCompOnlyPlayerCount() const return compOnlyPlayerCount; } +si8 CMapGenOptions::getPlayerLimit() const +{ + //How many players could we set with current template, ignoring other settings + si8 playerLimit = PlayerColor::PLAYER_LIMIT_I; + if (auto temp = getMapTemplate()) + { + playerLimit = static_cast(temp->getPlayers().maxValue()); + } + return playerLimit; +} + void CMapGenOptions::setCompOnlyPlayerCount(si8 value) { - assert(value == RANDOM_SIZE || (getPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= PlayerColor::PLAYER_LIMIT_I - getPlayerCount()))); + assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= getPlayerLimit() - getHumanOrCpuPlayerCount()))); compOnlyPlayerCount = value; resetPlayersMap(); @@ -135,7 +215,7 @@ void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value monsterStrength = value; } -void CMapGenOptions::resetPlayersMap() +void CMapGenOptions::initPlayersMap() { std::map rememberTownTypes; @@ -144,42 +224,156 @@ void CMapGenOptions::resetPlayersMap() for(const auto & p : players) { auto town = p.second.getStartingTown(); - if (town != RANDOM_SIZE) + if (town != FactionID::RANDOM) rememberTownTypes[p.first] = FactionID(town); rememberTeam[p.first] = p.second.getTeam(); } players.clear(); - int realPlayersCnt = playerCount; - int realCompOnlyPlayersCnt = (compOnlyPlayerCount == RANDOM_SIZE) ? (PlayerColor::PLAYER_LIMIT_I - realPlayersCnt) : compOnlyPlayerCount; - int totalPlayersLimit = realPlayersCnt + realCompOnlyPlayersCnt; - if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) - totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); + int realPlayersCnt = getHumanOrCpuPlayerCount(); - //FIXME: what happens with human players here? - for(int color = 0; color < totalPlayersLimit; ++color) + // Initialize settings for all color even if not present + for(int color = 0; color < getPlayerLimit(); ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); + auto playerType = EPlayerType::AI; - if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) + // Color doesn't have to be continuous. Player colors can later be changed manually + if (getHumanOrCpuPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) { playerType = EPlayerType::HUMAN; } - else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) - || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) + else if((getHumanOrCpuPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) + || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I - compOnlyPlayerCount))) { playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); - player.setTeam(rememberTeam[pc]); - players[pc] = player; - if (vstd::contains(rememberTownTypes, pc)) - players[pc].setStartingTown(rememberTownTypes[pc]); + players[pc] = player; } + savePlayersMap(); +} + +void CMapGenOptions::resetPlayersMap() +{ + // Called when number of player changes + // But do not update info about already made selections + + savePlayersMap(); + + int realPlayersCnt = getMaxPlayersCount(); + + //Trim the number of AI players, then CPU-only players, finally human players + auto eraseLastPlayer = [this](EPlayerType playerType) -> bool + { + for (auto it = players.rbegin(); it != players.rend(); ++it) + { + if (it->second.getPlayerType() == playerType) + { + players.erase(it->first); + return true; + } + } + return false; //Can't earse any player of this type + }; + + while (players.size() > realPlayersCnt) + { + while (eraseLastPlayer(EPlayerType::AI)); + while (eraseLastPlayer(EPlayerType::COMP_ONLY)); + while (eraseLastPlayer(EPlayerType::HUMAN)); + } + + //First colors from the list are assigned to human players, then to CPU players + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) + { + availableColors.push_back(PlayerColor(color)); + } + + auto removeUsedColors = [this, &availableColors](EPlayerType playerType) + { + for (auto& player : players) + { + if (player.second.getPlayerType() == playerType) + { + vstd::erase(availableColors, player.second.getColor()); + } + } + }; + removeUsedColors(EPlayerType::HUMAN); + removeUsedColors(EPlayerType::COMP_ONLY); + //removeUsedColors(EPlayerType::AI); + + //Assign unused colors to remaining AI players + while (players.size() < realPlayersCnt && !availableColors.empty()) + { + auto color = availableColors.front(); + players[color].setColor(color); + setPlayerTypeForStandardPlayer(color, EPlayerType::AI); + availableColors.erase(availableColors.begin()); + + if (vstd::contains(savedPlayerSettings, color)) + { + setPlayerTeam(color, savedPlayerSettings.at(color).getTeam()); + // TODO: setter + players[color].setStartingTown(savedPlayerSettings.at(color).getStartingTown()); + } + else + { + logGlobal->warn("Adding settings for player %s", color); + // Usually, all players should be initialized in initPlayersMap() + CPlayerSettings settings; + players[color] = settings; + } + } + + std::set occupiedTeams; + for(auto & player : players) + { + auto team = player.second.getTeam(); + if (team != TeamID::NO_TEAM) + { + occupiedTeams.insert(team); + } + } + // TODO: Handle situation when we remove a player and remaining players belong to only one team + + for(auto & player : players) + { + if (player.second.getTeam() == TeamID::NO_TEAM) + { + //Find first unused team + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + TeamID team(i); + if(!occupiedTeams.count(team)) + { + player.second.setTeam(team); + occupiedTeams.insert(team); + break; + } + } + } + } +} + +void CMapGenOptions::savePlayersMap() +{ + //Only save already configured players + for (const auto& player : players) + { + savedPlayerSettings[player.first] = player.second; + } +} + +const std::map & CMapGenOptions::getSavedPlayersMap() const +{ + return savedPlayerSettings; } const std::map & CMapGenOptions::getPlayersSettings() const @@ -187,19 +381,21 @@ const std::map & CMapGenOptions::g return players; } -void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, si32 town) +void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, FactionID town) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setStartingTown(town); } void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType) { + // FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color? assert(playerType != EPlayerType::COMP_ONLY); auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setPlayerType(playerType); + customizedPlayers = true; } const CRmgTemplate * CMapGenOptions::getMapTemplate() const @@ -209,6 +405,12 @@ const CRmgTemplate * CMapGenOptions::getMapTemplate() const void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) { + if (mapTemplate == value) + { + //Does not trigger during deserialization + return; + } + mapTemplate = value; //validate & adapt options according to template if(mapTemplate) @@ -220,13 +422,21 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) setHeight(sizes.first.y); setHasTwoLevels(sizes.first.z - 1); } - - if(!mapTemplate->getPlayers().isInRange(getPlayerCount())) - setPlayerCount(RANDOM_SIZE); - if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) + + si8 maxPlayerCount = getMaxPlayersCount(false); + si8 minPlayerCount = getMinPlayersCount(false); + + // Neither setting can fit within the template range + if(!mapTemplate->getPlayers().isInRange(minPlayerCount) && + !mapTemplate->getPlayers().isInRange(maxPlayerCount)) + { + setHumanOrCpuPlayerCount(RANDOM_SIZE); setCompOnlyPlayerCount(RANDOM_SIZE); + } if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) setWaterContent(EWaterContent::RANDOM); + + resetPlayersMap(); // Update teams and player count } } @@ -261,15 +471,16 @@ bool CMapGenOptions::isRoadEnabled() const void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & team) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setTeam(team); + customizedPlayers = true; } void CMapGenOptions::finalize(CRandomGenerator & 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", - static_cast(getPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), + static_cast(getHumanOrCpuPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), static_cast(getCompOnlyTeamCount()), static_cast(getWaterContent()), static_cast(getMonsterStrength())); if(!mapTemplate) @@ -280,25 +491,44 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) logGlobal->info("RMG template name: %s", mapTemplate->getName()); - if (getPlayerCount() == RANDOM_SIZE) + auto maxPlayers = getMaxPlayersCount(); + if (getHumanOrCpuPlayerCount() == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); //ignore all non-randomized players, make sure these players will not be missing after roll possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers())); + + vstd::erase_if(possiblePlayers, [maxPlayers](int i) + { + return i > maxPlayers; + }); assert(!possiblePlayers.empty()); - setPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); + setHumanOrCpuPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); updatePlayers(); } if(teamCount == RANDOM_SIZE) { - teamCount = rand.nextInt(getPlayerCount() - 1); + teamCount = rand.nextInt(getHumanOrCpuPlayerCount() - 1); if (teamCount == 1) teamCount = 0; } if(compOnlyPlayerCount == RANDOM_SIZE) { - auto possiblePlayers = mapTemplate->getCpuPlayers().getNumbers(); - compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + // Use remaining range + auto presentPlayers = getHumanOrCpuPlayerCount(); + auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); + vstd::erase_if(possiblePlayers, [maxPlayers, presentPlayers](int i) + { + return i > (maxPlayers - presentPlayers); + }); + if (possiblePlayers.empty()) + { + compOnlyPlayerCount = 0; + } + else + { + compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + } updateCompOnlyPlayers(); } if(compOnlyTeamCount == RANDOM_SIZE) @@ -328,11 +558,6 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) assert (vstd::iswithin(waterContent, EWaterContent::NONE, EWaterContent::ISLANDS)); assert (vstd::iswithin(monsterStrength, EMonsterStrength::GLOBAL_WEAK, EMonsterStrength::GLOBAL_STRONG)); - - //rectangular maps are the future of gaming - //setHeight(20); - //setWidth(50); - logGlobal->trace("Player config:"); int cpuOnlyPlayers = 0; for(const auto & player : players) @@ -355,19 +580,18 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) } logGlobal->trace("Player %d: %s", player.second.getColor(), playerType); } - setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automaticlaly (?) logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), static_cast(getCompOnlyPlayerCount())); } void CMapGenOptions::updatePlayers() { - // Remove AI players only from the end of the players map if necessary + // Remove non-human players only from the end of the players map if necessary for(auto itrev = players.end(); itrev != players.begin();) { auto it = itrev; --it; - if (players.size() == getPlayerCount()) break; - if(it->second.getPlayerType() == EPlayerType::AI) + if (players.size() == getHumanOrCpuPlayerCount()) break; + if(it->second.getPlayerType() != EPlayerType::HUMAN) { players.erase(it); } @@ -385,7 +609,7 @@ void CMapGenOptions::updateCompOnlyPlayers() { auto it = itrev; --it; - if (players.size() <= getPlayerCount()) break; + if (players.size() <= getHumanOrCpuPlayerCount()) break; if(it->second.getPlayerType() == EPlayerType::COMP_ONLY) { players.erase(it); @@ -397,11 +621,11 @@ void CMapGenOptions::updateCompOnlyPlayers() } // Add some comp only players if necessary - int compOnlyPlayersToAdd = static_cast(getPlayerCount() - players.size()); + int compOnlyPlayersToAdd = static_cast(getHumanOrCpuPlayerCount() - players.size()); if (compOnlyPlayersToAdd < 0) { - logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", playerCount, players.size()); + logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", humanOrCpuPlayerCount, players.size()); assert (compOnlyPlayersToAdd < 0); } for(int i = 0; i < compOnlyPlayersToAdd; ++i) @@ -457,6 +681,11 @@ bool CMapGenOptions::checkOptions() const } } +bool CMapGenOptions::arePlayersCustomized() const +{ + return customizedPlayers; +} + std::vector CMapGenOptions::getPossibleTemplates() const { int3 tplSize(width, height, (hasTwoLevels ? 2 : 1)); @@ -472,21 +701,38 @@ std::vector CMapGenOptions::getPossibleTemplates() const if(!tmpl->isWaterContentAllowed(getWaterContent())) return true; - if(getPlayerCount() != -1) + auto humanOrCpuPlayerCount = getHumanOrCpuPlayerCount(); + auto compOnlyPlayerCount = getCompOnlyPlayerCount(); + // Check if total number of players fall inside given range + + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE && compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getPlayers().isInRange(getPlayerCount())) + if (!tmpl->getPlayers().isInRange(humanOrCpuPlayerCount + compOnlyPlayerCount)) + return true; + + } + else if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + // We can always add any number CPU players, but not subtract + if (!(humanOrCpuPlayerCount <= tmpl->getPlayers().maxValue())) + return true; + } + else if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + //We must fit at least one more human player, but can add any number + if (!(compOnlyPlayerCount < tmpl->getPlayers().maxValue())) return true; } else { // Human players shouldn't be banned when playing with random player count - if(humanPlayers > *boost::min_element(tmpl->getPlayers().getNumbers())) + if(humanPlayers > tmpl->getPlayers().minValue()) return true; } - if(compOnlyPlayerCount != -1) + if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getCpuPlayers().isInRange(compOnlyPlayerCount)) + if (!tmpl->getHumanPlayers().isInRange(compOnlyPlayerCount)) return true; } @@ -506,7 +752,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand return *RandomGeneratorUtil::nextItem(templates, rand); } -CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(RANDOM_TOWN), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) +CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) { } @@ -522,17 +768,17 @@ void CMapGenOptions::CPlayerSettings::setColor(const PlayerColor & value) color = value; } -si32 CMapGenOptions::CPlayerSettings::getStartingTown() const +FactionID CMapGenOptions::CPlayerSettings::getStartingTown() const { return startingTown; } -void CMapGenOptions::CPlayerSettings::setStartingTown(si32 value) +void CMapGenOptions::CPlayerSettings::setStartingTown(FactionID value) { - assert(value >= -1); - if(value >= 0) + assert(value >= FactionID::RANDOM); + if(value != FactionID::RANDOM) { - assert(value < static_cast(VLC->townh->size())); + assert(value < FactionID(VLC->townh->size())); assert((*VLC->townh)[value]->town != nullptr); } startingTown = value; diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index aa5683ede..8e25f2619 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -42,8 +42,8 @@ public: /// The starting town of the player ranging from 0 to town max count or RANDOM_TOWN. /// The default value is RANDOM_TOWN. - si32 getStartingTown() const; - void setStartingTown(si32 value); + FactionID getStartingTown() const; + void setStartingTown(FactionID value); /// The default value is EPlayerType::AI. EPlayerType getPlayerType() const; @@ -53,12 +53,9 @@ public: TeamID getTeam() const; void setTeam(const TeamID & value); - /// Constant for a random town selection. - static const si32 RANDOM_TOWN = -1; - private: PlayerColor color; - si32 startingTown; + FactionID startingTown; EPlayerType playerType; TeamID team; @@ -88,8 +85,12 @@ public: /// The count of all (human or computer) players ranging from 1 to PlayerColor::PLAYER_LIMIT or RANDOM_SIZE for random. If you call /// this method, all player settings are reset to default settings. - si8 getPlayerCount() const; - void setPlayerCount(si8 value); + si8 getHumanOrCpuPlayerCount() const; + void setHumanOrCpuPlayerCount(si8 value); + + si8 getMinPlayersCount(bool withTemplateLimit = true) const; + si8 getMaxPlayersCount(bool withTemplateLimit = true) const; + si8 getPlayerLimit() const; /// The count of the teams ranging from 0 to or RANDOM_SIZE for random. si8 getTeamCount() const; @@ -117,7 +118,8 @@ public: /// The first player colors belong to standard players and the last player colors belong to comp only players. /// All standard players are by default of type EPlayerType::AI. const std::map & getPlayersSettings() const; - void setStartingTownForPlayer(const PlayerColor & color, si32 town); + const std::map & getSavedPlayersMap() const; + void setStartingTownForPlayer(const PlayerColor & color, FactionID town); /// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The /// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN. void setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType); @@ -139,11 +141,15 @@ public: /// Returns false if there is no template available which fits to the currently selected options. bool checkOptions() const; + /// Returns true if player colors or teams were set in game GUI + bool arePlayersCustomized() const; static const si8 RANDOM_SIZE = -1; private: + void initPlayersMap(); void resetPlayersMap(); + void savePlayersMap(); int countHumanPlayers() const; int countCompOnlyPlayers() const; PlayerColor getNextPlayerColor() const; @@ -153,11 +159,13 @@ private: si32 width, height; bool hasTwoLevels; - si8 playerCount, teamCount, compOnlyPlayerCount, compOnlyTeamCount; + si8 humanOrCpuPlayerCount, teamCount, compOnlyPlayerCount, compOnlyTeamCount; EWaterContent::EWaterContent waterContent; EMonsterStrength::EMonsterStrength monsterStrength; std::map players; + std::map savedPlayerSettings; std::set enabledRoads; + bool customizedPlayers; const CRmgTemplate * mapTemplate; @@ -168,7 +176,7 @@ public: h & width; h & height; h & hasTwoLevels; - h & playerCount; + h & humanOrCpuPlayerCount; h & teamCount; h & compOnlyPlayerCount; h & compOnlyTeamCount; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 30101ef2a..188ebb5df 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -19,7 +19,7 @@ #include "../mapping/CMapEditManager.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" #include "CZonePlacer.h" #include "TileInfo.h" @@ -51,8 +51,7 @@ int CMapGenerator::getRandomSeed() const void CMapGenerator::loadConfig() { - static const ResourceID path("config/randomMap.json"); - JsonNode randomMapJson(path); + JsonNode randomMapJson(JsonPath::builtin("config/randomMap.json")); config.shipyardGuard = randomMapJson["waterZone"]["shipyard"]["value"].Integer(); for(auto & treasure : randomMapJson["waterZone"]["treasure"].Vector()) @@ -99,13 +98,8 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const void CMapGenerator::initPrisonsRemaining() { - allowedPrisons = 0; - for (auto isAllowed : map->getMap(this).allowedHeroes) - { - if (isAllowed) - allowedPrisons++; - } - allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getPlayerCount()); //so at least 16 heroes will be available for every player + allowedPrisons = map->getMap(this).allowedHeroes.size(); + allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getHumanOrCpuPlayerCount()); //so at least 16 heroes will be available for every player } void CMapGenerator::initQuestArtsRemaining() @@ -163,7 +157,7 @@ std::string CMapGenerator::getMapDescription() const 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.getPlayerCount()) % + map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getHumanOrCpuPlayerCount()) % static_cast(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] % monsterStrengthStr[monsterStrengthIndex]); @@ -174,7 +168,7 @@ std::string CMapGenerator::getMapDescription() const { ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " is human"; } - if(pSettings.getStartingTown() != CMapGenOptions::CPlayerSettings::RANDOM_TOWN) + if(pSettings.getStartingTown() != FactionID::RANDOM) { ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " town choice is " << (*VLC->townh)[pSettings.getStartingTown()]->getNameTranslated(); @@ -186,84 +180,120 @@ std::string CMapGenerator::getMapDescription() const void CMapGenerator::addPlayerInfo() { - // Calculate which team numbers exist + // Teams are already configured in CMapGenOptions. However, it's not the case when it comes to map editor - enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class - std::array, 2> teamNumbers; - std::set teamsTotal; + std::set teamsTotal; - int teamOffset = 0; - int playerCount = 0; - int teamCount = 0; - - for (int i = CPHUMAN; i < AFTER_LAST; ++i) + if (mapGenOptions.arePlayersCustomized()) { - if (i == CPHUMAN) - { - playerCount = mapGenOptions.getPlayerCount(); - teamCount = mapGenOptions.getTeamCount(); - } - else - { - playerCount = mapGenOptions.getCompOnlyPlayerCount(); - teamCount = mapGenOptions.getCompOnlyTeamCount(); - } + // Simply copy existing settings set in GUI - if(playerCount == 0) + for (const auto & player : mapGenOptions.getPlayersSettings()) { - continue; + PlayerInfo playerInfo; + playerInfo.team = player.second.getTeam(); + if (player.second.getPlayerType() == EPlayerType::COMP_ONLY) + { + playerInfo.canHumanPlay = false; + } + else + { + playerInfo.canHumanPlay = true; + } + map->getMap(this).players[player.first.getNum()] = playerInfo; + teamsTotal.insert(player.second.getTeam()); } - int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); - int teamCountNorm = teamCount; - if(teamCountNorm == 0) + } + else + { + // Assign standard teams (in map editor) + + // Calculate which team numbers exist + + enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class + std::array, 2> teamNumbers; + + int teamOffset = 0; + int playerCount = 0; + int teamCount = 0; + + // FIXME: Player can be any color, not just 0 + for (int i = CPHUMAN; i < AFTER_LAST; ++i) { - teamCountNorm = playerCount; - } - for(int j = 0; j < teamCountNorm; ++j) - { - for(int k = 0; k < playersPerTeam; ++k) + if (i == CPHUMAN) + { + playerCount = mapGenOptions.getHumanOrCpuPlayerCount(); + teamCount = mapGenOptions.getTeamCount(); + } + else + { + playerCount = mapGenOptions.getCompOnlyPlayerCount(); + teamCount = mapGenOptions.getCompOnlyTeamCount(); + } + + if(playerCount == 0) + { + continue; + } + int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); + int teamCountNorm = teamCount; + if(teamCountNorm == 0) + { + teamCountNorm = playerCount; + } + for(int j = 0; j < teamCountNorm; ++j) + { + for(int k = 0; k < playersPerTeam; ++k) + { + teamNumbers[i].push_back(j + teamOffset); + } + } + for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) { teamNumbers[i].push_back(j + teamOffset); } + teamOffset += teamCountNorm; } - for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) - { - teamNumbers[i].push_back(j + teamOffset); - } - teamOffset += teamCountNorm; - } + logGlobal->info("Current player settings size: %d", mapGenOptions.getPlayersSettings().size()); - // Team numbers are assigned randomly to every player - //TODO: allow customize teams in rmg template - for(const auto & pair : mapGenOptions.getPlayersSettings()) - { - const auto & pSettings = pair.second; - PlayerInfo player; - player.canComputerPlay = true; - int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; - if (j == CPHUMAN) + // Team numbers are assigned randomly to every player + //TODO: allow to customize teams in rmg template + for(const auto & pair : mapGenOptions.getPlayersSettings()) { - player.canHumanPlay = true; - } - - if(pSettings.getTeam() != TeamID::NO_TEAM) - { - player.team = pSettings.getTeam(); - } - else - { - if (teamNumbers[j].empty()) + const auto & pSettings = pair.second; + PlayerInfo player; + player.canComputerPlay = true; + int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; + if (j == CPHUMAN) { - logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); - assert (teamNumbers[j].size()); + player.canHumanPlay = true; } - auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); - player.team = TeamID(*itTeam); - teamNumbers[j].erase(itTeam); + + if(pSettings.getTeam() != TeamID::NO_TEAM) + { + player.team = pSettings.getTeam(); + } + else + { + if (teamNumbers[j].empty()) + { + 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); + player.team = TeamID(*itTeam); + teamNumbers[j].erase(itTeam); + } + teamsTotal.insert(player.team); + map->getMap(this).players[pSettings.getColor().getNum()] = player; } - teamsTotal.insert(player.team.getNum()); - map->getMap(this).players[pSettings.getColor().getNum()] = player; + + logGlobal->info("Current team count: %d", teamsTotal.size()); + } + // FIXME: 0 + // Can't find info for player 0 (starting zone) + // Can't find info for player 1 (starting zone) map->getMap(this).howManyTeams = teamsTotal.size(); } @@ -408,8 +438,8 @@ void CMapGenerator::addHeaderInfo() m.width = mapGenOptions.getWidth(); m.height = mapGenOptions.getHeight(); m.twoLevel = mapGenOptions.getHasTwoLevels(); - m.name = VLC->generaltexth->allTexts[740]; - m.description = getMapDescription(); + m.name.appendLocalString(EMetaText::GENERAL_TXT, 740); + m.description.appendRawString(getMapDescription()); m.difficulty = 1; addPlayerInfo(); m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE); @@ -421,7 +451,7 @@ int CMapGenerator::getNextMonlithIndex() while (true) { if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size()) - throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); + throw rmgException(boost::str(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); else { //Skip modded Monoliths which can't beplaced on every terrain @@ -458,19 +488,16 @@ const std::vector CMapGenerator::getAllPossibleHeroes() const auto isWaterMap = map->getMap(this).isWaterMap(); //Skip heroes that were banned, including the ones placed in prisons std::vector ret; - for (int j = 0; j < map->getMap(this).allowedHeroes.size(); j++) + for (HeroTypeID hero : map->getMap(this).allowedHeroes) { - if (map->getMap(this).allowedHeroes[j]) + auto * h = dynamic_cast(VLC->heroTypes()->getById(hero)); + if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) { - auto * h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) - { - continue; - } - else - { - ret.push_back(HeroTypeID(j)); - } + continue; + } + else + { + ret.push_back(hero); } } return ret; @@ -479,7 +506,7 @@ const std::vector CMapGenerator::getAllPossibleHeroes() const void CMapGenerator::banQuestArt(const ArtifactID & id) { //TODO: Protect with mutex - map->getMap(this).allowedArtifact[id] = false; + map->getMap(this).allowedArtifact.erase(id); } void CMapGenerator::banHero(const HeroTypeID & id) diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index ccd0881c6..384d186a0 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -16,10 +16,10 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" -#include "../CModHandler.h" #include "../TerrainHandler.h" #include "../serializer/JsonSerializeFormat.h" -#include "../StringConstants.h" +#include "../modding/ModScope.h" +#include "../constants/StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -65,35 +65,6 @@ void CTreasureInfo::serializeJson(JsonSerializeFormat & handler) namespace rmg { -//FIXME: This is never used, instead TerrainID is used -class TerrainEncoder -{ -public: - static si32 decode(const std::string & identifier) - { - return *VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "terrain", identifier); - } - - static std::string encode(const si32 index) - { - return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); - } -}; - -class ZoneEncoder -{ -public: - static si32 decode(const std::string & json) - { - return std::stoi(json); - } - - static std::string encode(si32 id) - { - return std::to_string(id); - } -}; - const TRmgTemplateZoneId ZoneOptions::NO_ZONE = -1; ZoneOptions::CTownInfo::CTownInfo() @@ -156,7 +127,7 @@ TRmgTemplateZoneId ZoneOptions::getId() const void ZoneOptions::setId(TRmgTemplateZoneId value) { if(value <= 0) - throw std::runtime_error(boost::to_string(boost::format("Zone %d id should be greater than 0.") % id)); + throw std::runtime_error(boost::str(boost::format("Zone %d id should be greater than 0.") % id)); id = value; } @@ -217,13 +188,7 @@ std::set ZoneOptions::getDefaultTerrainTypes() const std::set ZoneOptions::getDefaultTownTypes() const { - std::set defaultTowns; - auto towns = VLC->townh->getDefaultAllowed(); - for(int i = 0; i < towns.size(); ++i) - { - if(towns[i]) defaultTowns.insert(FactionID(i)); - } - return defaultTowns; + return VLC->townh->getDefaultAllowed(); } const std::set ZoneOptions::getTownTypes() const @@ -374,15 +339,15 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) if(terrainTypeLikeZone == NO_ZONE) { - handler.serializeIdArray("terrainTypes", terrainTypes, std::set()); - handler.serializeIdArray("bannedTerrains", bannedTerrains, std::set()); + handler.serializeIdArray("terrainTypes", terrainTypes); + handler.serializeIdArray("bannedTerrains", bannedTerrains); } handler.serializeBool("townsAreSameType", townsAreSameType, false); - handler.serializeIdArray("allowedMonsters", monsterTypes, std::set()); - handler.serializeIdArray("bannedMonsters", bannedMonsters, std::set()); - handler.serializeIdArray("allowedTowns", townTypes, std::set()); - handler.serializeIdArray("bannedTowns", bannedTownTypes, std::set()); + handler.serializeIdArray("allowedMonsters", monsterTypes); + handler.serializeIdArray("bannedMonsters", bannedMonsters); + handler.serializeIdArray("allowedTowns", townTypes); + handler.serializeIdArray("bannedTowns", bannedTownTypes); { //TODO: add support for std::map to serializeEnum @@ -508,8 +473,24 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler) "random" }; - handler.serializeId("a", zoneA, -1); - handler.serializeId("b", zoneB, -1); + if (handler.saving) + { + std::string zoneNameA = std::to_string(zoneA); + std::string zoneNameB = std::to_string(zoneB); + handler.serializeString("a", zoneNameA); + handler.serializeString("b", zoneNameB); + } + else + { + std::string zoneNameA; + std::string zoneNameB; + handler.serializeString("a", zoneNameA); + handler.serializeString("b", zoneNameB); + + zoneA = std::stoi(zoneNameA); + zoneB = std::stoi(zoneNameB); + } + handler.serializeInt("guard", guardStrength, 0); handler.serializeEnum("type", connectionType, connectionTypes); handler.serializeEnum("road", hasRoad, roadOptions); @@ -570,9 +551,9 @@ const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getPlayers() const return players; } -const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getCpuPlayers() const +const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getHumanPlayers() const { - return cpuPlayers; + return humanPlayers; } const CRmgTemplate::Zones & CRmgTemplate::getZones() const @@ -650,7 +631,7 @@ std::string CRmgTemplate::CPlayerCountRange::toString() const } else { - ret += boost::to_string(boost::format("%d-%d") % p.first % p.second); + ret += boost::str(boost::format("%d-%d") % p.first % p.second); } } @@ -688,13 +669,23 @@ void CRmgTemplate::CPlayerCountRange::fromString(const std::string & value) } } +int CRmgTemplate::CPlayerCountRange::maxValue() const +{ + return *boost::max_element(getNumbers()); +} + +int CRmgTemplate::CPlayerCountRange::minValue() const +{ + return *boost::min_element(getNumbers()); +} + void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); serializeSize(handler, minSize, "minSize"); serializeSize(handler, maxSize, "maxSize"); serializePlayers(handler, players, "players"); - serializePlayers(handler, cpuPlayers, "cpu"); + serializePlayers(handler, humanPlayers, "humans"); // TODO: Rename this parameter { auto connectionsData = handler.enterArray("connections"); diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 3f5a91197..3281dddb5 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -231,6 +231,9 @@ public: std::string toString() const; void fromString(const std::string & value); + int maxValue() const; + int minValue() const; + private: std::vector > range; }; @@ -247,7 +250,7 @@ public: const std::string & getName() const; const CPlayerCountRange & getPlayers() const; - const CPlayerCountRange & getCpuPlayers() const; + const CPlayerCountRange & getHumanPlayers() const; std::pair getMapSizes() const; const Zones & getZones() const; const std::vector & getConnectedZoneIds() const; @@ -261,7 +264,7 @@ private: std::string id; std::string name; int3 minSize, maxSize; - CPlayerCountRange players, cpuPlayers; + CPlayerCountRange players, humanPlayers; Zones zones; std::vector connectedZoneIds; std::set allowedWaterContent; diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index 9947782bf..c340219fd 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -14,7 +14,6 @@ #include "CRmgTemplate.h" #include "../serializer/JsonDeserializer.h" -#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,7 +29,7 @@ void CRmgTemplateStorage::afterLoadFinalization() { for (auto& temp : templates) { - temp.second.afterLoad(); + temp.second->afterLoad(); } } @@ -40,10 +39,11 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const { JsonDeserializer handler(nullptr, data); auto fullKey = scope + ":" + name; //actually it's not used - templates[fullKey].setId(fullKey); - templates[fullKey].serializeJson(handler); - templates[fullKey].setName(name); - templates[fullKey].validate(); + templates[fullKey] = std::make_shared(); + templates[fullKey]->setId(fullKey); + templates[fullKey]->serializeJson(handler); + templates[fullKey]->setName(name); + templates[fullKey]->validate(); } catch(const std::exception & e) { @@ -51,12 +51,6 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const } } -std::vector CRmgTemplateStorage::getDefaultAllowed() const -{ - //all templates are allowed - return std::vector(); -} - std::vector CRmgTemplateStorage::loadLegacyData() { return std::vector(); @@ -68,7 +62,7 @@ const CRmgTemplate * CRmgTemplateStorage::getTemplate(const std::string & templa auto iter = templates.find(templateName); if(iter==templates.end()) return nullptr; - return &iter->second; + return iter->second.get(); } std::vector CRmgTemplateStorage::getTemplates() const @@ -77,7 +71,7 @@ std::vector CRmgTemplateStorage::getTemplates() const result.reserve(templates.size()); for(const auto & i : templates) { - result.push_back(&i.second); + result.push_back(i.second.get()); } return result; } diff --git a/lib/rmg/CRmgTemplateStorage.h b/lib/rmg/CRmgTemplateStorage.h index 2837fc262..4470c51d8 100644 --- a/lib/rmg/CRmgTemplateStorage.h +++ b/lib/rmg/CRmgTemplateStorage.h @@ -24,7 +24,6 @@ class DLL_LINKAGE CRmgTemplateStorage : public IHandlerBase public: CRmgTemplateStorage() = default; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; /// loads single object into game. Scope is namespace of this object, same as name of source mod @@ -37,7 +36,7 @@ public: std::vector getTemplates() const; private: - std::map templates; + std::map> templates; }; diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index abfb73421..63b3e76e6 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -442,13 +442,17 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const { auto player = PlayerColor(*owner - 1); auto playerSettings = map.getMapGenOptions().getPlayersSettings(); - si32 faction = CMapGenOptions::CPlayerSettings::RANDOM_TOWN; + FactionID faction = FactionID::RANDOM; if (vstd::contains(playerSettings, player)) + { faction = playerSettings[player].getStartingTown(); + } else - logGlobal->error("Can't find info for player %d (starting zone)", player.getNum()); + { + logGlobal->trace("Player %d (starting zone %d) does not participate in game", player.getNum(), zone.first); + } - if (faction == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) //TODO: check this after a town has already been randomized + if (faction == FactionID::RANDOM) //TODO: check this after a town has already been randomized zonesToPlace.push_back(zone); else { diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index 0e6320291..cdf8e0bb0 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -219,7 +219,7 @@ const CMapGenOptions& RmgMap::getMapGenOptions() const void RmgMap::assertOnMap(const int3& tile) const { if (!mapInstance->isInTheMap(tile)) - throw rmgException(boost::to_string(boost::format("Tile %s is outside the map") % tile.toString())); + throw rmgException(boost::str(boost::format("Tile %s is outside the map") % tile.toString())); } RmgMap::Zones & RmgMap::getZones() @@ -269,7 +269,7 @@ bool RmgMap::isRoad(const int3& tile) const return tiles[tile.x][tile.y][tile.z].isRoad(); } -void RmgMap::setOccupied(const int3 &tile, ETileType::ETileType state) +void RmgMap::setOccupied(const int3 &tile, ETileType state) { assertOnMap(tile); @@ -342,10 +342,10 @@ ui32 RmgMap::getTotalZoneCount() const bool RmgMap::isAllowedSpell(const SpellID & sid) const { - assert(sid >= 0); - if (sid < mapInstance->allowedSpells.size()) + assert(sid.getNum() >= 0); + if (sid.getNum() < mapInstance->allowedSpells.size()) { - return mapInstance->allowedSpells[sid]; + return mapInstance->allowedSpells.count(sid); } else return false; @@ -354,7 +354,7 @@ bool RmgMap::isAllowedSpell(const SpellID & sid) const void RmgMap::dump(bool zoneId) const { static int id = 0; - std::ofstream out(boost::to_string(boost::format("zone_%d.txt") % id++)); + std::ofstream out(boost::str(boost::format("zone_%d.txt") % id++)); int levels = mapInstance->levels(); int width = mapInstance->width; int height = mapInstance->height; diff --git a/lib/rmg/RmgMap.h b/lib/rmg/RmgMap.h index 8ceac0657..11213d031 100644 --- a/lib/rmg/RmgMap.h +++ b/lib/rmg/RmgMap.h @@ -54,7 +54,7 @@ public: int height() const; PlayerInfo & getPlayer(int playerId); - void setOccupied(const int3 &tile, ETileType::ETileType state); + void setOccupied(const int3 &tile, ETileType state); void setRoad(const int3 &tile, RoadId roadType); TileInfo getTileInfo(const int3 & tile) const; diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 540463948..19426e241 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -40,7 +40,9 @@ const Area & Object::Instance::getBlockedArea() const { dBlockedAreaCache.assign(dObject.getBlockedPos()); if(dObject.isVisitable() || dBlockedAreaCache.empty()) - dBlockedAreaCache.add(dObject.visitablePos()); + if (!dObject.isBlockedVisitable()) + // Do no assume blocked tile is accessible + dBlockedAreaCache.add(dObject.visitablePos()); } return dBlockedAreaCache; } @@ -85,9 +87,7 @@ void Object::Instance::setPosition(const int3 & position) dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } void Object::Instance::setPositionRaw(const int3 & position) @@ -97,9 +97,7 @@ void Object::Instance::setPositionRaw(const int3 & position) dObject.pos = dPosition + dParent.getPosition(); dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } auto shift = position + dParent.getPosition() - dObject.pos; @@ -111,26 +109,27 @@ void Object::Instance::setPositionRaw(const int3 & position) dObject.pos = dPosition + dParent.getPosition(); } -void Object::Instance::setAnyTemplate() +void Object::Instance::setAnyTemplate(CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(); + auto templates = dObject.getObjectHandler()->getTemplates(); if(templates.empty()) - throw rmgException(boost::to_string(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); + throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.getObjTypeIndex())); - dObject.appearance = templates.front(); + dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); setPosition(getPosition(false)); } -void Object::Instance::setTemplate(TerrainId terrain) +void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrain); + auto templates = dObject.getObjectHandler()->getTemplates(terrain); if (templates.empty()) { auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); - throw rmgException(boost::to_string(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.getObjTypeIndex() % terrainName)); } - dObject.appearance = templates.front(); + + dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); setPosition(getPosition(false)); } @@ -140,9 +139,7 @@ void Object::Instance::clear() delete &dObject; dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } bool Object::Instance::isVisitableFrom(const int3 & position) const @@ -151,6 +148,16 @@ bool Object::Instance::isVisitableFrom(const int3 & position) const return dObject.appearance->isVisitableFrom(relPosition.x, relPosition.y); } +bool Object::Instance::isBlockedVisitable() const +{ + return dObject.isBlockedVisitable(); +} + +bool Object::Instance::isRemovable() const +{ + return dObject.isRemovable(); +} + CGObjectInstance & Object::Instance::object() { return dObject; @@ -204,9 +211,7 @@ void Object::addInstance(Instance & object) setGuardedIfMonster(object); dInstances.push_back(object); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); } Object::Instance & Object::addInstance(CGObjectInstance & object) @@ -214,9 +219,7 @@ Object::Instance & Object::addInstance(CGObjectInstance & object) dInstances.emplace_back(*this, object); setGuardedIfMonster(dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); return dInstances.back(); } @@ -225,9 +228,7 @@ Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & p dInstances.emplace_back(*this, object, position); setGuardedIfMonster(dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); return dInstances.back(); } @@ -269,10 +270,56 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const return dAccessibleAreaFullCache; } +const rmg::Area & Object::getBlockVisitableArea() const +{ + if(dInstances.empty()) + return dBlockVisitableCache; + + for(const auto & i : dInstances) + { + // FIXME: Account for blockvis objects with multiple visitable tiles + if (i.isBlockedVisitable()) + dBlockVisitableCache.add(i.getVisitablePosition()); + } + + return dBlockVisitableCache; +} + +const rmg::Area & Object::getRemovableArea() const +{ + if(dInstances.empty()) + return dRemovableAreaCache; + + for(const auto & i : dInstances) + { + if (i.isRemovable()) + dRemovableAreaCache.unite(i.getBlockedArea()); + } + + return dRemovableAreaCache; +} + +const rmg::Area Object::getEntrableArea() const +{ + // Calculate Area that hero can freely pass + + // Do not use blockVisitTiles, unless they belong to removable objects (resources etc.) + // area = accessibleArea - (blockVisitableArea - removableArea) + + rmg::Area entrableArea = getAccessibleArea(); + rmg::Area blockVisitableArea = getBlockVisitableArea(); + blockVisitableArea.subtract(getRemovableArea()); + entrableArea.subtract(blockVisitableArea); + + return entrableArea; +} + void Object::setPosition(const int3 & position) { dAccessibleAreaCache.translate(position - dPosition); dAccessibleAreaFullCache.translate(position - dPosition); + dBlockVisitableCache.translate(position - dPosition); + dRemovableAreaCache.translate(position - dPosition); dFullAreaCache.translate(position - dPosition); dPosition = position; @@ -280,10 +327,10 @@ void Object::setPosition(const int3 & position) i.setPositionRaw(i.getPosition()); } -void Object::setTemplate(const TerrainId & terrain) +void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng) { for(auto& i : dInstances) - i.setTemplate(terrain); + i.setTemplate(terrain, rng); } const Area & Object::getArea() const @@ -325,62 +372,70 @@ void rmg::Object::setGuardedIfMonster(const Instance& object) } } -void Object::Instance::finalize(RmgMap & map) +void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) { if(!map.isOnMap(getPosition(true))) - throw rmgException(boost::to_string(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); + throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); //If no specific template was defined for this object, select any matching if (!dObject.appearance) { const auto * terrainType = map.getTile(getPosition(true)).terType; - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrainType->getId()); + auto templates = dObject.getObjectHandler()->getTemplates(terrainType->getId()); if (templates.empty()) { - throw rmgException(boost::to_string(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.getObjTypeIndex() % getPosition(true).toString() % terrainType)); } else { - setTemplate(terrainType->getId()); + setTemplate(terrainType->getId(), rng); } } if (dObject.isVisitable() && !map.isOnMap(dObject.visitablePos())) - throw rmgException(boost::to_string(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.pos.toString())); for(const auto & tile : dObject.getBlockedPos()) { if(!map.isOnMap(tile)) - throw rmgException(boost::to_string(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.pos.toString())); } for(const auto & tile : getBlockedArea().getTilesVector()) { - map.setOccupied(tile, ETileType::ETileType::USED); + map.setOccupied(tile, ETileType::USED); } map.getMapProxy()->insertObject(&dObject); } -void Object::finalize(RmgMap & map) +void Object::finalize(RmgMap & map, CRandomGenerator & rng) { if(dInstances.empty()) throw rmgException("Cannot finalize object without instances"); for(auto & dInstance : dInstances) { - dInstance.finalize(map); + dInstance.finalize(map, rng); } } +void Object::clearCachedArea() const +{ + dFullAreaCache.clear(); + dAccessibleAreaCache.clear(); + dAccessibleAreaFullCache.clear(); + dBlockVisitableCache.clear(); + dRemovableAreaCache.clear(); +} + void Object::clear() { for(auto & instance : dInstances) instance.clear(); dInstances.clear(); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + + clearCachedArea(); } diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index d444a90b4..f5c523dcc 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; +class CRandomGenerator; class RmgMap; namespace rmg { @@ -34,9 +35,11 @@ public: int3 getVisitablePosition() const; bool isVisitableFrom(const int3 & tile) const; + bool isBlockedVisitable() const; + bool isRemovable() const; const Area & getAccessibleArea() const; - void setTemplate(TerrainId terrain); //cache invalidation - void setAnyTemplate(); //cache invalidation + void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation + void setAnyTemplate(CRandomGenerator &); //cache invalidation int3 getTopTile() const; int3 getPosition(bool isAbsolute = false) const; @@ -45,7 +48,7 @@ public: const CGObjectInstance & object() const; CGObjectInstance & object(); - void finalize(RmgMap & map); //cache invalidation + void finalize(RmgMap & map, CRandomGenerator &); //cache invalidation void clear(); private: @@ -70,10 +73,13 @@ public: int3 getVisitablePosition() const; const Area & getAccessibleArea(bool exceptLast = false) const; + const Area & getBlockVisitableArea() const; + const Area & getRemovableArea() const; + const Area getEntrableArea() const; const int3 & getPosition() const; void setPosition(const int3 & position); - void setTemplate(const TerrainId & terrain); + void setTemplate(const TerrainId & terrain, CRandomGenerator &); const Area & getArea() const; //lazy cache invalidation const int3 getVisibleTop() const; @@ -81,13 +87,16 @@ public: bool isGuarded() const; void setGuardedIfMonster(const Instance & object); - void finalize(RmgMap & map); + void finalize(RmgMap & map, CRandomGenerator &); + void clearCachedArea() const; void clear(); private: std::list dInstances; mutable Area dFullAreaCache; mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache; + mutable Area dBlockVisitableCache; + mutable Area dRemovableAreaCache; int3 dPosition; ui32 dStrength; bool guarded; diff --git a/lib/rmg/TileInfo.cpp b/lib/rmg/TileInfo.cpp index 405a430b5..e1701a3b7 100644 --- a/lib/rmg/TileInfo.cpp +++ b/lib/rmg/TileInfo.cpp @@ -54,12 +54,12 @@ bool TileInfo::isUsed() const { return occupied == ETileType::USED; } -void TileInfo::setOccupied(ETileType::ETileType value) +void TileInfo::setOccupied(ETileType value) { occupied = value; } -ETileType::ETileType TileInfo::getTileType() const +ETileType TileInfo::getTileType() const { return occupied; } diff --git a/lib/rmg/TileInfo.h b/lib/rmg/TileInfo.h index 61e874a74..6df6fcc60 100644 --- a/lib/rmg/TileInfo.h +++ b/lib/rmg/TileInfo.h @@ -28,15 +28,15 @@ public: bool isFree() const; bool isUsed() const; bool isRoad() const; - void setOccupied(ETileType::ETileType value); + void setOccupied(ETileType value); TerrainId getTerrainType() const; - ETileType::ETileType getTileType() const; + ETileType getTileType() const; void setTerrainType(TerrainId value); void setRoadType(RoadId type); private: float nearestObjectDistance; - ETileType::ETileType occupied; + ETileType occupied; TerrainId terrain; RoadId roadType; }; diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index 8da771c6d..5f6e12d0c 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -127,10 +127,10 @@ rmg::Area & Zone::freePaths() FactionID Zone::getTownType() const { - return FactionID(townType); + return townType; } -void Zone::setTownType(si32 town) +void Zone::setTownType(FactionID town) { townType = town; } diff --git a/lib/rmg/Zone.h b/lib/rmg/Zone.h index 906605832..b6aa3b3ce 100644 --- a/lib/rmg/Zone.h +++ b/lib/rmg/Zone.h @@ -59,7 +59,7 @@ public: void fractalize(); FactionID getTownType() const; - void setTownType(si32 town); + void setTownType(FactionID town); TerrainId getTerrainType() const; void setTerrainType(TerrainId terrain); @@ -108,7 +108,7 @@ protected: std::vector possibleQuestArtifactPos; //template info - si32 townType; + FactionID townType; TerrainId terrainType; }; diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 5b923acb7..10ab3a184 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -25,7 +25,6 @@ #include "WaterAdopter.h" #include "WaterProxy.h" #include "TownPlacer.h" -#include VCMI_LIB_NAMESPACE_BEGIN @@ -317,8 +316,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c auto * gate2 = factory->create(); rmg::Object rmgGate1(*gate1); rmg::Object rmgGate2(*gate2); - rmgGate1.setTemplate(zone.getTerrainType()); - rmgGate2.setTemplate(otherZone->getTerrainType()); + rmgGate1.setTemplate(zone.getTerrainType(), zone.getRand()); + rmgGate2.setTemplate(otherZone->getTerrainType(), zone.getRand()); bool guarded1 = manager.addGuard(rmgGate1, connection.getGuardStrength(), true); bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true); int minDist = 3; @@ -335,16 +334,12 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c return -1.f; //This could fail is accessibleArea is below the map - rmg::Area toPlace(rmgGate1.getArea() + rmgGate1.getAccessibleArea()); - auto inTheMap = toPlace.getTilesVector(); - toPlace.clear(); - for (const int3& tile : inTheMap) + rmg::Area toPlace(rmgGate1.getArea()); + toPlace.unite(toPlace.getBorderOutside()); // Add a bit of extra space around + toPlace.erase_if([this](const int3 & tile) { - if (map.isOnMap(tile)) - { - toPlace.add(tile); - } - } + return !map.isOnMap(tile); + }); toPlace.translate(-zShift); path2 = managerOther.placeAndConnectObject(toPlace, rmgGate2, minDist, guarded2, true, ObjectManager::OptimizeType::NONE); diff --git a/lib/rmg/modificators/Modificator.cpp b/lib/rmg/modificators/Modificator.cpp index bfd03c664..9f2bfa004 100644 --- a/lib/rmg/modificators/Modificator.cpp +++ b/lib/rmg/modificators/Modificator.cpp @@ -119,7 +119,7 @@ void Modificator::postfunction(Modificator * modificator) void Modificator::dump() { - std::ofstream out(boost::to_string(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName())); + std::ofstream out(boost::str(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName())); int levels = map.levels(); int width = map.width(); int height = map.height(); diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 92570b791..5e9eacca8 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -42,8 +42,6 @@ void ObjectDistributor::init() void ObjectDistributor::distributeLimitedObjects() { - //FIXME: Must be called after TerrainPainter::process() - ObjectInfo oi; auto zones = map.getZones(); @@ -77,27 +75,18 @@ void ObjectDistributor::distributeLimitedObjects() auto rmgInfo = handler->getRMGInfo(); + // FIXME: Random order of distribution + RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand()); for (auto& zone : matchingZones) { - //We already know there are some templates - auto templates = handler->getTemplates(zone->getTerrainType()); - - //FIXME: Templates empty?! Maybe zone changed terrain type over time? - - //Assume the template with fewest terrains is the most suitable - auto temp = *boost::min_element(templates, [](std::shared_ptr lhs, std::shared_ptr rhs) -> bool + oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * { - return lhs->getAllowedTerrains().size() < rhs->getAllowedTerrains().size(); - }); - - oi.generateObject = [temp]() -> CGObjectInstance * - { - return VLC->objtypeh->getHandlerFor(temp->id, temp->subid)->create(temp); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; - oi.templ = temp; + oi.setTemplates(primaryID, secondaryID, zone->getTerrainType()); //Rounding up will make sure all possible objects are exhausted uint32_t mapLimit = rmgInfo.mapLimit.value(); @@ -109,7 +98,7 @@ void ObjectDistributor::distributeLimitedObjects() rmgInfo.setMapLimit(mapLimit - oi.maxPerZone); //Don't add objects with 0 count remaining - if (oi.maxPerZone) + if(oi.maxPerZone && !oi.templates.empty()) { zone->getModificator()->addObjectToRandomPool(oi); } diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 02267aaa8..f05ab1bc9 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -354,7 +354,7 @@ bool ObjectManager::createRequiredObjects() for(const auto & objInfo : requiredObjects) { rmg::Object rmgObject(*objInfo.obj); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY)); Zone::Lock lock(zone.areaMutex); @@ -394,7 +394,7 @@ bool ObjectManager::createRequiredObjects() auto possibleArea = zone.areaPossible(); rmg::Object rmgObject(*objInfo.obj); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY)); auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, [this, &rmgObject](const int3 & tile) @@ -447,7 +447,6 @@ bool ObjectManager::createRequiredObjects() instance->object().getObjectName(), instance->getPosition(true).toString()); mapProxy->removeObject(&instance->object()); } - rmgNearObject.clear(); } } @@ -480,7 +479,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD if (!monster->object().appearance) { //Needed to determine visitable offset - monster->setAnyTemplate(); + monster->setAnyTemplate(zone.getRand()); } object.getPosition(); auto visitableOffset = monster->object().getVisitableOffset(); @@ -492,7 +491,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD int3 parentOffset = monster->getPosition(true) - monster->getPosition(false); monster->setPosition(fixedPos - parentOffset); } - object.finalize(map); + object.finalize(map, zone.getRand()); Zone::Lock lock(zone.areaMutex); zone.areaPossible().subtract(object.getArea()); @@ -547,8 +546,12 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD objects.push_back(&instance->object()); if(auto * m = zone.getModificator()) { - //FIXME: Objects that can be removed, can be trespassed. Does not include Corpse - if(instance->object().appearance->isVisitableFromTop()) + if (instance->object().blockVisit && !instance->object().removable) + { + //Cannot be trespassed (Corpse) + continue; + } + else if(instance->object().appearance->isVisitableFromTop()) m->areaForRoads().add(instance->getVisitablePosition()); else { @@ -556,7 +559,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD } } - switch (instance->object().ID) + switch (instance->object().ID.toEnum()) { case Obj::RANDOM_TREASURE_ART: case Obj::RANDOM_MINOR_ART: //In OH3 quest artifacts have higher value than normal arts @@ -586,7 +589,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD case Obj::MONOLITH_ONE_WAY_EXIT: */ - switch (object.instances().front()->object().ID) + switch(object.instances().front()->object().ID.toEnum()) { case Obj::WATER_WHEEL: if (auto* m = zone.getModificator()) @@ -639,14 +642,14 @@ CGCreature * ObjectManager::chooseGuard(si32 strength, bool zoneGuard) if(!possibleCreatures.empty()) { creId = *RandomGeneratorUtil::nextItem(possibleCreatures, zone.getRand()); - amount = strength / VLC->creh->objects[creId]->getAIValue(); + amount = strength / creId.toEntity(VLC)->getAIValue(); if (amount >= 4) amount = static_cast(amount * zone.getRand().nextDouble(0.75, 1.25)); } else //just pick any available creature { - creId = CreatureID(132); //Azure Dragon - amount = strength / VLC->creh->objects[creId]->getAIValue(); + creId = CreatureID::AZURE_DRAGON; //Azure Dragon + amount = strength / creId.toEntity(VLC)->getAIValue(); } auto guardFactory = VLC->objtypeh->getHandlerFor(Obj::MONSTER, creId); @@ -665,7 +668,19 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard if(!guard) return false; - rmg::Area visitablePos({object.getVisitablePosition()}); + // Prefer non-blocking tiles, if any + auto entrableTiles = object.getEntrableArea().getTiles(); + int3 entrableTile(-1, -1, -1); + if (entrableTiles.empty()) + { + entrableTile = object.getVisitablePosition(); + } + else + { + entrableTile = *RandomGeneratorUtil::nextItem(entrableTiles, zone.getRand()); + } + + rmg::Area visitablePos({entrableTile}); visitablePos.unite(visitablePos.getBorderOutside()); auto accessibleArea = object.getAccessibleArea(); @@ -689,7 +704,7 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard }); auto & instance = object.addInstance(*guard); - instance.setAnyTemplate(); //terrain is irrelevant for monsters, but monsters need some template now + instance.setAnyTemplate(zone.getRand()); //terrain is irrelevant for monsters, but monsters need some template now //Fix HoTA monsters with offset template auto visitableOffset = instance.object().getVisitableOffset(); diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 5ce043c42..d9c7af3e1 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -387,17 +387,17 @@ void RiverPlacer::connectRiver(const int3 & tile) if(tmplates.size() > 3) { if(tmplates.size() % 4 != 0) - throw rmgException(boost::to_string(boost::format("River templates for (%d,%d) at terrain %s, river %s are incorrect") % + throw rmgException(boost::str(boost::format("River templates for (%d,%d) at terrain %s, river %s are incorrect") % RIVER_DELTA_ID % RIVER_DELTA_SUBTYPE % zone.getTerrainType() % river->shortIdentifier)); - std::string targetTemplateName = river->deltaName + std::to_string(deltaOrientations[pos]) + ".def"; + AnimationPath targetTemplateName = AnimationPath::builtinTODO(river->deltaName + std::to_string(deltaOrientations[pos]) + ".def"); for(auto & templ : tmplates) { if(templ->animationFile == targetTemplateName) { auto * obj = handler->create(templ); rmg::Object deltaObj(*obj, deltaPositions[pos]); - deltaObj.finalize(map); + deltaObj.finalize(map, zone.getRand()); } } } diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index c7e30e0a2..b081f9485 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -16,8 +16,9 @@ #include "../Functions.h" #include "../CMapGenerator.h" #include "../threadpool/MapProxy.h" -#include "../../CModHandler.h" #include "../../mapping/CMapEditManager.h" +#include "../../modding/IdentifierStorage.h" +#include "../../modding/ModScope.h" #include "../../TerrainHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -84,7 +85,7 @@ bool RoadPlacer::createRoad(const int3 & dst) { if(areaIsolated().contains(dst) || areaIsolated().contains(src)) { - return 1e30; + return 1e12; } } else @@ -143,7 +144,7 @@ void RoadPlacer::drawRoads(bool secondary) auto tiles = roads.getTilesVector(); std::string roadName = (secondary ? generator.getConfig().secondaryRoadType : generator.getConfig().defaultRoadType); - RoadId roadType(*VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "road", roadName)); + RoadId roadType(*VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "road", roadName)); //If our road type is not enabled, choose highest below it for (int8_t bestRoad = roadType.getNum(); bestRoad > RoadId(Road::NO_ROAD).getNum(); bestRoad--) @@ -200,7 +201,7 @@ void RoadPlacer::connectRoads() catch (const std::exception & e) { logGlobal->error("Unhandled exception while drawing road to node %s: %s", node.toString(), e.what()); - throw e; + throw; } } diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index ecb12d7b6..baa6b2d6e 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -60,7 +60,7 @@ void TownPlacer::placeTowns(ObjectManager & manager) player = PlayerColor(player_id); zone.setTownType(map.getMapGenOptions().getPlayersSettings().find(player)->second.getStartingTown()); - if(zone.getTownType() == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) + if(zone.getTownType() == FactionID::RANDOM) zone.setTownType(getRandomTownType(true)); } else //no player - randomize town @@ -140,7 +140,7 @@ int3 TownPlacer::placeMainTown(ObjectManager & manager, CGTownInstance & town) { //towns are big objects and should be centered around visitable position rmg::Object rmgObject(town); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); int3 position(-1, -1, -1); { @@ -179,7 +179,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player { for(int i = 0; i < count; i++) { - si32 subType = zone.getTownType(); + FactionID subType = zone.getTownType(); if(totalTowns>0) { @@ -223,7 +223,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player } } -si32 TownPlacer::getRandomTownType(bool matchUndergroundType) +FactionID TownPlacer::getRandomTownType(bool matchUndergroundType) { auto townTypesAllowed = (!zone.getTownTypes().empty() ? zone.getTownTypes() : zone.getDefaultTownTypes()); if(matchUndergroundType) diff --git a/lib/rmg/modificators/TownPlacer.h b/lib/rmg/modificators/TownPlacer.h index a144da17d..75028093b 100644 --- a/lib/rmg/modificators/TownPlacer.h +++ b/lib/rmg/modificators/TownPlacer.h @@ -28,7 +28,7 @@ public: protected: void cleanupBoundaries(const rmg::Object & rmgObject); void addNewTowns(int count, bool hasFort, const PlayerColor & player, ObjectManager & manager); - si32 getRandomTownType(bool matchUndergroundType = false); + FactionID getRandomTownType(bool matchUndergroundType = false); void placeTowns(ObjectManager & manager); bool placeMines(ObjectManager & manager); int3 placeMainTown(ObjectManager & manager, CGTownInstance & town); diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 4e84dc882..755bda88a 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -72,26 +72,16 @@ void TreasurePlacer::addAllPossibleObjects() continue; } - auto templates = handler->getTemplates(zone.getTerrainType()); - if (templates.empty()) - continue; - - //TODO: Reuse chooseRandomAppearance (eg. WoG treasure chests) - //Assume the template with fewest terrains is the most suitable - auto temp = *boost::min_element(templates, [](std::shared_ptr lhs, std::shared_ptr rhs) -> bool + oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * { - return lhs->getAllowedTerrains().size() < rhs->getAllowedTerrains().size(); - }); - - oi.generateObject = [temp]() -> CGObjectInstance * - { - return VLC->objtypeh->getHandlerFor(temp->id, temp->subid)->create(temp); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; - oi.templ = temp; + oi.setTemplates(primaryID, secondaryID, zone.getTerrainType()); oi.maxPerZone = rmgInfo.zoneLimit; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } } @@ -121,22 +111,22 @@ void TreasurePlacer::addAllPossibleObjects() auto factory = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0); auto* obj = dynamic_cast(factory->create()); - obj->subID = hid; //will be initialized later + obj->setHeroType(hid); //will be initialized later obj->exp = generator.getConfig().prisonExperience[i]; obj->setOwner(PlayerColor::NEUTRAL); generator.banHero(hid); - obj->appearance = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType()).front(); //can't init template with hero subID return obj; }; - oi.setTemplate(Obj::PRISON, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PRISON, 0, zone.getTerrainType()); oi.value = generator.getConfig().prisonValues[i]; oi.probability = 30; //Distribute all allowed prisons, starting from the most valuable oi.maxPerZone = (std::ceil((float)prisonsLeft / (i + 1))); prisonsLeft -= oi.maxPerZone; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } @@ -165,8 +155,8 @@ void TreasurePlacer::addAllPossibleObjects() if(dwellingType == Obj::CREATURE_GENERATOR1) { //don't spawn original "neutral" dwellings that got replaced by Conflux dwellings in AB - static int elementalConfluxROE[] = {7, 13, 16, 47}; - for(int & i : elementalConfluxROE) + static MapObjectSubID elementalConfluxROE[] = {7, 13, 16, 47}; + for(auto const & i : elementalConfluxROE) vstd::erase_if_present(subObjects, i); } @@ -183,22 +173,16 @@ void TreasurePlacer::addAllPossibleObjects() auto nativeZonesCount = static_cast(map.getZoneCount(cre->getFaction())); oi.value = static_cast(cre->getAIValue() * cre->getGrowth() * (1 + (nativeZonesCount / map.getTotalZoneCount()) + (nativeZonesCount / 2))); oi.probability = 40; - - for(const auto & tmplate : dwellingHandler->getTemplates()) + + oi.generateObject = [secondaryID, dwellingType]() -> CGObjectInstance * { - if(tmplate->canBePlacedAt(zone.getTerrainType())) - { - oi.generateObject = [tmplate, secondaryID, dwellingType]() -> CGObjectInstance * - { - auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(tmplate); - obj->tempOwner = PlayerColor::NEUTRAL; - return obj; - }; - - oi.templ = tmplate; - addObjectToRandomPool(oi); - } - } + auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(); + obj->tempOwner = PlayerColor::NEUTRAL; + return obj; + }; + oi.setTemplates(dwellingType, secondaryID, zone.getTerrainType()); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } } @@ -213,7 +197,7 @@ void TreasurePlacer::addAllPossibleObjects() for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - if(map.isAllowedSpell(spell->id) && spell->level == i + 1) + if(map.isAllowedSpell(spell->id) && spell->getLevel() == i + 1) { out.push_back(spell->id); } @@ -222,10 +206,11 @@ void TreasurePlacer::addAllPossibleObjects() obj->storedArtifact = a; return obj; }; - oi.setTemplate(Obj::SPELL_SCROLL, 0, zone.getTerrainType()); + oi.setTemplates(Obj::SPELL_SCROLL, 0, zone.getTerrainType()); oi.value = generator.getConfig().scrollValues[i]; oi.probability = 30; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with gold @@ -235,13 +220,19 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - obj->resources[EGameResID::GOLD] = i * 5000; + + Rewardable::VisitInfo reward; + reward.reward.resources[EGameResID::GOLD] = i * 5000; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = i * generator.getConfig().pandoraMultiplierGold; oi.probability = 5; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with experience @@ -251,13 +242,19 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - obj->gainedExp = i * 5000; + + Rewardable::VisitInfo reward; + reward.reward.heroExperience = i * 5000; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = i * generator.getConfig().pandoraMultiplierExperience; oi.probability = 20; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with creatures @@ -307,14 +304,19 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - auto * stack = new CStackInstance(creature, creaturesAmount); - obj->creatures.putStack(SlotID(0), stack); + + Rewardable::VisitInfo reward; + reward.reward.creatures.emplace_back(creature, creaturesAmount); + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + 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.probability = 3; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //Pandora with 12 spells of certain level @@ -328,22 +330,26 @@ void TreasurePlacer::addAllPossibleObjects() std::vector spells; for(auto spell : VLC->spellh->objects) { - if(map.isAllowedSpell(spell->id) && spell->level == i) + if(map.isAllowedSpell(spell->id) && spell->getLevel() == i) spells.push_back(spell); } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(12, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = (i + 1) * generator.getConfig().pandoraMultiplierSpells; //5000 - 15000 oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //Pandora with 15 spells of certain school @@ -362,17 +368,21 @@ void TreasurePlacer::addAllPossibleObjects() } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(15, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = generator.getConfig().pandoraSpellSchool; oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } // Pandora box with 60 random spells @@ -390,17 +400,21 @@ void TreasurePlacer::addAllPossibleObjects() } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(60, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = generator.getConfig().pandoraSpell60; oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); //Seer huts with creatures or generic rewards @@ -441,16 +455,14 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - obj->rewardType = CGSeerHut::CREATURE; - obj->rID = creature->getId(); - obj->rVal = creaturesAmount; - - obj->quest->missionType = CQuest::MISSION_ART; + Rewardable::VisitInfo reward; + reward.reward.creatures.emplace_back(creature->getId(), creaturesAmount); + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -458,7 +470,7 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; oi.probability = 3; - oi.setTemplate(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); + 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); if (oi.value > zone.getMaxTreasureValue()) { @@ -466,7 +478,8 @@ void TreasurePlacer::addAllPossibleObjects() } else { - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); } } @@ -475,7 +488,7 @@ void TreasurePlacer::addAllPossibleObjects() { int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); - oi.setTemplate(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); + oi.setTemplates(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); oi.value = generator.getConfig().questValues[i]; if (oi.value > zone.getMaxTreasureValue()) { @@ -490,16 +503,14 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - - obj->rewardType = CGSeerHut::EXPERIENCE; - obj->rID = 0; //unitialized? - obj->rVal = generator.getConfig().questRewardValues[i]; - obj->quest->missionType = CQuest::MISSION_ART; + Rewardable::VisitInfo reward; + reward.reward.heroExperience = generator.getConfig().questRewardValues[i]; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -507,21 +518,21 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - obj->rewardType = CGSeerHut::RESOURCES; - obj->rID = GameResID(EGameResID::GOLD); - obj->rVal = generator.getConfig().questRewardValues[i]; - obj->quest->missionType = CQuest::MISSION_ART; + Rewardable::VisitInfo reward; + reward.reward.resources[EGameResID::GOLD] = generator.getConfig().questRewardValues[i]; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -529,7 +540,8 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); } if (possibleSeerHuts.empty()) @@ -582,7 +594,12 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo if(!oi) //fail break; - if(oi->templ->isVisitableFromTop()) + bool visitableFromTop = true; + for(auto & t : oi->templates) + if(!t->isVisitableFromTop()) + visitableFromTop = false; + + if(visitableFromTop) { objectInfos.push_back(oi); } @@ -608,17 +625,31 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector for(const auto & oi : treasureInfos) { auto blockedArea = rmgObject.getArea(); - auto accessibleArea = rmgObject.getAccessibleArea(); + auto entrableArea = rmgObject.getEntrableArea(); + if(rmgObject.instances().empty()) - accessibleArea.add(int3()); + entrableArea.add(int3()); auto * object = oi->generateObject(); - object->appearance = oi->templ; + if(oi->templates.empty()) + continue; + + object->appearance = *RandomGeneratorUtil::nextItem(oi->templates, zone.getRand()); + + auto blockingIssue = object->isBlockedVisitable() && !object->isRemovable(); + if (blockingIssue) + { + // Do not place next to another such object (Corpse issue) + // Calculate this before instance is added to rmgObject + auto blockVisitProximity = rmgObject.getBlockVisitableArea().getBorderOutside(); + entrableArea.subtract(blockVisitProximity); + } + auto & instance = rmgObject.addInstance(*object); do { - if(accessibleArea.empty()) + if(entrableArea.empty()) { //fail - fallback rmgObject.clear(); @@ -629,12 +660,14 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector if(densePlacement) { int bestPositionsWeight = std::numeric_limits::max(); - for(const auto & t : accessibleArea.getTilesVector()) + for(const auto & t : entrableArea.getTilesVector()) { instance.setPosition(t); - int w = rmgObject.getAccessibleArea().getTilesVector().size(); - if(w < bestPositionsWeight) + int w = rmgObject.getEntrableArea().getTilesVector().size(); + + if(w && w < bestPositionsWeight) { + // Minimum 1 position must be entrable bestPositions.clear(); bestPositions.push_back(t); bestPositionsWeight = w; @@ -644,10 +677,12 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector bestPositions.push_back(t); } } + } - else + + if (bestPositions.empty()) { - bestPositions = accessibleArea.getTilesVector(); + bestPositions = entrableArea.getTilesVector(); } int3 nextPos = *RandomGeneratorUtil::nextItem(bestPositions, zone.getRand()); @@ -656,20 +691,19 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector auto instanceAccessibleArea = instance.getAccessibleArea(); if(instance.getBlockedArea().getTilesVector().size() == 1) { - if(instance.object().appearance->isVisitableFromTop() && instance.object().ID != Obj::CORPSE) + if(instance.object().appearance->isVisitableFromTop() && !instance.object().isBlockedVisitable()) instanceAccessibleArea.add(instance.getVisitablePosition()); } //first object is good if(rmgObject.instances().size() == 1) break; - - //condition for good position - if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea)) + + if(!blockedArea.overlap(instance.getBlockedArea()) && entrableArea.overlap(instanceAccessibleArea)) break; - + //fail - new position - accessibleArea.erase(nextPos); + entrableArea.erase(nextPos); } while(true); } return rmgObject; @@ -689,7 +723,12 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu if(oi.value > maxVal) break; //this assumes values are sorted in ascending order - if(!oi.templ->isVisitableFromTop() && !allowLargeObjects) + bool visitableFromTop = true; + for(auto & t : oi.templates) + if(!t->isVisitableFromTop()) + visitableFromTop = false; + + if(!visitableFromTop && !allowLargeObjects) continue; if(oi.value >= minValue && oi.maxPerZone > 0) @@ -797,13 +836,18 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); - for (ui32 attempt = 0; attempt <= 2; attempt++) + const ui32 maxPileGenerationAttemps = 2; + for (ui32 attempt = 0; attempt <= maxPileGenerationAttemps; attempt++) { auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); - if (rmgObject.instances().empty()) //handle incorrect placement + if (rmgObject.instances().empty()) { - restoreZoneLimits(treasurePileInfos); + // Restore once if all attemps failed + if (attempt == (maxPileGenerationAttemps - 1)) + { + restoreZoneLimits(treasurePileInfos); + } continue; } @@ -893,17 +937,13 @@ char TreasurePlacer::dump(const int3 & t) return Modificator::dump(t); } -void ObjectInfo::setTemplate(si32 type, si32 subtype, TerrainId terrainType) +void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType) { auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); if(!templHandler) return; - auto templates = templHandler->getTemplates(terrainType); - if(templates.empty()) - return; - - templ = templates.front(); + templates = templHandler->getTemplates(terrainType); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index 822bc793b..88d10f5a7 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -22,16 +22,14 @@ class CRandomGenerator; struct ObjectInfo { - std::shared_ptr templ; + std::vector> templates; ui32 value = 0; ui16 probability = 0; ui32 maxPerZone = 1; //ui32 maxPerMap; //unused std::function generateObject; - void setTemplate(si32 type, si32 subtype, TerrainId terrain); - - bool operator==(const ObjectInfo& oi) const { return (templ == oi.templ); } + void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); }; class TreasurePlacer: public Modificator diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index 023a13af1..839511d7c 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -254,7 +254,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout auto * boat = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(sailingBoatTypes, zone.getRand()))->create()); rmg::Object rmgObject(*boat); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); auto waterAvailable = zone.areaPossible() + zone.freePaths(); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles @@ -319,7 +319,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool shipyard->tempOwner = PlayerColor::NEUTRAL; rmg::Object rmgObject(*shipyard); - rmgObject.setTemplate(land.getTerrainType()); + rmgObject.setTemplate(land.getTerrainType(), zone.getRand()); bool guarded = manager->addGuard(rmgObject, guard); auto waterAvailable = zone.areaPossible() + zone.freePaths(); diff --git a/lib/rmg/threadpool/ThreadPool.h b/lib/rmg/threadpool/ThreadPool.h index 1de524c50..3fbb81612 100644 --- a/lib/rmg/threadpool/ThreadPool.h +++ b/lib/rmg/threadpool/ThreadPool.h @@ -169,7 +169,7 @@ inline void ThreadPool::cancel() auto ThreadPool::async(std::function&& f) const -> boost::future { - using TaskT = boost::packaged_task; + using TaskT = boost::packaged_task; { Lock lock(mx); @@ -189,4 +189,4 @@ auto ThreadPool::async(std::function&& f) const -> boost::future return fut; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 7fe1bfd5a..0cb95f14a 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -9,98 +9,18 @@ */ #include "StdInc.h" #include "BinaryDeserializer.h" -#include "../filesystem/FileStream.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinaryDeserializer & s); - -CLoadFile::CLoadFile(const boost::filesystem::path & fname, int minimalVersion) - : serializer(this) +BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) { - registerTypes(serializer); - openNextFile(fname, minimalVersion); -} + saving = false; + fileVersion = 0; + smartPointerSerialization = true; + reverseEndianess = false; -//must be instantiated in .cpp file for access to complete types of all member fields -CLoadFile::~CLoadFile() = default; - -int CLoadFile::read(void * data, unsigned size) -{ - sfile->read(reinterpret_cast(data), size); - return size; -} - -void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalVersion) -{ - assert(!serializer.reverseEndianess); - assert(minimalVersion <= SERIALIZATION_VERSION); - - try - { - fName = fname.string(); - sfile = std::make_unique(fname, std::ios::in | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to read %s!", fName); - - //we can read - char buffer[4]; - sfile->read(buffer, 4); - if(std::memcmp(buffer, "VCMI", 4) != 0) - THROW_FORMAT("Error: not a VCMI file(%s)!", fName); - - serializer & serializer.fileVersion; - if(serializer.fileVersion < minimalVersion) - THROW_FORMAT("Error: too old file format (%s)!", fName); - - if(serializer.fileVersion > SERIALIZATION_VERSION) - { - logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", serializer.fileVersion, SERIALIZATION_VERSION , fName); - - auto * versionptr = reinterpret_cast(&serializer.fileVersion); - std::reverse(versionptr, versionptr + 4); - logGlobal->warn("Version number reversed is %x, checking...", serializer.fileVersion); - - if(serializer.fileVersion == SERIALIZATION_VERSION) - { - logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); - serializer.reverseEndianess = true; - } - else - THROW_FORMAT("Error: too new file format (%s)!", fName); - } - } - catch(...) - { - clear(); //if anything went wrong, we delete file and rethrow - throw; - } -} - -void CLoadFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CLoadFile"); - if(!!sfile && *sfile) - out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); -} - -void CLoadFile::clear() -{ - sfile = nullptr; - fName.clear(); - serializer.fileVersion = 0; -} - -void CLoadFile::checkMagicBytes(const std::string &text) -{ - std::string loaded = text; - read((void *)loaded.data(), static_cast(text.length())); - if(loaded != text) - throw std::runtime_error("Magic bytes doesn't match!"); + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 6b55409af..74d8243ab 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -9,18 +9,12 @@ */ #pragma once -#include -#include - +#include "CSerializer.h" #include "CTypeList.h" #include "../mapObjects/CGHeroInstance.h" -#include "../../Global.h" VCMI_LIB_NAMESPACE_BEGIN -class CStackInstance; -class FileStream; - class DLL_LINKAGE CLoaderBase { protected: @@ -38,41 +32,6 @@ public: /// Effectively revesed version of BinarySerializer class DLL_LINKAGE BinaryDeserializer : public CLoaderBase { - template - struct VariantLoaderHelper - { - Source & source; - std::vector> funcs; - - template - struct mpl_types_impl; - - template - struct mpl_types_impl> { - using type = boost::mpl::vector; - }; - - template - using mpl_types = typename mpl_types_impl::type; - - VariantLoaderHelper(Source & source): - source(source) - { - boost::mpl::for_each>(std::ref(*this)); - } - - template - void operator()(Type) - { - funcs.push_back([&]() -> Variant - { - Type obj; - source.load(obj); - return Variant(obj); - }); - } - }; - template struct LoadIfStackInstance { @@ -139,40 +98,40 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase return length; } - template class CPointerLoader; + template class CPointerLoader; - class CBasicPointerLoader + class IPointerLoader { public: - virtual const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER - virtual ~CBasicPointerLoader(){} + virtual void * loadPtr(CLoaderBase &ar, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER + virtual ~IPointerLoader() = default; - template static CBasicPointerLoader *getApplier(const T * t=nullptr) + template static IPointerLoader *getApplier(const Type * t = nullptr) { - return new CPointerLoader(); + return new CPointerLoader(); } }; - template class CPointerLoader : public CBasicPointerLoader + template + class CPointerLoader : public IPointerLoader { public: - const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const override //data is pointer to the ACTUAL POINTER + void * loadPtr(CLoaderBase &ar, ui32 pid) const override //data is pointer to the ACTUAL POINTER { auto & s = static_cast(ar); - T *&ptr = *static_cast(data); //create new object under pointer - typedef typename std::remove_pointer::type npT; - ptr = ClassObjectCreator::invoke(); //does new npT or throws for abstract classes + Type * ptr = ClassObjectCreator::invoke(); //does new npT or throws for abstract classes s.ptrAllocated(ptr, pid); - //T is most derived known type, it's time to call actual serialize + assert(s.fileVersion != 0); ptr->serialize(s,s.fileVersion); - return &typeid(T); + + return static_cast(ptr); } }; - CApplier applier; + CApplier applier; int write(const void * data, unsigned size); @@ -181,18 +140,11 @@ public: si32 fileVersion; std::map loadedPointers; - std::map loadedPointersTypes; - std::map loadedSharedPointers; + std::map> loadedSharedPointers; bool smartPointerSerialization; bool saving; - BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) - { - saving = false; - fileVersion = 0; - smartPointerSerialization = true; - reverseEndianess = false; - } + BinaryDeserializer(IBinaryReader * r); template BinaryDeserializer & operator&(T & t) @@ -244,15 +196,6 @@ public: data = static_cast(read); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void load(T & data) - { - std::vector convData; - load(convData); - convData.resize(data.size()); - range::copy(convData, data.begin()); - } - template ::value, int >::type = 0> void load(std::vector &data) { @@ -265,14 +208,33 @@ public: template < typename T, typename std::enable_if < std::is_pointer::value, int >::type = 0 > void load(T &data) { - ui8 hlp; - load( hlp ); - if(!hlp) + bool isNull; + load( isNull ); + if(isNull) { data = nullptr; return; } + loadPointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 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 < !std::is_base_of_v>, int >::type = 0 > + void loadPointerImpl(T &data) + { if(reader->smartVectorMembersSerialization) { typedef typename std::remove_const::type>::type TObjectType; //eg: const CGHeroInstance * => CGHeroInstance @@ -307,12 +269,10 @@ public: { // We already got this pointer // Cast it in case we are loading it to a non-first base pointer - assert(loadedPointersTypes.count(pid)); - data = reinterpret_cast(typeList.castRaw(i->second, loadedPointersTypes.at(pid), &typeid(typename std::remove_const::type>::type))); + data = static_cast(i->second); return; } } - //get type id ui16 tid; load( tid ); @@ -334,8 +294,7 @@ public: data = nullptr; return; } - auto typeInfo = app->loadPtr(*this,&data, pid); - data = reinterpret_cast(typeList.castRaw((void*)data, typeInfo, &typeid(typename std::remove_const::type>::type))); + data = static_cast(app->loadPtr(*this, pid)); } } @@ -343,10 +302,7 @@ public: void ptrAllocated(const T *ptr, ui32 pid) { if(smartPointerSerialization && pid != 0xffffffff) - { - loadedPointersTypes[pid] = &typeid(T); 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) @@ -361,7 +317,7 @@ public: NonConstT *internalPtr; load(internalPtr); - void *internalPtrDerived = typeList.castToMostDerived(internalPtr); + void * internalPtrDerived = static_cast(internalPtr); if(internalPtr) { @@ -370,41 +326,24 @@ public: { // This pointers is already loaded. The "data" needs to be pointed to it, // so their shared state is actually shared. - try - { - auto actualType = typeList.getTypeInfo(internalPtr); - auto typeWeNeedToReturn = typeList.getTypeInfo(); - if(*actualType == *typeWeNeedToReturn) - { - // No casting needed, just unpack already stored shared_ptr and return it - data = std::any_cast>(itr->second); - } - else - { - // We need to perform series of casts - auto ret = typeList.castShared(itr->second, actualType, typeWeNeedToReturn); - data = std::any_cast>(ret); - } - } - catch(std::exception &e) - { - logGlobal->error(e.what()); - logGlobal->error("Failed to cast stored shared ptr. Real type: %s. Needed type %s. FIXME FIXME FIXME", itr->second.type().name(), typeid(std::shared_ptr).name()); - //TODO scenario with inheritance -> we can have stored ptr to base and load ptr to derived (or vice versa) - throw; - } + data = std::static_pointer_cast(itr->second); } else { auto hlp = std::shared_ptr(internalPtr); data = hlp; - loadedSharedPointers[internalPtrDerived] = typeList.castSharedToMostDerived(hlp); + loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast(hlp); } } else data.reset(); } + void load(std::monostate & data) + { + // no-op + } + template void load(std::shared_ptr & data) { @@ -506,17 +445,19 @@ public: this->read((void*)data.c_str(),length); } - template - void load(std::variant & data) + template + void load(std::variant & data) { - using TVariant = std::variant; - - VariantLoaderHelper loader(*this); - si32 which; load( which ); - assert(which < loader.funcs.size()); - data = loader.funcs.at(which)(); + assert(which < sizeof...(TN)); + + // Create array of variants that contains all default-constructed alternatives + const std::variant table[] = { TN{ }... }; + // use appropriate alternative for result + data = table[which]; + // perform actual load via std::visit dispatch + std::visit([&](auto& o) { load(o); }, data); } template @@ -575,30 +516,4 @@ public: } }; -class DLL_LINKAGE CLoadFile : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - - std::string fName; - std::unique_ptr sfile; - - CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! - virtual ~CLoadFile(); - int read(void * data, unsigned size) override; //throws! - - void openNextFile(const boost::filesystem::path & fname, int minimalVersion); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void checkMagicBytes(const std::string & text); - - template - CLoadFile & operator>>(T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 0bd67c038..8ba41babd 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -9,70 +9,15 @@ */ #include "StdInc.h" #include "BinarySerializer.h" -#include "../filesystem/FileStream.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinarySerializer & s); - -CSaveFile::CSaveFile(const boost::filesystem::path &fname) - : serializer(this) +BinarySerializer::BinarySerializer(IBinaryWriter * w): CSaverBase(w) { - registerTypes(serializer); - openNextFile(fname); -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CSaveFile::~CSaveFile() = default; - -int CSaveFile::write(const void * data, unsigned size) -{ - sfile->write((char *)data,size); - return size; -} - -void CSaveFile::openNextFile(const boost::filesystem::path &fname) -{ - fName = fname; - try - { - sfile = std::make_unique(fname, std::ios::out | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to write %s!", fname); - - sfile->write("VCMI",4); //write magic identifier - serializer & SERIALIZATION_VERSION; //write format version - } - catch(...) - { - logGlobal->error("Failed to save to %s", fname.string()); - clear(); - throw; - } -} - -void CSaveFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CSaveFile"); - if(sfile.get() && *sfile) - { - out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); - } -} - -void CSaveFile::clear() -{ - fName.clear(); - sfile = nullptr; -} - -void CSaveFile::putMagicBytes(const std::string &text) -{ - write(text.c_str(), static_cast(text.length())); + saving=true; + smartPointerSerialization = true; + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 14694ea9a..ef2ddf416 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -9,13 +9,12 @@ */ #pragma once +#include "CSerializer.h" #include "CTypeList.h" #include "../mapObjects/CArmedInstance.h" VCMI_LIB_NAMESPACE_BEGIN -class FileStream; - class DLL_LINKAGE CSaverBase { protected: @@ -116,11 +115,7 @@ public: bool smartPointerSerialization; bool saving; - BinarySerializer(IBinaryWriter * w): CSaverBase(w) - { - saving=true; - smartPointerSerialization = true; - } + BinarySerializer(IBinaryWriter * w); template void registerType(const Base * b = nullptr, const Derived * d = nullptr) @@ -142,14 +137,6 @@ public: save(writ); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void save(const T &data) - { - std::vector convData; - std::copy(data.begin(), data.end(), std::back_inserter(convData)); - save(convData); - } - template < class T, typename std::enable_if < std::is_fundamental::value && !std::is_same::value, int >::type = 0 > void save(const T &data) { @@ -176,16 +163,30 @@ public: void save(const T &data) { //write if pointer is not nullptr - ui8 hlp = (data!=nullptr); - save(hlp); + bool isNull = (data == nullptr); + save(isNull); //if pointer is nullptr then we don't need anything more... - if(!hlp) + if(data == nullptr) return; + savePointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { + auto index = data->getId(); + save(index); + } + + template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { + typedef typename std::remove_const::type>::type TObjectType; + if(writer->smartVectorMembersSerialization) { - typedef typename std::remove_const::type>::type TObjectType; typedef typename VectorizedTypeFor::type VType; typedef typename VectorizedIDType::type IDType; @@ -209,7 +210,7 @@ public: { // 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. - auto actualPointer = typeList.castToMostDerived(data); + const void * actualPointer = static_cast(data); auto i = savedPointers.find(actualPointer); if(i != savedPointers.end()) { @@ -225,13 +226,13 @@ public: } //write type identifier - ui16 tid = typeList.getTypeID(data); + uint16_t tid = CTypeList::getInstance().getTypeID(data); save(tid); if(!tid) save(*data); //if type is unregistered simply write all data in a standard way else - applier.getApplier(tid)->savePtr(*this, typeList.castToMostDerived(data)); //call serializer specific for our real type + applier.getApplier(tid)->savePtr(*this, static_cast(data)); //call serializer specific for our real type } template < typename T, typename std::enable_if < is_serializeable::value, int >::type = 0 > @@ -240,6 +241,11 @@ public: const_cast(data).serialize(*this, SERIALIZATION_VERSION); } + void save(const std::monostate & data) + { + // no-op + } + template void save(const std::shared_ptr &data) { @@ -386,30 +392,4 @@ public: } }; -class DLL_LINKAGE CSaveFile : public IBinaryWriter -{ -public: - BinarySerializer serializer; - - boost::filesystem::path fName; - std::unique_ptr sfile; - - CSaveFile(const boost::filesystem::path &fname); //throws! - ~CSaveFile(); - int write(const void * data, unsigned size) override; - - void openNextFile(const boost::filesystem::path &fname); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void putMagicBytes(const std::string &text); - - template - CSaveFile & operator<<(const T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.cpp b/lib/serializer/CLoadFile.cpp new file mode 100644 index 000000000..cf5ea34d7 --- /dev/null +++ b/lib/serializer/CLoadFile.cpp @@ -0,0 +1,100 @@ +/* + * CLoadFile.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 "CLoadFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CLoadFile::CLoadFile(const boost::filesystem::path & fname, int minimalVersion) + : serializer(this) +{ + openNextFile(fname, minimalVersion); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CLoadFile::~CLoadFile() = default; + +int CLoadFile::read(void * data, unsigned size) +{ + sfile->read(reinterpret_cast(data), size); + return size; +} + +void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalVersion) +{ + assert(!serializer.reverseEndianess); + assert(minimalVersion <= SERIALIZATION_VERSION); + + try + { + fName = fname.string(); + sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to read %s!", fName); + + //we can read + char buffer[4]; + sfile->read(buffer, 4); + if(std::memcmp(buffer, "VCMI", 4) != 0) + THROW_FORMAT("Error: not a VCMI file(%s)!", fName); + + serializer & serializer.fileVersion; + if(serializer.fileVersion < minimalVersion) + THROW_FORMAT("Error: too old file format (%s)!", fName); + + if(serializer.fileVersion > SERIALIZATION_VERSION) + { + logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", serializer.fileVersion, SERIALIZATION_VERSION , fName); + + auto * versionptr = reinterpret_cast(&serializer.fileVersion); + std::reverse(versionptr, versionptr + 4); + logGlobal->warn("Version number reversed is %x, checking...", serializer.fileVersion); + + if(serializer.fileVersion == SERIALIZATION_VERSION) + { + logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); + serializer.reverseEndianess = true; + } + else + THROW_FORMAT("Error: too new file format (%s)!", fName); + } + } + catch(...) + { + clear(); //if anything went wrong, we delete file and rethrow + throw; + } +} + +void CLoadFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CLoadFile"); + if(!!sfile && *sfile) + out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); +} + +void CLoadFile::clear() +{ + sfile = nullptr; + fName.clear(); + serializer.fileVersion = 0; +} + +void CLoadFile::checkMagicBytes(const std::string &text) +{ + std::string loaded = text; + read((void *)loaded.data(), static_cast(text.length())); + if(loaded != text) + throw std::runtime_error("Magic bytes doesn't match!"); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.h b/lib/serializer/CLoadFile.h new file mode 100644 index 000000000..8f9079bb9 --- /dev/null +++ b/lib/serializer/CLoadFile.h @@ -0,0 +1,42 @@ +/* + * CLoadFile.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 "BinaryDeserializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CLoadFile : public IBinaryReader +{ +public: + BinaryDeserializer serializer; + + std::string fName; + std::unique_ptr sfile; + + CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! + virtual ~CLoadFile(); + int read(void * data, unsigned size) override; //throws! + + void openNextFile(const boost::filesystem::path & fname, int minimalVersion); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void checkMagicBytes(const std::string & text); + + template + CLoadFile & operator>>(T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadIntegrityValidator.cpp b/lib/serializer/CLoadIntegrityValidator.cpp deleted file mode 100644 index c0a2c0e11..000000000 --- a/lib/serializer/CLoadIntegrityValidator.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * CLoadIntegrityValidator.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 "CLoadIntegrityValidator.h" -#include "../filesystem/FileStream.h" - -#include "../registerTypes/RegisterTypes.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CLoadIntegrityValidator::CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion) - : serializer(this), foundDesync(false) -{ - registerTypes(serializer); - primaryFile = std::make_unique(primaryFileName, minimalVersion); - controlFile = std::make_unique(controlFileName, minimalVersion); - - assert(primaryFile->serializer.fileVersion == controlFile->serializer.fileVersion); - serializer.fileVersion = primaryFile->serializer.fileVersion; -} - -int CLoadIntegrityValidator::read( void * data, unsigned size ) -{ - assert(primaryFile); - assert(controlFile); - - if(!size) - return size; - - std::vector controlData(size); - auto ret = primaryFile->read(data, size); - - if(!foundDesync) - { - controlFile->read(controlData.data(), size); - if(std::memcmp(data, controlData.data(), size) != 0) - { - logGlobal->error("Desync found! Position: %d", primaryFile->sfile->tellg()); - foundDesync = true; - //throw std::runtime_error("Savegame dsynchronized!"); - } - } - return ret; -} - -std::unique_ptr CLoadIntegrityValidator::decay() -{ - primaryFile->serializer.loadedPointers = this->serializer.loadedPointers; - primaryFile->serializer.loadedPointersTypes = this->serializer.loadedPointersTypes; - return std::move(primaryFile); -} - -void CLoadIntegrityValidator::checkMagicBytes(const std::string & text) const -{ - assert(primaryFile); - assert(controlFile); - - primaryFile->checkMagicBytes(text); - controlFile->checkMagicBytes(text); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadIntegrityValidator.h b/lib/serializer/CLoadIntegrityValidator.h deleted file mode 100644 index 5adb4f3d3..000000000 --- a/lib/serializer/CLoadIntegrityValidator.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * CLoadIntegrityValidator.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 "BinaryDeserializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Simple byte-to-byte saves comparator -class DLL_LINKAGE CLoadIntegrityValidator - : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - std::unique_ptr primaryFile, controlFile; - bool foundDesync; - - CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion = SERIALIZATION_VERSION); //throws! - - int read( void * data, unsigned size) override; //throws! - void checkMagicBytes(const std::string & text) const; - - std::unique_ptr decay(); //returns primary file. CLoadIntegrityValidator stops being usable anymore -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CMemorySerializer.cpp b/lib/serializer/CMemorySerializer.cpp index f9d62c619..797c10a6d 100644 --- a/lib/serializer/CMemorySerializer.cpp +++ b/lib/serializer/CMemorySerializer.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "CMemorySerializer.h" -#include "../registerTypes/RegisterTypes.h" - VCMI_LIB_NAMESPACE_BEGIN int CMemorySerializer::read(void * data, unsigned size) @@ -34,8 +32,6 @@ int CMemorySerializer::write(const void * data, unsigned size) CMemorySerializer::CMemorySerializer(): iser(this), oser(this), readPos(0) { - registerTypes(iser); - registerTypes(oser); iser.fileVersion = SERIALIZATION_VERSION; } diff --git a/lib/serializer/CSaveFile.cpp b/lib/serializer/CSaveFile.cpp new file mode 100644 index 000000000..5f377aed6 --- /dev/null +++ b/lib/serializer/CSaveFile.cpp @@ -0,0 +1,72 @@ +/* + * CSaveFile.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 "CSaveFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CSaveFile::CSaveFile(const boost::filesystem::path &fname) + : serializer(this) +{ + openNextFile(fname); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CSaveFile::~CSaveFile() = default; + +int CSaveFile::write(const void * data, unsigned size) +{ + sfile->write((char *)data,size); + return size; +} + +void CSaveFile::openNextFile(const boost::filesystem::path &fname) +{ + fName = fname; + try + { + sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to write %s!", fname); + + sfile->write("VCMI",4); //write magic identifier + serializer & SERIALIZATION_VERSION; //write format version + } + catch(...) + { + logGlobal->error("Failed to save to %s", fname.string()); + clear(); + throw; + } +} + +void CSaveFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CSaveFile"); + if(sfile.get() && *sfile) + { + out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); + } +} + +void CSaveFile::clear() +{ + fName.clear(); + sfile = nullptr; +} + +void CSaveFile::putMagicBytes(const std::string &text) +{ + write(text.c_str(), static_cast(text.length())); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSaveFile.h b/lib/serializer/CSaveFile.h new file mode 100644 index 000000000..f1b823bf2 --- /dev/null +++ b/lib/serializer/CSaveFile.h @@ -0,0 +1,42 @@ +/* + * CSaveFile.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 "BinarySerializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CSaveFile : public IBinaryWriter +{ +public: + BinarySerializer serializer; + + boost::filesystem::path fName; + std::unique_ptr sfile; + + CSaveFile(const boost::filesystem::path &fname); //throws! + ~CSaveFile(); + int write(const void * data, unsigned size) override; + + void openNextFile(const boost::filesystem::path &fname); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void putMagicBytes(const std::string &text); + + template + CSaveFile & operator<<(const T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 926d73b6c..76a372690 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 825; -const ui32 MINIMAL_SERIALIZATION_VERSION = 824; +const ui32 SERIALIZATION_VERSION = 831; +const ui32 MINIMAL_SERIALIZATION_VERSION = 831; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; @@ -53,14 +53,14 @@ struct VectorizedObjectInfo /// Base class for serializers capable of reading or writing data class DLL_LINKAGE CSerializer { - template - static si32 idToNumber(const T &t, typename std::enable_if::value>::type * dummy = 0) + template, bool> = true> + static int32_t idToNumber(const Numeric &t) { return t; } - template - static NT idToNumber(const BaseForID &t) + template, bool> = true> + static int32_t idToNumber(const IdentifierType &t) { return t.getNum(); } @@ -149,42 +149,31 @@ struct is_serializeable template //metafunction returning CGObjectInstance if T is its derivate or T elsewise struct VectorizedTypeFor { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>::type; + using type = std::conditional_t, CGObjectInstance, T>; }; -template + +template <> +struct VectorizedTypeFor +{ + using type = CGHeroInstance; +}; + +template struct VectorizedIDType { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>>>>>::type; + using type = std::conditional_t, ObjectInstanceID, int32_t>; +}; + +template <> +struct VectorizedIDType +{ + using type = ArtifactInstanceID; +}; + +template <> +struct VectorizedIDType +{ + using type = HeroTypeID; }; /// Base class for deserializers diff --git a/lib/serializer/CTypeList.cpp b/lib/serializer/CTypeList.cpp index 1c94dcf43..8c75517ca 100644 --- a/lib/serializer/CTypeList.cpp +++ b/lib/serializer/CTypeList.cpp @@ -14,128 +14,9 @@ VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(CTypeList & s); - -CTypeList typeList; - CTypeList::CTypeList() { registerTypes(*this); } -CTypeList::TypeInfoPtr CTypeList::registerType(const std::type_info *type) -{ - if(auto typeDescr = getTypeDescriptor(type, false)) - return typeDescr; //type found, return ptr to structure - - //type not found - add it to the list and return given ID - auto newType = std::make_shared(); - newType->typeID = static_cast(typeInfos.size() + 1); - newType->name = type->name(); - typeInfos[type] = newType; - - return newType; -} - -ui16 CTypeList::getTypeID(const std::type_info *type, bool throws) const -{ - auto descriptor = getTypeDescriptor(type, throws); - if (descriptor == nullptr) - { - return 0; - } - return descriptor->typeID; -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(ui16 typeID) const -{ - auto found = std::find_if(typeInfos.begin(), typeInfos.end(), [typeID](const std::pair & p) -> bool - { - return p.second->typeID == typeID; - }); - - if(found != typeInfos.end()) - { - return found->second; - } - - return TypeInfoPtr(); -} - -std::vector CTypeList::castSequence(TypeInfoPtr from, TypeInfoPtr to) const -{ - if(!strcmp(from->name, to->name)) - return std::vector(); - - // Perform a simple BFS in the class hierarchy. - - auto BFS = [&](bool upcast) - { - std::map previous; - std::queue q; - q.push(to); - while(!q.empty()) - { - auto typeNode = q.front(); - q.pop(); - for(auto & weakNode : (upcast ? typeNode->parents : typeNode->children) ) - { - auto nodeBase = weakNode.lock(); - if(!previous.count(nodeBase)) - { - previous[nodeBase] = typeNode; - q.push(nodeBase); - } - } - } - - std::vector ret; - - if(!previous.count(from)) - return ret; - - ret.push_back(from); - TypeInfoPtr ptr = from; - do - { - ptr = previous.at(ptr); - ret.push_back(ptr); - } while(ptr != to); - - return ret; - }; - - // Try looking both up and down. - auto ret = BFS(true); - if(ret.empty()) - ret = BFS(false); - - if(ret.empty()) - THROW_FORMAT("Cannot find relation between types %s and %s. Were they (and all classes between them) properly registered?", from->name % to->name); - - return ret; -} - -std::vector CTypeList::castSequence(const std::type_info *from, const std::type_info *to) const -{ - //This additional if is needed because getTypeDescriptor might fail if type is not registered - // (and if casting is not needed, then registereing should no be required) - if(!strcmp(from->name(), to->name())) - return std::vector(); - - return castSequence(getTypeDescriptor(from), getTypeDescriptor(to)); -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(const std::type_info *type, bool throws) const -{ - auto i = typeInfos.find(type); - if(i != typeInfos.end()) - return i->second; //type found, return ptr to structure - - if(!throws) - return nullptr; - - THROW_FORMAT("Cannot find type descriptor for type %s. Was it registered?", type->name()); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CTypeList.h b/lib/serializer/CTypeList.h index fc5b7c0ac..df0eec7bc 100644 --- a/lib/serializer/CTypeList.h +++ b/lib/serializer/CTypeList.h @@ -9,199 +9,72 @@ */ #pragma once -#include "CSerializer.h" - VCMI_LIB_NAMESPACE_BEGIN -struct IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const = 0; // takes From*, returns To* - virtual std::any castSharedPtr(const std::any &ptr) const = 0; // takes std::shared_ptr, performs dynamic cast, returns std::shared_ptr - virtual std::any castWeakPtr(const std::any &ptr) const = 0; // takes std::weak_ptr, performs dynamic cast, returns std::weak_ptr. The object under poitner must live. - //virtual std::any castUniquePtr(const std::any &ptr) const = 0; // takes std::unique_ptr, performs dynamic cast, returns std::unique_ptr - virtual ~IPointerCaster() = default; -}; - -template -struct PointerCaster : IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const override // takes void* pointing to From object, performs dynamic cast, returns void* pointing to To object - { - From * from = (From*)std::any_cast(ptr); - To * ret = static_cast(from); - return (void*)ret; - } - - // Helper function performing casts between smart pointers - template - std::any castSmartPtr(const std::any &ptr) const - { - try - { - auto from = std::any_cast(ptr); - auto ret = std::static_pointer_cast(from); - return ret; - } - catch(std::exception &e) - { - THROW_FORMAT("Failed cast %s -> %s. Given argument was %s. Error message: %s", typeid(From).name() % typeid(To).name() % ptr.type().name() % e.what()); - } - } - - virtual std::any castSharedPtr(const std::any &ptr) const override - { - return castSmartPtr>(ptr); - } - virtual std::any castWeakPtr(const std::any &ptr) const override - { - auto from = std::any_cast>(ptr); - return castSmartPtr>(from.lock()); - } -}; - /// Class that implements basic reflection-like mechanisms /// For every type registered via registerType() generates inheritance tree /// Rarely used directly - usually used as part of CApplier -class DLL_LINKAGE CTypeList: public boost::noncopyable +class CTypeList { -//public: - struct TypeDescriptor; - using TypeInfoPtr = std::shared_ptr; - using WeakTypeInfoPtr = std::weak_ptr; - struct TypeDescriptor - { - ui16 typeID; - const char *name; - std::vector children, parents; - }; - using TMutex = boost::shared_mutex; - using TUniqueLock = boost::unique_lock; - using TSharedLock = boost::shared_lock; + std::map typeInfos; -private: - mutable TMutex mx; - - std::map typeInfos; - std::map, std::unique_ptr> casters; //for each pair we provide a caster (each registered relations creates a single entry here) - - /// Returns sequence of types starting from "from" and ending on "to". Every next type is derived from the previous. - /// Throws if there is no link registered. - std::vector castSequence(TypeInfoPtr from, TypeInfoPtr to) const; - std::vector castSequence(const std::type_info *from, const std::type_info *to) const; - - template - std::any castHelper(std::any inputPtr, const std::type_info *fromArg, const std::type_info *toArg) const - { - TSharedLock lock(mx); - auto typesSequence = castSequence(fromArg, toArg); - - std::any ptr = inputPtr; - for(int i = 0; i < static_cast(typesSequence.size()) - 1; i++) - { - auto &from = typesSequence[i]; - auto &to = typesSequence[i + 1]; - auto castingPair = std::make_pair(from, to); - if(!casters.count(castingPair)) - THROW_FORMAT("Cannot find caster for conversion %s -> %s which is needed to cast %s -> %s", from->name % to->name % fromArg->name() % toArg->name()); - - const auto & caster = casters.at(castingPair); - ptr = (*caster.*CastingFunction)(ptr); //Why does unique_ptr not have operator->* ..? - } - - return ptr; - } - CTypeList & operator=(CTypeList &) = delete; - - TypeInfoPtr getTypeDescriptor(const std::type_info *type, bool throws = true) const; //if not throws, failure returns nullptr - TypeInfoPtr registerType(const std::type_info *type); - -public: - - CTypeList(); - - template - void registerType(const Base * b = nullptr, const Derived * d = nullptr) - { - TUniqueLock lock(mx); - static_assert(std::is_base_of::value, "First registerType template parameter needs to ba a base class of the second one."); - static_assert(std::has_virtual_destructor::value, "Base class needs to have a virtual destructor."); - static_assert(!std::is_same::value, "Parameters of registerTypes should be two different types."); - auto bt = getTypeInfo(b); - auto dt = getTypeInfo(d); //obtain std::type_info - auto bti = registerType(bt); - auto dti = registerType(dt); //obtain our TypeDescriptor - - // register the relation between classes - bti->children.push_back(dti); - dti->parents.push_back(bti); - casters[std::make_pair(bti, dti)] = std::make_unique>(); - casters[std::make_pair(dti, bti)] = std::make_unique>(); - } - - ui16 getTypeID(const std::type_info *type, bool throws = false) const; + DLL_LINKAGE CTypeList(); template - ui16 getTypeID(const T * t = nullptr, bool throws = false) const - { - return getTypeID(getTypeInfo(t), throws); - } - - TypeInfoPtr getTypeDescriptor(ui16 typeID) const; - - template - void * castToMostDerived(const TInput * inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr); - - if (strcmp(baseType.name(), derivedType->name()) == 0) - { - return const_cast(reinterpret_cast(inputPtr)); - } - - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>( - const_cast(reinterpret_cast(inputPtr)), &baseType, - derivedType)); - } - - template - std::any castSharedToMostDerived(const std::shared_ptr inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr.get()); - - if (!strcmp(baseType.name(), derivedType->name())) - return inputPtr; - - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, &baseType, derivedType); - } - - void * castRaw(void *inputPtr, const std::type_info *from, const std::type_info *to) const - { - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>(inputPtr, from, to)); - } - std::any castShared(std::any inputPtr, const std::type_info *from, const std::type_info *to) const - { - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, from, to); - } - - template const std::type_info * getTypeInfo(const T * t = nullptr) const + const std::type_info & getTypeInfo(const T * t = nullptr) const { if(t) - return &typeid(*t); + return typeid(*t); else - return &typeid(T); + return typeid(T); + } + +public: + static CTypeList & getInstance() + { + static CTypeList registry; + return registry; + } + + template + void registerType() + { + registerType(); + registerType(); + } + + template + void registerType() + { + const std::type_info & typeInfo = typeid(T); + + if (typeInfos.count(typeInfo.name()) != 0) + return; + + typeInfos[typeInfo.name()] = typeInfos.size() + 1; + } + + template + uint16_t getTypeID(T * typePtr) + { + static_assert(!std::is_pointer_v, "CTypeList does not supports pointers!"); + static_assert(!std::is_reference_v, "CTypeList does not supports references!"); + + const std::type_info & typeInfo = getTypeInfo(typePtr); + + if (typeInfos.count(typeInfo.name()) == 0) + return 0; + + return typeInfos.at(typeInfo.name()); } }; -extern DLL_LINKAGE CTypeList typeList; - /// 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; + std::map> apps; template void addApplier(ui16 ID) @@ -224,9 +97,8 @@ public: template void registerType(const Base * b = nullptr, const Derived * d = nullptr) { - typeList.registerType(b, d); - addApplier(typeList.getTypeID(b)); - addApplier(typeList.getTypeID(d)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); } }; diff --git a/lib/serializer/Cast.h b/lib/serializer/Cast.h index ebc7957af..7525f6d85 100644 --- a/lib/serializer/Cast.h +++ b/lib/serializer/Cast.h @@ -9,58 +9,18 @@ */ #pragma once -#include -#include -#include "CTypeList.h" - VCMI_LIB_NAMESPACE_BEGIN template inline const T * dynamic_ptr_cast(const F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } template inline T * dynamic_ptr_cast(F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index ac007fa0a..de5f3b85d 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -10,8 +10,7 @@ #include "StdInc.h" #include "Connection.h" -#include "../registerTypes/RegisterTypes.h" -#include "../mapping/CMapHeader.h" +#include "../networkPacks/NetPacksBase.h" #include @@ -45,8 +44,6 @@ void CConnection::init() enableSmartPointerSerialization(); disableStackSendingByID(); - registerTypes(iser); - registerTypes(oser); #ifndef VCMI_ENDIAN_BIG myEndianess = true; #else @@ -82,7 +79,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, if(error) { logNetwork->error("Problem with resolving: \n%s", error.message()); - throw std::runtime_error("Can't establish connection: Problem with resolving"); + throw std::runtime_error("Problem with resolving"); } pom = endpoint_iterator; if(pom != end) @@ -90,7 +87,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, else { logNetwork->error("Critical problem: No endpoints found!"); - throw std::runtime_error("Can't establish connection: No endpoints found!"); + throw std::runtime_error("No endpoints found!"); } while(pom != end) { @@ -109,7 +106,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, } else { - throw std::runtime_error("Can't establish connection: Failed to connect!"); + throw std::runtime_error("Failed to connect!"); } endpoint_iterator++; } @@ -224,10 +221,16 @@ int CConnection::read(void * data, unsigned size) CConnection::~CConnection() { - if(handler) - handler->join(); - close(); + + if(handler) + { + // ugly workaround to avoid self-join if last strong reference to shared_ptr that owns this class has been released in this very thread, e.g. on netpack processing + if (boost::this_thread::get_id() != handler->get_id()) + handler->join(); + else + handler->detach(); + } } template @@ -243,6 +246,15 @@ void CConnection::close() { if(socket) { + try + { + socket->shutdown(boost::asio::ip::tcp::socket::shutdown_receive); + } + catch (const boost::system::system_error & e) + { + logNetwork->error("error closing socket: %s", e.what()); + } + socket->close(); socket.reset(); } @@ -272,13 +284,7 @@ CPack * CConnection::retrievePack() iser & pack; logNetwork->trace("Received CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); if(pack == nullptr) - { logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); - } - else - { - pack->c = this->shared_from_this(); - } enableBufferedRead = false; diff --git a/lib/serializer/ILICReader.cpp b/lib/serializer/ILICReader.cpp deleted file mode 100644 index a8bcc3793..000000000 --- a/lib/serializer/ILICReader.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * JsonTreeSerializer.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 "ILICReader.h" - -#include "../JsonNode.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - const std::string type = typeid(decltype(this)).name(); - - const si32 rawId = decoder(identifier); - if(rawId >= 0) - { - if(rawId < value.size()) - value[rawId] = val; - else - logGlobal->error("%s::serializeLIC: id out of bounds %d", type, rawId); - } - } -} - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - - const si32 rawId = decoder(identifier); - if(rawId != -1) - value.insert(rawId); - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/ILICReader.h b/lib/serializer/ILICReader.h deleted file mode 100644 index 83d16b10f..000000000 --- a/lib/serializer/ILICReader.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * ILICReader.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 "JsonTreeSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE ILICReader -{ -protected: - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const; - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonDeserializer.cpp b/lib/serializer/JsonDeserializer.cpp index 5cb7c1398..a2b840777 100644 --- a/lib/serializer/JsonDeserializer.cpp +++ b/lib/serializer/JsonDeserializer.cpp @@ -107,6 +107,20 @@ void JsonDeserializer::serializeInternal(const std::string & fieldName, si32 & v value = rawValue; } +void JsonDeserializer::serializeInternal(const std::string & fieldName, std::vector & value) +{ + const JsonVector & data = currentObject->operator[](fieldName).Vector(); + + value.clear(); + value.reserve(data.size()); + + for(const JsonNode& elem : data) + { + std::string rawId = elem.String(); + value.push_back(rawId); + } +} + void JsonDeserializer::serializeInternal(std::string & value) { value = currentObject->String(); @@ -117,75 +131,11 @@ void JsonDeserializer::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonDeserializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonDeserializer::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonDeserializer.h b/lib/serializer/JsonDeserializer.h index e11892b27..9a3aab5ee 100644 --- a/lib/serializer/JsonDeserializer.h +++ b/lib/serializer/JsonDeserializer.h @@ -9,18 +9,16 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN -class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer { public: JsonDeserializer(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; @@ -33,6 +31,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; diff --git a/lib/serializer/JsonSerializeFormat.cpp b/lib/serializer/JsonSerializeFormat.cpp index aff5c8eb9..4da52c445 100644 --- a/lib/serializer/JsonSerializeFormat.cpp +++ b/lib/serializer/JsonSerializeFormat.cpp @@ -91,17 +91,6 @@ size_t JsonArraySerializer::size() const return thisNode->Vector().size(); } -//JsonSerializeFormat::LIC -JsonSerializeFormat::LIC::LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder): - standard(Standard), - decoder(std::move(Decoder)), - encoder(std::move(Encoder)) -{ - any.resize(standard.size(), false); - all.resize(standard.size(), false); - none.resize(standard.size(), false); -} - JsonSerializeFormat::LICSet::LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder): standard(Standard), decoder(std::move(Decoder)), @@ -140,4 +129,16 @@ void JsonSerializeFormat::serializeBool(const std::string & fieldName, bool & va serializeBool(fieldName, value, true, false, defaultValue); } +void JsonSerializeFormat::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const +{ + for(const auto & index : part.Vector()) + { + const std::string & identifier = index.String(); + + const si32 rawId = decoder(identifier); + if(rawId != -1) + value.insert(rawId); + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index fd1401833..cb4a6c248 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -10,7 +10,8 @@ #pragma once #include "../JsonNode.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModScope.h" #include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -73,18 +74,44 @@ public: ///String <-> Json string void serializeString(const size_t index, std::string & value); - ///vector of serializable <-> Json vector of structs + ///vector of anything int-convertible <-> Json vector of integers + template + void serializeArray(std::vector & value) + { + syncSize(value, JsonNode::JsonType::DATA_STRUCT); + + for(size_t idx = 0; idx < size(); idx++) + serializeInt(idx, value[idx]); + } + + ///vector of strings <-> Json vector of strings + void serializeArray(std::vector & value) + { + syncSize(value, JsonNode::JsonType::DATA_STRUCT); + + for(size_t idx = 0; idx < size(); idx++) + serializeString(idx, value[idx]); + } + + ///vector of anything with custom serializing function <-> Json vector of structs template - void serializeStruct(std::vector & value) + void serializeStruct(std::vector & value, std::function serializer) { syncSize(value, JsonNode::JsonType::DATA_STRUCT); for(size_t idx = 0; idx < size(); idx++) { auto s = enterStruct(idx); - value[idx].serializeJson(*owner); + serializer(*owner, value[idx]); } } + + ///vector of serializable <-> Json vector of structs + template + void serializeStruct(std::vector & value) + { + serializeStruct(value, [](JsonSerializeFormat & h, Element & e){e.serializeJson(h);}); + } void resize(const size_t newSize); void resize(const size_t newSize, JsonNode::JsonType type); @@ -110,18 +137,6 @@ public: ///may assume that object index is valid using TEncoder = std::function; - using TSerialize = std::function; - - struct LIC - { - LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder); - - const std::vector & standard; - const TDecoder decoder; - const TEncoder encoder; - std::vector all, any, none; - }; - struct LICSet { LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder); @@ -184,13 +199,28 @@ public: * @param value target value, must be resized properly * */ - virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) = 0; + virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) = 0; - /** @brief Complete serialization of Logical identifier condition - */ - virtual void serializeLIC(const std::string & fieldName, LIC & value) = 0; + template + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) + { + std::set standardInt; + std::set valueInt; - /** @brief Complete serialization of Logical identifier condition. (Special version) + for (auto entry : standard) + standardInt.insert(entry.getNum()); + + for (auto entry : value) + valueInt.insert(entry.getNum()); + + serializeLIC(fieldName, decoder, encoder, standardInt, valueInt); + + value.clear(); + for (auto entry : valueInt) + value.insert(T(entry)); + } + + /** @brief Complete serialization of Logical identifier condition. * Assumes that all values are allowed by default, and standard contains them */ virtual void serializeLIC(const std::string & fieldName, LICSet & value) = 0; @@ -268,39 +298,62 @@ public: } ///si32-convertible identifier <-> Json string - template - void serializeId(const std::string & fieldName, T & value, const U & defaultValue) + template + void serializeId(const std::string & fieldName, IdentifierType & value, const IdentifierTypeBase & defaultValue = IdentifierType::NONE) { - doSerializeInternal(fieldName, value, defaultValue, &E::decode, &E::encode); + static_assert(std::is_base_of_v, "This method can only serialize Identifier classes!"); + + if (saving) + { + if (value != defaultValue) + { + std::string fieldValue = IdentifierType::encode(value.getNum()); + serializeString(fieldName, fieldValue); + } + } + else + { + std::string fieldValue; + serializeString(fieldName, fieldValue); + + if (!fieldValue.empty()) + { + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), IdentifierType::entityType(), fieldValue, [&value](int32_t index){ + value = IdentifierType(index); + }); + } + else + { + value = IdentifierType(defaultValue); + } + } } ///si32-convertible identifier vector <-> Json array of string template void serializeIdArray(const std::string & fieldName, std::vector & value) { - std::vector temp; - - if(saving) + if (saving) { - temp.reserve(value.size()); + std::vector fieldValue; for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } + fieldValue.push_back(E::encode(vitem.getNum())); + + serializeInternal(fieldName, fieldValue); } - - serializeInternal(fieldName, temp, &E::decode, &E::encode); - if(!saving) + else { - value.clear(); - value.reserve(temp.size()); + std::vector fieldValue; + serializeInternal(fieldName, fieldValue); - for(const si32 item : temp) + value.resize(fieldValue.size()); + + for(size_t i = 0; i < fieldValue.size(); ++i) { - T vitem = static_cast(item); - value.push_back(vitem); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue[i], [&value, i](int32_t index){ + value[i] = T(index); + }); } } } @@ -309,103 +362,25 @@ public: template void serializeIdArray(const std::string & fieldName, std::set & value) { - std::vector temp; - - if(saving) + if (saving) { - temp.reserve(value.size()); + std::vector fieldValue; for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } + fieldValue.push_back(U::encode(vitem.getNum())); + + serializeInternal(fieldName, fieldValue); } - - serializeInternal(fieldName, temp, &U::decode, &U::encode); - if(!saving) + else { - value.clear(); + std::vector fieldValue; + serializeInternal(fieldName, fieldValue); - for(const si32 item : temp) + for(size_t i = 0; i < fieldValue.size(); ++i) { - T vitem = static_cast(item); - value.insert(vitem); - } - } - } - - ///si32-convertible identifier set <-> Json array of string - ///Type U is only used for code & decode - ///TODO: Auto deduce U based on T? - template - void serializeIdArray(const std::string & fieldName, std::set & value, const std::set & defaultValue) - { - std::vector temp; - - if(saving && value != defaultValue) - { - temp.reserve(value.size()); - - for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } - serializeInternal(fieldName, temp, &U::decode, &U::encode); - } - - if(!saving) - { - JsonNode node; - serializeRaw(fieldName, node, std::nullopt); - if(node.Vector().empty()) - { - value = defaultValue; - } - else - { - value.clear(); - - for(const auto & id : node.Vector()) - { - VLC->modh->identifiers.requestIdentifier(U::entityType(), id, [&value](int32_t identifier) - { - value.emplace(identifier); - }); - } - } - } - } - - ///bitmask <-> Json array of string - template - void serializeIdArray(const std::string & fieldName, T & value, const T & defaultValue, const TDecoder & decoder, const TEncoder & encoder) - { - static_assert(8 * sizeof(T) >= Size, "Mask size too small"); - - std::vector temp; - temp.reserve(Size); - - if(saving && value != defaultValue) - { - for(si32 i = 0; i < Size; i++) - if(value & (1 << i)) - temp.push_back(i); - serializeInternal(fieldName, temp, decoder, encoder); - } - - if(!saving) - { - serializeInternal(fieldName, temp, decoder, encoder); - - if(temp.empty()) - value = defaultValue; - else - { - value = 0; - for(auto i : temp) - value |= (1 << i); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), U::entityType(), fieldValue[i], [&value](int32_t index){ + value.insert(T(index)); + }); } } } @@ -415,9 +390,26 @@ public: void serializeInstance(const std::string & fieldName, T & value, const T & defaultValue) { const TDecoder decoder = std::bind(&IInstanceResolver::decode, instanceResolver, _1); - const TEncoder endoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); + const TEncoder encoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); - serializeId(fieldName, value, defaultValue, decoder, endoder); + if (saving) + { + if (value != defaultValue) + { + std::string fieldValue = encoder(value.getNum()); + serializeString(fieldName, fieldValue); + } + } + else + { + std::string fieldValue; + serializeString(fieldName, fieldValue); + + if (!fieldValue.empty()) + value = T(decoder(fieldValue)); + else + value = T(defaultValue); + } } ///any serializable object <-> Json struct @@ -451,6 +443,9 @@ protected: ///Enum/Numeric <-> Json string enum virtual void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) = 0; + ///String vector <-> Json string vector + virtual void serializeInternal(const std::string & fieldName, std::vector & value) = 0; + virtual void pop() = 0; virtual void pushStruct(const std::string & fieldName) = 0; virtual void pushArray(const std::string & fieldName) = 0; @@ -462,6 +457,8 @@ protected: virtual void serializeInternal(std::string & value) = 0; virtual void serializeInternal(int64_t & value) = 0; + void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; + private: const IInstanceResolver * instanceResolver; diff --git a/lib/serializer/JsonSerializer.cpp b/lib/serializer/JsonSerializer.cpp index 4db9b5817..430a6809a 100644 --- a/lib/serializer/JsonSerializer.cpp +++ b/lib/serializer/JsonSerializer.cpp @@ -63,9 +63,25 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, std::vecto for(const si32 rawId : value) { - JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); - jsonElement.String() = encoder(rawId); - data.push_back(std::move(jsonElement)); + JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); + jsonElement.String() = encoder(rawId); + data.push_back(std::move(jsonElement)); + } +} + +void JsonSerializer::serializeInternal(const std::string & fieldName, std::vector & value) +{ + if(value.empty()) + return; + + JsonVector & data = currentObject->operator[](fieldName).Vector(); + data.reserve(value.size()); + + for(const auto & rawId : value) + { + JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); + jsonElement.String() = rawId; + data.push_back(std::move(jsonElement)); } } @@ -79,24 +95,14 @@ void JsonSerializer::serializeInternal(int64_t & value) currentObject->Integer() = value; } -void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - assert(standard.size() == value.size()); if(standard == value) return; writeLICPart(fieldName, "anyOf", encoder, value); } -void JsonSerializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - if(value.any != value.standard) - writeLICPart(fieldName, "anyOf", value.encoder, value.any); - - writeLICPart(fieldName, "allOf", value.encoder, value.all); - writeLICPart(fieldName, "noneOf", value.encoder, value.none); -} - void JsonSerializer::serializeLIC(const std::string & fieldName, LICSet & value) { if(value.any != value.standard) diff --git a/lib/serializer/JsonSerializer.h b/lib/serializer/JsonSerializer.h index 18afa0909..89bbbf26d 100644 --- a/lib/serializer/JsonSerializer.h +++ b/lib/serializer/JsonSerializer.h @@ -18,8 +18,7 @@ class DLL_LINKAGE JsonSerializer : public JsonTreeSerializer public: JsonSerializer(const IInstanceResolver * instanceResolver_, JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; @@ -32,6 +31,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; diff --git a/lib/serializer/JsonUpdater.cpp b/lib/serializer/JsonUpdater.cpp index c0dce33a6..6a29bc910 100644 --- a/lib/serializer/JsonUpdater.cpp +++ b/lib/serializer/JsonUpdater.cpp @@ -61,6 +61,11 @@ void JsonUpdater::serializeInternal(const std::string & fieldName, std::vector & value) +{ + // TODO +} + void JsonUpdater::serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) { const JsonNode & data = currentObject->operator[](fieldName); @@ -100,81 +105,11 @@ void JsonUpdater::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonUpdater::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonUpdater::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonUpdater.h b/lib/serializer/JsonUpdater.h index 47e55ea74..e0cd5f508 100644 --- a/lib/serializer/JsonUpdater.h +++ b/lib/serializer/JsonUpdater.h @@ -9,20 +9,18 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN class CBonusSystemNode; -class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer { public: JsonUpdater(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; @@ -37,6 +35,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; diff --git a/lib/spells/AbilityCaster.cpp b/lib/spells/AbilityCaster.cpp index d7e138d11..f6365684d 100644 --- a/lib/spells/AbilityCaster.cpp +++ b/lib/spells/AbilityCaster.cpp @@ -28,14 +28,14 @@ AbilityCaster::AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpe AbilityCaster::~AbilityCaster() = default; -int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { auto skill = baseSpellLevel; const auto * unit = dynamic_cast(actualCaster); if(spell->getLevel() > 0) { - vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(ESpellSchool::ANY))); + vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); } vstd::amax(skill, 0); diff --git a/lib/spells/AbilityCaster.h b/lib/spells/AbilityCaster.h index bbfd723d7..35685c409 100644 --- a/lib/spells/AbilityCaster.h +++ b/lib/spells/AbilityCaster.h @@ -23,7 +23,7 @@ public: AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpellLevel_); virtual ~AbilityCaster(); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; void getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 63126b36f..6e8b02233 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -14,12 +14,12 @@ #include "CSpellHandler.h" +#include "../CGameInfoCallback.h" +#include "../CPlayerState.h" #include "../CRandomGenerator.h" #include "../mapObjects/CGHeroInstance.h" -#include "../NetPacks.h" -#include "../CGameInfoCallback.h" #include "../mapping/CMap.h" -#include "../CPlayerState.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN @@ -83,7 +83,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron for(const Bonus & b : bonuses) { GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = b; env->apply(&gb); } @@ -202,6 +202,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment ChangeObjPos cop; cop.objid = nearest->id; cop.nPos = summonPos; + cop.initiator = parameters.caster->getCasterOwner(); env->apply(&cop); } else if(schoolLevel < 2) //none or basic level -> cannot create boat :( @@ -215,8 +216,9 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment { NewObject no; no.ID = Obj::BOAT; - no.subID = BoatId(EBoatId::NECROPOLIS); + no.subID = BoatId::NECROPOLIS; no.targetPos = summonPos; + no.initiator = parameters.caster->getCasterOwner(); env->apply(&no); } return ESpellCastResult::OK; @@ -257,7 +259,8 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen } RemoveObject ro; - ro.id = t->visitableObjects.back()->id; + ro.initiator = parameters.caster->getCasterOwner(); + ro.objectID = t->visitableObjects.back()->id; env->apply(&ro); return ESpellCastResult::OK; } @@ -309,7 +312,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; - if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, owner->id), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn + if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -320,8 +323,8 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm } GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); - gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, owner->id); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); + gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); env->apply(&gb); if(!dest->isClear(curr)) //wrong dest tile @@ -479,11 +482,11 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons if(!parameters.pos.valid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) { - auto queryCallback = [=](const JsonNode & reply) -> void + auto queryCallback = [=](std::optional reply) -> void { - if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) + if(reply.has_value()) { - ObjectInstanceID townId(static_cast(reply.Integer())); + ObjectInstanceID townId(*reply); const CGObjectInstance * o = env->getCb()->getObj(townId, true); if(o == nullptr) @@ -525,8 +528,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons request.player = parameters.caster->getCasterOwner(); request.title.appendLocalString(EMetaText::JK_TXT, 40); request.description.appendLocalString(EMetaText::JK_TXT, 41); - request.icon.id = Component::EComponentType::SPELL; - request.icon.subtype = owner->id.toEnum(); + request.icon = Component(ComponentType::SPELL, owner->id); env->genericQuery(&request, request.player, queryCallback); @@ -598,7 +600,7 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner); - const auto fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; + const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; for(const CGObjectInstance * obj : env->getMap()->objects) { diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 82a16a276..fc333fc93 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -16,9 +16,9 @@ #include "../battle/IBattleState.h" #include "../battle/CBattleInfoCallback.h" - +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/SetStackEffect.h" #include "../CStack.h" -#include "../NetPacks.h" VCMI_LIB_NAMESPACE_BEGIN @@ -252,6 +252,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) sc.side = casterSide; sc.spellID = getSpellId(); + sc.battleID = battle()->getBattle()->getBattleID(); sc.tile = target.at(0).hexValue; sc.castByHero = mode == Mode::HERO; @@ -299,6 +300,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) beforeCast(sc, *server->getRNG(), target); BattleLogMessage castDescription; + castDescription.battleID = battle()->getBattle()->getBattleID(); switch (mode) { @@ -344,8 +346,9 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) // send empty event to client // temporary(?) workaround to force animations to trigger - StacksInjured fake_event; - server->apply(&fake_event); + StacksInjured fakeEvent; + fakeEvent.battleID = battle()->getBattle()->getBattleID(); + server->apply(&fakeEvent); } void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target) @@ -448,6 +451,7 @@ std::set BattleSpellMechanics::collectTargets() const void BattleSpellMechanics::doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector) { SetStackEffect sse; + sse.battleID = battle()->getBattle()->getBattleID(); for(const auto * unit : targets) { @@ -472,7 +476,7 @@ bool BattleSpellMechanics::counteringSelector(const Bonus * bonus) const for(const SpellID & id : owner->counteredSpells) { - if(bonus->sid == id.toEnum()) + if(bonus->sid.as() == id) return true; } diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index b89f1f444..29ad70a94 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -73,7 +73,7 @@ private: std::set collectTargets() const; - static void doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector); + void doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector); std::set spellRangeInHexes(BattleHex centralHex) const; diff --git a/lib/spells/BonusCaster.cpp b/lib/spells/BonusCaster.cpp index 19ad943af..5c635c97a 100644 --- a/lib/spells/BonusCaster.cpp +++ b/lib/spells/BonusCaster.cpp @@ -45,7 +45,7 @@ void BonusCaster::getCastDescription(const Spell * spell, const std::vectorgetIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 5d470f79b..1acb58c6b 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -20,8 +20,7 @@ #include "../CGeneralTextHandler.h" #include "../filesystem/Filesystem.h" -#include "../CModHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" #include "../battle/CBattleInfoCallback.h" @@ -29,6 +28,8 @@ #include "../mapObjects/CGHeroInstance.h" //todo: remove #include "../serializer/CSerializer.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModUtility.h" #include "ISpellMechanics.h" @@ -41,23 +42,19 @@ static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"} const spells::SchoolInfo SCHOOL[4] = { { - ESpellSchool::AIR, - BonusType::AIR_IMMUNITY, + SpellSchool::AIR, "air" }, { - ESpellSchool::FIRE, - BonusType::FIRE_IMMUNITY, + SpellSchool::FIRE, "fire" }, { - ESpellSchool::WATER, - BonusType::WATER_IMMUNITY, + SpellSchool::WATER, "water" }, { - ESpellSchool::EARTH, - BonusType::EARTH_IMMUNITY, + SpellSchool::EARTH, "earth" } }; @@ -65,10 +62,10 @@ const spells::SchoolInfo SCHOOL[4] = //order as described in http://bugs.vcmi.eu/view.php?id=91 static const SpellSchool SCHOOL_ORDER[4] = { - ESpellSchool::AIR, //=0 - ESpellSchool::FIRE, //=1 - ESpellSchool::EARTH,//=3(!) - ESpellSchool::WATER //=2(!) + SpellSchool::AIR, //=0 + SpellSchool::FIRE, //=1 + SpellSchool::EARTH,//=3(!) + SpellSchool::WATER //=2(!) }; } //namespace SpellConfig @@ -127,7 +124,7 @@ int64_t CSpell::calculateDamage(const spells::Caster * caster) const return caster->getSpellBonus(this, rawDamage, nullptr); } -bool CSpell::hasSchool(ESpellSchool which) const +bool CSpell::hasSchool(SpellSchool which) const { return school.count(which) && school.at(which); } @@ -152,15 +149,15 @@ spells::AimType CSpell::getTargetType() const return targetType; } -void CSpell::forEachSchool(const std::function& cb) const +void CSpell::forEachSchool(const std::function& cb) const { bool stop = false; for(auto iter : SpellConfig::SCHOOL_ORDER) { - const spells::SchoolInfo & cnf = SpellConfig::SCHOOL[iter]; + const spells::SchoolInfo & cnf = SpellConfig::SCHOOL[iter.getNum()]; if(school.at(cnf.id)) { - cb(cnf, stop); + cb(cnf.id, stop); if(stop) break; @@ -313,7 +310,7 @@ const std::string & CSpell::getIconScroll() const return iconScroll; } -const std::string & CSpell::getCastSound() const +const AudioPath & CSpell::getCastSound() const { return castSound; } @@ -382,31 +379,32 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni //affected creature-specific part if(nullptr != affectedCreature) { - const auto * bearer = affectedCreature; + const auto * bearer = affectedCreature->getBonusBearer(); //applying protections - when spell has more then one elements, only one protection should be applied (I think) - forEachSchool([&](const spells::SchoolInfo & cnf, bool & stop) + forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id)) + if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf))) { - ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id); + ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf)); ret /= 100; stop = true; //only bonus from one school is used } }); - CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)); + CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::ANY)); + auto cachingStr = "type_SPELL_DAMAGE_REDUCTION_s_ANY"; //general spell dmg reduction, works only on magical effects - if(bearer->hasBonus(selector) && isMagical()) + if(bearer->hasBonus(selector, cachingStr) && isMagical()) { - ret *= 100 - bearer->valOfBonuses(selector); + ret *= 100 - bearer->valOfBonuses(selector, cachingStr); ret /= 100; } //dmg increasing - if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, id)) + if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, BonusSubtypeID(id))) { - ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, id.toEnum()); + ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, BonusSubtypeID(id)); ret /= 100; } } @@ -458,7 +456,7 @@ JsonNode CSpell::convertTargetCondition(const BTVector & immunity, const BTVecto auto iter = bonusNameRMap.find(bonusType); if(iter != bonusNameRMap.end()) { - auto fullId = CModHandler::makeFullIdentifier("", "bonus", iter->second); + auto fullId = ModUtility::makeFullIdentifier("", "bonus", iter->second); res[targetName][fullId].String() = value; } else @@ -518,9 +516,9 @@ CSpell::AnimationItem::AnimationItem() : } ///CSpell::AnimationInfo -std::string CSpell::AnimationInfo::selectProjectile(const double angle) const +AnimationPath CSpell::AnimationInfo::selectProjectile(const double angle) const { - std::string res; + AnimationPath res; double maximum = 0.0; for(const auto & info : projectile) @@ -563,7 +561,7 @@ std::vector CSpellHandler::loadLegacyData() using namespace SpellConfig; std::vector legacyData; - CLegacyConfigParser parser("DATA/SPTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/SPTRAITS.TXT")); auto readSchool = [&](JsonMap & schools, const std::string & name) { @@ -610,7 +608,7 @@ std::vector CSpellHandler::loadLegacyData() auto & chances = lineNode["gainChance"].Struct(); - for(const auto & name : ETownType::names) + for(const auto & name : NFaction::names) chances[name].Integer() = static_cast(parser.readNumber()); auto AIVals = parser.readNumArray(GameConstants::SPELL_SCHOOL_LEVELS); @@ -713,7 +711,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { const int chance = static_cast(node.second.Integer()); - VLC->modh->identifiers.requestIdentifier(node.second.meta, "faction", node.first, [=](si32 factionID) + VLC->identifiers()->requestIdentifier(node.second.meta, "faction", node.first, [=](si32 factionID) { spell->probabilities[FactionID(factionID)] = chance; }); @@ -736,7 +734,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { if(counteredSpell.second.Bool()) { - VLC->modh->identifiers.requestIdentifier(counteredSpell.second.meta, counteredSpell.first, [=](si32 id) + VLC->identifiers()->requestIdentifier(counteredSpell.second.meta, "spell", counteredSpell.first, [=](si32 id) { spell->counteredSpells.emplace_back(id); }); @@ -863,10 +861,10 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & CSpell::TAnimation newItem; if(item.getType() == JsonNode::JsonType::DATA_STRING) - newItem.resourceName = item.String(); + newItem.resourceName = AnimationPath::fromJson(item); else if(item.getType() == JsonNode::JsonType::DATA_STRUCT) { - newItem.resourceName = item["defName"].String(); + newItem.resourceName = AnimationPath::fromJson(item["defName"]); newItem.effectName = item["effectName"].String(); auto vPosStr = item["verticalPosition"].String(); @@ -891,14 +889,14 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & for(const JsonNode & item : projectile) { CSpell::ProjectileInfo info; - info.resourceName = item["defName"].String(); + info.resourceName = AnimationPath::fromJson(item["defName"]); info.minimumAngle = item["minimumAngle"].Float(); spell->animationInfo.projectile.push_back(info); } const JsonNode & soundsNode = json["sounds"]; - spell->castSound = soundsNode["cast"].String(); + spell->castSound = AudioPath::fromJson(soundsNode["cast"]); //load level attributes const int levelsCount = GameConstants::SPELL_SCHOOL_LEVELS; @@ -927,7 +925,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = spell->id; //for all + b->sid = BonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) @@ -942,7 +940,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = spell->id; //for all + b->sid = BonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) @@ -988,15 +986,13 @@ void CSpellHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSpellHandler::getDefaultAllowed() const +std::set CSpellHandler::getDefaultAllowed() const { - std::vector allowedSpells; - allowedSpells.reserve(objects.size()); + std::set allowedSpells; for(const CSpell * s : objects) - { - allowedSpells.push_back( !(s->isSpecial() || s->isCreatureAbility())); - } + if (!s->isSpecial() && !s->isCreatureAbility()) + allowedSpells.insert(s->getId()); return allowedSpells; } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 769a3ebb1..efe807831 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -20,6 +20,7 @@ #include "../GameConstants.h" #include "../battle/BattleHex.h" #include "../bonuses/Bonus.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -44,7 +45,6 @@ class IBattleCast; struct SchoolInfo { SpellSchool id; //backlink - BonusType immunityBonus; std::string jsonName; }; @@ -66,7 +66,7 @@ public: double minimumAngle; ///resource name - std::string resourceName; + AnimationPath resourceName; template void serialize(Handler & h, const int version) { @@ -77,7 +77,7 @@ public: struct AnimationItem { - std::string resourceName; + AnimationPath resourceName; std::string effectName; VerticalPosition verticalPosition; int pause; @@ -120,7 +120,7 @@ public: h & affect; } - std::string selectProjectile(const double angle) const; + AnimationPath selectProjectile(const double angle) const; } animationInfo; public: @@ -188,17 +188,10 @@ public: using BTVector = std::vector; - si32 level; std::map school; - - si32 power; //spell's power - std::map probabilities; //% chance to gain for castles - bool combat; //is this spell combat (true) or adventure (false) - bool creatureAbility; //if true, only creatures can use this spell - si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative bool onlyOnWaterMap; //Spell will be banned on maps without water std::vector counteredSpells; //spells that are removed when effect of this spell is placed on creature (for bless-curse, haste-slow, and similar pairs) @@ -209,14 +202,14 @@ public: int64_t calculateDamage(const spells::Caster * caster) const override; - bool hasSchool(ESpellSchool school) const override; + bool hasSchool(SpellSchool school) const override; /** * Calls cb for each school this spell belongs to * * Set stop to true to abort looping */ - void forEachSchool(const std::function & cb) const override; + void forEachSchool(const std::function & cb) const override; spells::AimType getTargetType() const; @@ -269,44 +262,11 @@ public: const std::string & getIconScenarioBonus() const; const std::string & getIconScroll() const; - const std::string & getCastSound() const override; + const AudioPath & getCastSound() const; void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) - { - h & identifier; - if (version > 820) - h & modScope; - h & id; - h & level; - h & power; - h & probabilities; - h & attributes; - h & combat; - h & creatureAbility; - h & positiveness; - h & counteredSpells; - h & rising; - h & damage; - h & offensive; - h & targetType; - h & targetCondition; - h & iconImmune; - h & defaultProbability; - h & special; - h & castSound; - h & iconBook; - h & iconEffect; - h & iconScenarioBonus; - h & iconScroll; - h & levels; - h & school; - h & animationInfo; - h & nonMagical; - h & onlyOnWaterMap; - } friend class CSpellHandler; friend class Graphics; friend class test::CSpellTest; @@ -361,10 +321,16 @@ private: std::string iconScroll; ///sound related stuff - std::string castSound; + AudioPath castSound; std::vector levels; + si32 level; + si32 power; //spell's power + bool combat; //is this spell combat (true) or adventure (false) + bool creatureAbility; //if true, only creatures can use this 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 std::unique_ptr adventureMechanics;//(!) do not serialize }; @@ -383,16 +349,7 @@ public: * Gets a list of default allowed spells. OH3 spells are all allowed by default. * */ - std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - if(!h.saving) - { - afterLoadFinalization(); - } - } + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/spells/ExternalCaster.cpp b/lib/spells/ExternalCaster.cpp index a48735bb9..6c8fd8e29 100644 --- a/lib/spells/ExternalCaster.cpp +++ b/lib/spells/ExternalCaster.cpp @@ -42,7 +42,7 @@ void ExternalCaster::spendMana(ServerCallback * server, const int32_t spellCost) //do nothing } -int32_t ExternalCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ExternalCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { return schoolLevel; } diff --git a/lib/spells/ExternalCaster.h b/lib/spells/ExternalCaster.h index 22e91fcb1..4fcdf0dfa 100644 --- a/lib/spells/ExternalCaster.h +++ b/lib/spells/ExternalCaster.h @@ -14,6 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN +class SpellSchool; + namespace spells { @@ -27,7 +29,7 @@ public: void setActualCaster(const Caster * actualCaster); void setSpellSchoolLevel(int level); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; }; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index f8709b882..f83773dd7 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -36,7 +36,6 @@ #include "effects/Timed.h" #include "CSpellHandler.h" -#include "../NetPacks.h" #include "../CHeroHandler.h"//todo: remove #include "../IGameCallback.h"//todo: remove @@ -468,7 +467,7 @@ bool BaseMechanics::adaptGenericProblem(Problem & target) const return false; } -bool BaseMechanics::adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const +bool BaseMechanics::adaptProblem(ESpellCastProblem source, Problem & target) const { if(source == ESpellCastProblem::OK) return true; @@ -490,11 +489,11 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem::ESpellCastProblem source, Pr { //The %s prevents %s from casting 3rd level or higher spells. text.appendLocalString(EMetaText::GENERAL_TXT, 536); - text.replaceLocalString(EMetaText::ART_NAMES, b->sid); + text.replaceName(b->sid.as()); caster->getCasterName(text); target.add(std::move(text), spells::Problem::NORMAL); } - else if(b && b->source == BonusSource::TERRAIN_OVERLAY && VLC->battlefields()->getByIndex(b->sid)->identifier == "cursed_ground") + else if(b && b->source == BonusSource::TERRAIN_OVERLAY && VLC->battlefields()->getById(b->sid.as())->identifier == "cursed_ground") { text.appendLocalString(EMetaText::GENERAL_TXT, 537); target.add(std::move(text), spells::Problem::NORMAL); @@ -620,18 +619,6 @@ int64_t BaseMechanics::calculateRawEffectValue(int32_t basePowerMultiplier, int3 return owner->calculateRawEffectValue(getEffectLevel(), basePowerMultiplier, levelPowerMultiplier); } -std::vector BaseMechanics::getElementalImmunity() const -{ - std::vector ret; - - owner->forEachSchool([&](const SchoolInfo & cnf, bool & stop) - { - ret.push_back(cnf.immunityBonus); - }); - - return ret; -} - bool BaseMechanics::ownerMatches(const battle::Unit * unit) const { return ownerMatches(unit, owner->getPositiveness()); @@ -730,7 +717,7 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s) std::unique_ptr IAdventureSpellMechanics::createMechanics(const CSpell * s) { - switch (s->id) + switch(s->id.toEnum()) { case SpellID::SUMMON_BOAT: return std::make_unique(s); @@ -750,7 +737,7 @@ std::unique_ptr IAdventureSpellMechanics::createMechan case SpellID::VIEW_AIR: return std::make_unique(s); default: - return s->combat ? std::unique_ptr() : std::make_unique(s); + return s->isCombat() ? std::unique_ptr() : std::make_unique(s); } } diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index e540bf20d..2a0f8603d 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -23,6 +23,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct Query; class IBattleState; class CRandomGenerator; +class CreatureService; class CMap; class CGameInfoCallback; class CBattleInfoCallback; @@ -32,6 +33,11 @@ class CStack; class CGObjectInstance; class CGHeroInstance; +namespace spells +{ +class Service; +} + namespace vstd { class RNG; @@ -56,7 +62,7 @@ public: virtual bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) = 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 + virtual void genericQuery(Query * request, PlayerColor color, std::function)> callback) = 0;//TODO: type safety on query, use generic query packet when implemented }; namespace spells @@ -180,7 +186,7 @@ class DLL_LINKAGE Mechanics public: virtual ~Mechanics(); - virtual bool adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const = 0; + virtual bool adaptProblem(ESpellCastProblem source, Problem & target) const = 0; virtual bool adaptGenericProblem(Problem & target) const = 0; virtual std::vector rangeInHexes(BattleHex centralHex) const = 0; @@ -235,8 +241,6 @@ public: virtual int64_t applySpecificSpellBonus(int64_t value) const = 0; virtual int64_t calculateRawEffectValue(int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const = 0; - virtual std::vector getElementalImmunity() const = 0; - //Battle facade virtual bool ownerMatches(const battle::Unit * unit) const = 0; virtual bool ownerMatches(const battle::Unit * unit, const boost::logic::tribool positivness) const = 0; @@ -264,7 +268,7 @@ class DLL_LINKAGE BaseMechanics : public Mechanics public: virtual ~BaseMechanics(); - bool adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const override; + bool adaptProblem(ESpellCastProblem source, Problem & target) const override; bool adaptGenericProblem(Problem & target) const override; int32_t getSpellIndex() const override; @@ -296,8 +300,6 @@ public: int64_t applySpecificSpellBonus(int64_t value) const override; int64_t calculateRawEffectValue(int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const override; - std::vector getElementalImmunity() const override; - bool ownerMatches(const battle::Unit * unit) const override; bool ownerMatches(const battle::Unit * unit, const boost::logic::tribool positivness) const override; diff --git a/lib/spells/ObstacleCasterProxy.cpp b/lib/spells/ObstacleCasterProxy.cpp index 5b1d5ea78..874871ec5 100644 --- a/lib/spells/ObstacleCasterProxy.cpp +++ b/lib/spells/ObstacleCasterProxy.cpp @@ -22,7 +22,7 @@ ObstacleCasterProxy::ObstacleCasterProxy(PlayerColor owner_, const Caster * hero { } -int32_t ObstacleCasterProxy::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ObstacleCasterProxy::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { return obs.spellLevel; } diff --git a/lib/spells/ObstacleCasterProxy.h b/lib/spells/ObstacleCasterProxy.h index 761eea2b5..cd1c540d5 100644 --- a/lib/spells/ObstacleCasterProxy.h +++ b/lib/spells/ObstacleCasterProxy.h @@ -9,7 +9,6 @@ */ #include "ProxyCaster.h" -#include "../lib/NetPacksBase.h" #include "../battle/BattleHex.h" #include "../battle/CObstacleInstance.h" @@ -36,7 +35,7 @@ class DLL_LINKAGE ObstacleCasterProxy : public SilentCaster public: ObstacleCasterProxy(PlayerColor owner_, const Caster * hero_, const SpellCreatedObstacle & obs_); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; int64_t getSpellBonus(const Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int32_t getEffectPower(const Spell * spell) const override; diff --git a/lib/spells/ProxyCaster.cpp b/lib/spells/ProxyCaster.cpp index 2ffaa49c7..7622392bb 100644 --- a/lib/spells/ProxyCaster.cpp +++ b/lib/spells/ProxyCaster.cpp @@ -36,7 +36,7 @@ int32_t ProxyCaster::getCasterUnitId() const return -1; } -int32_t ProxyCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t ProxyCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { if(actualCaster) return actualCaster->getSpellSchoolLevel(spell, outSelectedSchool); diff --git a/lib/spells/ProxyCaster.h b/lib/spells/ProxyCaster.h index 85fcf240c..67557dc02 100644 --- a/lib/spells/ProxyCaster.h +++ b/lib/spells/ProxyCaster.h @@ -24,7 +24,7 @@ public: virtual ~ProxyCaster(); int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; int64_t getSpellBonus(const Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const Spell * spell, int64_t base) const override; diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index cc0cbf0d5..bda1f32df 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -18,9 +18,12 @@ #include "../bonuses/BonusParams.h" #include "../bonuses/BonusList.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModUtility.h" #include "../serializer/JsonSerializeFormat.h" #include "../VCMI_Lib.h" -#include "../CModHandler.h" + +#include VCMI_LIB_NAMESPACE_BEGIN @@ -154,7 +157,7 @@ protected: { std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, m->getSpellIndex(), 1), cachingStr.str()); + return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId()), 1), cachingStr.str()); } }; @@ -172,25 +175,25 @@ protected: bool check(const Mechanics * m, const battle::Unit * target) const override { bool elementalImmune = false; + auto bearer = target->getBonusBearer(); - auto filter = m->getElementalImmunity(); - - for(auto element : filter) + m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if(target->hasBonusOfType(element, 0)) //always resist if immune to all spells altogether + if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, BonusSubtypeID(cnf))) { elementalImmune = true; - break; + stop = true; //only bonus from one school is used } else if(!m->isPositiveSpell()) //negative or indifferent { - if(target->hasBonusOfType(element, 1)) + if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSubtypeID(cnf))) { elementalImmune = true; - break; + stop = true; //only bonus from one school is used } } - } + }); + return elementalImmune; } }; @@ -228,7 +231,7 @@ public: protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, m->getSpellIndex()); + return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId())); } }; @@ -256,7 +259,7 @@ public: builder << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; cachingString = builder.str(); - selector = Selector::source(BonusSource::SPELL_EFFECT, spellID.num); + selector = Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)); } protected: @@ -289,8 +292,8 @@ class ImmunityNegationCondition : public TargetConditionItemBase protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, 0); - const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, 1); + const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusCustomSubtype::immunityBattleWide); + const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusCustomSubtype::immunityEnemyHero); //Non-magical effects is not affected by orb of vulnerability if(!m->isMagicalEffect()) return false; @@ -371,7 +374,7 @@ public: } else if(type == "creature") { - auto rawId = VLC->modh->identifiers.getIdentifier(scope, type, identifier, true); + auto rawId = VLC->identifiers()->getIdentifier(scope, type, identifier, true); if(rawId) return std::make_shared(CreatureID(rawId.value())); @@ -380,7 +383,7 @@ public: } else if(type == "spell") { - auto rawId = VLC->modh->identifiers.getIdentifier(scope, type, identifier, true); + auto rawId = VLC->identifiers()->getIdentifier(scope, type, identifier, true); if(rawId) return std::make_shared(SpellID(rawId.value())); @@ -539,7 +542,7 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo std::string type; std::string identifier; - CModHandler::parseIdentifier(keyValue.first, scope, type, identifier); + ModUtility::parseIdentifier(keyValue.first, scope, type, identifier); item = itemFactory->createConfigurable(keyValue.second.meta, type, identifier); } diff --git a/lib/spells/ViewSpellInt.cpp b/lib/spells/ViewSpellInt.cpp index 2b8e36ec7..1507d761a 100644 --- a/lib/spells/ViewSpellInt.cpp +++ b/lib/spells/ViewSpellInt.cpp @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN ObjectPosInfo::ObjectPosInfo(const CGObjectInstance * obj): - pos(obj->visitablePos()), id(obj->ID), subId(obj->subID), owner(obj->tempOwner) + pos(obj->visitablePos()), id(obj->ID), subId(obj->getObjTypeIndex()), owner(obj->tempOwner) { } diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 10fa74a8e..7b189d52b 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -14,11 +14,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../mapObjects/CGTownInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -72,6 +72,7 @@ void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const return; CatapultAttack ca; + ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); for(int i = 0; i < targetsToAttack; i++) @@ -137,6 +138,7 @@ void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const attack.damageDealt = getRandomDamage(server); CatapultAttack ca; //package for clients + ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); ca.attackedParts.push_back(attack); server->apply(&ca); @@ -188,6 +190,7 @@ int Catapult::getRandomDamage (ServerCallback * server) const void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) const { BattleUnitsChanged removeUnits; + removeUnits.battleID = m->battle()->getBattle()->getBattleID(); for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER }) { diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index 4d7a4d021..491fe5023 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -12,9 +12,11 @@ #include "Clone.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" +#include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -60,6 +62,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg info.summoned = true; BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); server->apply(&pack); @@ -67,6 +70,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg //TODO: use BattleUnitsChanged with UPDATE operation BattleUnitsChanged cloneFlags; + cloneFlags.battleID = m->battle()->getBattle()->getBattleID(); const auto *cloneUnit = m->battle()->battleGetUnitByID(unitId); @@ -89,7 +93,9 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg server->apply(&cloneFlags); SetStackEffect sse; - Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, SpellID::CLONE); //TODO: use special bonus type + sse.battleID = m->battle()->getBattle()->getBattleID(); + + Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(SpellID(SpellID::CLONE))); //TODO: use special bonus type lifeTimeMarker.turnsRemain = m->getEffectDuration(); std::vector buffer; buffer.push_back(lifeTimeMarker); diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 93295f93e..6a28a5349 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -14,10 +14,11 @@ #include "../CSpellHandler.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../CStack.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../CGeneralTextHandler.h" #include "../../serializer/JsonSerializeFormat.h" @@ -34,6 +35,9 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar { StacksInjured stacksInjured; BattleLogMessage blm; + stacksInjured.battleID = m->battle()->getBattle()->getBattleID(); + blm.battleID = m->battle()->getBattle()->getBattleID(); + size_t targetIndex = 0; const battle::Unit * firstTarget = nullptr; const bool describe = server->describeChanges(); @@ -48,6 +52,7 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar if(unit && unit->alive()) { BattleStackAttacked bsa; + bsa.battleID = m->battle()->getBattle()->getBattleID(); bsa.damageAmount = damageForTarget(targetIndex, m, unit); bsa.stackAttacked = unit->unitId(); bsa.attackerID = -1; @@ -85,11 +90,11 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const if(!UnitEffect::isReceptive(m, unit)) return false; - bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)) >= 100); //General spell damage immunity + bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::ANY)) >= 100); //General spell damage immunity //elemental immunity for damage - m->getSpell()->forEachSchool([&](const SchoolInfo & cnf, bool & stop) + m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id) >= 100); //100% reduction is immunity + isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf)) >= 100); //100% reduction is immunity }); return !isImmune; @@ -171,7 +176,7 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, { MetaString line; line.appendLocalString(EMetaText::GENERAL_TXT, 376); // Spell %s does %d damage - line.replaceLocalString(EMetaText::SPELL_NAME, m->getSpellIndex()); + line.replaceName(m->getSpellId()); line.replaceNumber(static_cast(damage)); log.push_back(line); diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index cd4661b88..8f5376702 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -12,9 +12,10 @@ #include "DemonSummon.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/BattleInfo.h" #include "../../battle/CUnitState.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,6 +28,7 @@ namespace effects void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const Destination & dest : target) { @@ -47,7 +49,7 @@ void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const Effe break; } - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); int32_t deadCount = targetStack->unitBaseAmount(); int32_t deadTotalHealth = targetStack->getTotalHealth(); @@ -109,7 +111,7 @@ bool DemonSummon::isValidTarget(const Mechanics * m, const battle::Unit * unit) if (unit->isGhost()) return false; - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); if (unit->getTotalHealth() < creatureType->getMaxHealth()) return false; diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 62fb5bfd2..8ad649f9a 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -16,9 +16,13 @@ #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" +#include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../bonuses/BonusList.h" +#include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -33,6 +37,8 @@ void Dispel::apply(ServerCallback * server, const Mechanics * m, const EffectTar const bool describe = server->describeChanges(); SetStackEffect sse; BattleLogMessage blm; + blm.battleID = m->battle()->getBattle()->getBattleID(); + sse.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & t : target) { @@ -87,7 +93,7 @@ std::shared_ptr Dispel::getBonuses(const Mechanics * m, const b { if(bonus->source == BonusSource::SPELL_EFFECT) { - const Spell * sourceSpell = SpellID(bonus->sid).toSpell(m->spells()); + const Spell * sourceSpell = bonus->sid.as().toEntity(m->spells()); if(!sourceSpell) return false;//error diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index e026d12b6..d3d84d8a9 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -13,11 +13,12 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,7 +36,11 @@ void Heal::apply(ServerCallback * server, const Mechanics * m, const EffectTarge void Heal::apply(int64_t value, ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleLogMessage logMessage; + logMessage.battleID = m->battle()->getBattle()->getBattleID(); + BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); + prepareHealEffect(value, pack, logMessage, *server->getRNG(), m, target); if(!pack.changedStacks.empty()) server->apply(&pack); diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 0c8a6c93c..359f2460b 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -14,12 +14,13 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../mapObjects/CGTownInstance.h" #include "../../bonuses/Limiters.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../networkPacks/PacksForClient.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN @@ -84,12 +85,12 @@ void Moat::convertBonus(const Mechanics * m, std::vector & converted) con if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL) { - nb.sid = Bonus::getSid32(m->battle()->battleGetDefendedTown()->getFaction(), BuildingID::CITADEL); + nb.sid = BonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); nb.source = BonusSource::TOWN_STRUCTURE; } else { - nb.sid = m->getSpellIndex(); //for all + nb.sid = BonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all } std::set flatMoatHexes; @@ -116,6 +117,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge for(auto & b : converted) { GiveBonus gb(GiveBonus::ETarget::BATTLE); + gb.id = m->battle()->getBattle()->getBattleID(); gb.bonus = b; server->apply(&gb); } @@ -128,6 +130,7 @@ void Moat::placeObstacles(ServerCallback * server, const Mechanics * m, const Ef assert(m->casterSide == BattleSide::DEFENDER); // Moats are always cast by defender BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 5a3247694..b9dbe3b0b 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -14,10 +14,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN @@ -85,9 +86,9 @@ void ObstacleSideOptions::serializeJson(JsonSerializeFormat & handler) serializeRelativeShape(handler, "shape", shape); serializeRelativeShape(handler, "range", range); - handler.serializeString("appearSound", appearSound); - handler.serializeString("appearAnimation", appearAnimation); - handler.serializeString("animation", animation); + handler.serializeStruct("appearSound", appearSound); + handler.serializeStruct("appearAnimation", appearAnimation); + handler.serializeStruct("animation", animation); handler.serializeInt("offsetY", offsetY); } @@ -270,6 +271,7 @@ void Obstacle::placeObstacles(ServerCallback * server, const Mechanics * m, cons const ObstacleSideOptions & options = sideOptions.at(m->casterSide); BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); diff --git a/lib/spells/effects/Obstacle.h b/lib/spells/effects/Obstacle.h index 4bf9159b3..1dd256e1a 100644 --- a/lib/spells/effects/Obstacle.h +++ b/lib/spells/effects/Obstacle.h @@ -30,9 +30,9 @@ public: RelativeShape shape; //shape of single obstacle relative to obstacle position RelativeShape range; //position of obstacles relative to effect destination - std::string appearSound; - std::string appearAnimation; - std::string animation; + AudioPath appearSound; + AnimationPath appearAnimation; + AnimationPath animation; int offsetY = 0; diff --git a/lib/spells/effects/RemoveObstacle.cpp b/lib/spells/effects/RemoveObstacle.cpp index 112435dcb..f24b0e881 100644 --- a/lib/spells/effects/RemoveObstacle.cpp +++ b/lib/spells/effects/RemoveObstacle.cpp @@ -14,10 +14,10 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/CObstacleInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -44,6 +44,7 @@ bool RemoveObstacle::applicable(Problem & problem, const Mechanics * m, const Ef void RemoveObstacle::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & obstacle : getTargets(m, target, false)) { diff --git a/lib/spells/effects/Sacrifice.cpp b/lib/spells/effects/Sacrifice.cpp index 7beb1a201..978d0f4d4 100644 --- a/lib/spells/effects/Sacrifice.cpp +++ b/lib/spells/effects/Sacrifice.cpp @@ -13,11 +13,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN @@ -123,6 +123,7 @@ void Sacrifice::apply(ServerCallback * server, const Mechanics * m, const Effect Heal::apply(calculateHealEffectValue(m, victim), server, m, healTarget); BattleUnitsChanged removeUnits; + removeUnits.battleID = m->battle()->getBattle()->getBattleID(); removeUnits.changedStacks.emplace_back(victim->unitId(), UnitChanges::EOperation::REMOVE); server->apply(&removeUnits); } diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index c0eb0bbe1..ac3c0206b 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -13,14 +13,15 @@ #include "Registry.h" #include "../ISpellMechanics.h" +#include "../../MetaString.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/BattleInfo.h" #include "../../battle/Unit.h" -#include "../../NetPacks.h" #include "../../serializer/JsonSerializeFormat.h" - #include "../../CCreatureHandler.h" #include "../../CHeroHandler.h" #include "../../mapObjects/CGHeroInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN @@ -65,7 +66,7 @@ bool Summon::applicable(Problem & problem, const Mechanics * m) const { text.replaceRawString(caster->getNameTranslated()); - text.replaceLocalString(EMetaText::CRE_PL_NAMES, elemental->creatureIndex()); + text.replaceNamePlural(elemental->creatureId()); if(caster->type->gender == EHeroGender::FEMALE) text.replaceLocalString(EMetaText::GENERAL_TXT, 540); @@ -87,6 +88,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar auto valueWithBonus = m->applySpecificSpellBonus(m->calculateRawEffectValue(0, m->getEffectPower()));//TODO: consider use base power too BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & dest : target) { @@ -105,7 +107,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar if(summonByHealth) { - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); auto creatureMaxHealth = creatureType->getMaxHealth(); amount = static_cast(valueWithBonus / creatureMaxHealth); } diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index c845793da..4d29e9483 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -12,10 +12,11 @@ #include "Teleport.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" -#include "../../serializer/JsonSerializeFormat.h" #include "../../battle/Unit.h" +#include "../../networkPacks/PacksForClientBattle.h" +#include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -76,6 +77,7 @@ void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectT const auto destination = target[1].hexValue; BattleStackMoved pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); pack.distance = 0; pack.stack = targetUnit->unitId(); std::vector tiles; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 8fac34e2d..e006d37a4 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -13,9 +13,13 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" +#include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -47,7 +51,7 @@ static void describeEffect(std::vector & log, const Mechanics * m, c { case BonusType::NOT_ACTIVE: { - switch(bonus.subtype) + switch(bonus.subtype.as().toEnum()) { case SpellID::STONE_GAZE: addLogLine(558, boost::logic::indeterminate); @@ -108,14 +112,16 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg const auto *casterHero = dynamic_cast(m->caster); if(casterHero) { - peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, m->getSpellIndex())); - addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, m->getSpellIndex())); - fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, m->getSpellIndex())); + peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(m->getSpellId()))); + addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); + fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); } //TODO: does hero specialty should affects his stack casting spells? SetStackEffect sse; BattleLogMessage blm; + blm.battleID = m->battle()->getBattle()->getBattleID(); + sse.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & t : target) { @@ -219,14 +225,14 @@ void Timed::convertBonus(const Mechanics * m, int32_t & duration, std::vectorgetSpellIndex(); //for all + nb.sid = BonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all //fix to original config: shield should display damage reduction - if((nb.sid == SpellID::SHIELD || nb.sid == SpellID::AIR_SHIELD) && (nb.type == BonusType::GENERAL_DAMAGE_REDUCTION)) + if((nb.sid.as() == SpellID::SHIELD || nb.sid.as() == SpellID::AIR_SHIELD) && (nb.type == BonusType::GENERAL_DAMAGE_REDUCTION)) nb.val = 100 - nb.val; //we need to know who cast Bind - else if(nb.sid == SpellID::BIND && nb.type == BonusType::BIND_EFFECT && m->caster->getHeroCaster() == nullptr) + else if(nb.sid.as() == SpellID::BIND && nb.type == BonusType::BIND_EFFECT && m->caster->getHeroCaster() == nullptr) nb.additionalInfo = m->caster->getCasterUnitId(); converted.push_back(nb); diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index 646f54aec..3d1be5842 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -14,7 +14,6 @@ #include "../ISpellMechanics.h" #include "../../bonuses/BonusSelector.h" -#include "../../NetPacksBase.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" @@ -253,7 +252,7 @@ bool UnitEffect::isReceptive(const Mechanics * m, const battle::Unit * unit) con //SPELL_IMMUNITY absolute case std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, m->getSpellIndex(), 1), cachingStr.str()); + return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId()), 1), cachingStr.str()); } else { diff --git a/lib/vcmi_endian.h b/lib/vcmi_endian.h index c1a81357d..fe0b17581 100644 --- a/lib/vcmi_endian.h +++ b/lib/vcmi_endian.h @@ -1,81 +1,81 @@ -/* - * vcmi_endian.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 //FIXME: use std::byteswap in C++23 - -VCMI_LIB_NAMESPACE_BEGIN - -/* Reading values from memory. - * - * read_le_u16, read_le_u32 : read a little endian value from - * memory. On big endian machines, the value will be byteswapped. - */ - -#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER) - -#if defined(_MSC_VER) -#define PACKED_STRUCT_BEGIN __pragma( pack(push, 1) ) -#define PACKED_STRUCT_END __pragma( pack(pop) ) -#else -#define PACKED_STRUCT_BEGIN -#define PACKED_STRUCT_END __attribute__(( packed )) -#endif - -PACKED_STRUCT_BEGIN struct unaligned_Uint16 { ui16 val; } PACKED_STRUCT_END; -PACKED_STRUCT_BEGIN struct unaligned_Uint32 { ui32 val; } PACKED_STRUCT_END; - -static inline ui16 read_unaligned_u16(const void *p) -{ - const auto * v = reinterpret_cast(p); - return v->val; -} - -static inline ui32 read_unaligned_u32(const void *p) -{ - const auto * v = reinterpret_cast(p); - return v->val; -} - -#define read_le_u16(p) (boost::endian::native_to_little(read_unaligned_u16(p))) -#define read_le_u32(p) (boost::endian::native_to_little(read_unaligned_u32(p))) - -#else - -#warning UB: unaligned memory access - -#define read_le_u16(p) (boost::endian::native_to_little(* reinterpret_cast(p))) -#define read_le_u32(p) (boost::endian::native_to_little(* reinterpret_cast(p))) - -#define PACKED_STRUCT_BEGIN -#define PACKED_STRUCT_END - -#endif - -static inline char readChar(const ui8 * buffer, int & i) -{ - return buffer[i++]; -} - -static inline std::string readString(const ui8 * buffer, int & i) -{ - int len = read_le_u32(buffer + i); - i += 4; - assert(len >= 0 && len <= 500000); //not too long - std::string ret; - ret.reserve(len); - for(int gg = 0; gg < len; ++gg) - { - ret += buffer[i++]; - } - return ret; -} - -VCMI_LIB_NAMESPACE_END +/* + * vcmi_endian.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 //FIXME: use std::byteswap in C++23 + +VCMI_LIB_NAMESPACE_BEGIN + +/* Reading values from memory. + * + * read_le_u16, read_le_u32 : read a little endian value from + * memory. On big endian machines, the value will be byteswapped. + */ + +#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER) + +#if defined(_MSC_VER) +#define PACKED_STRUCT_BEGIN __pragma( pack(push, 1) ) +#define PACKED_STRUCT_END __pragma( pack(pop) ) +#else +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END __attribute__(( packed )) +#endif + +PACKED_STRUCT_BEGIN struct unaligned_Uint16 { ui16 val; } PACKED_STRUCT_END; +PACKED_STRUCT_BEGIN struct unaligned_Uint32 { ui32 val; } PACKED_STRUCT_END; + +static inline ui16 read_unaligned_u16(const void *p) +{ + const auto * v = reinterpret_cast(p); + return v->val; +} + +static inline ui32 read_unaligned_u32(const void *p) +{ + const auto * v = reinterpret_cast(p); + return v->val; +} + +#define read_le_u16(p) (boost::endian::native_to_little(read_unaligned_u16(p))) +#define read_le_u32(p) (boost::endian::native_to_little(read_unaligned_u32(p))) + +#else + +#warning UB: unaligned memory access + +#define read_le_u16(p) (boost::endian::native_to_little(* reinterpret_cast(p))) +#define read_le_u32(p) (boost::endian::native_to_little(* reinterpret_cast(p))) + +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END + +#endif + +static inline char readChar(const ui8 * buffer, int & i) +{ + return buffer[i++]; +} + +static inline std::string readString(const ui8 * buffer, int & i) +{ + int len = read_le_u32(buffer + i); + i += 4; + assert(len >= 0 && len <= 500000); //not too long + std::string ret; + ret.reserve(len); + for(int gg = 0; gg < len; ++gg) + { + ret += buffer[i++]; + } + return ret; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp new file mode 100644 index 000000000..02f0cd377 --- /dev/null +++ b/lib/vstd/DateUtils.cpp @@ -0,0 +1,44 @@ +#include "StdInc.h" +#include + +#if defined(VCMI_ANDROID) +#include "../CAndroidVMHelper.h" +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) + { +#if defined(VCMI_ANDROID) + CAndroidVMHelper vmHelper; + return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); +#endif + + std::tm tm = *std::localtime(&dt); + std::stringstream s; + try + { + s.imbue(std::locale("")); + } + catch(const std::runtime_error & e) + { + // locale not be available - keep default / global + } + s << std::put_time(&tm, "%x %X"); + return s.str(); + } + + DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt) + { + std::tm tm = *std::localtime(&dt); + std::stringstream s; + s << std::put_time(&tm, "%Y%m%dT%H%M%S"); + return s.str(); + } + +} + +VCMI_LIB_NAMESPACE_END diff --git a/license.txt b/license.txt index 60a54ffbd..88f62e95c 100644 --- a/license.txt +++ b/license.txt @@ -1,280 +1,280 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) 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 -this service 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 make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. 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. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -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 -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the 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 a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE 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. - + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) 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 +this service 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 make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. 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. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +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 +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE 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. + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 45d003f24..84e6a2be0 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -14,6 +14,7 @@ #include "Animation.h" #include "BitmapHandler.h" +#include "graphics.h" #include "../lib/vcmi_endian.h" #include "../lib/filesystem/Filesystem.h" @@ -81,7 +82,7 @@ class FileCache static const int cacheSize = 50; //Max number of cached files struct FileData { - ResourceID name; + ResourcePath name; size_t size; std::unique_ptr data; @@ -91,7 +92,7 @@ class FileCache std::copy(data.get(), data.get() + size, ret.get()); return ret; } - FileData(ResourceID name_, size_t size_, std::unique_ptr data_): + FileData(ResourcePath name_, size_t size_, std::unique_ptr data_): name{std::move(name_)}, size{size_}, data{std::move(data_)} @@ -100,7 +101,7 @@ class FileCache std::deque cache; public: - std::unique_ptr getCachedFile(ResourceID rid) + std::unique_ptr getCachedFile(ResourcePath rid) { for(auto & file : cache) { @@ -169,7 +170,7 @@ DefFile::DefFile(std::string Name): qRgba(0, 0, 0, 128), // 50% - shadow body below selection qRgba(0, 0, 0, 64) // 75% - shadow border below selection }; - data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); + data = animationCache.getCachedFile(AnimationPath::builtin("SPRITES/" + Name)); palette = std::make_unique>(256); int it = 0; @@ -583,10 +584,10 @@ void Animation::init() source[defEntry.first].resize(defEntry.second); } - ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); + JsonPath resID = JsonPath::builtin("SPRITES/" + name); - //if(vstd::contains(graphics->imageLists, resID.getName())) - //initFromJson(graphics->imageLists[resID.getName()]); + if(vstd::contains(graphics->imageLists, resID.getName())) + initFromJson(graphics->imageLists[resID.getName()]); auto configList = CResourceHandler::get()->getResourcesWithName(resID); @@ -656,7 +657,7 @@ Animation::Animation(std::string Name): name.erase(dotPos); std::transform(name.begin(), name.end(), name.begin(), toupper); - ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); + auto resource = AnimationPath::builtin("SPRITES/" + name); if(CResourceHandler::get()->existsResource(resource)) defFile = std::make_shared(name); diff --git a/mapeditor/BitmapHandler.cpp b/mapeditor/BitmapHandler.cpp index 79034c88c..67c5d4224 100644 --- a/mapeditor/BitmapHandler.cpp +++ b/mapeditor/BitmapHandler.cpp @@ -92,13 +92,13 @@ namespace BitmapHandler logGlobal->warn("Call to loadBitmap with void fname!"); return QImage(); } - if(!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) + if(!CResourceHandler::get()->existsResource(ResourcePath(path + fname, EResType::IMAGE))) { return QImage(); } - auto fullpath = CResourceHandler::get()->getResourceName(ResourceID(path + fname, EResType::IMAGE)); - auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll(); + auto fullpath = CResourceHandler::get()->getResourceName(ResourcePath(path + fname, EResType::IMAGE)); + auto readFile = CResourceHandler::get()->load(ResourcePath(path + fname, EResType::IMAGE))->readAll(); if(isPCX(readFile.first.get())) {//H3-style PCX diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index ae8588ed9..3dd588fed 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -12,7 +12,16 @@ set(editor_SRCS generatorprogress.cpp mapview.cpp objectbrowser.cpp - mapsettings.cpp + mapsettings/abstractsettings.cpp + mapsettings/mapsettings.cpp + mapsettings/generalsettings.cpp + mapsettings/modsettings.cpp + mapsettings/timedevent.cpp + mapsettings/victoryconditions.cpp + mapsettings/loseconditions.cpp + mapsettings/eventsettings.cpp + mapsettings/rumorsettings.cpp + mapsettings/translations.cpp playersettings.cpp playerparams.cpp scenelayer.cpp @@ -24,6 +33,9 @@ set(editor_SRCS inspector/messagewidget.cpp inspector/rewardswidget.cpp inspector/questwidget.cpp + inspector/heroskillswidget.cpp + inspector/PickObjectDelegate.cpp + inspector/portraitwidget.cpp resourceExtractor/ResourceConverter.cpp ) @@ -40,7 +52,16 @@ set(editor_HEADERS generatorprogress.h mapview.h objectbrowser.h - mapsettings.h + mapsettings/abstractsettings.h + mapsettings/mapsettings.h + mapsettings/generalsettings.h + mapsettings/modsettings.h + mapsettings/timedevent.h + mapsettings/victoryconditions.h + mapsettings/loseconditions.h + mapsettings/eventsettings.h + mapsettings/rumorsettings.h + mapsettings/translations.h playersettings.h playerparams.h scenelayer.h @@ -52,6 +73,9 @@ set(editor_HEADERS inspector/messagewidget.h inspector/rewardswidget.h inspector/questwidget.h + inspector/heroskillswidget.h + inspector/PickObjectDelegate.h + inspector/portraitwidget.h resourceExtractor/ResourceConverter.h ) @@ -59,7 +83,15 @@ set(editor_FORMS mainwindow.ui windownewmap.ui generatorprogress.ui - mapsettings.ui + mapsettings/mapsettings.ui + mapsettings/generalsettings.ui + mapsettings/modsettings.ui + mapsettings/timedevent.ui + mapsettings/victoryconditions.ui + mapsettings/loseconditions.ui + mapsettings/eventsettings.ui + mapsettings/rumorsettings.ui + mapsettings/translations.ui playersettings.ui playerparams.ui validator.ui @@ -68,6 +100,8 @@ set(editor_FORMS inspector/messagewidget.ui inspector/rewardswidget.ui inspector/questwidget.ui + inspector/heroskillswidget.ui + inspector/portraitwidget.ui ) set(editor_TS @@ -78,6 +112,7 @@ set(editor_TS translation/russian.ts translation/spanish.ts translation/ukrainian.ts + translation/vietnamese.ts ) assign_source_group(${editor_SRCS} ${editor_HEADERS} mapeditor.rc) @@ -151,8 +186,8 @@ enable_pch(vcmieditor) # Copy to build directory for easier debugging add_custom_command(TARGET vcmieditor POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/ - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation ) install(TARGETS vcmieditor DESTINATION ${BIN_DIR}) diff --git a/mapeditor/StdInc.cpp b/mapeditor/StdInc.cpp index b64b59be5..6237e6e6f 100644 --- a/mapeditor/StdInc.cpp +++ b/mapeditor/StdInc.cpp @@ -1 +1 @@ -#include "StdInc.h" +#include "StdInc.h" diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 817f9433c..4dc45a2ef 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -1,55 +1,55 @@ -#pragma once - -#include "../Global.h" - -#define VCMI_EDITOR_VERSION "0.1" -#define VCMI_EDITOR_NAME "VCMI Map Editor" - -#include -#include -#include -#include -#include -#include -#include - -VCMI_LIB_USING_NAMESPACE - -using NumericPointer = typename std::conditional::type; - -template -NumericPointer data_cast(Type * _pointer) -{ - static_assert(sizeof(Type *) == sizeof(NumericPointer), - "Cannot compile for that architecture, see NumericPointer definition"); - - return reinterpret_cast(_pointer); -} - -template -Type * data_cast(NumericPointer _numeric) -{ - static_assert(sizeof(Type *) == sizeof(NumericPointer), - "Cannot compile for that architecture, see NumericPointer definition"); - - return reinterpret_cast(_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 -} +#pragma once + +#include "../Global.h" + +#define VCMI_EDITOR_VERSION "0.2" +#define VCMI_EDITOR_NAME "VCMI Map Editor" + +#include +#include +#include +#include +#include +#include +#include + +VCMI_LIB_USING_NAMESPACE + +using NumericPointer = typename std::conditional::type; + +template +NumericPointer data_cast(Type * _pointer) +{ + static_assert(sizeof(Type *) == sizeof(NumericPointer), + "Cannot compile for that architecture, see NumericPointer definition"); + + return reinterpret_cast(_pointer); +} + +template +Type * data_cast(NumericPointer _numeric) +{ + static_assert(sizeof(Type *) == sizeof(NumericPointer), + "Cannot compile for that architecture, see NumericPointer definition"); + + return reinterpret_cast(_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 722e4328e..147fa0577 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -24,7 +24,6 @@ #include "../lib/filesystem/CBinaryReader.h" #include "Animation.h" #include "../lib/CThreadHelper.h" -#include "../lib/CModHandler.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" @@ -41,7 +40,7 @@ Graphics * graphics = nullptr; void Graphics::loadPaletteAndColors() { - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); std::string pals((char*)textFile.first.get(), textFile.second); playerColorPalette.resize(256); @@ -60,7 +59,7 @@ void Graphics::loadPaletteAndColors() neutralColorPalette.resize(32); - auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); + auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); CBinaryReader reader(stream.get()); for(int i = 0; i < 32; ++i) @@ -130,8 +129,8 @@ void Graphics::loadHeroAnimations() { for(auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates()) { - if(!heroAnimations.count(templ->animationFile)) - heroAnimations[templ->animationFile] = loadHeroAnimation(templ->animationFile); + if(!heroAnimations.count(templ->animationFile.getName())) + heroAnimations[templ->animationFile.getName()] = loadHeroAnimation(templ->animationFile.getName()); } } @@ -228,7 +227,7 @@ void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player) if(sur->format() == QImage::Format_Indexed8) { auto palette = sur->colorTable(); - if(player < PlayerColor::PLAYER_LIMIT) + if(player.isValidPlayer()) { for(int i = 0; i < 32; ++i) palette[224 + i] = playerColorPalette[player.getNum() * 32 + i]; @@ -239,7 +238,7 @@ void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player) } else { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); + logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); return; } //FIXME: not all player colored images have player palette at last 32 indexes @@ -271,7 +270,7 @@ std::shared_ptr Graphics::getHeroAnimation(const std::shared_ptr(); } - std::shared_ptr ret = loadHeroAnimation(info->animationFile); + std::shared_ptr ret = loadHeroAnimation(info->animationFile.getName()); //already loaded if(ret) @@ -280,8 +279,8 @@ std::shared_ptr Graphics::getHeroAnimation(const std::shared_ptr(info->animationFile); - heroAnimations[info->animationFile] = ret; + ret = std::make_shared(info->animationFile.getOriginalName()); + heroAnimations[info->animationFile.getName()] = ret; ret->preload(); return ret; @@ -295,7 +294,7 @@ std::shared_ptr Graphics::getAnimation(const std::shared_ptr(); } - std::shared_ptr ret = mapObjectAnimations[info->animationFile]; + std::shared_ptr ret = mapObjectAnimations[info->animationFile.getName()]; //already loaded if(ret) @@ -304,8 +303,8 @@ std::shared_ptr Graphics::getAnimation(const std::shared_ptr(info->animationFile); - mapObjectAnimations[info->animationFile] = ret; + ret = std::make_shared(info->animationFile.getOriginalName()); + mapObjectAnimations[info->animationFile.getName()] = ret; ret->preload(); return ret; diff --git a/mapeditor/graphics.h b/mapeditor/graphics.h index bd04c2246..009cf4399 100644 --- a/mapeditor/graphics.h +++ b/mapeditor/graphics.h @@ -11,6 +11,7 @@ //code is copied from vcmiclient/Graphics.h with minimal changes #include "../lib/GameConstants.h" +#include "../lib/filesystem/ResourcePath.h" #include VCMI_LIB_NAMESPACE_BEGIN diff --git a/mapeditor/icons/brush-0.png b/mapeditor/icons/brush-0.png new file mode 100644 index 000000000..2aa81a4e7 Binary files /dev/null and b/mapeditor/icons/brush-0.png differ diff --git a/mapeditor/icons/brush-6.png b/mapeditor/icons/brush-6.png new file mode 100644 index 000000000..76c926b39 Binary files /dev/null and b/mapeditor/icons/brush-6.png differ diff --git a/mapeditor/icons/brush-7.png b/mapeditor/icons/brush-7.png new file mode 100644 index 000000000..0b075bd33 Binary files /dev/null and b/mapeditor/icons/brush-7.png differ diff --git a/mapeditor/icons/lock-closed.png b/mapeditor/icons/lock-closed.png new file mode 100644 index 000000000..9d7e19c17 Binary files /dev/null and b/mapeditor/icons/lock-closed.png differ diff --git a/mapeditor/icons/lock-open.png b/mapeditor/icons/lock-open.png new file mode 100644 index 000000000..d382de4c4 Binary files /dev/null and b/mapeditor/icons/lock-open.png differ diff --git a/mapeditor/icons/translations.png b/mapeditor/icons/translations.png new file mode 100644 index 000000000..1935db72f Binary files /dev/null and b/mapeditor/icons/translations.png differ diff --git a/mapeditor/icons/zoom_base.png b/mapeditor/icons/zoom_base.png new file mode 100644 index 000000000..62a5dae81 Binary files /dev/null and b/mapeditor/icons/zoom_base.png differ diff --git a/mapeditor/icons/zoom_minus.png b/mapeditor/icons/zoom_minus.png new file mode 100644 index 000000000..7bba8cbc1 Binary files /dev/null and b/mapeditor/icons/zoom_minus.png differ diff --git a/mapeditor/icons/zoom_plus.png b/mapeditor/icons/zoom_plus.png new file mode 100644 index 000000000..0d07f5456 Binary files /dev/null and b/mapeditor/icons/zoom_plus.png differ diff --git a/mapeditor/icons/zoom_zero.png b/mapeditor/icons/zoom_zero.png new file mode 100644 index 000000000..8a45945b4 Binary files /dev/null and b/mapeditor/icons/zoom_zero.png differ diff --git a/mapeditor/inspector/PickObjectDelegate.cpp b/mapeditor/inspector/PickObjectDelegate.cpp new file mode 100644 index 000000000..369b5ee90 --- /dev/null +++ b/mapeditor/inspector/PickObjectDelegate.cpp @@ -0,0 +1,59 @@ +/* + * PickObjectDelegate.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 "PickObjectDelegate.h" + +#include "../mapcontroller.h" +#include "../../lib/mapObjects/CGObjectInstance.h" + +PickObjectDelegate::PickObjectDelegate(MapController & c): controller(c) +{ + filter = [](const CGObjectInstance *) + { + return true; + }; +} + +PickObjectDelegate::PickObjectDelegate(MapController & c, std::function f): controller(c), filter(f) +{ + +} + +void PickObjectDelegate::onObjectPicked(const CGObjectInstance * o) +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &PickObjectDelegate::onObjectPicked); + } + + QMap data; + data[Qt::DisplayRole] = QVariant("None"); + data[Qt::UserRole] = QVariant::fromValue(data_cast(o)); + if(o) + data[Qt::DisplayRole] = QVariant(QString::fromStdString(o->instanceName)); + const_cast(modelIndex.model())->setItemData(modelIndex, data); +} + +QWidget * PickObjectDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(filter); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &PickObjectDelegate::onObjectPicked); + } + + modelIndex = index; + return nullptr; +} diff --git a/mapeditor/inspector/PickObjectDelegate.h b/mapeditor/inspector/PickObjectDelegate.h new file mode 100644 index 000000000..f1a0089ac --- /dev/null +++ b/mapeditor/inspector/PickObjectDelegate.h @@ -0,0 +1,45 @@ +/* + * PickObjectDelegate.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 + +class MapController; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; + +VCMI_LIB_NAMESPACE_END + +class PickObjectDelegate : public QItemDelegate +{ + Q_OBJECT +public: + PickObjectDelegate(MapController &); + PickObjectDelegate(MapController &, std::function); + + template + static bool typedFilter(const CGObjectInstance * o) + { + return dynamic_cast(o) != nullptr; + } + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + +public slots: + void onObjectPicked(const CGObjectInstance *); + +private: + MapController & controller; + std::function filter; + mutable QModelIndex modelIndex; +}; diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 426b5a04b..fcba00cf0 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -31,7 +31,6 @@ ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) : for(int i = 0; i < TOTAL_SLOTS; ++i) { - uiCounts[i]->setText("1"); uiSlots[i]->addItem(""); uiSlots[i]->setItemData(0, -1); @@ -64,7 +63,7 @@ void ArmyWidget::obtainData() { auto * creature = army.getCreature(SlotID(i)); uiSlots[i]->setCurrentIndex(searchItemIndex(i, creature->getId())); - uiCounts[i]->setText(QString::number(army.getStackCount(SlotID(i)))); + uiCounts[i]->setValue(army.getStackCount(SlotID(i))); } } @@ -80,7 +79,7 @@ bool ArmyWidget::commitChanges() for(int i = 0; i < TOTAL_SLOTS; ++i) { CreatureID creId(uiSlots[i]->itemData(uiSlots[i]->currentIndex()).toInt()); - if(creId == -1) + if(creId == CreatureID::NONE) { if(army.hasStackAtSlot(SlotID(i))) army.eraseStack(SlotID(i)); @@ -88,7 +87,7 @@ bool ArmyWidget::commitChanges() else { isArmed = true; - int amount = uiCounts[i]->text().toInt(); + int amount = uiCounts[i]->value(); if(amount) { army.setCreature(SlotID(i), creId, amount); @@ -102,7 +101,7 @@ bool ArmyWidget::commitChanges() } } - army.setFormation(ui->formationTight->isChecked()); + army.setFormation(ui->formationTight->isChecked() ? EArmyFormation::TIGHT : EArmyFormation::LOOSE ); return isArmed; } @@ -138,12 +137,7 @@ void ArmyDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, cons { if(auto * ed = qobject_cast(editor)) { - auto isArmed = ed->commitChanges(); - model->setData(index, "dummy"); - if(isArmed) - model->setData(index, "HAS ARMY"); - else - model->setData(index, ""); + ed->commitChanges(); } else { diff --git a/mapeditor/inspector/armywidget.h b/mapeditor/inspector/armywidget.h index e26953057..c4eaabe32 100644 --- a/mapeditor/inspector/armywidget.h +++ b/mapeditor/inspector/armywidget.h @@ -35,7 +35,7 @@ private: Ui::ArmyWidget *ui; CArmedInstance & army; - std::array uiCounts; + std::array uiCounts; std::array uiSlots; }; diff --git a/mapeditor/inspector/armywidget.ui b/mapeditor/inspector/armywidget.ui index a808f0c3a..a6a512393 100644 --- a/mapeditor/inspector/armywidget.ui +++ b/mapeditor/inspector/armywidget.ui @@ -2,6 +2,9 @@ ArmyWidget + + Qt::NonModal + 0 @@ -19,9 +22,12 @@ Army settings + + true + - - + + 0 @@ -40,6 +46,16 @@ + + + + + 0 + 0 + + + + @@ -50,164 +66,6 @@ - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - Wide formation - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - @@ -218,33 +76,8 @@ - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - + + 0 @@ -253,29 +86,14 @@ - - + + - + 0 0 - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - @@ -291,6 +109,131 @@
    + + + + + 0 + 0 + + + + Wide formation + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + diff --git a/mapeditor/inspector/heroskillswidget.cpp b/mapeditor/inspector/heroskillswidget.cpp new file mode 100644 index 000000000..d1254e4b8 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.cpp @@ -0,0 +1,149 @@ +/* + * heroskillswidget.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 "heroskillswidget.h" +#include "ui_heroskillswidget.h" +#include "../../lib/constants/StringConstants.h" +#include "../../lib/CSkillHandler.h" +#include "inspector.h" + +static QList> LevelIdentifiers +{ + {QObject::tr("Beginner"), QVariant::fromValue(1)}, + {QObject::tr("Advanced"), QVariant::fromValue(2)}, + {QObject::tr("Expert"), QVariant::fromValue(3)}, +}; + +HeroSkillsWidget::HeroSkillsWidget(CGHeroInstance & h, QWidget *parent) : + QDialog(parent), + ui(new Ui::HeroSkillsWidget), + hero(h) +{ + ui->setupUi(this); + + ui->labelAttack->setText(QString::fromStdString(NPrimarySkill::names[0])); + ui->labelDefence->setText(QString::fromStdString(NPrimarySkill::names[1])); + ui->labelPower->setText(QString::fromStdString(NPrimarySkill::names[2])); + ui->labelKnowledge->setText(QString::fromStdString(NPrimarySkill::names[3])); + + auto * delegate = new InspectorDelegate; + for(auto s : VLC->skillh->objects) + delegate->options.push_back({QString::fromStdString(s->getNameTranslated()), QVariant::fromValue(s->getId().getNum())}); + ui->skills->setItemDelegateForColumn(0, delegate); + + delegate = new InspectorDelegate; + delegate->options = LevelIdentifiers; + ui->skills->setItemDelegateForColumn(1, delegate); +} + +HeroSkillsWidget::~HeroSkillsWidget() +{ + delete ui; +} + +void HeroSkillsWidget::on_addButton_clicked() +{ + ui->skills->setRowCount(ui->skills->rowCount() + 1); +} + +void HeroSkillsWidget::on_removeButton_clicked() +{ + ui->skills->removeRow(ui->skills->currentRow()); +} + +void HeroSkillsWidget::on_checkBox_toggled(bool checked) +{ + ui->skills->setEnabled(checked); + ui->addButton->setEnabled(checked); + ui->removeButton->setEnabled(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)); + + if(!hero.secSkills.empty() && hero.secSkills.front().first.getNum() == -1) + return; + + ui->checkBox->setChecked(true); + ui->skills->setRowCount(hero.secSkills.size()); + + int i = 0; + for(auto & s : hero.secSkills) + { + auto * itemSkill = new QTableWidgetItem; + itemSkill->setText(QString::fromStdString(VLC->skillh->getById(s.first)->getNameTranslated())); + itemSkill->setData(Qt::UserRole, QVariant::fromValue(s.first.getNum())); + ui->skills->setItem(i, 0, itemSkill); + + auto * itemLevel = new QTableWidgetItem; + itemLevel->setText(LevelIdentifiers[s.second - 1].first); + itemLevel->setData(Qt::UserRole, LevelIdentifiers[s.second - 1].second); + ui->skills->setItem(i++, 1, itemLevel); + } +} + +void HeroSkillsWidget::commitChanges() +{ + hero.pushPrimSkill(PrimarySkill::ATTACK, ui->attack->value()); + hero.pushPrimSkill(PrimarySkill::DEFENSE, ui->defence->value()); + hero.pushPrimSkill(PrimarySkill::SPELL_POWER, ui->power->value()); + hero.pushPrimSkill(PrimarySkill::KNOWLEDGE, ui->knowledge->value()); + + hero.secSkills.clear(); + + if(!ui->checkBox->isChecked()) + { + hero.secSkills.push_back(std::make_pair(SecondarySkill(-1), -1)); + return; + } + + for(int i = 0; i < ui->skills->rowCount(); ++i) + { + if(ui->skills->item(i, 0) && ui->skills->item(i, 1)) + hero.secSkills.push_back(std::make_pair(SecondarySkill(ui->skills->item(i, 0)->data(Qt::UserRole).toInt()), ui->skills->item(i, 1)->data(Qt::UserRole).toInt())); + } +} + +HeroSkillsDelegate::HeroSkillsDelegate(CGHeroInstance & h): hero(h), QStyledItemDelegate() +{ +} + +QWidget * HeroSkillsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new HeroSkillsWidget(hero, parent); +} + +void HeroSkillsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void HeroSkillsDelegate::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/heroskillswidget.h b/mapeditor/inspector/heroskillswidget.h new file mode 100644 index 000000000..110988767 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.h @@ -0,0 +1,59 @@ +/* + * heroskillswidget.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 HeroSkillsWidget; +} + +class HeroSkillsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit HeroSkillsWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~HeroSkillsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void on_addButton_clicked(); + + void on_removeButton_clicked(); + + void on_checkBox_toggled(bool checked); + +private: + Ui::HeroSkillsWidget *ui; + + CGHeroInstance & hero; + + std::set occupiedSkills; +}; + +class HeroSkillsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + HeroSkillsDelegate(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/heroskillswidget.ui b/mapeditor/inspector/heroskillswidget.ui new file mode 100644 index 000000000..3cbcdfda1 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.ui @@ -0,0 +1,174 @@ + + + HeroSkillsWidget + + + + 0 + 0 + 464 + 301 + + + + Hero skills + + + true + + + + 4 + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + 90 + 0 + + + + Add + + + + + + + false + + + + 90 + 0 + + + + Remove + + + + + + + + + false + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + 120 + + + true + + + false + + + 21 + + + + Skill + + + + + Level + + + + + + + + Customize skills + + + + + + + + diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b9541c12d..3587dd1b5 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -18,12 +18,26 @@ #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/mapping/CMap.h" +#include "../lib/constants/StringConstants.h" #include "townbulidingswidget.h" #include "armywidget.h" #include "messagewidget.h" #include "rewardswidget.h" #include "questwidget.h" +#include "heroskillswidget.h" +#include "portraitwidget.h" +#include "PickObjectDelegate.h" +#include "../mapcontroller.h" + +static QList> CharacterIdentifiers +{ + {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, + {QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY))}, + {QObject::tr("Aggressive"), QVariant::fromValue(int(CGCreature::Character::AGGRESSIVE))}, + {QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE))}, + {QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE))}, +}; //===============IMPLEMENT OBJECT INITIALIZATION FUNCTIONS================ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : defaultPlayer(pl) @@ -39,22 +53,16 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default INIT_OBJ_TYPE(CGDwelling); INIT_OBJ_TYPE(CGTownInstance); INIT_OBJ_TYPE(CGCreature); + INIT_OBJ_TYPE(CGHeroPlaceholder); INIT_OBJ_TYPE(CGHeroInstance); INIT_OBJ_TYPE(CGSignBottle); INIT_OBJ_TYPE(CGLighthouse); + //INIT_OBJ_TYPE(CRewardableObject); //INIT_OBJ_TYPE(CGPandoraBox); //INIT_OBJ_TYPE(CGEvent); //INIT_OBJ_TYPE(CGSeerHut); } -bool stringToBool(const QString & s) -{ - if(s == "TRUE") - return true; - //if(s == "FALSE") - return false; -} - void Initializer::initialize(CArmedInstance * o) { if(!o) return; @@ -79,14 +87,6 @@ void Initializer::initialize(CGDwelling * o) if(!o) return; o->tempOwner = defaultPlayer; - - switch(o->ID) - { - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - o->initRandomObjectInfo(); - } } void Initializer::initialize(CGGarrison * o) @@ -111,6 +111,19 @@ void Initializer::initialize(CGLighthouse * o) o->tempOwner = defaultPlayer; } +void Initializer::initialize(CGHeroPlaceholder * o) +{ + if(!o) return; + + o->tempOwner = defaultPlayer; + + if(!o->powerRank.has_value() && !o->heroType.has_value()) + o->powerRank = 0; + + if(o->powerRank.has_value() && o->heroType.has_value()) + o->powerRank.reset(); +} + void Initializer::initialize(CGHeroInstance * o) { if(!o) @@ -135,12 +148,13 @@ void Initializer::initialize(CGHeroInstance * o) } } - if(!o->type) - o->type = VLC->heroh->objects.at(o->subID); - - o->gender = o->type->gender; - o->portrait = o->type->imageIndex; - o->randomizeArmy(o->type->heroClass->faction); + if(o->type) + { + // o->type = VLC->heroh->objects.at(o->subID); + + o->gender = o->type->gender; + o->randomizeArmy(o->type->heroClass->faction); + } } void Initializer::initialize(CGTownInstance * o) @@ -172,7 +186,7 @@ void Initializer::initialize(CGArtifact * o) std::vector out; for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - //if(map->isAllowedSpell(spell->id)) + if(VLC->spellh->getDefaultAllowed().count(spell->id) != 0) { out.push_back(spell->id); } @@ -189,8 +203,8 @@ void Initializer::initialize(CGMine * o) o->tempOwner = defaultPlayer; if(o->isAbandoned()) { - for(auto r = 0; r < GameConstants::RESOURCE_QUANTITY - 1; ++r) - o->abandonedMineResources.insert(GameResID(r)); + for(auto r = GameResID(0); r < GameResID::COUNT; ++r) + o->abandonedMineResources.insert(r); } else { @@ -220,6 +234,12 @@ void Inspector::updateProperties(CGDwelling * o) if(!o) return; addProperty("Owner", o->tempOwner, false); + + if (o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL) + { + auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); + addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); + } } void Inspector::updateProperties(CGLighthouse * o) @@ -234,7 +254,7 @@ void Inspector::updateProperties(CGGarrison * o) if(!o) return; addProperty("Owner", o->tempOwner, false); - addProperty("Removable units", o->removableUnits, InspectorDelegate::boolDelegate(), false); + addProperty("Removable units", o->removableUnits, false); } void Inspector::updateProperties(CGShipyard * o) @@ -244,31 +264,67 @@ void Inspector::updateProperties(CGShipyard * o) addProperty("Owner", o->tempOwner, false); } +void Inspector::updateProperties(CGHeroPlaceholder * o) +{ + if(!o) return; + + addProperty("Owner", o->tempOwner, false); + + bool type = false; + if(o->heroType.has_value()) + type = true; + else if(!o->powerRank.has_value()) + assert(0); //one of values must be initialized + + { + auto * delegate = new InspectorDelegate; + delegate->options = {{"POWER RANK", QVariant::fromValue(false)}, {"HERO TYPE", QVariant::fromValue(true)}}; + addProperty("Placeholder type", delegate->options[type].first, delegate, false); + } + + addProperty("Power rank", o->powerRank.has_value() ? o->powerRank.value() : 0, type); + + { + auto * delegate = new InspectorDelegate; + for(int i = 0; i < VLC->heroh->objects.size(); ++i) + { + delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); + } + addProperty("Hero type", o->heroType.has_value() ? VLC->heroh->getById(o->heroType.value())->getNameTranslated() : "", delegate, !type); + } +} + void Inspector::updateProperties(CGHeroInstance * o) { if(!o) return; addProperty("Owner", o->tempOwner, o->ID == Obj::PRISON); //field is not editable for prison addProperty("Experience", o->exp, false); - addProperty("Hero class", o->type->heroClass->getNameTranslated(), true); + addProperty("Hero class", o->type ? o->type->heroClass->getNameTranslated() : "", true); - { //Sex + { //Gender auto * delegate = new InspectorDelegate; - delegate->options << "MALE" << "FEMALE"; + delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}}; addProperty("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false); } - addProperty("Name", o->nameCustom, false); - addProperty("Biography", o->biographyCustom, new MessageDelegate, false); - addProperty("Portrait", o->portrait, false); + addProperty("Name", o->getNameTranslated(), false); + addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false); + addProperty("Portrait", PropertyEditorPlaceholder(), new PortraitDelegate(*o), false); + auto * delegate = new HeroSkillsDelegate(*o); + addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); + + if(o->type) { //Hero type auto * delegate = new InspectorDelegate; for(int i = 0; i < VLC->heroh->objects.size(); ++i) { - if(map->allowedHeroes.at(i)) + if(controller.map()->allowedHeroes.count(HeroTypeID(i)) != 0) { if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) - delegate->options << QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()); + { + delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); + } } } addProperty("Hero type", o->type->getNameTranslated(), delegate, false); @@ -295,15 +351,15 @@ void Inspector::updateProperties(CGArtifact * o) if(instance) { SpellID spellId = instance->getScrollSpellID(); - if(spellId != -1) + if(spellId != SpellID::NONE) { auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) { - //if(map->isAllowedSpell(spell->id)) - delegate->options << QObject::tr(spell->getJsonKey().c_str()); + if(controller.map()->allowedSpells.count(spell->id) != 0) + delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } - addProperty("Spell", VLC->spellh->getById(spellId)->getJsonKey(), delegate, false); + addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); } } } @@ -339,60 +395,64 @@ void Inspector::updateProperties(CGCreature * o) addProperty("Message", o->message, false); { //Character auto * delegate = new InspectorDelegate; - delegate->options << "COMPLIANT" << "FRIENDLY" << "AGRESSIVE" << "HOSTILE" << "SAVAGE"; + delegate->options = CharacterIdentifiers; addProperty("Character", (CGCreature::Character)o->character, delegate, false); } - addProperty("Never flees", o->neverFlees, InspectorDelegate::boolDelegate(), false); - addProperty("Not growing", o->notGrowingTeam, InspectorDelegate::boolDelegate(), false); + addProperty("Never flees", o->neverFlees, false); + addProperty("Not growing", o->notGrowingTeam, false); addProperty("Artifact reward", o->gainedArtifact); //TODO: implement in setProperty addProperty("Army", PropertyEditorPlaceholder(), true); addProperty("Amount", o->stacks[SlotID(0)]->count, false); //addProperty("Resources reward", o->resources); //TODO: implement in setProperty } +void Inspector::updateProperties(CRewardableObject * o) +{ + if(!o) return; + + auto * delegate = new RewardsDelegate(*controller.map(), *o); + addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); +} + void Inspector::updateProperties(CGPandoraBox * o) { if(!o) return; addProperty("Message", o->message, new MessageDelegate, false); - - auto * delegate = new RewardsPandoraDelegate(*map, *o); - addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); } void Inspector::updateProperties(CGEvent * o) { if(!o) return; - addProperty("Remove after", o->removeAfterVisit, InspectorDelegate::boolDelegate(), false); - addProperty("Human trigger", o->humanActivate, InspectorDelegate::boolDelegate(), false); - addProperty("Cpu trigger", o->computerActivate, InspectorDelegate::boolDelegate(), false); + addProperty("Remove after", o->removeAfterVisit, false); + addProperty("Human trigger", o->humanActivate, false); + addProperty("Cpu trigger", o->computerActivate, false); //ui8 availableFor; //players whom this event is available for } void Inspector::updateProperties(CGSeerHut * o) { - if(!o) return; - - { //Mission type - auto * delegate = new InspectorDelegate; - delegate->options << "Reach level" << "Stats" << "Kill hero" << "Kill creature" << "Artifact" << "Army" << "Resources" << "Hero" << "Player"; - addProperty("Mission type", o->quest->missionType, delegate, false); - } + if(!o || !o->quest) return; addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); + addProperty("Repeat quest", o->quest->repeatedQuest, false); + addProperty("Time limit", o->quest->lastDay, false); { //Quest - auto * delegate = new QuestDelegate(*map, *o); + auto * delegate = new QuestDelegate(controller, *o->quest); addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); } +} + +void Inspector::updateProperties(CGQuestGuard * o) +{ + if(!o || !o->quest) return; - { //Reward - auto * delegate = new RewardsSeerhutDelegate(*map, *o); - addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); - } + addProperty("Reward", PropertyEditorPlaceholder(), nullptr, true); + addProperty("Repeat quest", o->quest->repeatedQuest, true); } void Inspector::updateProperties() @@ -415,10 +475,10 @@ void Inspector::updateProperties() } auto * delegate = new InspectorDelegate(); - delegate->options << "NEUTRAL"; - for(int p = 0; p < map->players.size(); ++p) - if(map->players[p].canAnyonePlay()) - delegate->options << QString("PLAYER %1").arg(p); + delegate->options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum())}); + for(int p = 0; p < controller.map()->players.size(); ++p) + if(controller.map()->players[p].canAnyonePlay()) + delegate->options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum())}); addProperty("Owner", obj->tempOwner, delegate, true); UPDATE_OBJ_PROPERTIES(CArmedInstance); @@ -430,31 +490,44 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGDwelling); UPDATE_OBJ_PROPERTIES(CGTownInstance); UPDATE_OBJ_PROPERTIES(CGCreature); + UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder); UPDATE_OBJ_PROPERTIES(CGHeroInstance); UPDATE_OBJ_PROPERTIES(CGSignBottle); UPDATE_OBJ_PROPERTIES(CGLighthouse); + UPDATE_OBJ_PROPERTIES(CRewardableObject); UPDATE_OBJ_PROPERTIES(CGPandoraBox); UPDATE_OBJ_PROPERTIES(CGEvent); UPDATE_OBJ_PROPERTIES(CGSeerHut); + UPDATE_OBJ_PROPERTIES(CGQuestGuard); table->show(); } //===============IMPLEMENT PROPERTY UPDATE================================ +void Inspector::setProperty(const QString & key, const QTableWidgetItem * item) +{ + if(!item->data(Qt::UserRole).isNull()) + { + setProperty(key, item->data(Qt::UserRole)); + return; + } + + if(item->flags() & Qt::ItemIsUserCheckable) + { + setProperty(key, QVariant::fromValue(item->checkState() == Qt::Checked)); + return; + } + + setProperty(key, item->text()); +} + void Inspector::setProperty(const QString & key, const QVariant & value) { if(!obj) return; if(key == "Owner") - { - PlayerColor owner(value.toString().mid(6).toInt()); //receiving PLAYER N, N has index 6 - if(value == "NEUTRAL") - owner = PlayerColor::NEUTRAL; - if(value == "UNFLAGGABLE") - owner = PlayerColor::UNFLAGGABLE; - obj->tempOwner = owner; - } + obj->tempOwner = PlayerColor(value.toInt()); SET_PROPERTIES(CArmedInstance); SET_PROPERTIES(CGTownInstance); @@ -464,13 +537,16 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGDwelling); SET_PROPERTIES(CGGarrison); SET_PROPERTIES(CGCreature); + SET_PROPERTIES(CGHeroPlaceholder); SET_PROPERTIES(CGHeroInstance); SET_PROPERTIES(CGShipyard); SET_PROPERTIES(CGSignBottle); SET_PROPERTIES(CGLighthouse); + SET_PROPERTIES(CRewardableObject); SET_PROPERTIES(CGPandoraBox); SET_PROPERTIES(CGEvent); SET_PROPERTIES(CGSeerHut); + SET_PROPERTIES(CGQuestGuard); } void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVariant & value) @@ -483,12 +559,18 @@ void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVarian if(!o) return; } +void Inspector::setProperty(CRewardableObject * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVariant & value) { if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -496,13 +578,13 @@ void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & v if(!o) return; if(key == "Remove after") - o->removeAfterVisit = stringToBool(value.toString()); + o->removeAfterVisit = value.toBool(); if(key == "Human trigger") - o->humanActivate = stringToBool(value.toString()); + o->humanActivate = value.toBool(); if(key == "Cpu trigger") - o->computerActivate = stringToBool(value.toString()); + o->computerActivate = value.toBool(); } void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVariant & value) @@ -510,7 +592,8 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTranslated(value.toString().toStdString()); + o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -518,7 +601,8 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -534,24 +618,28 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { - for(auto spell : VLC->spellh->objects) - { - if(spell->getJsonKey() == value.toString().toStdString()) - { - o->storedArtifact = ArtifactUtils::createScroll(spell->getId()); - break; - } - } + o->storedArtifact = ArtifactUtils::createScroll(SpellID(value.toInt())); } } void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant & value) { if(!o) return; + + if(key == "Same as town") + { + if (!o->randomizationInfo.has_value()) + o->randomizationInfo = CGDwellingRandomizationInfo(); + + o->randomizationInfo->instanceId = ""; + if(CGTownInstance * town = data_cast(value.toLongLong())) + o->randomizationInfo->instanceId = town->instanceName; + } } void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant & value) @@ -559,7 +647,38 @@ void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant if(!o) return; if(key == "Removable units") - o->removableUnits = stringToBool(value.toString()); + o->removableUnits = value.toBool(); +} + +void Inspector::setProperty(CGHeroPlaceholder * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Placeholder type") + { + if(value.toBool()) + { + if(!o->heroType.has_value()) + o->heroType = HeroTypeID(0); + o->powerRank.reset(); + } + else + { + if(!o->powerRank.has_value()) + o->powerRank = 0; + o->heroType.reset(); + } + + updateProperties(); + } + + if(key == "Power rank") + o->powerRank = value.toInt(); + + if(key == "Hero type") + { + o->heroType = HeroTypeID(value.toInt()); + } } void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVariant & value) @@ -567,23 +686,30 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari if(!o) return; if(key == "Gender") - o->gender = value.toString() == "MALE" ? EHeroGender::MALE : EHeroGender::FEMALE; + o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustom = value.toString().toStdString(); + o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + + if(key == "Biography") + o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") - o->exp = value.toInt(); + o->exp = value.toString().toInt(); if(key == "Hero type") { for(auto t : VLC->heroh->objects) { - if(t->getNameTranslated() == value.toString().toStdString()) + if(t->getId() == value.toInt()) + { + o->subID = value.toInt(); o->type = t.get(); + } } o->gender = o->type->gender; - o->portrait = o->type->imageIndex; o->randomizeArmy(o->type->heroClass->faction); updateProperties(); //updating other properties after change } @@ -607,25 +733,14 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") - { - //COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 - if(value == "COMPLIANT") - o->character = CGCreature::Character::COMPLIANT; - if(value == "FRIENDLY") - o->character = CGCreature::Character::FRIENDLY; - if(value == "AGRESSIVE") - o->character = CGCreature::Character::AGRESSIVE; - if(value == "HOSTILE") - o->character = CGCreature::Character::HOSTILE; - if(value == "SAVAGE") - o->character = CGCreature::Character::SAVAGE; - } + o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") - o->neverFlees = stringToBool(value.toString()); + o->neverFlees = value.toBool(); if(key == "Not growing") - o->notGrowingTeam = stringToBool(value.toString()); + o->notGrowingTeam = value.toBool(); if(key == "Amount") o->stacks[SlotID(0)]->count = value.toString().toInt(); } @@ -634,63 +749,64 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & { if(!o) return; - if(key == "Mission type") - { - if(value == "Reach level") - o->quest->missionType = CQuest::Emission::MISSION_LEVEL; - if(value == "Stats") - o->quest->missionType = CQuest::Emission::MISSION_PRIMARY_STAT; - if(value == "Kill hero") - o->quest->missionType = CQuest::Emission::MISSION_KILL_HERO; - if(value == "Kill creature") - o->quest->missionType = CQuest::Emission::MISSION_KILL_CREATURE; - if(value == "Artifact") - o->quest->missionType = CQuest::Emission::MISSION_ART; - if(value == "Army") - o->quest->missionType = CQuest::Emission::MISSION_ARMY; - if(value == "Resources") - o->quest->missionType = CQuest::Emission::MISSION_RESOURCES; - if(value == "Hero") - o->quest->missionType = CQuest::Emission::MISSION_HERO; - if(value == "Player") - o->quest->missionType = CQuest::Emission::MISSION_PLAYER; - } - if(key == "First visit text") - o->quest->firstVisitText = value.toString().toStdString(); + o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText = value.toString().toStdString(); + o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText = value.toString().toStdString(); + o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + if(key == "Repeat quest") + o->quest->repeatedQuest = value.toBool(); + if(key == "Time limit") + o->quest->lastDay = value.toString().toInt(); +} + +void Inspector::setProperty(CGQuestGuard * o, const QString & key, const QVariant & value) +{ + if(!o) return; } //===============IMPLEMENT PROPERTY VALUE TYPE============================ QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value) { - return new QTableWidgetItem(QString::number(data_cast(value))); + auto * item = new QTableWidgetItem(QString::number(data_cast(value))); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(Inspector::PropertyEditorPlaceholder value) { - auto item = new QTableWidgetItem(""); - item->setData(Qt::UserRole, QString("PropertyEditor")); + auto item = new QTableWidgetItem("..."); + item->setFlags(Qt::NoItemFlags); return item; } QTableWidgetItem * Inspector::addProperty(unsigned int value) { - return new QTableWidgetItem(QString::number(value)); + auto * item = new QTableWidgetItem(QString::number(value)); + item->setFlags(Qt::NoItemFlags); + //item->setData(Qt::UserRole, QVariant::fromValue(value)); + return item; } QTableWidgetItem * Inspector::addProperty(int value) { - return new QTableWidgetItem(QString::number(value)); + auto * item = new QTableWidgetItem(QString::number(value)); + item->setFlags(Qt::NoItemFlags); + //item->setData(Qt::UserRole, QVariant::fromValue(value)); + return item; } QTableWidgetItem * Inspector::addProperty(bool value) { - return new QTableWidgetItem(value ? "TRUE" : "FALSE"); + auto item = new QTableWidgetItem; + item->setFlags(Qt::ItemIsUserCheckable); + item->setCheckState(value ? Qt::Checked : Qt::Unchecked); + return item; } QTableWidgetItem * Inspector::addProperty(const std::string & value) @@ -698,125 +814,74 @@ QTableWidgetItem * Inspector::addProperty(const std::string & value) return addProperty(QString::fromStdString(value)); } +QTableWidgetItem * Inspector::addProperty(const TextIdentifier & value) +{ + return addProperty(VLC->generaltexth->translate(value.get())); +} + +QTableWidgetItem * Inspector::addProperty(const MetaString & value) +{ + return addProperty(value.toString()); +} + QTableWidgetItem * Inspector::addProperty(const QString & value) { - return new QTableWidgetItem(value); + auto * item = new QTableWidgetItem(value); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(const int3 & value) { - return new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z)); + auto * item = new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z)); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(const PlayerColor & value) { - auto str = QString("PLAYER %1").arg(value.getNum()); + auto str = QObject::tr("UNFLAGGABLE"); if(value == PlayerColor::NEUTRAL) - str = "NEUTRAL"; - if(value == PlayerColor::UNFLAGGABLE) - str = "UNFLAGGABLE"; - return new QTableWidgetItem(str); + str = QObject::tr("neutral"); + + if(value.isValidPlayer()) + str = QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[value]); + + auto * item = new QTableWidgetItem(str); + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(value.getNum())); + return item; } QTableWidgetItem * Inspector::addProperty(const GameResID & value) { - QString str; - switch (value.toEnum()) { - case EGameResID::WOOD: - str = "WOOD"; - break; - case EGameResID::ORE: - str = "ORE"; - break; - case EGameResID::SULFUR: - str = "SULFUR"; - break; - case EGameResID::GEMS: - str = "GEMS"; - break; - case EGameResID::MERCURY: - str = "MERCURY"; - break; - case EGameResID::CRYSTAL: - str = "CRYSTAL"; - break; - case EGameResID::GOLD: - str = "GOLD"; - break; - default: - break; - } - return new QTableWidgetItem(str); + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[value.toEnum()])); + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(value.getNum())); + return item; } QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) { - QString str; - switch (value) { - case CGCreature::Character::COMPLIANT: - str = "COMPLIANT"; - break; - case CGCreature::Character::FRIENDLY: - str = "FRIENDLY"; - break; - case CGCreature::Character::AGRESSIVE: - str = "AGRESSIVE"; - break; - case CGCreature::Character::HOSTILE: - str = "HOSTILE"; - break; - case CGCreature::Character::SAVAGE: - str = "SAVAGE"; - break; - default: + auto * item = new QTableWidgetItem; + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(int(value))); + + for(auto & i : CharacterIdentifiers) + { + if(i.second.toInt() == value) + { + item->setText(i.first); break; + } } - return new QTableWidgetItem(str); -} - -QTableWidgetItem * Inspector::addProperty(CQuest::Emission value) -{ - QString str; - switch (value) { - case CQuest::Emission::MISSION_LEVEL: - str = "Reach level"; - break; - case CQuest::Emission::MISSION_PRIMARY_STAT: - str = "Stats"; - break; - case CQuest::Emission::MISSION_KILL_HERO: - str = "Kill hero"; - break; - case CQuest::Emission::MISSION_KILL_CREATURE: - str = "Kill creature"; - break; - case CQuest::Emission::MISSION_ART: - str = "Artifact"; - break; - case CQuest::Emission::MISSION_ARMY: - str = "Army"; - break; - case CQuest::Emission::MISSION_RESOURCES: - str = "Resources"; - break; - case CQuest::Emission::MISSION_HERO: - str = "Hero"; - break; - case CQuest::Emission::MISSION_PLAYER: - str = "Player"; - break; - case CQuest::Emission::MISSION_KEYMASTER: - str = "Key master"; - break; - default: - break; - } - return new QTableWidgetItem(str); + + return item; } //======================================================================== -Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), map(m) +Inspector::Inspector(MapController & c, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), controller(c) { } @@ -824,13 +889,6 @@ Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), * Delegates */ -InspectorDelegate * InspectorDelegate::boolDelegate() -{ - auto * d = new InspectorDelegate; - d->options << "TRUE" << "FALSE"; - return d; -} - QWidget * InspectorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { return new QComboBox(parent); @@ -840,7 +898,11 @@ void InspectorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) { if(QComboBox *ed = qobject_cast(editor)) { - ed->addItems(options); + for(auto & i : options) + { + ed->addItem(i.first); + ed->setItemData(ed->count() - 1, i.second); + } } else { @@ -855,7 +917,8 @@ void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, if(!options.isEmpty()) { QMap data; - data[0] = options[ed->currentIndex()]; + data[Qt::DisplayRole] = options[ed->currentIndex()].first; + data[Qt::UserRole] = options[ed->currentIndex()].second; model->setItemData(index, data); } } @@ -864,4 +927,3 @@ void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QStyledItemDelegate::setModelData(editor, model, index); } } - diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index f440a3374..432e40ab0 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -17,7 +17,10 @@ #include "../lib/GameConstants.h" #include "../lib/mapObjects/CGCreature.h" #include "../lib/mapObjects/MapObjects.h" +#include "../lib/mapObjects/CRewardableObject.h" +#include "../lib/CGeneralTextHandler.h" #include "../lib/ResourceSet.h" +#include "../lib/MetaString.h" #define DECLARE_OBJ_TYPE(x) void initialize(x*); #define DECLARE_OBJ_PROPERTY_METHODS(x) \ @@ -29,6 +32,7 @@ void setProperty(x*, const QString &, const QVariant &); #define SET_PROPERTIES(x) setProperty(dynamic_cast(obj), key, value) +class MapController; class Initializer { public: @@ -41,10 +45,12 @@ public: DECLARE_OBJ_TYPE(CGResource); DECLARE_OBJ_TYPE(CGDwelling); DECLARE_OBJ_TYPE(CGGarrison); + DECLARE_OBJ_TYPE(CGHeroPlaceholder); DECLARE_OBJ_TYPE(CGHeroInstance); DECLARE_OBJ_TYPE(CGCreature); DECLARE_OBJ_TYPE(CGSignBottle); DECLARE_OBJ_TYPE(CGLighthouse); + //DECLARE_OBJ_TYPE(CRewardableObject); //DECLARE_OBJ_TYPE(CGEvent); //DECLARE_OBJ_TYPE(CGPandoraBox); //DECLARE_OBJ_TYPE(CGSeerHut); @@ -69,17 +75,22 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGResource); DECLARE_OBJ_PROPERTY_METHODS(CGDwelling); DECLARE_OBJ_PROPERTY_METHODS(CGGarrison); + DECLARE_OBJ_PROPERTY_METHODS(CGHeroPlaceholder); DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance); DECLARE_OBJ_PROPERTY_METHODS(CGCreature); DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle); DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse); + DECLARE_OBJ_PROPERTY_METHODS(CRewardableObject); DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); DECLARE_OBJ_PROPERTY_METHODS(CGEvent); DECLARE_OBJ_PROPERTY_METHODS(CGSeerHut); + DECLARE_OBJ_PROPERTY_METHODS(CGQuestGuard); //===============DECLARE PROPERTY VALUE TYPE============================== QTableWidgetItem * addProperty(unsigned int value); QTableWidgetItem * addProperty(int value); + QTableWidgetItem * addProperty(const MetaString & value); + QTableWidgetItem * addProperty(const TextIdentifier & value); QTableWidgetItem * addProperty(const std::string & value); QTableWidgetItem * addProperty(const QString & value); QTableWidgetItem * addProperty(const int3 & value); @@ -88,14 +99,15 @@ protected: QTableWidgetItem * addProperty(bool value); QTableWidgetItem * addProperty(CGObjectInstance * value); QTableWidgetItem * addProperty(CGCreature::Character value); - QTableWidgetItem * addProperty(CQuest::Emission value); QTableWidgetItem * addProperty(PropertyEditorPlaceholder value); //===============END OF DECLARATION======================================= public: - Inspector(CMap *, CGObjectInstance *, QTableWidget *); + Inspector(MapController &, CGObjectInstance *, QTableWidget *); + void setProperty(const QString & key, const QTableWidgetItem * item); + void setProperty(const QString & key, const QVariant & value); void updateProperties(); @@ -106,8 +118,10 @@ protected: void addProperty(const QString & key, const T & value, QAbstractItemDelegate * delegate, bool restricted) { auto * itemValue = addProperty(value); - if(restricted) - itemValue->setFlags(Qt::NoItemFlags); + if(!restricted) + itemValue->setFlags(itemValue->flags() | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + if(!(itemValue->flags() & Qt::ItemIsUserCheckable)) + itemValue->setFlags(itemValue->flags() | Qt::ItemIsEditable); QTableWidgetItem * itemKey = nullptr; if(keyItems.contains(key)) @@ -120,15 +134,16 @@ protected: else { itemKey = new QTableWidgetItem(key); - itemKey->setFlags(Qt::NoItemFlags); keyItems[key] = itemKey; table->setRowCount(row + 1); table->setItem(row, 0, itemKey); table->setItem(row, 1, itemValue); - table->setItemDelegateForRow(row, delegate); + if(delegate) + table->setItemDelegateForRow(row, delegate); ++row; } + itemKey->setFlags(restricted ? Qt::NoItemFlags : Qt::ItemIsEnabled); } template @@ -136,30 +151,25 @@ protected: { addProperty(key, value, nullptr, restricted); } - + protected: int row = 0; QTableWidget * table; CGObjectInstance * obj; QMap keyItems; - CMap * map; + MapController & controller; }; - - class InspectorDelegate : public QStyledItemDelegate { Q_OBJECT public: - static InspectorDelegate * boolDelegate(); - using QStyledItemDelegate::QStyledItemDelegate; 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; - QStringList options; + QList> options; }; - diff --git a/mapeditor/inspector/messagewidget.ui b/mapeditor/inspector/messagewidget.ui index 925d8073e..369d0dc84 100644 --- a/mapeditor/inspector/messagewidget.ui +++ b/mapeditor/inspector/messagewidget.ui @@ -2,6 +2,9 @@ MessageWidget + + Qt::NonModal + 0 diff --git a/mapeditor/inspector/portraitwidget.cpp b/mapeditor/inspector/portraitwidget.cpp new file mode 100644 index 000000000..669bb880c --- /dev/null +++ b/mapeditor/inspector/portraitwidget.cpp @@ -0,0 +1,136 @@ +/* + * portraitwidget.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 "portraitwidget.h" +#include "ui_portraitwidget.h" +#include "../../lib/CHeroHandler.h" +#include "../Animation.h" + +PortraitWidget::PortraitWidget(CGHeroInstance & h, QWidget *parent): + QDialog(parent), + ui(new Ui::PortraitWidget), + hero(h), + portraitIndex(0) +{ + ui->setupUi(this); + ui->portraitView->setScene(&scene); + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); + show(); +} + +PortraitWidget::~PortraitWidget() +{ + delete ui; +} + +void PortraitWidget::obtainData() +{ + portraitIndex = VLC->heroh->getById(hero.getPortraitSource())->getIndex(); + if(hero.customPortraitSource.isValid()) + { + ui->isDefault->setChecked(true); + } + + drawPortrait(); +} + +void PortraitWidget::commitChanges() +{ + if(portraitIndex == VLC->heroh->getById(HeroTypeID(hero.subID))->getIndex()) + hero.customPortraitSource = HeroTypeID::NONE; + else + hero.customPortraitSource = VLC->heroh->getByIndex(portraitIndex)->getId(); +} + +void PortraitWidget::drawPortrait() +{ + static Animation portraitAnimation(AnimationPath::builtin("PortraitsLarge").getOriginalName()); + portraitAnimation.preload(); + auto icon = VLC->heroTypes()->getByIndex(portraitIndex)->getIconIndex(); + pixmap = QPixmap::fromImage(*portraitAnimation.getImage(icon)); + scene.addPixmap(pixmap); + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); +} + +void PortraitWidget::resizeEvent(QResizeEvent *) +{ + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); +} + +void PortraitWidget::on_isDefault_toggled(bool checked) +{ + if(checked) + { + ui->buttonNext->setEnabled(false); + ui->buttonPrev->setEnabled(false); + portraitIndex = VLC->heroh->getById(HeroTypeID(hero.subID))->getIndex(); + } + else + { + ui->buttonNext->setEnabled(true); + ui->buttonPrev->setEnabled(true); + } + drawPortrait(); +} + + +void PortraitWidget::on_buttonNext_clicked() +{ + if(portraitIndex < VLC->heroh->size() - 1) + ++portraitIndex; + else + portraitIndex = 0; + + drawPortrait(); +} + + +void PortraitWidget::on_buttonPrev_clicked() +{ + if(portraitIndex > 0) + --portraitIndex; + else + portraitIndex = VLC->heroh->size() - 1; + + drawPortrait(); +} + +PortraitDelegate::PortraitDelegate(CGHeroInstance & h): hero(h), QStyledItemDelegate() +{ +} + +QWidget * PortraitDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new PortraitWidget(hero, parent); +} + +void PortraitDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void PortraitDelegate::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/portraitwidget.h b/mapeditor/inspector/portraitwidget.h new file mode 100644 index 000000000..2ca79e801 --- /dev/null +++ b/mapeditor/inspector/portraitwidget.h @@ -0,0 +1,64 @@ +/* + * portraitwidget.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 PortraitWidget; +} + +class PortraitWidget : public QDialog +{ + Q_OBJECT + +public: + explicit PortraitWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~PortraitWidget(); + + void obtainData(); + void commitChanges(); + + void resizeEvent(QResizeEvent *) override; + +private slots: + void on_isDefault_toggled(bool checked); + + void on_buttonNext_clicked(); + + void on_buttonPrev_clicked(); + +private: + void drawPortrait(); + + Ui::PortraitWidget *ui; + QGraphicsScene scene; + QPixmap pixmap; + + CGHeroInstance & hero; + int portraitIndex; +}; + +class PortraitDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + PortraitDelegate(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/portraitwidget.ui b/mapeditor/inspector/portraitwidget.ui new file mode 100644 index 000000000..aa0b923ff --- /dev/null +++ b/mapeditor/inspector/portraitwidget.ui @@ -0,0 +1,96 @@ + + + PortraitWidget + + + + 0 + 0 + 286 + 305 + + + + Portrait + + + true + + + + + + 0 + + + + + + 116 + 128 + + + + + + + + 0 + + + + + false + + + + 0 + 0 + + + + ... + + + Qt::UpArrow + + + + + + + false + + + + 0 + 0 + + + + ... + + + Qt::DownArrow + + + + + + + + + + + Default + + + true + + + + + + + + diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index bd1329f46..982138741 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -10,21 +10,124 @@ #include "StdInc.h" #include "questwidget.h" #include "ui_questwidget.h" +#include "../mapcontroller.h" #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGCreature.h" -QuestWidget::QuestWidget(const CMap & _map, CGSeerHut & _sh, QWidget *parent) : +QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *parent) : QDialog(parent), - map(_map), - seerhut(_sh), + controller(_controller), + quest(_sh), ui(new Ui::QuestWidget) { + setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); + + ui->lDayOfWeek->addItem(tr("None")); + for(int i = 1; i <= 7; ++i) + ui->lDayOfWeek->addItem(tr("Day %1").arg(i)); + + //fill resources + ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) + { + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + ui->lResources->setItem(i, 0, item); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + ui->lResources->setCellWidget(i, 1, spinBox); + } + + //fill artifacts + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(controller.map()->allowedArtifact.count(i) == 0) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + ui->lArtifacts->addItem(item); + } + + //fill spells + for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(controller.map()->allowedSpells.count(i) == 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) + { + auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + + auto * widget = new QComboBox; + for(auto & s : NSecondarySkill::levels) + widget->addItem(QString::fromStdString(s)); + + if(controller.map()->allowedAbilities.count(i) == 0) + { + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + widget->setEnabled(false); + } + + ui->lSkills->setItem(i, 0, item); + ui->lSkills->setCellWidget(i, 1, widget); + } + + //fill creatures + for(auto & creature : VLC->creh->objects) + { + ui->lCreatureId->addItem(QString::fromStdString(creature->getNameSingularTranslated())); + ui->lCreatureId->setItemData(ui->lCreatureId->count() - 1, creature->getIndex()); + } + + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } } QuestWidget::~QuestWidget() @@ -34,137 +137,259 @@ QuestWidget::~QuestWidget() void QuestWidget::obtainData() { - assert(seerhut.quest); - bool activeId = false; - bool activeAmount = false; - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - activeAmount = true; - ui->targetId->addItem("Reach level"); - ui->targetAmount->setText(QString::number(seerhut.quest->m13489val)); - break; - case CQuest::Emission::MISSION_PRIMARY_STAT: - activeId = true; - activeAmount = true; - for(auto s : PrimarySkill::names) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m2stats.size(); ++i) - { - if(seerhut.quest->m2stats[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m2stats[i])); - break; //TODO: support multiple stats - } - } - break; - case CQuest::Emission::MISSION_KILL_HERO: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_KILL_CREATURE: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_ART: - activeId = true; - for(int i = 0; i < map.allowedArtifact.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->arth->objects.at(i)->getNameTranslated())); - if(!seerhut.quest->m5arts.empty()) - ui->targetId->setCurrentIndex(seerhut.quest->m5arts.front()); - //TODO: support multiple artifacts - break; - case CQuest::Emission::MISSION_ARMY: - activeId = true; - activeAmount = true; - break; - case CQuest::Emission::MISSION_RESOURCES: - activeId = true; - activeAmount = true; - for(auto s : GameConstants::RESOURCE_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m7resources.size(); ++i) - { - if(seerhut.quest->m7resources[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m7resources[i])); - break; //TODO: support multiple resources - } - } - break; - case CQuest::Emission::MISSION_HERO: - activeId = true; - for(int i = 0; i < map.allowedHeroes.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->heroh->objects.at(i)->getNameTranslated())); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_PLAYER: - activeId = true; - for(auto s : GameConstants::PLAYER_COLOR_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_KEYMASTER: - break; - default: - break; + ui->lDayOfWeek->setCurrentIndex(quest.mission.dayOfWeek); + ui->lDaysPassed->setValue(quest.mission.daysPassed); + ui->lHeroLevel->setValue(quest.mission.heroLevel); + ui->lHeroExperience->setValue(quest.mission.heroExperience); + ui->lManaPoints->setValue(quest.mission.manaPoints); + ui->lManaPercentage->setValue(quest.mission.manaPercentage); + ui->lAttack->setValue(quest.mission.primary[0]); + ui->lDefence->setValue(quest.mission.primary[1]); + ui->lPower->setValue(quest.mission.primary[2]); + ui->lKnowledge->setValue(quest.mission.primary[3]); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + widget->setValue(quest.mission.resources[i]); } - ui->targetId->setEnabled(activeId); - ui->targetAmount->setEnabled(activeAmount); -} - -QString QuestWidget::commitChanges() -{ - assert(seerhut.quest); - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - seerhut.quest->m13489val = ui->targetAmount->text().toInt(); - return QString("Reach lvl ").append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_PRIMARY_STAT: - seerhut.quest->m2stats.resize(sizeof(PrimarySkill::names), 0); - seerhut.quest->m2stats[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support multiple stats - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_KILL_HERO: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_KILL_CREATURE: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_ART: - seerhut.quest->m5arts.clear(); - seerhut.quest->m5arts.push_back(ArtifactID(ui->targetId->currentIndex())); - //TODO: support multiple artifacts - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_ARMY: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_RESOURCES: - seerhut.quest->m7resources[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support resources - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_HERO: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_PLAYER: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_KEYMASTER: - return QString("N/A"); - default: - return QString("N/A"); + 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); + for(auto & i : quest.mission.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); } + for(auto & i : quest.mission.creatures) + { + int index = i.type->getIndex(); + ui->lCreatureId->setCurrentIndex(index); + ui->lCreatureAmount->setValue(i.count); + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); + } + for(auto & i : quest.mission.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.mission.heroClasses) + { + for(int e = 0; e < ui->lHeroClasses->count(); ++e) + { + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.mission.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + + if(quest.killTarget != ObjectInstanceID::NONE && quest.killTarget < controller.map()->objects.size()) + ui->lKillTarget->setText(QString::fromStdString(controller.map()->objects[quest.killTarget]->instanceName)); + else + quest.killTarget = ObjectInstanceID::NONE; } -QuestDelegate::QuestDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), QStyledItemDelegate() +bool QuestWidget::commitChanges() +{ + quest.mission.dayOfWeek = ui->lDayOfWeek->currentIndex(); + quest.mission.daysPassed = ui->lDaysPassed->value(); + quest.mission.heroLevel = ui->lHeroLevel->value(); + quest.mission.heroExperience = ui->lHeroExperience->value(); + quest.mission.manaPoints = ui->lManaPoints->value(); + quest.mission.manaPercentage = ui->lManaPercentage->value(); + quest.mission.primary[0] = ui->lAttack->value(); + quest.mission.primary[1] = ui->lDefence->value(); + quest.mission.primary[2] = ui->lPower->value(); + quest.mission.primary[3] = ui->lKnowledge->value(); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + quest.mission.resources[i] = widget->value(); + } + + quest.mission.artifacts.clear(); + for(int i = 0; i < ui->lArtifacts->count(); ++i) + { + if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) + quest.mission.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + quest.mission.spells.clear(); + for(int i = 0; i < ui->lSpells->count(); ++i) + { + if(ui->lSpells->item(i)->checkState() == Qt::Checked) + quest.mission.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + quest.mission.secondary.clear(); + for(int i = 0; i < ui->lSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) + { + if(widget->currentIndex() > 0) + quest.mission.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + quest.mission.creatures.clear(); + for(int i = 0; i < ui->lCreatures->rowCount(); ++i) + { + int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) + if(widget->value()) + quest.mission.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + quest.mission.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + quest.mission.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + quest.mission.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + quest.mission.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + quest.mission.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + quest.mission.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } + + //quest.killTarget is set directly in object picking + + return true; +} + +void QuestWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) +{ + QTableWidgetItem * item = nullptr; + QSpinBox * widget = nullptr; + for(int i = 0; i < listWidget->rowCount(); ++i) + { + if(auto * cname = listWidget->item(i, 0)) + { + if(cname->data(Qt::UserRole).toInt() == comboWidget->currentData().toInt()) + { + item = cname; + widget = qobject_cast(listWidget->cellWidget(i, 1)); + break; + } + } + } + + if(!item) + { + listWidget->setRowCount(listWidget->rowCount() + 1); + item = new QTableWidgetItem(comboWidget->currentText()); + listWidget->setItem(listWidget->rowCount() - 1, 0, item); + } + + item->setData(Qt::UserRole, comboWidget->currentData()); + + if(!widget) + { + widget = new QSpinBox; + widget->setRange(spinWidget->minimum(), spinWidget->maximum()); + listWidget->setCellWidget(listWidget->rowCount() - 1, 1, widget); + } + + widget->setValue(spinWidget->value()); +} + +void QuestWidget::on_lKillTargetSelect_clicked() +{ + auto pred = [](const CGObjectInstance * obj) -> bool + { + if(auto * o = dynamic_cast(obj)) + return o->ID != Obj::PRISON; + if(dynamic_cast(obj)) + return true; + return false; + }; + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(pred); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + hide(); +} + +void QuestWidget::onTargetPicked(const CGObjectInstance * obj) +{ + show(); + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + if(!obj) //discarded + { + quest.killTarget = ObjectInstanceID::NONE; + ui->lKillTarget->setText(""); + return; + } + + ui->lKillTarget->setText(QString::fromStdString(obj->instanceName)); + quest.killTarget = obj->id; +} + +void QuestWidget::on_lCreatureAdd_clicked() +{ + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); +} + + +void QuestWidget::on_lCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->lCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->lCreatures->removeRow(i); +} + +QuestDelegate::QuestDelegate(MapController & c, CQuest & t): controller(c), quest(t), QStyledItemDelegate() { } QWidget * QuestDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const { - return new QuestWidget(map, seerhut, parent); + return new QuestWidget(controller, quest, parent); } void QuestDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const @@ -183,11 +408,26 @@ void QuestDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, c { if(auto *ed = qobject_cast(editor)) { - auto quest = ed->commitChanges(); - model->setData(index, quest); + ed->commitChanges(); } else { QStyledItemDelegate::setModelData(editor, model, index); } } + +bool QuestDelegate::eventFilter(QObject * object, QEvent * event) +{ + if(auto * ed = qobject_cast(object)) + { + if(event->type() == QEvent::Hide || event->type() == QEvent::FocusOut) + return false; + if(event->type() == QEvent::Close) + { + emit commitData(ed); + emit closeEditor(ed); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} diff --git a/mapeditor/inspector/questwidget.h b/mapeditor/inspector/questwidget.h index cef1ad551..3237c0b65 100644 --- a/mapeditor/inspector/questwidget.h +++ b/mapeditor/inspector/questwidget.h @@ -16,20 +16,33 @@ namespace Ui { class QuestWidget; } +class MapController; + class QuestWidget : public QDialog { Q_OBJECT public: - explicit QuestWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr); + explicit QuestWidget(MapController &, CQuest &, QWidget *parent = nullptr); ~QuestWidget(); void obtainData(); - QString commitChanges(); + bool commitChanges(); + +private slots: + void onTargetPicked(const CGObjectInstance *); + + void on_lKillTargetSelect_clicked(); + + void on_lCreatureAdd_clicked(); + + void on_lCreatureRemove_clicked(); private: - CGSeerHut & seerhut; - const CMap & map; + void onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget); + + CQuest & quest; + MapController & controller; Ui::QuestWidget *ui; }; @@ -39,13 +52,16 @@ class QuestDelegate : public QStyledItemDelegate public: using QStyledItemDelegate::QStyledItemDelegate; - QuestDelegate(const CMap &, CGSeerHut &); + QuestDelegate(MapController &, CQuest &); 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; +protected: + bool eventFilter(QObject * object, QEvent * event) override; + private: - CGSeerHut & seerhut; - const CMap & map; + CQuest & quest; + MapController & controller; }; diff --git a/mapeditor/inspector/questwidget.ui b/mapeditor/inspector/questwidget.ui index 0f2f7c69a..f08b3bf94 100644 --- a/mapeditor/inspector/questwidget.ui +++ b/mapeditor/inspector/questwidget.ui @@ -2,45 +2,633 @@ QuestWidget + + Qt::NonModal + 0 0 - 429 - 89 + 531 + 495 Mission goal - + + true + + - - - - 0 - 0 - + + + + + Day of week + + + + + + + + 120 + 0 + + + + + + + + Days passed + + + + + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + 999 + + + + + + + % + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Kill hero/monster + + + + + + + true + + + + + + + ... + + + + + + + + + Primary skills + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + + + + Defence + + + + + + + + + + Spell power + + + + + + + + + + Knowledge + + + + + + + - - - - 0 - 0 - + + + Qt::LeftToRight - - - 60 - 16777215 - + + QTabWidget::North - - Qt::ImhDigitsOnly + + QTabWidget::Rounded + + 0 + + + Qt::ElideNone + + + true + + + false + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index ea2b94e9e..373588a3c 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -13,35 +13,199 @@ #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/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" +#include "../lib/rewardable/Configuration.h" +#include "../lib/rewardable/Limiter.h" +#include "../lib/rewardable/Reward.h" +#include "../lib/mapObjects/CGPandoraBox.h" +#include "../lib/mapObjects/CQuest.h" -RewardsWidget::RewardsWidget(const CMap & m, CGPandoraBox & p, QWidget *parent) : +RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : QDialog(parent), map(m), - pandora(&p), - seerhut(nullptr), + object(p), ui(new Ui::RewardsWidget) { ui->setupUi(this); - for(auto & type : rewardTypes) - ui->rewardType->addItem(QString::fromStdString(type)); -} - -RewardsWidget::RewardsWidget(const CMap & m, CGSeerHut & p, QWidget *parent) : - QDialog(parent), - map(m), - pandora(nullptr), - seerhut(&p), - ui(new Ui::RewardsWidget) -{ - ui->setupUi(this); + //fill core elements + for(const auto & s : Rewardable::VisitModeString) + ui->visitMode->addItem(QString::fromStdString(s)); - for(auto & type : rewardTypes) - ui->rewardType->addItem(QString::fromStdString(type)); + for(const auto & s : Rewardable::SelectModeString) + ui->selectMode->addItem(QString::fromStdString(s)); + + for(auto s : {"AUTO", "MODAL", "INFO"}) + ui->windowMode->addItem(QString::fromStdString(s)); + + ui->lDayOfWeek->addItem(tr("None")); + for(int i = 1; i <= 7; ++i) + ui->lDayOfWeek->addItem(tr("Day %1").arg(i)); + + //fill resources + ui->rResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) + { + for(auto * w : {ui->rResources, ui->lResources}) + { + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + w->setItem(i, 0, item); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + if(w == ui->rResources) + spinBox->setMinimum(i == GameResID::GOLD ? -999999 : -999); + w->setCellWidget(i, 1, spinBox); + } + } + + //fill artifacts + for(int i = 0; i < map.allowedArtifact.size(); ++i) + { + for(auto * w : {ui->rArtifacts, ui->lArtifacts}) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(map.allowedArtifact.count(i) == 0) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + w->addItem(item); + } + } + + //fill spells + for(int i = 0; i < map.allowedSpells.size(); ++i) + { + for(auto * w : {ui->rSpells, ui->lSpells}) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(map.allowedSpells.count(i) == 0) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + w->addItem(item); + } + + //spell cast + if(VLC->spells()->getByIndex(i)->isAdventure()) + { + ui->castSpell->addItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + ui->castSpell->setItemData(ui->castSpell->count() - 1, QVariant::fromValue(i)); + } + } + + //fill skills + ui->rSkills->setRowCount(map.allowedAbilities.size()); + ui->lSkills->setRowCount(map.allowedAbilities.size()); + for(int i = 0; i < map.allowedAbilities.size(); ++i) + { + for(auto * w : {ui->rSkills, ui->lSkills}) + { + auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + + auto * widget = new QComboBox; + for(auto & s : NSecondarySkill::levels) + widget->addItem(QString::fromStdString(s)); + + if(map.allowedAbilities.count(i) == 0) + { + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + widget->setEnabled(false); + } + + w->setItem(i, 0, item); + w->setCellWidget(i, 1, widget); + } + } + + //fill creatures + for(auto & creature : VLC->creh->objects) + { + for(auto * w : {ui->rCreatureId, ui->lCreatureId}) + { + w->addItem(QString::fromStdString(creature->getNameSingularTranslated())); + w->setItemData(w->count() - 1, creature->getIndex()); + } + } + + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } + + //fill spell cast + for(auto & s : NSecondarySkill::levels) + ui->castLevel->addItem(QString::fromStdString(s)); + on_castSpellCheck_toggled(false); + + //fill bonuses + for(auto & s : bonusDurationMap) + ui->bonusDuration->addItem(QString::fromStdString(s.first)); + for(auto & s : bonusNameMap) + ui->bonusType->addItem(QString::fromStdString(s.first)); + + //set default values + if(dynamic_cast(&object)) + { + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); + ui->visitMode->setEnabled(false); + ui->selectMode->setCurrentIndex(vstd::find_pos(Rewardable::SelectModeString, "selectFirst")); + ui->selectMode->setEnabled(false); + ui->windowMode->setEnabled(false); + ui->canRefuse->setEnabled(false); + } + + if(auto * e = dynamic_cast(&object)) + { + ui->selectMode->setEnabled(true); + if(!e->removeAfterVisit) + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "unlimited")); + } + + if(dynamic_cast(&object)) + { + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); + ui->visitMode->setEnabled(false); + ui->windowMode->setEnabled(false); + ui->canRefuse->setChecked(true); + ui->canRefuse->setEnabled(false); + } + + //hide elements + ui->eventInfoGroup->hide(); } RewardsWidget::~RewardsWidget() @@ -49,337 +213,508 @@ RewardsWidget::~RewardsWidget() delete ui; } -QList RewardsWidget::getListForType(RewardType typeId) -{ - assert(typeId < rewardTypes.size()); - QList result; - - switch (typeId) { - case RewardType::RESOURCE: - //to convert string to index WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - result.append("Wood"); - result.append("Mercury"); - result.append("Ore"); - result.append("Sulfur"); - result.append("Crystals"); - result.append("Gems"); - result.append("Gold"); - break; - - case RewardType::PRIMARY_SKILL: - for(auto s : PrimarySkill::names) - result.append(QString::fromStdString(s)); - break; - - case RewardType::SECONDARY_SKILL: - for(int i = 0; i < map.allowedAbilities.size(); ++i) - { - if(map.allowedAbilities[i]) - result.append(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::ARTIFACT: - for(int i = 0; i < map.allowedArtifact.size(); ++i) - { - if(map.allowedArtifact[i]) - result.append(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::SPELL: - for(int i = 0; i < map.allowedSpells.size(); ++i) - { - if(map.allowedSpells[i]) - result.append(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::CREATURE: - for(auto creature : VLC->creh->objects) - { - result.append(QString::fromStdString(creature->getNameSingularTranslated())); - } - break; - } - return result; -} - -void RewardsWidget::on_rewardType_activated(int index) -{ - ui->rewardList->clear(); - ui->rewardList->setEnabled(true); - assert(index < rewardTypes.size()); - - auto l = getListForType(RewardType(index)); - if(l.empty()) - ui->rewardList->setEnabled(false); - - for(auto & s : l) - ui->rewardList->addItem(s); -} void RewardsWidget::obtainData() { - if(pandora) - { - if(pandora->gainedExp > 0) - addReward(RewardType::EXPERIENCE, 0, pandora->gainedExp); - if(pandora->manaDiff) - addReward(RewardType::MANA, 0, pandora->manaDiff); - if(pandora->moraleDiff) - addReward(RewardType::MORALE, 0, pandora->moraleDiff); - if(pandora->luckDiff) - addReward(RewardType::LUCK, 0, pandora->luckDiff); - if(pandora->resources.nonZero()) - { - for(ResourceSet::nziterator resiter(pandora->resources); resiter.valid(); ++resiter) - addReward(RewardType::RESOURCE, resiter->resType, resiter->resVal); - } - for(int idx = 0; idx < pandora->primskills.size(); ++idx) - { - if(pandora->primskills[idx]) - addReward(RewardType::PRIMARY_SKILL, idx, pandora->primskills[idx]); - } - assert(pandora->abilities.size() == pandora->abilityLevels.size()); - for(int idx = 0; idx < pandora->abilities.size(); ++idx) - { - addReward(RewardType::SECONDARY_SKILL, pandora->abilities[idx].getNum(), pandora->abilityLevels[idx]); - } - for(auto art : pandora->artifacts) - { - addReward(RewardType::ARTIFACT, art.getNum(), 1); - } - for(auto spell : pandora->spells) - { - addReward(RewardType::SPELL, spell.getNum(), 1); - } - for(int i = 0; i < pandora->creatures.Slots().size(); ++i) - { - if(auto c = pandora->creatures.getCreature(SlotID(i))) - addReward(RewardType::CREATURE, c->getId(), pandora->creatures.getStackCount(SlotID(i))); - } - } + //common parameters + ui->visitMode->setCurrentIndex(object.configuration.visitMode); + ui->selectMode->setCurrentIndex(object.configuration.selectMode); + ui->windowMode->setCurrentIndex(int(object.configuration.infoWindowType)); + ui->onSelectText->setText(QString::fromStdString(object.configuration.onSelect.toString())); + ui->canRefuse->setChecked(object.configuration.canRefuse); - if(seerhut) - { - switch(seerhut->rewardType) - { - case CGSeerHut::ERewardType::EXPERIENCE: - addReward(RewardType::EXPERIENCE, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::MANA_POINTS: - addReward(RewardType::MANA, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::MORALE_BONUS: - addReward(RewardType::MORALE, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::LUCK_BONUS: - addReward(RewardType::LUCK, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::RESOURCES: - addReward(RewardType::RESOURCE, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::PRIMARY_SKILL: - addReward(RewardType::PRIMARY_SKILL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::SECONDARY_SKILL: - addReward(RewardType::SECONDARY_SKILL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::ARTIFACT: - addReward(RewardType::ARTIFACT, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::SPELL: - addReward(RewardType::SPELL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::CREATURE: - addReward(RewardType::CREATURE, seerhut->rID, seerhut->rVal); - break; - - default: - break; - } - } + //reset parameters + ui->resetPeriod->setValue(object.configuration.resetParameters.period); + ui->resetVisitors->setChecked(object.configuration.resetParameters.visitors); + ui->resetRewards->setChecked(object.configuration.resetParameters.rewards); + + ui->visitInfoList->clear(); + + for([[maybe_unused]] auto & a : object.configuration.info) + ui->visitInfoList->addItem(tr("Reward %1").arg(ui->visitInfoList->count() + 1)); + + if(ui->visitInfoList->currentItem()) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } bool RewardsWidget::commitChanges() { - bool haveRewards = false; - if(pandora) + //common parameters + object.configuration.visitMode = ui->visitMode->currentIndex(); + object.configuration.selectMode = ui->selectMode->currentIndex(); + object.configuration.infoWindowType = EInfoWindowMode(ui->windowMode->currentIndex()); + if(ui->onSelectText->text().isEmpty()) + object.configuration.onSelect.clear(); + else + object.configuration.onSelect = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "onSelect"), ui->onSelectText->text().toStdString())); + object.configuration.canRefuse = ui->canRefuse->isChecked(); + + //reset parameters + object.configuration.resetParameters.period = ui->resetPeriod->value(); + object.configuration.resetParameters.visitors = ui->resetVisitors->isChecked(); + object.configuration.resetParameters.rewards = ui->resetRewards->isChecked(); + + if(ui->visitInfoList->currentItem()) + saveCurrentVisitInfo(ui->visitInfoList->currentRow()); + + return true; +} + +void RewardsWidget::saveCurrentVisitInfo(int index) +{ + auto & vinfo = object.configuration.info.at(index); + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + if(ui->rewardMessage->text().isEmpty()) + vinfo.message.clear(); + else + vinfo.message = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "info", index, "message"), ui->rewardMessage->text().toStdString())); + + vinfo.reward.heroLevel = ui->rHeroLevel->value(); + vinfo.reward.heroExperience = ui->rHeroExperience->value(); + vinfo.reward.manaDiff = ui->rManaDiff->value(); + vinfo.reward.manaPercentage = ui->rManaPercentage->value(); + vinfo.reward.manaOverflowFactor = ui->rOverflowFactor->value(); + vinfo.reward.movePoints = ui->rMovePoints->value(); + vinfo.reward.movePercentage = ui->rMovePercentage->value(); + vinfo.reward.removeObject = ui->removeObject->isChecked(); + vinfo.reward.primary.resize(4); + vinfo.reward.primary[0] = ui->rAttack->value(); + vinfo.reward.primary[1] = ui->rDefence->value(); + vinfo.reward.primary[2] = ui->rPower->value(); + vinfo.reward.primary[3] = ui->rKnowledge->value(); + for(int i = 0; i < ui->rResources->rowCount(); ++i) { - pandora->abilities.clear(); - pandora->abilityLevels.clear(); - pandora->primskills.resize(GameConstants::PRIMARY_SKILLS, 0); - pandora->resources = ResourceSet(); - pandora->artifacts.clear(); - pandora->spells.clear(); - pandora->creatures.clearSlots(); - - for(int row = 0; row < rewards; ++row) + if(auto * widget = qobject_cast(ui->rResources->cellWidget(i, 1))) + vinfo.reward.resources[i] = widget->value(); + } + + vinfo.reward.artifacts.clear(); + for(int i = 0; i < ui->rArtifacts->count(); ++i) + { + if(ui->rArtifacts->item(i)->checkState() == Qt::Checked) + vinfo.reward.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + vinfo.reward.spells.clear(); + for(int i = 0; i < ui->rSpells->count(); ++i) + { + if(ui->rSpells->item(i)->checkState() == Qt::Checked) + vinfo.reward.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + vinfo.reward.secondary.clear(); + for(int i = 0; i < ui->rSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(i, 1))) { - haveRewards = true; - int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); - int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; - int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); - switch(typeId) + if(widget->currentIndex() > 0) + vinfo.reward.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + vinfo.reward.creatures.clear(); + for(int i = 0; i < ui->rCreatures->rowCount(); ++i) + { + int index = ui->rCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->rCreatures->cellWidget(i, 1))) + if(widget->value()) + vinfo.reward.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + vinfo.reward.spellCast.first = SpellID::NONE; + if(ui->castSpellCheck->isChecked()) + { + vinfo.reward.spellCast.first = VLC->spells()->getByIndex(ui->castSpell->itemData(ui->castSpell->currentIndex()).toInt())->getId(); + vinfo.reward.spellCast.second = ui->castLevel->currentIndex(); + } + + vinfo.reward.bonuses.clear(); + for(int i = 0; i < ui->bonuses->rowCount(); ++i) + { + auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString()); + auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString()); + auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt(); + vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(object.id)); + } + + vinfo.limiter.dayOfWeek = ui->lDayOfWeek->currentIndex(); + vinfo.limiter.daysPassed = ui->lDaysPassed->value(); + vinfo.limiter.heroLevel = ui->lHeroLevel->value(); + vinfo.limiter.heroExperience = ui->lHeroExperience->value(); + vinfo.limiter.manaPoints = ui->lManaPoints->value(); + vinfo.limiter.manaPercentage = ui->lManaPercentage->value(); + vinfo.limiter.primary.resize(4); + vinfo.limiter.primary[0] = ui->lAttack->value(); + vinfo.limiter.primary[1] = ui->lDefence->value(); + vinfo.limiter.primary[2] = ui->lPower->value(); + vinfo.limiter.primary[3] = ui->lKnowledge->value(); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + vinfo.limiter.resources[i] = widget->value(); + } + + vinfo.limiter.artifacts.clear(); + for(int i = 0; i < ui->lArtifacts->count(); ++i) + { + if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) + vinfo.limiter.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + vinfo.limiter.spells.clear(); + for(int i = 0; i < ui->lSpells->count(); ++i) + { + if(ui->lSpells->item(i)->checkState() == Qt::Checked) + vinfo.limiter.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + vinfo.limiter.secondary.clear(); + for(int i = 0; i < ui->lSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) + { + if(widget->currentIndex() > 0) + vinfo.limiter.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + vinfo.limiter.creatures.clear(); + for(int i = 0; i < ui->lCreatures->rowCount(); ++i) + { + int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) + if(widget->value()) + vinfo.limiter.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + vinfo.limiter.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + vinfo.limiter.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } +} + +void RewardsWidget::loadCurrentVisitInfo(int index) +{ + for(auto * w : {ui->rArtifacts, ui->rSpells, ui->lArtifacts, ui->lSpells}) + for(int i = 0; i < w->count(); ++i) + w->item(i)->setCheckState(Qt::Unchecked); + + for(auto * w : {ui->rSkills, ui->lSkills}) + for(int i = 0; i < w->rowCount(); ++i) + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(i, 1))) + widget->setCurrentIndex(0); + + ui->rCreatures->setRowCount(0); + ui->lCreatures->setRowCount(0); + ui->bonuses->setRowCount(0); + + const auto & vinfo = object.configuration.info.at(index); + ui->rewardMessage->setText(QString::fromStdString(vinfo.message.toString())); + + ui->rHeroLevel->setValue(vinfo.reward.heroLevel); + ui->rHeroExperience->setValue(vinfo.reward.heroExperience); + ui->rManaDiff->setValue(vinfo.reward.manaDiff); + ui->rManaPercentage->setValue(vinfo.reward.manaPercentage); + ui->rOverflowFactor->setValue(vinfo.reward.manaOverflowFactor); + ui->rMovePoints->setValue(vinfo.reward.movePoints); + ui->rMovePercentage->setValue(vinfo.reward.movePercentage); + ui->removeObject->setChecked(vinfo.reward.removeObject); + ui->rAttack->setValue(vinfo.reward.primary[0]); + ui->rDefence->setValue(vinfo.reward.primary[1]); + ui->rPower->setValue(vinfo.reward.primary[2]); + ui->rKnowledge->setValue(vinfo.reward.primary[3]); + for(int i = 0; i < ui->rResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->rResources->cellWidget(i, 1))) + widget->setValue(vinfo.reward.resources[i]); + } + + for(auto i : vinfo.reward.artifacts) + ui->rArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : vinfo.reward.spells) + ui->rArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : vinfo.reward.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); + } + for(auto & i : vinfo.reward.creatures) + { + int index = i.type->getIndex(); + ui->rCreatureId->setCurrentIndex(index); + ui->rCreatureAmount->setValue(i.count); + onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount); + } + + ui->castSpellCheck->setChecked(vinfo.reward.spellCast.first != SpellID::NONE); + if(ui->castSpellCheck->isChecked()) + { + int index = VLC->spells()->getById(vinfo.reward.spellCast.first)->getIndex(); + ui->castSpell->setCurrentIndex(index); + ui->castLevel->setCurrentIndex(vinfo.reward.spellCast.second); + } + + for(auto & i : vinfo.reward.bonuses) + { + auto dur = vstd::findKey(bonusDurationMap, i.duration); + for(int i = 0; i < ui->bonusDuration->count(); ++i) + { + if(ui->bonusDuration->itemText(i) == QString::fromStdString(dur)) { - case RewardType::EXPERIENCE: - pandora->gainedExp = amount; - break; - - case RewardType::MANA: - pandora->manaDiff = amount; - break; - - case RewardType::MORALE: - pandora->moraleDiff = amount; - break; - - case RewardType::LUCK: - pandora->luckDiff = amount; - break; - - case RewardType::RESOURCE: - pandora->resources[listId] = amount; - break; - - case RewardType::PRIMARY_SKILL: - pandora->primskills[listId] = amount; - break; - - case RewardType::SECONDARY_SKILL: - pandora->abilities.push_back(SecondarySkill(listId)); - pandora->abilityLevels.push_back(amount); - break; - - case RewardType::ARTIFACT: - pandora->artifacts.push_back(ArtifactID(listId)); - break; - - case RewardType::SPELL: - pandora->spells.push_back(SpellID(listId)); - break; - - case RewardType::CREATURE: - auto slot = pandora->creatures.getFreeSlot(); - if(slot != SlotID() && amount > 0) - pandora->creatures.addToSlot(slot, CreatureID(listId), amount); - break; + ui->bonusDuration->setCurrentIndex(i); + break; + } + } + + auto typ = vstd::findKey(bonusNameMap, i.type); + for(int i = 0; i < ui->bonusType->count(); ++i) + { + if(ui->bonusType->itemText(i) == QString::fromStdString(typ)) + { + ui->bonusType->setCurrentIndex(i); + break; + } + } + + ui->bonusValue->setValue(i.val); + on_bonusAdd_clicked(); + } + + ui->lDayOfWeek->setCurrentIndex(vinfo.limiter.dayOfWeek); + ui->lDaysPassed->setValue(vinfo.limiter.daysPassed); + ui->lHeroLevel->setValue(vinfo.limiter.heroLevel); + ui->lHeroExperience->setValue(vinfo.limiter.heroExperience); + ui->lManaPoints->setValue(vinfo.limiter.manaPoints); + ui->lManaPercentage->setValue(vinfo.limiter.manaPercentage); + ui->lAttack->setValue(vinfo.limiter.primary[0]); + ui->lDefence->setValue(vinfo.limiter.primary[1]); + ui->lPower->setValue(vinfo.limiter.primary[2]); + ui->lKnowledge->setValue(vinfo.limiter.primary[3]); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + widget->setValue(vinfo.limiter.resources[i]); + } + + for(auto i : vinfo.limiter.artifacts) + ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : vinfo.limiter.spells) + ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : vinfo.limiter.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); + } + for(auto & i : vinfo.limiter.creatures) + { + int index = i.type->getIndex(); + ui->lCreatureId->setCurrentIndex(index); + ui->lCreatureAmount->setValue(i.count); + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); + } + + for(auto & i : vinfo.limiter.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; } } } - if(seerhut) + for(auto & i : vinfo.limiter.heroClasses) { - for(int row = 0; row < rewards; ++row) + for(int e = 0; e < ui->lHeroClasses->count(); ++e) { - haveRewards = true; - int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); - int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; - int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); - seerhut->rewardType = CGSeerHut::ERewardType(typeId + 1); - seerhut->rID = listId; - seerhut->rVal = amount; + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : vinfo.limiter.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } } } - return haveRewards; } -void RewardsWidget::on_rewardList_activated(int index) +void RewardsWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) { - ui->rewardAmount->setText(QStringLiteral("1")); + QTableWidgetItem * item = nullptr; + QSpinBox * widget = nullptr; + for(int i = 0; i < listWidget->rowCount(); ++i) + { + if(auto * cname = listWidget->item(i, 0)) + { + if(cname->data(Qt::UserRole).toInt() == comboWidget->currentData().toInt()) + { + item = cname; + widget = qobject_cast(listWidget->cellWidget(i, 1)); + break; + } + } + } + + if(!item) + { + listWidget->setRowCount(listWidget->rowCount() + 1); + item = new QTableWidgetItem(comboWidget->currentText()); + listWidget->setItem(listWidget->rowCount() - 1, 0, item); + } + + item->setData(Qt::UserRole, comboWidget->currentData()); + + if(!widget) + { + widget = new QSpinBox; + widget->setRange(spinWidget->minimum(), spinWidget->maximum()); + listWidget->setCellWidget(listWidget->rowCount() - 1, 1, widget); + } + + widget->setValue(spinWidget->value()); } -void RewardsWidget::addReward(RewardsWidget::RewardType typeId, int listId, int amount) +void RewardsWidget::on_addVisitInfo_clicked() { - //for seerhut there could be the only one reward - if(!pandora && seerhut && rewards) + ui->visitInfoList->addItem(tr("Reward %1").arg(ui->visitInfoList->count() + 1)); + object.configuration.info.emplace_back(); +} + + +void RewardsWidget::on_removeVisitInfo_clicked() +{ + int index = ui->visitInfoList->currentRow(); + object.configuration.info.erase(std::next(object.configuration.info.begin(), index)); + ui->visitInfoList->blockSignals(true); + delete ui->visitInfoList->currentItem(); + ui->visitInfoList->blockSignals(false); + on_visitInfoList_itemSelectionChanged(); + if(ui->visitInfoList->currentItem()) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); +} + +void RewardsWidget::on_selectMode_currentIndexChanged(int index) +{ + ui->onSelectText->setEnabled(index == vstd::find_pos(Rewardable::SelectModeString, "selectPlayer")); +} + +void RewardsWidget::on_resetPeriod_valueChanged(int arg1) +{ + ui->resetRewards->setEnabled(arg1); + ui->resetVisitors->setEnabled(arg1); +} + + +void RewardsWidget::on_visitInfoList_itemSelectionChanged() +{ + if(ui->visitInfoList->currentItem() == nullptr) + { + ui->eventInfoGroup->hide(); return; - - ui->rewardsTable->setRowCount(++rewards); - - auto itemType = new QTableWidgetItem(QString::fromStdString(rewardTypes[typeId])); - itemType->setData(Qt::UserRole, typeId); - ui->rewardsTable->setItem(rewards - 1, 0, itemType); - - auto l = getListForType(typeId); - if(!l.empty()) - { - auto itemCurr = new QTableWidgetItem(getListForType(typeId)[listId]); - itemCurr->setData(Qt::UserRole, listId); - ui->rewardsTable->setItem(rewards - 1, 1, itemCurr); } - QString am = QString::number(amount); - switch(ui->rewardType->currentIndex()) - { - case 6: - if(amount <= 1) - am = "Basic"; - if(amount == 2) - am = "Advanced"; - if(amount >= 3) - am = "Expert"; - break; - - case 7: - case 8: - am = ""; - amount = 1; - break; - } - auto itemCount = new QTableWidgetItem(am); - itemCount->setData(Qt::UserRole, amount); - ui->rewardsTable->setItem(rewards - 1, 2, itemCount); + ui->eventInfoGroup->show(); } - -void RewardsWidget::on_buttonAdd_clicked() +void RewardsWidget::on_visitInfoList_currentItemChanged(QListWidgetItem * current, QListWidgetItem * previous) { - addReward(RewardType(ui->rewardType->currentIndex()), ui->rewardList->currentIndex(), ui->rewardAmount->text().toInt()); -} - - -void RewardsWidget::on_buttonRemove_clicked() -{ - auto currentRow = ui->rewardsTable->currentRow(); - if(currentRow != -1) - { - ui->rewardsTable->removeRow(currentRow); - --rewards; - } -} - - -void RewardsWidget::on_buttonClear_clicked() -{ - ui->rewardsTable->clear(); - rewards = 0; -} - - -void RewardsWidget::on_rewardsTable_itemSelectionChanged() -{ - /*auto type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 0); - ui->rewardType->setCurrentIndex(type->data(Qt::UserRole).toInt()); - ui->rewardType->activated(ui->rewardType->currentIndex()); + if(previous) + saveCurrentVisitInfo(ui->visitInfoList->row(previous)); - type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 1); - ui->rewardList->setCurrentIndex(type->data(Qt::UserRole).toInt()); - ui->rewardList->activated(ui->rewardList->currentIndex()); - - type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 2); - ui->rewardAmount->setText(QString::number(type->data(Qt::UserRole).toInt()));*/ + if(current) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } + +void RewardsWidget::on_rCreatureAdd_clicked() +{ + onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount); +} + + +void RewardsWidget::on_rCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->rCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->rCreatures->removeRow(i); +} + + +void RewardsWidget::on_lCreatureAdd_clicked() +{ + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); +} + + +void RewardsWidget::on_lCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->lCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->lCreatures->removeRow(i); +} + +void RewardsWidget::on_castSpellCheck_toggled(bool checked) +{ + ui->castSpell->setEnabled(checked); + ui->castLevel->setEnabled(checked); +} + +void RewardsWidget::on_bonusAdd_clicked() +{ + auto * itemType = new QTableWidgetItem(ui->bonusType->currentText()); + auto * itemDur = new QTableWidgetItem(ui->bonusDuration->currentText()); + auto * itemVal = new QTableWidgetItem(QString::number(ui->bonusValue->value())); + itemVal->setData(Qt::UserRole, ui->bonusValue->value()); + + ui->bonuses->setRowCount(ui->bonuses->rowCount() + 1); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 0, itemDur); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 1, itemType); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 2, itemVal); +} + +void RewardsWidget::on_bonusRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->bonuses->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->bonuses->removeRow(i); +} + + void RewardsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if(auto * ed = qobject_cast(editor)) @@ -396,12 +731,7 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c { if(auto * ed = qobject_cast(editor)) { - auto hasReward = ed->commitChanges(); - model->setData(index, "dummy"); - if(hasReward) - model->setData(index, "HAS REWARD"); - else - model->setData(index, ""); + ed->commitChanges(); } else { @@ -409,20 +739,11 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c } } -RewardsPandoraDelegate::RewardsPandoraDelegate(const CMap & m, CGPandoraBox & t): map(m), pandora(t), RewardsDelegate() +RewardsDelegate::RewardsDelegate(CMap & m, CRewardableObject & t): map(m), object(t) { } -QWidget * RewardsPandoraDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +QWidget * RewardsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { - return new RewardsWidget(map, pandora, parent); -} - -RewardsSeerhutDelegate::RewardsSeerhutDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), RewardsDelegate() -{ -} - -QWidget * RewardsSeerhutDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - return new RewardsWidget(map, seerhut, parent); + return new RewardsWidget(map, object, parent); } diff --git a/mapeditor/inspector/rewardswidget.h b/mapeditor/inspector/rewardswidget.h index 422c31768..aeffe17e2 100644 --- a/mapeditor/inspector/rewardswidget.h +++ b/mapeditor/inspector/rewardswidget.h @@ -10,88 +10,77 @@ #pragma once #include "../StdInc.h" #include -#include "../lib/mapObjects/CGPandoraBox.h" -#include "../lib/mapObjects/CQuest.h" +#include "../lib/mapObjects/CRewardableObject.h" namespace Ui { class RewardsWidget; } -const std::array rewardTypes{"Experience", "Mana", "Morale", "Luck", "Resource", "Primary skill", "Secondary skill", "Artifact", "Spell", "Creature"}; - class RewardsWidget : public QDialog { Q_OBJECT public: - enum RewardType - { - EXPERIENCE = 0, MANA, MORALE, LUCK, RESOURCE, PRIMARY_SKILL, SECONDARY_SKILL, ARTIFACT, SPELL, CREATURE - }; - explicit RewardsWidget(const CMap &, CGPandoraBox &, QWidget *parent = nullptr); - explicit RewardsWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr); + explicit RewardsWidget(CMap &, CRewardableObject &, QWidget *parent = nullptr); ~RewardsWidget(); void obtainData(); bool commitChanges(); private slots: - void on_rewardType_activated(int index); + void on_addVisitInfo_clicked(); - void on_rewardList_activated(int index); + void on_removeVisitInfo_clicked(); - void on_buttonAdd_clicked(); + void on_selectMode_currentIndexChanged(int index); - void on_buttonRemove_clicked(); + void on_resetPeriod_valueChanged(int arg1); - void on_buttonClear_clicked(); + void on_visitInfoList_itemSelectionChanged(); - void on_rewardsTable_itemSelectionChanged(); + void on_visitInfoList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + + void on_rCreatureAdd_clicked(); + + void on_rCreatureRemove_clicked(); + + void on_lCreatureAdd_clicked(); + + void on_lCreatureRemove_clicked(); + + void on_castSpellCheck_toggled(bool checked); + + void on_bonusAdd_clicked(); + + void on_bonusRemove_clicked(); private: - void addReward(RewardType typeId, int listId, int amount); - QList getListForType(RewardType typeId); + + void saveCurrentVisitInfo(int index); + void loadCurrentVisitInfo(int index); + + void onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget); Ui::RewardsWidget *ui; - CGPandoraBox * pandora; - CGSeerHut * seerhut; - const CMap & map; - int rewards = 0; + CRewardableObject & object; + CMap & map; }; class RewardsDelegate : public QStyledItemDelegate { Q_OBJECT public: + RewardsDelegate(CMap &, CRewardableObject &); + using QStyledItemDelegate::QStyledItemDelegate; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; -}; -class RewardsPandoraDelegate : public RewardsDelegate -{ - Q_OBJECT -public: - RewardsPandoraDelegate(const CMap &, CGPandoraBox &); - QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - CGPandoraBox & pandora; - const CMap & map; -}; -class RewardsSeerhutDelegate : public RewardsDelegate -{ - Q_OBJECT -public: - RewardsSeerhutDelegate(const CMap &, CGSeerHut &); - - QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - private: - CGSeerHut & seerhut; - const CMap & map; + CRewardableObject & object; + CMap & map; }; diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index 32dbcfe7d..2ae3c6676 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -2,80 +2,1558 @@ RewardsWidget + + Qt::NonModal + 0 0 - 645 - 335 + 806 + 561 Rewards - - - - - Remove selected + + true + + + + 3 + + + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + 0 + + + + + + + + + Visit mode + + + + + + + + 1 + 0 + + + + + + + + + + + + Select mode + + + + + + + + 1 + 0 + + + + + + + + + + + + + On select text + + + + + + + Can refuse + + + + + + + Reset parameters + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Period + + + + + + + days + + + 99 + + + + + + + + + Reset visitors + + + + + + + Reset rewards + + + + + + + + + + + + Window type + + + + + + + + + + + + + + + 0 + 0 + + + Event info + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + Message to be displayed on granting of this reward + + + + + + + 0 + + + + Reward + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + -999 + + + 999 + + + + + + + % + + + -100 + + + 1000 + + + + + + + Overflow + + + + + + + % + + + 100 + + + 100 + + + + + + + + + + + Movement + + + + + + + -999 + + + 999 + + + + + + + % + + + -100 + + + 1000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remove object + + + + + + + + + Primary skills + + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + -99 + + + + + + + Defence + + + + + + + -99 + + + + + + + Spell power + + + + + + + -99 + + + + + + + Knowledge + + + + + + + -99 + + + + + + + + + + 0 + + + Qt::ElideNone + + + true + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + + + + + + + + Bonuses + + + + 3 + + + 3 + + + 3 + + + + + + + Duration + + + + + + + + + + Type + + + + + + + + + + Value + + + + + + + -999 + + + 999 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 150 + + + false + + + false + + + 24 + + + + Duration + + + + + Type + + + + + Value + + + + + + + + + Cast + + + + + + Cast an adventure map spell + + + + + + + + + Spell + + + + + + + + 0 + 0 + + + + + + + + + + + + Magic school level + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Limiter + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Day of week + + + + + + + + 120 + 0 + + + + + + + + Days passed + + + + + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + 999 + + + + + + + % + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Primary skills + + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + + + + Defence + + + + + + + + + + Spell power + + + + + + + + + + Knowledge + + + + + + + + + + + + + 0 + + + Qt::ElideNone + + + true + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + + + + + - - - - - 80 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - Delete all - - - - - - - Add or change - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - 3 - - - false - - - - - - - - - - - - diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp index 35ef4f8c4..b4f4c87d3 100644 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "townbulidingswidget.h" #include "ui_townbulidingswidget.h" -#include "../lib/CModHandler.h" #include "../lib/CGeneralTextHandler.h" std::string defaultBuildingIdConversion(BuildingID bId) @@ -131,7 +130,7 @@ QStandardItem * TownBulidingsWidget::addBuilding(const CTown & ctown, int bId, s for(int i = 0; i < model.rowCount(pindex); ++i) { QModelIndex index = model.index(i, 0, pindex); - if(building->upgrade == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) + if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) { parent = model.itemFromIndex(index); break; @@ -247,10 +246,6 @@ void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *mo { town.forbiddenBuildings = ed->getForbiddenBuildings(); town.builtBuildings = ed->getBuiltBuildings(); - - auto data = model->itemData(index); - model->setData(index, "dummy"); - model->setItemData(index, data); //dummy change to trigger signal } else { diff --git a/mapeditor/inspector/townbulidingswidget.ui b/mapeditor/inspector/townbulidingswidget.ui index 942c5d5f5..f9eece29d 100644 --- a/mapeditor/inspector/townbulidingswidget.ui +++ b/mapeditor/inspector/townbulidingswidget.ui @@ -2,6 +2,9 @@ TownBulidingsWidget + + Qt::NonModal + 0 diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp index ccf7bd629..a10129c14 100644 --- a/mapeditor/jsonutils.cpp +++ b/mapeditor/jsonutils.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "jsonutils.h" -#include "../lib/filesystem/FileStream.h" static QVariantMap JsonToMap(const JsonMap & json) { @@ -120,7 +119,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 156bcdeb4..aaf5b19f5 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -21,7 +21,6 @@ #include "../lib/VCMI_Lib.h" #include "../lib/logging/CBasicLogConfigurator.h" #include "../lib/CConfigHandler.h" -#include "../lib/CModHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/GameConstants.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" @@ -31,6 +30,7 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/MapFormat.h" +#include "../lib/modding/ModIncompatibility.h" #include "../lib/RoadHandler.h" #include "../lib/RiverHandler.h" #include "../lib/TerrainHandler.h" @@ -41,7 +41,8 @@ #include "windownewmap.h" #include "objectbrowser.h" #include "inspector/inspector.h" -#include "mapsettings.h" +#include "mapsettings/mapsettings.h" +#include "mapsettings/translations.h" #include "playersettings.h" #include "validator.h" @@ -67,6 +68,10 @@ QPixmap pixmapFromJson(const QJsonValue &val) void init() { loadDLLClasses(); + + Settings config = settings.write["session"]["editor"]; + config->Bool() = true; + logGlobal->info("Initializing VCMI_Lib"); } @@ -104,7 +109,7 @@ void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions) {"e", QCoreApplication::translate("main", "Extract original H3 archives into a separate folder.")}, {"s", QCoreApplication::translate("main", "From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's.")}, {"c", QCoreApplication::translate("main", "From an extracted archive, Converts single Images (found in Images folder) from .pcx to png.")}, - {"d", QCoreApplication::translate("main", "Delete original files, for the ones splitted / converted.")}, + {"d", QCoreApplication::translate("main", "Delete original files, for the ones split / converted.")}, }); parser.process(qApp->arguments()); @@ -181,7 +186,7 @@ MainWindow::MainWindow(QWidget* parent) : // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) -> bool { - if (CResourceHandler::get()->existsResource(ResourceID(filename))) + if (CResourceHandler::get()->existsResource(ResourcePath(filename))) return true; logGlobal->error("Error: %s was not found!", message); @@ -212,6 +217,7 @@ MainWindow::MainWindow(QWidget* parent) : ui->mapView->setController(&controller); ui->mapView->setOptimizationFlags(QGraphicsView::DontSavePainterState | QGraphicsView::DontAdjustForAntialiasing); connect(ui->mapView, &MapView::openObjectProperties, this, &MainWindow::loadInspector); + connect(ui->mapView, &MapView::currentCoordinates, this, &MainWindow::currentCoordinatesChanged); ui->minimapView->setScene(controller.miniScene(0)); ui->minimapView->setController(&controller); @@ -294,12 +300,15 @@ void MainWindow::initializeMap(bool isNew) ui->mapView->setScene(controller.scene(mapLevel)); ui->minimapView->setScene(controller.miniScene(mapLevel)); ui->minimapView->dimensions(); + if(initialScale.isValid()) + on_actionZoom_reset_triggered(); + initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); - setStatusMessage(QString("Scene objects: %1").arg(ui->mapView->scene()->items().size())); - //enable settings ui->actionMapSettings->setEnabled(true); ui->actionPlayers_settings->setEnabled(true); + ui->actionTranslations->setEnabled(true); + ui->actionLevel->setEnabled(controller.map()->twoLevel); //set minimal players count if(isNew) @@ -311,13 +320,13 @@ void MainWindow::initializeMap(bool isNew) onPlayersChanged(); } -bool MainWindow::openMap(const QString & filenameSelect) +std::unique_ptr MainWindow::openMapInternal(const QString & filenameSelect) { QFileInfo fi(filenameSelect); std::string fname = fi.fileName().toStdString(); std::string fdir = fi.dir().path().toStdString(); - ResourceID resId("MAPEDITOR/" + fname, EResType::MAP); + ResourcePath resId("MAPEDITOR/" + fname, EResType::MAP); //addFilesystem takes care about memory deallocation if case of failure, no memory leak here auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0); @@ -325,35 +334,40 @@ bool MainWindow::openMap(const QString & filenameSelect) CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem); if(!CResourceHandler::get("mapEditor")->existsResource(resId)) - { - QMessageBox::warning(this, tr("Failed to open map"), tr("Cannot open map from this folder")); - return false; - } + throw std::runtime_error("Cannot open map from this folder"); CMapService mapService; + if(auto header = mapService.loadMapHeader(resId)) + { + auto missingMods = CMapService::verifyMapHeaderMods(*header); + ModIncompatibility::ModListWithVersion modList; + for(const auto & m : missingMods) + modList.push_back({m.second.name, m.second.version.toString()}); + + if(!modList.empty()) + throw ModIncompatibility(modList); + + return mapService.loadMap(resId); + } + else + throw std::runtime_error("Corrupted map"); +} + +bool MainWindow::openMap(const QString & filenameSelect) +{ try { - if(auto header = mapService.loadMapHeader(resId)) - { - auto missingMods = CMapService::verifyMapHeaderMods(*header); - CModHandler::Incompatibility::ModList modList; - for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); - - if(!modList.empty()) - throw CModHandler::Incompatibility(std::move(modList)); - - controller.setMap(mapService.loadMap(resId)); - } + controller.setMap(openMapInternal(filenameSelect)); } - catch(const CModHandler::Incompatibility & e) + catch(const ModIncompatibility & e) { - QMessageBox::warning(this, "Mods requiered", e.what()); + assert(e.whatExcessive().empty()); + QMessageBox::warning(this, "Mods are required", QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) { - QMessageBox::critical(this, "Failed to open map", e.what()); + QMessageBox::critical(this, "Failed to open map", tr(e.what())); return false; } @@ -397,6 +411,8 @@ void MainWindow::saveMap() else QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found"); } + + Translations::cleanupRemovedItems(*controller.map()); CMapService mapService; try @@ -418,7 +434,7 @@ void MainWindow::on_actionSave_as_triggered() if(!controller.map()) return; - auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); + auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), lastSavingDir, tr("VCMI maps (*.vmap)")); if(filenameSelect.isNull()) return; @@ -427,6 +443,7 @@ void MainWindow::on_actionSave_as_triggered() return; filename = filenameSelect; + lastSavingDir = filenameSelect.remove(QUrl(filenameSelect).fileName()); saveMap(); } @@ -444,16 +461,14 @@ void MainWindow::on_actionSave_triggered() return; if(filename.isNull()) - { - auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); + on_actionSave_as_triggered(); + else + saveMap(); +} - if(filenameSelect.isNull()) - return; - - filename = filenameSelect; - } - - saveMap(); +void MainWindow::currentCoordinatesChanged(int x, int y) +{ + setStatusMessage(QString("x: %1 y: %2").arg(x).arg(y)); } void MainWindow::terrainButtonClicked(TerrainId terrain) @@ -512,13 +527,13 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust auto templ = templates[templateId]; //selecting file - const std::string & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile; + const AnimationPath & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile; //creating picture QPixmap preview(128, 128); preview.fill(QColor(255, 255, 255)); QPainter painter(&preview); - Animation animation(afile); + Animation animation(afile.getOriginalName()); animation.preload(); auto picture = animation.getImage(0); if(picture && picture->width() && picture->height()) @@ -528,19 +543,21 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust painter.scale(scale, scale); painter.drawImage(QPoint(0, 0), *picture); } - + + //create object to extract name + std::unique_ptr temporaryObj(factory->create(templ)); + QString translated = useCustomName ? QString::fromStdString(temporaryObj->getObjectName().c_str()) : subGroupName; + itemType->setText(translated); + //add parameters QJsonObject data{{"id", QJsonValue(ID)}, {"subid", QJsonValue(secondaryID)}, {"template", QJsonValue(templateId)}, - {"animationEditor", QString::fromStdString(templ->editorAnimationFile)}, - {"animation", QString::fromStdString(templ->animationFile)}, - {"preview", jsonFromPixmap(preview)}}; - - //create object to extract name - std::unique_ptr temporaryObj(factory->create(templ)); - QString translated = useCustomName ? tr(temporaryObj->getObjectName().c_str()) : subGroupName; - itemType->setText(translated); + {"animationEditor", QString::fromStdString(templ->editorAnimationFile.getOriginalName())}, + {"animation", QString::fromStdString(templ->animationFile.getOriginalName())}, + {"preview", jsonFromPixmap(preview)}, + {"typeName", QString::fromStdString(factory->getJsonKey())} + }; //do not have extra level if(singleTemplate) @@ -822,99 +839,12 @@ void MainWindow::changeBrushState(int idx) } -void MainWindow::on_toolBrush_clicked(bool checked) -{ - //ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolBrush2_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - //ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush2; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - - -void MainWindow::on_toolBrush4_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - //ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush4; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolArea_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - //ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Area; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolLasso_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - //ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Lasso; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - void MainWindow::on_actionErase_triggered() -{ - on_toolErase_clicked(); -} - -void MainWindow::on_toolErase_clicked() { if(controller.map()) { controller.commitObjectErase(mapLevel); } - ui->tabWidget->setCurrentIndex(0); } void MainWindow::preparePreview(const QModelIndex &index) @@ -931,16 +861,14 @@ void MainWindow::preparePreview(const QModelIndex &index) scenePreview->addPixmap(objPreview); } } + + ui->objectPreview->fitInView(scenePreview->itemsBoundingRect(), Qt::KeepAspectRatio); } void MainWindow::treeViewSelected(const QModelIndex & index, const QModelIndex & deselected) { - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); + ui->toolSelect->setChecked(true); ui->mapView->selectionTool = MapView::SelectionTool::None; preparePreview(index); @@ -990,13 +918,13 @@ void MainWindow::loadInspector(CGObjectInstance * obj, bool switchTab) { if(switchTab) ui->tabWidget->setCurrentIndex(1); - Inspector inspector(controller.map(), obj, ui->inspectorWidget); + Inspector inspector(controller, obj, ui->inspectorWidget); inspector.updateProperties(); } void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) { - if(!item->isSelected()) + if(!item->isSelected() && !(item->flags() & Qt::ItemIsUserCheckable)) return; int r = item->row(); @@ -1014,8 +942,8 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) auto param = tableWidget->item(r, c - 1)->text(); //set parameter - Inspector inspector(controller.map(), obj, tableWidget); - inspector.setProperty(param, item->text()); + Inspector inspector(controller, obj, tableWidget); + inspector.setProperty(param, item); controller.commitObjectChange(mapLevel); } @@ -1100,11 +1028,7 @@ void MainWindow::onSelectionMade(int level, bool anythingSelected) { if (level == mapLevel) { - auto info = QString::asprintf("Selection on layer %d: %s", level, anythingSelected ? "true" : "false"); - setStatusMessage(info); - ui->actionErase->setEnabled(anythingSelected); - ui->toolErase->setEnabled(anythingSelected); } } void MainWindow::displayStatus(const QString& message, int timeout /* = 2000 */) @@ -1175,9 +1099,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() } app = templates.front(); } - auto tiles = controller.mapHandler()->getTilesUnderObject(obj); obj->appearance = app; - controller.mapHandler()->invalidate(tiles); controller.mapHandler()->invalidate(obj); controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj); } @@ -1188,7 +1110,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() if(errors) - QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occured. %1 objects were not updated")).arg(errors)); + QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occurred. %1 objects were not updated")).arg(errors)); } @@ -1228,7 +1150,7 @@ void MainWindow::on_actionPaste_triggered() void MainWindow::on_actionExport_triggered() { - QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), QCoreApplication::applicationDirPath(), "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); + QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), lastSavingDir, "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); if(!fileName.isNull()) { QImage image(ui->mapView->scene()->sceneRect().size().toSize(), QImage::Format_RGB888); @@ -1238,3 +1160,181 @@ void MainWindow::on_actionExport_triggered() } } + +void MainWindow::on_actionTranslations_triggered() +{ + auto translationsDialog = new Translations(*controller.map(), this); + translationsDialog->show(); +} + +void MainWindow::on_actionh3m_converter_triggered() +{ + auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to convert"), + QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()), + tr("HoMM3 maps(*.h3m)")); + if(mapFiles.empty()) + return; + + auto saveDirectory = QFileDialog::getExistingDirectory(this, tr("Choose directory to save converted maps"), QCoreApplication::applicationDirPath()); + if(saveDirectory.isEmpty()) + return; + + try + { + for(auto & m : mapFiles) + { + CMapService mapService; + auto map = openMapInternal(m); + controller.repairMap(map.get()); + mapService.saveMap(map, (saveDirectory + '/' + QFileInfo(m).completeBaseName() + ".vmap").toStdString()); + } + QMessageBox::information(this, tr("Operation completed"), tr("Successfully converted %1 maps").arg(mapFiles.size())); + } + catch(const std::exception & e) + { + QMessageBox::critical(this, tr("Failed to convert the map. Abort operation"), tr(e.what())); + } +} + + +void MainWindow::on_actionLock_triggered() +{ + if(controller.map()) + { + if(controller.scene(mapLevel)->selectionObjectsView.getSelection().empty()) + { + for(auto obj : controller.map()->objects) + { + controller.scene(mapLevel)->selectionObjectsView.setLockObject(obj, true); + controller.scene(mapLevel)->objectsView.setLockObject(obj, true); + } + } + else + { + for(auto * obj : controller.scene(mapLevel)->selectionObjectsView.getSelection()) + { + controller.scene(mapLevel)->selectionObjectsView.setLockObject(obj, true); + controller.scene(mapLevel)->objectsView.setLockObject(obj, true); + } + controller.scene(mapLevel)->selectionObjectsView.clear(); + } + controller.scene(mapLevel)->objectsView.update(); + controller.scene(mapLevel)->selectionObjectsView.update(); + } +} + + +void MainWindow::on_actionUnlock_triggered() +{ + if(controller.map()) + { + controller.scene(mapLevel)->selectionObjectsView.unlockAll(); + controller.scene(mapLevel)->objectsView.unlockAll(); + } + controller.scene(mapLevel)->objectsView.update(); +} + + +void MainWindow::on_actionZoom_in_triggered() +{ + auto rect = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); + rect -= QMargins{32 + 1, 32 + 1, 32 + 2, 32 + 2}; //compensate bounding box + ui->mapView->fitInView(rect, Qt::KeepAspectRatioByExpanding); +} + + +void MainWindow::on_actionZoom_out_triggered() +{ + auto rect = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); + rect += QMargins{32 - 1, 32 - 1, 32 - 2, 32 - 2}; //compensate bounding box + ui->mapView->fitInView(rect, Qt::KeepAspectRatioByExpanding); +} + + +void MainWindow::on_actionZoom_reset_triggered() +{ + auto center = ui->mapView->mapToScene(ui->mapView->viewport()->geometry().center()); + ui->mapView->fitInView(initialScale, Qt::KeepAspectRatioByExpanding); + ui->mapView->centerOn(center); +} + + +void MainWindow::on_toolLine_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Line; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush2_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush2; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush4_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush4; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolLasso_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Lasso; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolArea_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Area; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolFill_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Fill; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolSelect_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::None; + ui->tabWidget->setCurrentIndex(0); + } +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index f509b0882..919267557 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -31,6 +31,8 @@ class MainWindow : public QMainWindow #ifdef ENABLE_QT_TRANSLATIONS QTranslator translator; #endif + + std::unique_ptr openMapInternal(const QString &); public: explicit MainWindow(QWidget *parent = nullptr); @@ -74,14 +76,9 @@ private slots: void on_actionGrid_triggered(bool checked); - void on_toolBrush_clicked(bool checked); - - void on_toolArea_clicked(bool checked); - void terrainButtonClicked(TerrainId terrain); void roadOrRiverButtonClicked(ui8 type, bool isRoad); - - void on_toolErase_clicked(); + void currentCoordinatesChanged(int x, int y); void on_terrainFilterCombo_currentIndexChanged(int index); @@ -89,12 +86,6 @@ private slots: void on_actionFill_triggered(); - void on_toolBrush2_clicked(bool checked); - - void on_toolBrush4_clicked(bool checked); - - void on_toolLasso_clicked(bool checked); - void on_inspectorWidget_itemChanged(QTableWidgetItem *item); void on_actionMapSettings_triggered(); @@ -117,6 +108,36 @@ private slots: void on_actionExport_triggered(); + void on_actionTranslations_triggered(); + + void on_actionh3m_converter_triggered(); + + void on_actionLock_triggered(); + + void on_actionUnlock_triggered(); + + void on_actionZoom_in_triggered(); + + void on_actionZoom_out_triggered(); + + void on_actionZoom_reset_triggered(); + + void on_toolLine_toggled(bool checked); + + void on_toolBrush2_toggled(bool checked); + + void on_toolBrush_toggled(bool checked); + + void on_toolBrush4_toggled(bool checked); + + void on_toolLasso_toggled(bool checked); + + void on_toolArea_toggled(bool checked); + + void on_toolFill_toggled(bool checked); + + void on_toolSelect_toggled(bool checked); + public slots: void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected); @@ -153,12 +174,13 @@ private: ObjectBrowserProxyModel * objectBrowser = nullptr; QGraphicsScene * scenePreview; - QString filename; + QString filename, lastSavingDir; bool unsaved = false; QStandardItemModel objectsModel; int mapLevel = 0; + QRectF initialScale; std::set catalog; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 7247dcf7c..bedb9c465 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -14,20 +14,20 @@ VCMI Map Editor - + - 2 + 0 - 2 + 0 - 2 + 0 - 2 + 0 - + @@ -51,7 +51,7 @@ 0 0 1024 - 22 + 37 @@ -63,6 +63,7 @@ + @@ -70,6 +71,7 @@ + @@ -84,6 +86,8 @@ + + @@ -92,6 +96,10 @@ + + + + @@ -140,6 +148,14 @@ + + + + + + + + @@ -156,10 +172,13 @@ - 192 + 524287 214 + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + Minimap @@ -200,12 +219,6 @@ 192 - - - 192 - 192 - - Qt::ScrollBarAlwaysOff @@ -219,7 +232,7 @@ - + 0 0 @@ -236,6 +249,9 @@ 524287 + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + Map Objects View @@ -244,7 +260,7 @@ - + 0 0 @@ -271,7 +287,7 @@ - 0 + 1 @@ -390,6 +406,9 @@ 2 + + true + false @@ -423,14 +442,11 @@ 0 - - - 128 - 496 - + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable - Terrains View + Tools 1 @@ -455,6 +471,384 @@ + + 3 + + + 3 + + + 3 + + + 3 + + + + + 0 + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-1.pngicons:brush-1.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-2.pngicons:brush-2.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-4.pngicons:brush-4.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-3.pngicons:brush-3.png + + + true + + + true + + + false + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-7.pngicons:brush-7.png + + + true + + + true + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-5.pngicons:brush-5.png + + + true + + + true + + + false + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-6.pngicons:brush-6.png + + + true + + + true + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-0.pngicons:brush-0.png + + + true + + + true + + + true + + + false + + + + + + + + + + + + 0 + 0 + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Painting + + + 1 + + + 0 @@ -467,267 +861,6 @@ 0 - - - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - Brush - - - - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-1.pngicons:brush-1.png - - - - 16 - 16 - - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-2.pngicons:brush-2.png - - - - 16 - 16 - - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-4.pngicons:brush-4.png - - - - 16 - 16 - - - - true - - - false - - - - - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-5.pngicons:brush-5.png - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-3.pngicons:brush-3.png - - - true - - - false - - - - - - - false - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:edit-clear.pngicons:edit-clear.png - - - false - - - false - - - - - - @@ -750,8 +883,8 @@ 0 0 - 128 - 251 + 256 + 120 @@ -793,8 +926,8 @@ 0 0 - 128 - 251 + 256 + 120 @@ -820,7 +953,11 @@ 0 - + + + 1 + + @@ -829,8 +966,8 @@ 0 0 - 128 - 251 + 256 + 120 @@ -856,12 +993,43 @@ 0 - + + + 1 + + + + + + + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + + + Preview + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + @@ -870,12 +1038,6 @@ 128 - - - 128 - 128 - - @@ -1234,6 +1396,95 @@ Export as... + + + false + + + + icons:translations.pngicons:translations.png + + + Translations + + + Ctrl+T + + + + + h3m converter + + + h3m converter + + + + + + icons:lock-closed.pngicons:lock-closed.png + + + Lock + + + Lock objects on map to avoid unnecessary changes + + + Ctrl+L + + + + + + icons:lock-open.pngicons:lock-open.png + + + Unlock + + + Unlock all objects on the map + + + Ctrl+Shift+L + + + + + + icons:zoom_plus.pngicons:zoom_plus.png + + + Zoom in + + + Ctrl+= + + + + + + icons:zoom_minus.pngicons:zoom_minus.png + + + Zoom out + + + Ctrl+- + + + + + + icons:zoom_zero.pngicons:zoom_zero.png + + + Zoom reset + + + Ctrl+Shift+= + + diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index a4c52d2d6..b26bf03c1 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -19,11 +19,12 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/ObstacleProxy.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/CModInfo.h" #include "../lib/TerrainHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" #include "../lib/serializer/CMemorySerializer.h" #include "mapview.h" #include "scenelayer.h" @@ -85,26 +86,28 @@ MinimapScene * MapController::miniScene(int level) void MapController::repairMap() { - //there might be extra skills, arts and spells not imported from map - if(VLC->skillh->getDefaultAllowed().size() > map()->allowedAbilities.size()) - { - map()->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size()); - } - if(VLC->arth->getDefaultAllowed().size() > map()->allowedArtifact.size()) - { - map()->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size()); - } - if(VLC->spellh->getDefaultAllowed().size() > map()->allowedSpells.size()) - { - map()->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size()); - } - if(VLC->heroh->getDefaultAllowed().size() > map()->allowedHeroes.size()) - { - map()->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); - } + repairMap(map()); +} + +void MapController::repairMap(CMap * map) const +{ + if(!map) + return; + + //make sure events/rumors has name to have proper identifiers + int emptyNameId = 1; + for(auto & e : map->events) + if(e.name.empty()) + e.name = "event_" + std::to_string(emptyNameId++); + emptyNameId = 1; + for(auto & e : map->rumors) + if(e.name.empty()) + e.name = "rumor_" + std::to_string(emptyNameId++); //fix owners for objects - for(auto obj : _map->objects) + auto allImpactedObjects(map->objects); + 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())) @@ -128,7 +131,7 @@ void MapController::repairMap() //fix hero instance if(auto * nih = dynamic_cast(obj.get())) { - map()->allowedHeroes.at(nih->subID) = true; + map->allowedHeroes.insert(nih->getHeroType()); auto type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); //TODO: find a way to get proper type name @@ -144,7 +147,8 @@ void MapController::repairMap() nih->subID = 0; } - nih->type = type; + if(obj->ID != Obj::RANDOM_HERO) + nih->type = type; if(nih->ID == Obj::HERO) //not prison nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); @@ -152,15 +156,16 @@ void MapController::repairMap() if(nih->spellbookContainsSpell(SpellID::PRESET)) { nih->removeSpellFromSpellbook(SpellID::PRESET); - } - else - { for(auto spellID : type->spells) nih->addSpellToSpellbook(spellID); } - //fix portrait - if(nih->portrait < 0 || nih->portrait == 255) - nih->portrait = type->imageIndex; + if(nih->spellbookContainsSpell(SpellID::SPELLBOOK_PRESET)) + { + nih->removeSpellFromSpellbook(SpellID::SPELLBOOK_PRESET); + if(!nih->getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) + nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + } + } //fix town instance if(auto * tnh = dynamic_cast(obj.get())) @@ -194,7 +199,7 @@ void MapController::repairMap() art->storedArtifact = a; } else - map()->allowedArtifact.at(art->subID) = true; + map->allowedArtifact.insert(art->getArtifact()); } } } @@ -327,10 +332,10 @@ void MapController::commitObjectErase(int level) return; } - for (auto obj : selectedObjects) + for (auto & obj : selectedObjects) { //invalidate tiles under objects - _mapHandler->invalidate(_mapHandler->getTilesUnderObject(obj)); + _mapHandler->removeObject(obj); _scenes[level]->objectsView.setDirty(obj); } @@ -431,14 +436,16 @@ void MapController::commitObstacleFill(int level) for(auto & sel : _obstaclePainters) { - sel.second->placeObstacles(CRandomGenerator::getDefault()); + for(auto * o : sel.second->placeObstacles(CRandomGenerator::getDefault())) + { + _mapHandler->invalidate(o); + _scenes[level]->objectsView.setDirty(o); + } } - - _mapHandler->invalidateObjects(); _scenes[level]->selectionTerrainView.clear(); _scenes[level]->selectionTerrainView.draw(); - _scenes[level]->objectsView.draw(false); //TODO: enable smart invalidation (setDirty) + _scenes[level]->objectsView.draw(); _scenes[level]->passabilityView.update(); _miniscenes[level]->updateViews(); @@ -477,10 +484,8 @@ void MapController::commitObjectShift(int level) pos.z = level; pos.x += shift.x(); pos.y += shift.y(); - auto prevPositions = _mapHandler->getTilesUnderObject(obj); _scenes[level]->objectsView.setDirty(obj); //set dirty before movement _map->getEditManager()->moveObject(obj, pos); - _mapHandler->invalidate(prevPositions); _mapHandler->invalidate(obj); } } @@ -586,7 +591,7 @@ ModCompatibilityInfo MapController::modAssessmentAll() auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } } return result; @@ -600,10 +605,10 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) if(obj->ID == Obj::HERO) continue; //stub! - auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + auto handler = obj->getObjectHandler(); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } //TODO: terrains? return result; diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 1c602c0a0..7b8a246eb 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,10 +13,10 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/CModVersion.h" +#include "../lib/modding/CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END @@ -30,8 +30,9 @@ public: ~MapController(); void setMap(std::unique_ptr); - void initObstaclePainters(CMap* map); + void initObstaclePainters(CMap * map); + void repairMap(CMap * map) const; void repairMap(); const std::unique_ptr & getMapUniquePtr() const; //to be used for map saving diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 17a43e0d5..8d4ff8b30 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -20,20 +20,19 @@ #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" -#include "../lib/CModHandler.h" #include "../lib/GameConstants.h" #include "../lib/JsonDetail.h" const int tileSize = 32; -static bool objectBlitOrderSorter(const TileObject & a, const TileObject & b) +static bool objectBlitOrderSorter(const ObjectRect & a, const ObjectRect & b) { return MapHandler::compareObjectBlitOrder(a.obj, b.obj); } int MapHandler::index(int x, int y, int z) const { - return z * (sizes.x * sizes.y) + y * sizes.x + x; + return z * (map->width * map->height) + y * map->width + x; } int MapHandler::index(const int3 & p) const @@ -49,14 +48,7 @@ MapHandler::MapHandler() void MapHandler::reset(const CMap * Map) { - ttiles.clear(); map = Map; - - //sizes of terrain - sizes.x = map->width; - sizes.y = map->height; - sizes.z = map->twoLevel ? 2 : 1; - initObjectRects(); logGlobal->info("\tMaking object rects"); } @@ -82,15 +74,15 @@ void MapHandler::initTerrainGraphics() std::map riverFiles; for(const auto & terrain : VLC->terrainTypeHandler->objects) { - terrainFiles[terrain->getJsonKey()] = terrain->tilesFilename; + terrainFiles[terrain->getJsonKey()] = terrain->tilesFilename.getName(); } for(const auto & river : VLC->riverTypeHandler->objects) { - riverFiles[river->getJsonKey()] = river->tilesFilename; + riverFiles[river->getJsonKey()] = river->tilesFilename.getName(); } for(const auto & road : VLC->roadTypeHandler->objects) { - roadFiles[road->getJsonKey()] = road->tilesFilename; + roadFiles[road->getJsonKey()] = road->tilesFilename.getName(); } loadFlipped(terrainAnimations, terrainImages, terrainFiles); @@ -177,67 +169,101 @@ void setPlayerColor(QImage * sur, PlayerColor player) logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); } -void MapHandler::initObjectRects() +std::shared_ptr MapHandler::getObjectImage(const CGObjectInstance * obj) { - ttiles.resize(sizes.x * sizes.y * sizes.z); - - //initializing objects / rects - for(const CGObjectInstance * elem : map->objects) + if( !obj + || (obj->ID==Obj::HERO && static_cast(obj)->inTownGarrison) //garrisoned hero + || (obj->ID==Obj::BOAT && static_cast(obj)->hero)) //boat with hero (hero graphics is used) { - CGObjectInstance *obj = const_cast(elem); - if( !obj - || (obj->ID==Obj::HERO && static_cast(obj)->inTownGarrison) //garrisoned hero - || (obj->ID==Obj::BOAT && static_cast(obj)->hero)) //boat with hero (hero graphics is used) + return nullptr; + } + + std::shared_ptr animation = graphics->getAnimation(obj); + + //no animation at all + if(!animation) + return nullptr; + + //empty animation + if(animation->size(0) == 0) + return nullptr; + + auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); + if(!image) + { + //workaround for prisons + image = animation->getImage(0, 0); + } + + return image; +} + +std::set MapHandler::removeObject(const CGObjectInstance *object) +{ + std::set result = tilesCache[object]; + for(auto & t : result) + { + auto & objects = getObjects(t); + for(auto iter = objects.begin(); iter != objects.end(); ++iter) { - continue; - } - - std::shared_ptr animation = graphics->getAnimation(obj); - - //no animation at all - if(!animation) - continue; - - //empty animation - if(animation->size(0) == 0) - continue; - - auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); - if(!image) - { - //workaround for prisons - image = animation->getImage(0, 0); - if(!image) - continue; - } - - - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) + if(iter->obj == object) { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - QRect cr(image->width() - fx * tileSize - tileSize, - image->height() - fy * tileSize - tileSize, - image->width(), - image->height()); - - TileObject toAdd(obj, cr); - - if( map->isInTheMap(currTile) && // within map - cr.x() + cr.width() > 0 && // image has data on this tile - cr.y() + cr.height() > 0) - { - ttiles[index(currTile)].push_back(toAdd); - } + objects.erase(iter); + break; } } } - for(auto & tt : ttiles) + tilesCache.erase(object); + return result; +} + +std::set MapHandler::addObject(const CGObjectInstance * object) +{ + auto image = getObjectImage(object); + if(!image) + return std::set{}; + + for(int fx = 0; fx < object->getWidth(); ++fx) { - stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter); + for(int fy = 0; fy < object->getHeight(); ++fy) + { + int3 currTile(object->pos.x - fx, object->pos.y - fy, object->pos.z); + QRect cr(image->width() - fx * tileSize - tileSize, + image->height() - fy * tileSize - tileSize, + tileSize, + tileSize); + + if( map->isInTheMap(currTile) && // within map + cr.x() + cr.width() > 0 && // image has data on this tile + cr.y() + cr.height() > 0) + { + getObjects(currTile).emplace_back(object, cr); + tilesCache[object].insert(currTile); + } + } } + + return tilesCache[object]; +} + +void MapHandler::initObjectRects() +{ + tileObjects.clear(); + tilesCache.clear(); + if(!map) + return; + + tileObjects.resize(map->width * map->height * (map->twoLevel ? 2 : 1)); + + //initializing objects / rects + for(const CGObjectInstance * elem : map->objects) + { + addObject(elem); + } + + for(auto & tt : tileObjects) + stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter); } bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) @@ -266,13 +292,13 @@ bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObje return false; } -TileObject::TileObject(CGObjectInstance * obj_, QRect rect_) +ObjectRect::ObjectRect(const CGObjectInstance * obj_, QRect rect_) : obj(obj_), rect(rect_) { } -TileObject::~TileObject() +ObjectRect::~ObjectRect() { } @@ -296,35 +322,43 @@ std::shared_ptr MapHandler::findFlagBitmapInternal(std::shared_ptrgetImage((anim / 4) % groupSize, group); } -MapHandler::AnimBitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const +MapHandler::BitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const { if(!obj) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); // normal object std::shared_ptr animation = graphics->getAnimation(obj); size_t groupSize = animation->size(group); if(groupSize == 0) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); animation->playerColored(obj->tempOwner); auto bitmap = animation->getImage(anim % groupSize, group); if(!bitmap) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); setPlayerColor(bitmap.get(), obj->tempOwner); - return MapHandler::AnimBitmapHolder(bitmap); + return MapHandler::BitmapHolder(bitmap); } -std::vector & MapHandler::getObjects(int x, int y, int z) +std::vector & MapHandler::getObjects(const int3 & tile) { - return ttiles[index(x, y, z)]; + return tileObjects[index(tile)]; } -void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) +std::vector & MapHandler::getObjects(int x, int y, int z) { + return tileObjects[index(x, y, z)]; +} + +void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked) +{ + painter.setRenderHint(QPainter::Antialiasing, false); + painter.setRenderHint(QPainter::SmoothPixmapTransform, false); + for(auto & object : getObjects(x, y, z)) { const CGObjectInstance * obj = object.obj; @@ -344,8 +378,15 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) { auto pos = obj->getPosition(); - painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect); - + painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect, Qt::AutoColor | Qt::NoOpaqueDetection); + + if(locked.count(obj)) + { + painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); + painter.fillRect(x * tileSize, y * tileSize, object.rect.width(), object.rect.height(), Qt::Dense4Pattern); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + if(objData.flagBitmap) { if(x == pos.x && y == pos.y) @@ -355,36 +396,6 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) } } -void MapHandler::drawObject(QPainter & painter, const TileObject & object) -{ - const CGObjectInstance * obj = object.obj; - if (!obj) - { - logGlobal->error("Stray map object that isn't fading"); - return; - } - - uint8_t animationFrame = 0; - - auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0); - if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer()) - objData.flagBitmap = findFlagBitmap(dynamic_cast(obj), 0, obj->tempOwner, 0); - - if (objData.objBitmap) - { - auto pos = obj->getPosition(); - - painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.objBitmap); - - if (objData.flagBitmap) - { - if(object.rect.x() == pos.x && object.rect.y() == pos.y) - painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.flagBitmap); - } - } -} - - void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y) { if (!obj) @@ -401,10 +412,10 @@ void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, if (objData.objBitmap) { - painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.objBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.objBitmap); if (objData.flagBitmap) - painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.flagBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.flagBitmap); } } @@ -420,7 +431,7 @@ QRgb MapHandler::getTileColor(int x, int y, int z) if(player == PlayerColor::NEUTRAL) return graphics->neutralColor; else - if (player < PlayerColor::PLAYER_LIMIT) + if (player.isValidPlayer()) return graphics->playerColors[player.getNum()]; } @@ -441,107 +452,19 @@ void MapHandler::drawMinimapTile(QPainter & painter, int x, int y, int z) painter.drawPoint(x, y); } -void MapHandler::invalidate(int x, int y, int z) +std::set MapHandler::invalidate(const CGObjectInstance * obj) { - auto & objects = getObjects(x, y, z); + auto t1 = removeObject(obj); + auto t2 = addObject(obj); + t1.insert(t2.begin(), t2.end()); - for(auto obj = objects.begin(); obj != objects.end();) - { - //object was removed - if(std::find(map->objects.begin(), map->objects.end(), obj->obj) == map->objects.end()) - { - obj = objects.erase(obj); - continue; - } - - //object was moved - auto & pos = obj->obj->pos; - if(pos.z != z || pos.x < x || pos.y < y || pos.x - obj->obj->getWidth() >= x || pos.y - obj->obj->getHeight() >= y) - { - obj = objects.erase(obj); - continue; - } - - ++obj; - } + for(auto & tt : t2) + stable_sort(tileObjects[index(tt)].begin(), tileObjects[index(tt)].end(), objectBlitOrderSorter); - stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); -} - -void MapHandler::invalidate(CGObjectInstance * obj) -{ - std::shared_ptr animation = graphics->getAnimation(obj); - - //no animation at all or empty animation - if(!animation || animation->size(0) == 0) - return; - - auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); - if(!image) - return; - - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) - { - //object presented on the tile - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - QRect cr(image->width() - fx * tileSize - tileSize, image->height() - fy * tileSize - tileSize, image->width(), image->height()); - - if( map->isInTheMap(currTile) && // within map - cr.x() + cr.width() > 0 && // image has data on this tile - cr.y() + cr.height() > 0) - { - auto & objects = ttiles[index(currTile)]; - bool found = false; - for(auto & o : objects) - { - if(o.obj == obj) - { - o.rect = cr; - found = true; - break; - } - } - if(!found) - objects.emplace_back(obj, cr); - - stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); - } - } - } -} - -std::vector MapHandler::getTilesUnderObject(CGObjectInstance * obj) const -{ - std::vector result; - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) - { - //object presented on the tile - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - if(map->isInTheMap(currTile)) // within map - { - result.push_back(currTile); - } - } - } - return result; + return t1; } void MapHandler::invalidateObjects() { - for(auto obj : map->objects) - { - invalidate(obj); - } -} - -void MapHandler::invalidate(const std::vector & tiles) -{ - for(auto & currTile : tiles) - { - invalidate(currTile.x, currTile.y, currTile.z); - } + initObjectRects(); } diff --git a/mapeditor/maphandler.h b/mapeditor/maphandler.h index 2372f29f1..76c7cfe41 100644 --- a/mapeditor/maphandler.h +++ b/mapeditor/maphandler.h @@ -29,26 +29,26 @@ class PlayerColor; VCMI_LIB_NAMESPACE_END -struct TileObject +struct ObjectRect { - CGObjectInstance *obj; + const CGObjectInstance * obj; QRect rect; - TileObject(CGObjectInstance *obj_, QRect rect_); - ~TileObject(); + ObjectRect(const CGObjectInstance * obj_, QRect rect_); + ~ObjectRect(); }; -using TileObjects = std::vector; //pointers to objects being on this tile with rects to be easier to blit this tile on screen +using TileObjects = std::vector; //pointers to objects being on this tile with rects to be easier to blit this tile on screen class MapHandler { public: - struct AnimBitmapHolder + struct BitmapHolder { std::shared_ptr objBitmap; // main object bitmap std::shared_ptr flagBitmap; // flag bitmap for the object (probably only for heroes and boats with heroes) - AnimBitmapHolder(std::shared_ptr objBitmap_ = nullptr, std::shared_ptr flagBitmap_ = nullptr) + BitmapHolder(std::shared_ptr objBitmap_ = nullptr, std::shared_ptr flagBitmap_ = nullptr) : objBitmap(objBitmap_), flagBitmap(flagBitmap_) {} @@ -61,7 +61,7 @@ private: std::shared_ptr findFlagBitmapInternal(std::shared_ptr animation, int anim, int group, ui8 dir, bool moving) const; std::shared_ptr findFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor color, int group) const; - AnimBitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const; + BitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const; //FIXME: unique_ptr should be enough, but fails to compile in MSVS 2013 typedef std::map> TFlippedAnimations; //[type, rotation] @@ -76,26 +76,22 @@ private: TFlippedAnimations riverAnimations;//[river type, rotation] TFlippedCache riverImages;//[river type, view type, rotation] - std::vector ttiles; //informations about map tiles - int3 sizes; //map size (x = width, y = height, z = number of levels) - const CMap * map; - - enum class EMapCacheType : char - { - TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST - }; + std::vector tileObjects; //informations about map tiles + std::map> tilesCache; //set of tiles beloging to object + + const CMap * map = nullptr; void initObjectRects(); void initTerrainGraphics(); QRgb getTileColor(int x, int y, int z); + + std::shared_ptr getObjectImage(const CGObjectInstance * obj); public: MapHandler(); ~MapHandler() = default; void reset(const CMap * Map); - - void updateWater(); void drawTerrainTile(QPainter & painter, int x, int y, int z); /// draws a river segment on current tile @@ -103,17 +99,21 @@ public: /// draws a road segment on current tile void drawRoad(QPainter & painter, int x, int y, int z); - void invalidate(int x, int y, int z); //invalidates all objects in particular tile - void invalidate(CGObjectInstance *); //invalidates object rects - void invalidate(const std::vector &); //invalidates all tiles + std::set invalidate(const CGObjectInstance *); //invalidates object rects void invalidateObjects(); //invalidates all objects on the map - std::vector getTilesUnderObject(CGObjectInstance *) const; + const std::set & getTilesUnderObject(const CGObjectInstance *) const; + + //get objects at position + std::vector & getObjects(const int3 & tile); + std::vector & getObjects(int x, int y, int z); + + //returns set of tiles to draw + std::set removeObject(const CGObjectInstance * object); + std::set addObject(const CGObjectInstance * object); /// draws all objects on current tile (higher-level logic, unlike other draw*** methods) - void drawObjects(QPainter & painter, int x, int y, int z); - void drawObject(QPainter & painter, const TileObject & object); + void drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked); void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y); - std::vector & getObjects(int x, int y, int z); void drawMinimapTile(QPainter & painter, int x, int y, int z); diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp deleted file mode 100644 index 42041f755..000000000 --- a/mapeditor/mapsettings.cpp +++ /dev/null @@ -1,1018 +0,0 @@ -/* - * mapsettings.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 "mapsettings.h" -#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/CGeneralTextHandler.h" -#include "../lib/CModHandler.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGCreature.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/StringConstants.h" -#include "inspector/townbulidingswidget.h" //to convert BuildingID to string - -//parses date for lose condition (1m 1w 1d) -int expiredDate(const QString & date) -{ - int result = 0; - for(auto component : date.split(" ")) - { - int days = component.left(component.lastIndexOf('d')).toInt(); - int weeks = component.left(component.lastIndexOf('w')).toInt(); - int months = component.left(component.lastIndexOf('m')).toInt(); - result += days > 0 ? days - 1 : 0; - result += (weeks > 0 ? weeks - 1 : 0) * 7; - result += (months > 0 ? months - 1 : 0) * 28; - } - return result; -} - -QString expiredDate(int date) -{ - QString result; - int m = date / 28; - int w = (date % 28) / 7; - int d = date % 7; - if(m) - result += QString::number(m) + "m"; - if(w) - { - if(!result.isEmpty()) - result += " "; - result += QString::number(w) + "w"; - } - if(d) - { - if(!result.isEmpty()) - result += " "; - result += QString::number(d) + "d"; - } - return result; -} - -int3 posFromJson(const JsonNode & json) -{ - return int3(json.Vector()[0].Integer(), json.Vector()[1].Integer(), json.Vector()[2].Integer()); -} - -std::vector linearJsonArray(const JsonNode & json) -{ - std::vector result; - if(json.getType() == JsonNode::JsonType::DATA_STRUCT) - result.push_back(json); - if(json.getType() == JsonNode::JsonType::DATA_VECTOR) - { - for(auto & node : json.Vector()) - { - auto subvector = linearJsonArray(node); - result.insert(result.end(), subvector.begin(), subvector.end()); - } - } - return result; -} - -void traverseNode(QTreeWidgetItem * item, std::function action) -{ - // Do something with item - action(item); - for (int i = 0; i < item->childCount(); ++i) - traverseNode(item->child(i), action); -} - -MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : - QDialog(parent), - ui(new Ui::MapSettings), - controller(ctrl) -{ - ui->setupUi(this); - - assert(controller.map()); - - ui->mapNameEdit->setText(tr(controller.map()->name.c_str())); - ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str())); - ui->heroLevelLimit->setValue(controller.map()->levelLimit); - ui->heroLevelLimitCheck->setChecked(controller.map()->levelLimit); - - show(); - - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); - ui->listAbilities->addItem(item); - } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedSpells[i] ? Qt::Checked : Qt::Unchecked); - ui->listSpells->addItem(item); - } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); - ui->listArts->addItem(item); - } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); - ui->listHeroes->addItem(item); - } - - //set difficulty - switch(controller.map()->difficulty) - { - case 0: - ui->diffRadio1->setChecked(true); - break; - - case 1: - ui->diffRadio2->setChecked(true); - break; - - case 2: - ui->diffRadio3->setChecked(true); - break; - - case 3: - ui->diffRadio4->setChecked(true); - break; - - case 4: - ui->diffRadio5->setChecked(true); - break; - }; - - //victory & loss messages - ui->victoryMessageEdit->setText(QString::fromStdString(controller.map()->victoryMessage.toString())); - ui->defeatMessageEdit->setText(QString::fromStdString(controller.map()->defeatMessage.toString())); - - //victory & loss conditions - const std::array conditionStringsWin = { - QT_TR_NOOP("No special victory"), - QT_TR_NOOP("Capture artifact"), - QT_TR_NOOP("Hire creatures"), - QT_TR_NOOP("Accumulate resources"), - QT_TR_NOOP("Construct building"), - QT_TR_NOOP("Capture town"), - QT_TR_NOOP("Defeat hero"), - QT_TR_NOOP("Transport artifact") - }; - const std::array conditionStringsLose = { - QT_TR_NOOP("No special loss"), - QT_TR_NOOP("Lose castle"), - QT_TR_NOOP("Lose hero"), - QT_TR_NOOP("Time expired"), - QT_TR_NOOP("Days without town") - }; - - for(auto & s : conditionStringsWin) - { - ui->victoryComboBox->addItem(QString::fromStdString(s)); - } - ui->standardVictoryCheck->setChecked(false); - ui->onlyForHumansCheck->setChecked(false); - - for(auto & s : conditionStringsLose) - { - ui->loseComboBox->addItem(QString::fromStdString(s)); - } - ui->standardLoseCheck->setChecked(false); - - auto conditionToJson = [](const EventCondition & event) -> JsonNode - { - JsonNode result; - result["condition"].Integer() = event.condition; - result["value"].Integer() = event.value; - result["objectType"].Integer() = event.objectType; - result["objectSubytype"].Integer() = event.objectSubtype; - result["objectInstanceName"].String() = event.objectInstanceName; - result["metaType"].Integer() = (ui8)event.metaType; - { - auto & position = result["position"].Vector(); - position.resize(3); - position[0].Float() = event.position.x; - position[1].Float() = event.position.y; - position[2].Float() = event.position.z; - } - return result; - }; - - for(auto & ev : controller.map()->triggeredEvents) - { - if(ev.effect.type == EventEffect::VICTORY) - { - if(ev.identifier == "standardVictory") - ui->standardVictoryCheck->setChecked(true); - - if(ev.identifier == "specialVictory") - { - auto readjson = ev.trigger.toJson(conditionToJson); - auto linearNodes = linearJsonArray(readjson); - - for(auto & json : linearNodes) - { - switch(json["condition"].Integer()) - { - case EventCondition::HAVE_ARTIFACT: { - ui->victoryComboBox->setCurrentIndex(1); - assert(victoryTypeWidget); - victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); - break; - } - - case EventCondition::HAVE_CREATURES: { - ui->victoryComboBox->setCurrentIndex(2); - assert(victoryTypeWidget); - assert(victoryValueWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - victoryValueWidget->setText(QString::number(json["value"].Integer())); - break; - } - - case EventCondition::HAVE_RESOURCES: { - ui->victoryComboBox->setCurrentIndex(3); - assert(victoryTypeWidget); - assert(victoryValueWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - victoryValueWidget->setText(QString::number(json["value"].Integer())); - break; - } - - case EventCondition::HAVE_BUILDING: { - ui->victoryComboBox->setCurrentIndex(4); - assert(victoryTypeWidget); - assert(victorySelectWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victorySelectWidget->findData(townIdx); - victorySelectWidget->setCurrentIndex(idx); - } - break; - } - - case EventCondition::CONTROL: { - ui->victoryComboBox->setCurrentIndex(5); - assert(victoryTypeWidget); - if(json["objectType"].Integer() == Obj::TOWN) - { - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victoryTypeWidget->findData(townIdx); - victoryTypeWidget->setCurrentIndex(idx); - } - } - //TODO: support control other objects (dwellings, mines) - break; - } - - case EventCondition::DESTROY: { - ui->victoryComboBox->setCurrentIndex(6); - assert(victoryTypeWidget); - if(json["objectType"].Integer() == Obj::HERO) - { - int heroIdx = getObjectByPos(posFromJson(json["position"])); - if(heroIdx >= 0) - { - auto idx = victoryTypeWidget->findData(heroIdx); - victoryTypeWidget->setCurrentIndex(idx); - } - } - //TODO: support control other objects (monsters) - break; - } - - case EventCondition::TRANSPORT: { - ui->victoryComboBox->setCurrentIndex(7); - assert(victoryTypeWidget); - assert(victorySelectWidget); - victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victorySelectWidget->findData(townIdx); - victorySelectWidget->setCurrentIndex(idx); - } - break; - } - - case EventCondition::IS_HUMAN: { - ui->onlyForHumansCheck->setChecked(true); - break; - } - }; - } - } - } - - if(ev.effect.type == EventEffect::DEFEAT) - { - if(ev.identifier == "standardDefeat") - ui->standardLoseCheck->setChecked(true); - - if(ev.identifier == "specialDefeat") - { - auto readjson = ev.trigger.toJson(conditionToJson); - auto linearNodes = linearJsonArray(readjson); - - for(auto & json : linearNodes) - { - switch(json["condition"].Integer()) - { - case EventCondition::CONTROL: { - if(json["objectType"].Integer() == Obj::TOWN) - { - ui->loseComboBox->setCurrentIndex(1); - assert(loseTypeWidget); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = loseTypeWidget->findData(townIdx); - loseTypeWidget->setCurrentIndex(idx); - } - } - if(json["objectType"].Integer() == Obj::HERO) - { - ui->loseComboBox->setCurrentIndex(2); - assert(loseTypeWidget); - int heroIdx = getObjectByPos(posFromJson(json["position"])); - if(heroIdx >= 0) - { - auto idx = loseTypeWidget->findData(heroIdx); - loseTypeWidget->setCurrentIndex(idx); - } - } - - break; - } - - case EventCondition::DAYS_PASSED: { - ui->loseComboBox->setCurrentIndex(3); - assert(loseValueWidget); - loseValueWidget->setText(expiredDate(json["value"].Integer())); - break; - } - - case EventCondition::DAYS_WITHOUT_TOWN: { - ui->loseComboBox->setCurrentIndex(4); - assert(loseValueWidget); - loseValueWidget->setText(QString::number(json["value"].Integer())); - break; - - case EventCondition::IS_HUMAN: - break; //ignore because always applicable for defeat conditions - } - - }; - } - } - } - } - - //mods management - //collect all active mods - QMap addedMods; - QSet modsToProcess; - ui->treeMods->blockSignals(true); - - auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) - { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); - item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, controller.map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); - //set parent check - if(parent && item->checkState(0) == Qt::Checked) - parent->setCheckState(0, Qt::Checked); - return item; - }; - - for(const auto & modName : VLC->modh->getActiveMods()) - { - QString qmodName = QString::fromStdString(modName); - if(qmodName.split(".").size() == 1) - { - const auto & modInfo = VLC->modh->getModInfo(modName); - addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo); - ui->treeMods->addTopLevelItem(addedMods[qmodName]); - } - else - { - modsToProcess.insert(qmodName); - } - } - - for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();) - { - auto qmodName = *qmodIter; - auto pieces = qmodName.split("."); - assert(pieces.size() > 1); - - QString qs; - for(int i = 0; i < pieces.size() - 1; ++i) - qs += pieces[i]; - - if(addedMods.count(qs)) - { - const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString()); - addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo); - modsToProcess.erase(qmodIter); - qmodIter = modsToProcess.begin(); - } - else - ++qmodIter; - } - ui->treeMods->blockSignals(false); -} - -MapSettings::~MapSettings() -{ - delete ui; -} - -std::string MapSettings::getTownName(int townObjectIdx) -{ - std::string name; - if(auto town = dynamic_cast(controller.map()->objects[townObjectIdx].get())) - { - auto * ctown = town->town; - if(!ctown) - ctown = VLC->townh->randomTown; - - name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; - } - return name; -} - -std::string MapSettings::getHeroName(int townObjectIdx) -{ - std::string name; - if(auto hero = dynamic_cast(controller.map()->objects[townObjectIdx].get())) - { - name = hero->getNameTranslated(); - } - return name; -} - -std::string MapSettings::getMonsterName(int monsterObjectIdx) -{ - std::string name; - [[maybe_unused]] auto monster = dynamic_cast(controller.map()->objects[monsterObjectIdx].get()); - if(monster) - { - //TODO: get proper name - //name = hero->name; - } - return name; -} - -void MapSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) -{ - //Mod management - auto widgetAction = [&](QTreeWidgetItem * item) - { - auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked); - }; - - for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) - { - QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); - traverseNode(item, widgetAction); - } -} - -void MapSettings::on_pushButton_clicked() -{ - controller.map()->name = ui->mapNameEdit->text().toStdString(); - controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); - if(ui->heroLevelLimitCheck->isChecked()) - controller.map()->levelLimit = ui->heroLevelLimit->value(); - else - controller.map()->levelLimit = 0; - controller.commitChangeWithoutRedraw(); - - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) - { - auto * item = ui->listAbilities->item(i); - controller.map()->allowedAbilities[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) - { - auto * item = ui->listSpells->item(i); - controller.map()->allowedSpells[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - { - auto * item = ui->listArts->item(i); - controller.map()->allowedArtifact[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) - { - auto * item = ui->listHeroes->item(i); - controller.map()->allowedHeroes[i] = item->checkState() == Qt::Checked; - } - - //set difficulty - if(ui->diffRadio1->isChecked()) controller.map()->difficulty = 0; - if(ui->diffRadio2->isChecked()) controller.map()->difficulty = 1; - if(ui->diffRadio3->isChecked()) controller.map()->difficulty = 2; - if(ui->diffRadio4->isChecked()) controller.map()->difficulty = 3; - if(ui->diffRadio5->isChecked()) controller.map()->difficulty = 4; - - //victory & loss messages - - controller.map()->victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); - controller.map()->defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); - - //victory & loss conditions - EventCondition victoryCondition(EventCondition::STANDARD_WIN); - EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); - defeatCondition.value = 7; - - //Victory condition - defeat all - TriggeredEvent standardVictory; - standardVictory.effect.type = EventEffect::VICTORY; - standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - standardVictory.identifier = "standardVictory"; - standardVictory.description.clear(); // TODO: display in quest window - standardVictory.onFulfill.appendTextID("core.genrltxt.659"); - standardVictory.trigger = EventExpression(victoryCondition); - - //Loss condition - 7 days without town - TriggeredEvent standardDefeat; - standardDefeat.effect.type = EventEffect::DEFEAT; - standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); - standardDefeat.identifier = "standardDefeat"; - standardDefeat.description.clear(); // TODO: display in quest window - standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); - standardDefeat.trigger = EventExpression(defeatCondition); - - controller.map()->triggeredEvents.clear(); - - //VICTORY - if(ui->victoryComboBox->currentIndex() == 0) - { - controller.map()->triggeredEvents.push_back(standardVictory); - controller.map()->victoryIconIndex = 11; - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); - } - else - { - int vicCondition = ui->victoryComboBox->currentIndex() - 1; - - TriggeredEvent specialVictory; - specialVictory.effect.type = EventEffect::VICTORY; - specialVictory.identifier = "specialVictory"; - specialVictory.description.clear(); // TODO: display in quest window - - controller.map()->victoryIconIndex = vicCondition; - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1]); - - switch(vicCondition) - { - case 0: { - EventCondition cond(EventCondition::HAVE_ARTIFACT); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); - specialVictory.onFulfill.appendTextID("core.genrltxt.280"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 1: { - EventCondition cond(EventCondition::HAVE_CREATURES); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - cond.value = victoryValueWidget->text().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); - specialVictory.onFulfill.appendTextID("core.genrltxt.276"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 2: { - EventCondition cond(EventCondition::HAVE_RESOURCES); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - cond.value = victoryValueWidget->text().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); - specialVictory.onFulfill.appendTextID("core.genrltxt.278"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 3: { - EventCondition cond(EventCondition::HAVE_BUILDING); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - int townIdx = victorySelectWidget->currentData().toInt(); - if(townIdx > -1) - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); - specialVictory.onFulfill.appendTextID("core.genrltxt.282"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 4: { - EventCondition cond(EventCondition::CONTROL); - assert(victoryTypeWidget); - cond.objectType = Obj::TOWN; - int townIdx = victoryTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); - specialVictory.onFulfill.appendTextID("core.genrltxt.249"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 5: { - EventCondition cond(EventCondition::DESTROY); - assert(victoryTypeWidget); - cond.objectType = Obj::HERO; - int heroIdx = victoryTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[heroIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); - specialVictory.onFulfill.appendTextID("core.genrltxt.252"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 6: { - EventCondition cond(EventCondition::TRANSPORT); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - int townIdx = victorySelectWidget->currentData().toInt(); - if(townIdx > -1) - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); - specialVictory.onFulfill.appendTextID("core.genrltxt.292"); - specialVictory.trigger = EventExpression(cond); - break; - } - - } - - // if condition is human-only turn it into following construction: AllOf(human, condition) - if(ui->onlyForHumansCheck->isChecked()) - { - EventExpression::OperatorAll oper; - EventCondition notAI(EventCondition::IS_HUMAN); - notAI.value = 1; - oper.expressions.push_back(notAI); - oper.expressions.push_back(specialVictory.trigger.get()); - specialVictory.trigger = EventExpression(oper); - } - - // if normal victory allowed - add one more quest - if(ui->standardVictoryCheck->isChecked()) - { - controller.map()->victoryMessage.appendRawString(" / "); - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); - controller.map()->triggeredEvents.push_back(standardVictory); - } - controller.map()->triggeredEvents.push_back(specialVictory); - } - - //DEFEAT - if(ui->loseComboBox->currentIndex() == 0) - { - controller.map()->triggeredEvents.push_back(standardDefeat); - controller.map()->defeatIconIndex = 3; - controller.map()->defeatMessage.appendTextID("core.lcdesc.0"); - } - else - { - int lossCondition = ui->loseComboBox->currentIndex() - 1; - - TriggeredEvent specialDefeat; - specialDefeat.effect.type = EventEffect::DEFEAT; - specialDefeat.identifier = "specialDefeat"; - specialDefeat.description.clear(); // TODO: display in quest window - - controller.map()->defeatIconIndex = lossCondition; - - switch(lossCondition) - { - case 0: { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - assert(loseTypeWidget); - int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - noneOf.expressions.push_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); - specialDefeat.trigger = EventExpression(noneOf); - controller.map()->defeatMessage.appendTextID("core.lcdesc.1"); - break; - } - - case 1: { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; - assert(loseTypeWidget); - int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - noneOf.expressions.push_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); - specialDefeat.trigger = EventExpression(noneOf); - controller.map()->defeatMessage.appendTextID("core.lcdesc.2"); - break; - } - - case 2: { - EventCondition cond(EventCondition::DAYS_PASSED); - assert(loseValueWidget); - cond.value = expiredDate(loseValueWidget->text()); - specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); - specialDefeat.trigger = EventExpression(cond); - controller.map()->defeatMessage.appendTextID("core.lcdesc.3"); - break; - } - - case 3: { - EventCondition cond(EventCondition::DAYS_WITHOUT_TOWN); - assert(loseValueWidget); - cond.value = loseValueWidget->text().toInt(); - specialDefeat.onFulfill.appendTextID("core.genrltxt.7"); - specialDefeat.trigger = EventExpression(cond); - break; - } - } - - EventExpression::OperatorAll allOf; - EventCondition isHuman(EventCondition::IS_HUMAN); - isHuman.value = 1; - - allOf.expressions.push_back(specialDefeat.trigger.get()); - allOf.expressions.push_back(isHuman); - specialDefeat.trigger = EventExpression(allOf); - - if(ui->standardLoseCheck->isChecked()) - { - controller.map()->triggeredEvents.push_back(standardDefeat); - } - controller.map()->triggeredEvents.push_back(specialDefeat); - } - - //Mod management - auto widgetAction = [&](QTreeWidgetItem * item) - { - if(item->checkState(0) == Qt::Checked) - { - auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - controller.map()->mods[modName] = VLC->modh->getModInfo(modName).version; - } - }; - - controller.map()->mods.clear(); - for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) - { - QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); - traverseNode(item, widgetAction); - } - - close(); -} - -void MapSettings::on_victoryComboBox_currentIndexChanged(int index) -{ - delete victoryTypeWidget; - delete victoryValueWidget; - delete victorySelectWidget; - victoryTypeWidget = nullptr; - victoryValueWidget = nullptr; - victorySelectWidget = nullptr; - - if(index == 0) - { - ui->standardVictoryCheck->setChecked(true); - ui->standardVictoryCheck->setEnabled(false); - ui->onlyForHumansCheck->setChecked(false); - ui->onlyForHumansCheck->setEnabled(false); - return; - } - ui->onlyForHumansCheck->setEnabled(true); - ui->standardVictoryCheck->setEnabled(true); - - int vicCondition = index - 1; - switch(vicCondition) - { - case 0: { //EventCondition::HAVE_ARTIFACT - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); - break; - } - - case 1: { //EventCondition::HAVE_CREATURES - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < VLC->creh->objects.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->creh->objects[i]->getNamePluralTranslated()), QVariant::fromValue(i)); - - victoryValueWidget = new QLineEdit; - ui->victoryParamsLayout->addWidget(victoryValueWidget); - victoryValueWidget->setText("1"); - break; - } - - case 2: { //EventCondition::HAVE_RESOURCES - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - { - for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType) - { - auto resName = QString::fromStdString(GameConstants::RESOURCE_NAMES[resType]); - victoryTypeWidget->addItem(resName, QVariant::fromValue(resType)); - } - } - - victoryValueWidget = new QLineEdit; - ui->victoryParamsLayout->addWidget(victoryValueWidget); - victoryValueWidget->setText("1"); - break; - } - - case 3: { //EventCondition::HAVE_BUILDING - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - auto * ctown = VLC->townh->randomTown; - for(int bId : ctown->getAllBuildings()) - victoryTypeWidget->addItem(QString::fromStdString(defaultBuildingIdConversion(BuildingID(bId))), QVariant::fromValue(bId)); - - victorySelectWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victorySelectWidget); - victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); - for(int i : getObjectIndexes()) - victorySelectWidget->addItem(getTownName(i).c_str(), QVariant::fromValue(i)); - break; - } - - case 4: { //EventCondition::CONTROL (Obj::TOWN) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 5: { //EventCondition::DESTROY (Obj::HERO) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getHeroName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); - - victorySelectWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victorySelectWidget); - for(int i : getObjectIndexes()) - victorySelectWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - - //TODO: support this vectory type - // in order to do that, need to implement finding creature by position - // selecting from map would be the best user experience - /*case 7: { //EventCondition::DESTROY (Obj::MONSTER) - victoryTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getMonsterName(i).c_str()), QVariant::fromValue(i)); - break; - }*/ - - - } -} - - -void MapSettings::on_loseComboBox_currentIndexChanged(int index) -{ - delete loseTypeWidget; - delete loseValueWidget; - delete loseSelectWidget; - loseTypeWidget = nullptr; - loseValueWidget = nullptr; - loseSelectWidget = nullptr; - - if(index == 0) - { - ui->standardLoseCheck->setChecked(true); - ui->standardLoseCheck->setEnabled(false); - return; - } - ui->standardLoseCheck->setEnabled(true); - - int loseCondition = index - 1; - switch(loseCondition) - { - case 0: { //EventCondition::CONTROL (Obj::TOWN) - loseTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes()) - loseTypeWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 1: { //EventCondition::CONTROL (Obj::HERO) - loseTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes()) - loseTypeWidget->addItem(tr(getHeroName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 2: { //EventCondition::DAYS_PASSED - loseValueWidget = new QLineEdit; - ui->loseParamsLayout->addWidget(loseValueWidget); - loseValueWidget->setText("2m 1w 1d"); - break; - } - - case 3: { //EventCondition::DAYS_WITHOUT_TOWN - loseValueWidget = new QLineEdit; - ui->loseParamsLayout->addWidget(loseValueWidget); - loseValueWidget->setText("7"); - break; - } - } -} - - -void MapSettings::on_heroLevelLimitCheck_toggled(bool checked) -{ - ui->heroLevelLimit->setEnabled(checked); -} - -void MapSettings::on_modResolution_map_clicked() -{ - updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller.map())); -} - - -void MapSettings::on_modResolution_full_clicked() -{ - updateModWidgetBasedOnMods(MapController::modAssessmentAll()); -} - -void MapSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column) -{ - //set state for children - for (int i = 0; i < item->childCount(); ++i) - item->child(i)->setCheckState(0, item->checkState(0)); - - //set state for parent - ui->treeMods->blockSignals(true); - if(item->checkState(0) == Qt::Checked) - { - while(item->parent()) - { - item->parent()->setCheckState(0, Qt::Checked); - item = item->parent(); - } - } - ui->treeMods->blockSignals(false); -} - diff --git a/mapeditor/mapsettings.h b/mapeditor/mapsettings.h deleted file mode 100644 index faf074043..000000000 --- a/mapeditor/mapsettings.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * mapsettings.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 "mapcontroller.h" -#include "../lib/mapping/CMap.h" - -namespace Ui { -class MapSettings; -} - -class MapSettings : public QDialog -{ - Q_OBJECT - -public: - explicit MapSettings(MapController & controller, QWidget *parent = nullptr); - ~MapSettings(); - -private slots: - void on_pushButton_clicked(); - - void on_victoryComboBox_currentIndexChanged(int index); - - void on_loseComboBox_currentIndexChanged(int index); - - void on_heroLevelLimitCheck_toggled(bool checked); - - void on_modResolution_map_clicked(); - - void on_modResolution_full_clicked(); - - void on_treeMods_itemChanged(QTreeWidgetItem *item, int column); - -private: - - std::string getTownName(int townObjectIdx); - std::string getHeroName(int townObjectIdx); - std::string getMonsterName(int townObjectIdx); - - void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods); - - template - std::vector getObjectIndexes() const - { - std::vector result; - for(int i = 0; i < controller.map()->objects.size(); ++i) - { - if(auto town = dynamic_cast(controller.map()->objects[i].get())) - result.push_back(i); - } - return result; - } - - template - int getObjectByPos(const int3 & pos) - { - for(int i = 0; i < controller.map()->objects.size(); ++i) - { - if(auto town = dynamic_cast(controller.map()->objects[i].get())) - { - if(town->pos == pos) - return i; - } - } - return -1; - } - - Ui::MapSettings *ui; - MapController & controller; - - QComboBox * victoryTypeWidget = nullptr, * loseTypeWidget = nullptr; - QComboBox * victorySelectWidget = nullptr, * loseSelectWidget = nullptr; - QLineEdit * victoryValueWidget = nullptr, * loseValueWidget = nullptr; -}; diff --git a/mapeditor/mapsettings.ui b/mapeditor/mapsettings.ui deleted file mode 100644 index 5a314b917..000000000 --- a/mapeditor/mapsettings.ui +++ /dev/null @@ -1,447 +0,0 @@ - - - MapSettings - - - Qt::ApplicationModal - - - - 0 - 0 - 543 - 494 - - - - - 0 - 0 - - - - Map settings - - - - - - 0 - - - - General - - - - - - Map name - - - - - - - - - - Map description - - - - - - - - - - 10 - - - - - false - - - - 48 - 0 - - - - - - - - - 0 - 0 - - - - Limit maximum heroes level - - - - - - - - - Difficulty - - - - - - 1 - - - - - - - 2 - - - - - - - 3 - - - - - - - 4 - - - - - - - 5 - - - - - - - - - - - Mods - - - - - - Mandatory mods for playing this map - - - - - - - QAbstractScrollArea::AdjustIgnored - - - 320 - - - - Mod name - - - - - Version - - - - - - - - - - Automatic assignment - - - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - Map objects mods - - - false - - - - - - - Set all mods having a game content as mandatory - - - Full content mods - - - false - - - - - - - - - - Events - - - - - - 1 - - - - Victory - - - - - - 0 - - - 0 - - - - - Victory message - - - - - - - - - - - - - - - Only for human players - - - - - - - Allow standard victory - - - - - - - - 0 - 0 - - - - Parameters - - - - 12 - - - - - - - - - - - - Loss - - - - - - - - - 7 days without town - - - - - - - Defeat message - - - - - - - - - - - 0 - 0 - - - - Parameters - - - - - - - - - - - - - - - - - Abilities - - - - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Adjust - - - QListView::Batched - - - 30 - - - - - - - - Spells - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - Artifacts - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - Heroes - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - - - - Ok - - - - - - - - diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp new file mode 100644 index 000000000..3ea3dcbe6 --- /dev/null +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -0,0 +1,141 @@ +/* + * abstractsettings.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 "abstractsettings.h" +#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) +int expiredDate(const QString & date) +{ + int result = 0; + for(auto component : date.split(" ")) + { + int days = component.left(component.lastIndexOf('d')).toInt(); + int weeks = component.left(component.lastIndexOf('w')).toInt(); + int months = component.left(component.lastIndexOf('m')).toInt(); + result += days > 0 ? days - 1 : 0; + result += (weeks > 0 ? weeks - 1 : 0) * 7; + result += (months > 0 ? months - 1 : 0) * 28; + } + return result; +} + +QString expiredDate(int date) +{ + QString result; + int m = date / 28; + int w = (date % 28) / 7; + int d = date % 7; + if(m) + result += QString::number(m) + "m"; + if(w) + { + if(!result.isEmpty()) + result += " "; + result += QString::number(w) + "w"; + } + if(d) + { + if(!result.isEmpty()) + result += " "; + result += QString::number(d) + "d"; + } + return result; +} + +int3 posFromJson(const JsonNode & json) +{ + return int3(json.Vector()[0].Integer(), json.Vector()[1].Integer(), json.Vector()[2].Integer()); +} + +std::vector linearJsonArray(const JsonNode & json) +{ + std::vector result; + if(json.getType() == JsonNode::JsonType::DATA_STRUCT) + result.push_back(json); + if(json.getType() == JsonNode::JsonType::DATA_VECTOR) + { + for(auto & node : json.Vector()) + { + auto subvector = linearJsonArray(node); + result.insert(result.end(), subvector.begin(), subvector.end()); + } + } + return result; +} + +AbstractSettings::AbstractSettings(QWidget *parent) + : QWidget{parent} +{ + +} + +void AbstractSettings::initialize(MapController & c) +{ + controller = &c; +} + +std::string AbstractSettings::getTownName(const CMap & map, int objectIdx) +{ + std::string name; + if(auto town = dynamic_cast(map.objects[objectIdx].get())) + { + name = town->getNameTranslated(); + + if(name.empty()) + name = town->getTown()->faction->getNameTranslated(); + } + return name; +} + +std::string AbstractSettings::getHeroName(const CMap & map, int objectIdx) +{ + std::string name; + if(auto hero = dynamic_cast(map.objects[objectIdx].get())) + { + name = hero->getNameTranslated(); + } + return name; +} + +std::string AbstractSettings::getMonsterName(const CMap & map, int objectIdx) +{ + std::string name; + [[maybe_unused]] auto monster = dynamic_cast(map.objects[objectIdx].get()); + if(monster) + { + //TODO: get proper name + //name = hero->name; + } + return name; +} + +JsonNode AbstractSettings::conditionToJson(const EventCondition & event) +{ + JsonNode result; + result["condition"].Integer() = event.condition; + result["value"].Integer() = event.value; + result["objectType"].String() = event.objectType.toString(); + result["objectInstanceName"].String() = event.objectInstanceName; + { + auto & position = result["position"].Vector(); + position.resize(3); + position[0].Float() = event.position.x; + position[1].Float() = event.position.y; + position[2].Float() = event.position.z; + } + return result; +}; diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h new file mode 100644 index 000000000..53ab653e5 --- /dev/null +++ b/mapeditor/mapsettings/abstractsettings.h @@ -0,0 +1,70 @@ +/* + * abstractsettings.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/mapping/CMap.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +//parses date for lose condition (1m 1w 1d) +int expiredDate(const QString & date); +QString expiredDate(int date); +int3 posFromJson(const JsonNode & json); +std::vector linearJsonArray(const JsonNode & json); + +class MapController; + +class AbstractSettings : public QWidget +{ + Q_OBJECT +public: + explicit AbstractSettings(QWidget *parent = nullptr); + virtual ~AbstractSettings() = default; + + virtual void initialize(MapController & controller); + virtual void update() = 0; + + static std::string getTownName(const CMap & map, int objectIdx); + static std::string getHeroName(const CMap & map, int objectIdx); + static std::string getMonsterName(const CMap & map, int objectIdx); + + static JsonNode conditionToJson(const EventCondition & event); + + template + static std::vector getObjectIndexes(const CMap & map) + { + std::vector result; + for(int i = 0; i < map.objects.size(); ++i) + { + if(auto obj = dynamic_cast(map.objects[i].get())) + result.push_back(i); + } + return result; + } + + template + static int getObjectByPos(const CMap & map, const int3 & pos) + { + for(int i = 0; i < map.objects.size(); ++i) + { + if(auto obj = dynamic_cast(map.objects[i].get())) + { + if(obj->pos == pos) + return i; + } + } + return -1; + } + +protected: + MapController * controller = nullptr; + +}; diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp new file mode 100644 index 000000000..a128aa6ef --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -0,0 +1,120 @@ +/* + * eventsettings.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 "eventsettings.h" +#include "timedevent.h" +#include "ui_eventsettings.h" +#include "../mapcontroller.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +QVariant toVariant(const TResources & resources) +{ + QVariantMap result; + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + result[QString::fromStdString(GameConstants::RESOURCE_NAMES[i])] = QVariant::fromValue(resources[i]); + return result; +} + +TResources resourcesFromVariant(const QVariant & v) +{ + JsonNode vJson; + for(auto r : v.toMap().keys()) + vJson[r.toStdString()].Integer() = v.toMap().value(r).toInt(); + return TResources(vJson); + +} + +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["humanAffected"] = QVariant::fromValue(event.humanAffected); + result["computerAffected"] = QVariant::fromValue(event.computerAffected); + result["firstOccurence"] = QVariant::fromValue(event.firstOccurence); + result["nextOccurence"] = QVariant::fromValue(event.nextOccurence); + result["resources"] = toVariant(event.resources); + return QVariant(result); +} + +CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant) +{ + CMapEvent result; + 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.humanAffected = v.value("humanAffected").toInt(); + result.computerAffected = v.value("computerAffected").toInt(); + result.firstOccurence = v.value("firstOccurence").toInt(); + result.nextOccurence = v.value("nextOccurence").toInt(); + result.resources = resourcesFromVariant(v.value("resources")); + return result; +} + +EventSettings::EventSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::EventSettings) +{ + ui->setupUi(this); +} + +EventSettings::~EventSettings() +{ + delete ui; +} + +void EventSettings::initialize(MapController & c) +{ + AbstractSettings::initialize(c); + for(const auto & event : controller->map()->events) + { + auto * item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(Qt::UserRole, toVariant(event)); + ui->eventsList->addItem(item); + } +} + +void EventSettings::update() +{ + controller->map()->events.clear(); + for(int i = 0; i < ui->eventsList->count(); ++i) + { + const auto * item = ui->eventsList->item(i); + controller->map()->events.push_back(eventFromVariant(*controller->map(), item->data(Qt::UserRole))); + } +} + +void EventSettings::on_timedEventAdd_clicked() +{ + CMapEvent event; + event.name = tr("New event").toStdString(); + auto * item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(Qt::UserRole, toVariant(event)); + ui->eventsList->addItem(item); + on_eventsList_itemActivated(item); +} + + +void EventSettings::on_timedEventRemove_clicked() +{ + if(auto * item = ui->eventsList->currentItem()) + ui->eventsList->takeItem(ui->eventsList->row(item)); +} + + +void EventSettings::on_eventsList_itemActivated(QListWidgetItem *item) +{ + new TimedEvent(item, parentWidget()); +} + diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h new file mode 100644 index 000000000..ef29f0308 --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.h @@ -0,0 +1,39 @@ +/* + * eventsettings.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 "abstractsettings.h" + +namespace Ui { +class EventSettings; +} + +class EventSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit EventSettings(QWidget *parent = nullptr); + ~EventSettings(); + + void initialize(MapController & map) override; + void update() override; + +private slots: + void on_timedEventAdd_clicked(); + + void on_timedEventRemove_clicked(); + + void on_eventsList_itemActivated(QListWidgetItem *item); + +private: + Ui::EventSettings *ui; +}; + diff --git a/mapeditor/mapsettings/eventsettings.ui b/mapeditor/mapsettings/eventsettings.ui new file mode 100644 index 000000000..cd9ea89b9 --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.ui @@ -0,0 +1,86 @@ + + + EventSettings + + + + 0 + 0 + 672 + 456 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Timed events + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + Remove + + + + + + + + + + + + + diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp new file mode 100644 index 000000000..a15599e84 --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -0,0 +1,81 @@ +/* + * generalsettings.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 "generalsettings.h" +#include "ui_generalsettings.h" +#include "../mapcontroller.h" + +GeneralSettings::GeneralSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::GeneralSettings) +{ + ui->setupUi(this); +} + +GeneralSettings::~GeneralSettings() +{ + delete ui; +} + +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->heroLevelLimit->setValue(controller->map()->levelLimit); + ui->heroLevelLimitCheck->setChecked(controller->map()->levelLimit); + + //set difficulty + switch(controller->map()->difficulty) + { + case 0: + ui->diffRadio1->setChecked(true); + break; + + case 1: + ui->diffRadio2->setChecked(true); + break; + + case 2: + ui->diffRadio3->setChecked(true); + break; + + case 3: + ui->diffRadio4->setChecked(true); + break; + + case 4: + ui->diffRadio5->setChecked(true); + break; + }; +} + +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())); + if(ui->heroLevelLimitCheck->isChecked()) + controller->map()->levelLimit = ui->heroLevelLimit->value(); + else + controller->map()->levelLimit = 0; + + //set difficulty + if(ui->diffRadio1->isChecked()) controller->map()->difficulty = 0; + if(ui->diffRadio2->isChecked()) controller->map()->difficulty = 1; + if(ui->diffRadio3->isChecked()) controller->map()->difficulty = 2; + if(ui->diffRadio4->isChecked()) controller->map()->difficulty = 3; + if(ui->diffRadio5->isChecked()) controller->map()->difficulty = 4; +} + +void GeneralSettings::on_heroLevelLimitCheck_toggled(bool checked) +{ + ui->heroLevelLimit->setEnabled(checked); +} + diff --git a/mapeditor/mapsettings/generalsettings.h b/mapeditor/mapsettings/generalsettings.h new file mode 100644 index 000000000..b95606d73 --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.h @@ -0,0 +1,34 @@ +/* + * generalsettings.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 "abstractsettings.h" + +namespace Ui { +class GeneralSettings; +} + +class GeneralSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit GeneralSettings(QWidget *parent = nullptr); + ~GeneralSettings(); + + void initialize(MapController & map) override; + void update() override; + +private slots: + void on_heroLevelLimitCheck_toggled(bool checked); + +private: + Ui::GeneralSettings *ui; +}; diff --git a/mapeditor/mapsettings/generalsettings.ui b/mapeditor/mapsettings/generalsettings.ui new file mode 100644 index 000000000..e9759ff4f --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.ui @@ -0,0 +1,130 @@ + + + GeneralSettings + + + + 0 + 0 + 651 + 481 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Map name + + + + + + + + + + Map description + + + + + + + + + + 0 + + + + + false + + + + 48 + 0 + + + + + + + + + 0 + 0 + + + + Limit maximum heroes level + + + + + + + + + Difficulty + + + + + + 1 + + + + + + + 2 + + + + + + + 3 + + + + + + + 4 + + + + + + + 5 + + + + + + + + + + + diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp new file mode 100644 index 000000000..dc679b685 --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -0,0 +1,339 @@ +/* + * loseconditions.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 "loseconditions.h" +#include "ui_loseconditions.h" +#include "../mapcontroller.h" +#include "../lib/CGeneralTextHandler.h" + +LoseConditions::LoseConditions(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::LoseConditions) +{ + ui->setupUi(this); +} + +LoseConditions::~LoseConditions() +{ + delete ui; +} + +void LoseConditions::initialize(MapController & c) +{ + AbstractSettings::initialize(c); + + //loss messages + ui->defeatMessageEdit->setText(QString::fromStdString(controller->map()->defeatMessage.toString())); + + //loss conditions + const std::array conditionStringsLose = { + QT_TR_NOOP("No special loss"), + QT_TR_NOOP("Lose castle"), + QT_TR_NOOP("Lose hero"), + QT_TR_NOOP("Time expired"), + QT_TR_NOOP("Days without town") + }; + + for(auto & s : conditionStringsLose) + { + ui->loseComboBox->addItem(QString::fromStdString(s)); + } + ui->standardLoseCheck->setChecked(false); + + for(auto & ev : controller->map()->triggeredEvents) + { + if(ev.effect.type == EventEffect::DEFEAT) + { + if(ev.identifier == "standardDefeat") + ui->standardLoseCheck->setChecked(true); + + if(ev.identifier == "specialDefeat") + { + auto readjson = ev.trigger.toJson(AbstractSettings::conditionToJson); + auto linearNodes = linearJsonArray(readjson); + + for(auto & json : linearNodes) + { + switch(json["condition"].Integer()) + { + case EventCondition::CONTROL: { + if(json["objectType"].Integer() == Obj::TOWN) + { + ui->loseComboBox->setCurrentIndex(1); + assert(loseTypeWidget); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = loseTypeWidget->findData(townIdx); + loseTypeWidget->setCurrentIndex(idx); + } + } + if(json["objectType"].Integer() == Obj::HERO) + { + ui->loseComboBox->setCurrentIndex(2); + assert(loseTypeWidget); + int heroIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(heroIdx >= 0) + { + auto idx = loseTypeWidget->findData(heroIdx); + loseTypeWidget->setCurrentIndex(idx); + } + } + + break; + } + + case EventCondition::DAYS_PASSED: { + ui->loseComboBox->setCurrentIndex(3); + assert(loseValueWidget); + loseValueWidget->setText(expiredDate(json["value"].Integer())); + break; + } + + case EventCondition::DAYS_WITHOUT_TOWN: { + ui->loseComboBox->setCurrentIndex(4); + assert(loseValueWidget); + loseValueWidget->setText(QString::number(json["value"].Integer())); + break; + + case EventCondition::IS_HUMAN: + break; //ignore because always applicable for defeat conditions + } + + }; + } + } + } + } +} + +void LoseConditions::update() +{ + //loss messages + bool customMessage = true; + + //loss conditions + EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); + defeatCondition.value = 7; + + //Loss condition - 7 days without town + TriggeredEvent standardDefeat; + standardDefeat.effect.type = EventEffect::DEFEAT; + standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); + standardDefeat.identifier = "standardDefeat"; + standardDefeat.description.clear(); // TODO: display in quest window + standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); + standardDefeat.trigger = EventExpression(defeatCondition); + + //DEFEAT + if(ui->loseComboBox->currentIndex() == 0) + { + controller->map()->triggeredEvents.push_back(standardDefeat); + controller->map()->defeatIconIndex = 3; + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.0"); + customMessage = false; + } + else + { + int lossCondition = ui->loseComboBox->currentIndex() - 1; + + TriggeredEvent specialDefeat; + specialDefeat.effect.type = EventEffect::DEFEAT; + specialDefeat.identifier = "specialDefeat"; + specialDefeat.description.clear(); // TODO: display in quest window + + controller->map()->defeatIconIndex = lossCondition; + + switch(lossCondition) + { + case 0: { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj(Obj::TOWN); + assert(loseTypeWidget); + int townIdx = loseTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[townIdx]->pos; + noneOf.expressions.push_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); + specialDefeat.trigger = EventExpression(noneOf); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.1"); + customMessage = false; + break; + } + + case 1: { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj(Obj::HERO); + assert(loseTypeWidget); + int townIdx = loseTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[townIdx]->pos; + noneOf.expressions.push_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); + specialDefeat.trigger = EventExpression(noneOf); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.2"); + customMessage = false; + break; + } + + case 2: { + EventCondition cond(EventCondition::DAYS_PASSED); + assert(loseValueWidget); + cond.value = expiredDate(loseValueWidget->text()); + specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); + specialDefeat.trigger = EventExpression(cond); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.3"); + customMessage = false; + break; + } + + case 3: { + EventCondition cond(EventCondition::DAYS_WITHOUT_TOWN); + assert(loseValueWidget); + cond.value = loseValueWidget->text().toInt(); + specialDefeat.onFulfill.appendTextID("core.genrltxt.7"); + specialDefeat.trigger = EventExpression(cond); + break; + } + } + + EventExpression::OperatorAll allOf; + EventCondition isHuman(EventCondition::IS_HUMAN); + isHuman.value = 1; + + allOf.expressions.push_back(specialDefeat.trigger.get()); + allOf.expressions.push_back(isHuman); + specialDefeat.trigger = EventExpression(allOf); + + if(ui->standardLoseCheck->isChecked()) + { + controller->map()->triggeredEvents.push_back(standardDefeat); + } + controller->map()->triggeredEvents.push_back(specialDefeat); + } + + if(customMessage) + { + controller->map()->defeatMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "defeatMessage"), ui->defeatMessageEdit->text().toStdString())); + } +} + +void LoseConditions::on_loseComboBox_currentIndexChanged(int index) +{ + delete loseTypeWidget; + delete loseValueWidget; + delete loseSelectWidget; + delete pickObjectButton; + loseTypeWidget = nullptr; + loseValueWidget = nullptr; + loseSelectWidget = nullptr; + pickObjectButton = nullptr; + + if(index == 0) + { + ui->standardLoseCheck->setChecked(true); + ui->standardLoseCheck->setEnabled(false); + return; + } + ui->standardLoseCheck->setEnabled(true); + + int loseCondition = index - 1; + switch(loseCondition) + { + case 0: { //EventCondition::CONTROL (Obj::TOWN) + loseTypeWidget = new QComboBox; + ui->loseParamsLayout->addWidget(loseTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + loseTypeWidget->addItem(QString::fromStdString(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); + ui->loseParamsLayout->addWidget(pickObjectButton); + break; + } + + case 1: { //EventCondition::CONTROL (Obj::HERO) + loseTypeWidget = new QComboBox; + ui->loseParamsLayout->addWidget(loseTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + loseTypeWidget->addItem(QString::fromStdString(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); + ui->loseParamsLayout->addWidget(pickObjectButton); + break; + } + + case 2: { //EventCondition::DAYS_PASSED + loseValueWidget = new QLineEdit; + ui->loseParamsLayout->addWidget(loseValueWidget); + loseValueWidget->setText("2m 1w 1d"); + break; + } + + case 3: { //EventCondition::DAYS_WITHOUT_TOWN + loseValueWidget = new QLineEdit; + ui->loseParamsLayout->addWidget(loseValueWidget); + loseValueWidget->setText("7"); + break; + } + } +} + +void LoseConditions::onObjectSelect() +{ + int loseCondition = ui->loseComboBox->currentIndex() - 1; + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + switch(loseCondition) + { + case 0: { //EventCondition::CONTROL (Obj::TOWN) + l.highlight(); + break; + } + + case 1: { //EventCondition::CONTROL (Obj::HERO) + l.highlight(); + break; + } + default: + return; + } + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &LoseConditions::onObjectPicked); + } + + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); +} + +void LoseConditions::onObjectPicked(const CGObjectInstance * obj) +{ + 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, &LoseConditions::onObjectPicked); + } + + if(!obj) //discarded + return; + + for(int i = 0; i < loseTypeWidget->count(); ++i) + { + auto data = controller->map()->objects.at(loseTypeWidget->itemData(i).toInt()); + if(data == obj) + { + loseTypeWidget->setCurrentIndex(i); + break; + } + } +} diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h new file mode 100644 index 000000000..9ae7aa509 --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.h @@ -0,0 +1,44 @@ +/* + * loseconditions.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 "abstractsettings.h" + +namespace Ui { +class LoseConditions; +} + +class LoseConditions : public AbstractSettings +{ + Q_OBJECT + +public: + explicit LoseConditions(QWidget *parent = nullptr); + ~LoseConditions(); + + void initialize(MapController & map) override; + void update() override; + +public slots: + void onObjectSelect(); + void onObjectPicked(const CGObjectInstance *); + +private slots: + void on_loseComboBox_currentIndexChanged(int index); + +private: + Ui::LoseConditions *ui; + + QComboBox * loseTypeWidget = nullptr; + QComboBox * loseSelectWidget = nullptr; + QLineEdit * loseValueWidget = nullptr; + QToolButton * pickObjectButton = nullptr; +}; + diff --git a/mapeditor/mapsettings/loseconditions.ui b/mapeditor/mapsettings/loseconditions.ui new file mode 100644 index 000000000..35d3cb7ce --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.ui @@ -0,0 +1,85 @@ + + + LoseConditions + + + + 0 + 0 + 650 + 485 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + + + Defeat message + + + + + + + true + + + + + + + + + + + + 7 days without town + + + + + + + + 0 + 0 + + + + Parameters + + + + + + + + + + + + + diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp new file mode 100644 index 000000000..369e1007f --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -0,0 +1,109 @@ +/* + * mapsettings.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 "mapsettings.h" +#include "ui_mapsettings.h" +#include "mainwindow.h" + +#include "../../lib/CSkillHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CHeroHandler.h" + + +MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : + QDialog(parent), + ui(new Ui::MapSettings), + controller(ctrl) +{ + ui->setupUi(this); + + assert(controller.map()); + + show(); + + for(auto objectPtr : VLC->skillh->objects) + { + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedAbilities.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); + ui->listAbilities->addItem(item); + } + for(auto objectPtr : VLC->spellh->objects) + { + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedSpells.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); + ui->listSpells->addItem(item); + } + for(auto objectPtr : VLC->arth->objects) + { + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedArtifact.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); + ui->listArts->addItem(item); + } + for(auto objectPtr : VLC->heroh->objects) + { + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedHeroes.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); + ui->listHeroes->addItem(item); + } + + ui->general->initialize(controller); + ui->mods->initialize(controller); + ui->victory->initialize(controller); + ui->lose->initialize(controller); + ui->events->initialize(controller); + ui->rumors->initialize(controller); +} + +MapSettings::~MapSettings() +{ + delete ui; +} + +void MapSettings::on_pushButton_clicked() +{ + auto updateMapArray = [](const QListWidget * widget, auto & arr) + { + arr.clear(); + for(int i = 0; i < arr.size(); ++i) + { + auto * item = widget->item(i); + if (item->checkState() == Qt::Checked) + arr.emplace(i); + } + }; + + updateMapArray(ui->listAbilities, controller.map()->allowedAbilities); + updateMapArray(ui->listSpells, controller.map()->allowedSpells); + updateMapArray(ui->listArts, controller.map()->allowedArtifact); + updateMapArray(ui->listHeroes, controller.map()->allowedHeroes); + + controller.map()->triggeredEvents.clear(); + + ui->general->update(); + ui->mods->update(); + ui->victory->update(); + ui->lose->update(); + ui->events->update(); + ui->rumors->update(); + + controller.commitChangeWithoutRedraw(); + + close(); +} diff --git a/mapeditor/mapsettings/mapsettings.h b/mapeditor/mapsettings/mapsettings.h new file mode 100644 index 000000000..fc4501531 --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.h @@ -0,0 +1,36 @@ +/* + * mapsettings.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 "mapcontroller.h" +#include "../lib/mapping/CMap.h" + +namespace Ui { +class MapSettings; +} + +class MapSettings : public QDialog +{ + Q_OBJECT + +public: + explicit MapSettings(MapController & controller, QWidget *parent = nullptr); + ~MapSettings(); + +private slots: + void on_pushButton_clicked(); + +private: + + Ui::MapSettings *ui; + MapController & controller; +}; diff --git a/mapeditor/mapsettings/mapsettings.ui b/mapeditor/mapsettings/mapsettings.ui new file mode 100644 index 000000000..360bea673 --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.ui @@ -0,0 +1,368 @@ + + + MapSettings + + + Qt::ApplicationModal + + + + 0 + 0 + 543 + 494 + + + + + 0 + 0 + + + + Map settings + + + + 0 + + + 3 + + + 3 + + + + + 0 + + + + General + + + + 12 + + + 12 + + + 3 + + + + + + + + + Mods + + + + 12 + + + 12 + + + 3 + + + + + + + + + Events + + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Victory + + + + 12 + + + 12 + + + 12 + + + + + + + + + Loss + + + + 12 + + + 12 + + + 12 + + + + + + + + + Timed + + + + 12 + + + 12 + + + 12 + + + + + + + + + Rumors + + + + 12 + + + 12 + + + 12 + + + + + + + + + + + + + Abilities + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Adjust + + + QListView::Batched + + + 30 + + + + + + + + Spells + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Artifacts + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Heroes + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + + + + Ok + + + + + + + + GeneralSettings + QWidget +
    mapsettings/generalsettings.h
    + 1 +
    + + ModSettings + QWidget +
    mapsettings/modsettings.h
    + 1 +
    + + VictoryConditions + QWidget +
    mapsettings/victoryconditions.h
    + 1 +
    + + LoseConditions + QWidget +
    mapsettings/loseconditions.h
    + 1 +
    + + EventSettings + QWidget +
    mapsettings/eventsettings.h
    + 1 +
    + + RumorSettings + QWidget +
    mapsettings/rumorsettings.h
    + 1 +
    +
    + + +
    diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp new file mode 100644 index 000000000..5926542e5 --- /dev/null +++ b/mapeditor/mapsettings/modsettings.cpp @@ -0,0 +1,163 @@ +/* + * modsettings.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 "modsettings.h" +#include "ui_modsettings.h" +#include "../mapcontroller.h" +#include "../../lib/modding/CModHandler.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/modding/CModInfo.h" + +void traverseNode(QTreeWidgetItem * item, std::function action) +{ + // Do something with item + action(item); + for (int i = 0; i < item->childCount(); ++i) + traverseNode(item->child(i), action); +} + +ModSettings::ModSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::ModSettings) +{ + ui->setupUi(this); +} + +ModSettings::~ModSettings() +{ + delete ui; +} + +void ModSettings::initialize(MapController & c) +{ + AbstractSettings::initialize(c); + + //mods management + //collect all active mods + QMap addedMods; + QSet modsToProcess; + ui->treeMods->blockSignals(true); + + auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & 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))); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + //set parent check + if(parent && item->checkState(0) == Qt::Checked) + parent->setCheckState(0, Qt::Checked); + return item; + }; + + for(const auto & modName : VLC->modh->getActiveMods()) + { + QString qmodName = QString::fromStdString(modName); + if(qmodName.split(".").size() == 1) + { + const auto & modInfo = VLC->modh->getModInfo(modName); + addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo); + ui->treeMods->addTopLevelItem(addedMods[qmodName]); + } + else + { + modsToProcess.insert(qmodName); + } + } + + for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();) + { + auto qmodName = *qmodIter; + auto pieces = qmodName.split("."); + assert(pieces.size() > 1); + + QString qs; + for(int i = 0; i < pieces.size() - 1; ++i) + qs += pieces[i]; + + if(addedMods.count(qs)) + { + const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString()); + addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo); + modsToProcess.erase(qmodIter); + qmodIter = modsToProcess.begin(); + } + else + ++qmodIter; + } + + ui->treeMods->blockSignals(false); +} + +void ModSettings::update() +{ + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + if(item->checkState(0) == Qt::Checked) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + controller->map()->mods[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); + } + }; + + controller->map()->mods.clear(); + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } +} + +void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) +{ + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked); + }; + + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } +} + +void ModSettings::on_modResolution_map_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller->map())); +} + + +void ModSettings::on_modResolution_full_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentAll()); +} + +void ModSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column) +{ + //set state for children + for (int i = 0; i < item->childCount(); ++i) + item->child(i)->setCheckState(0, item->checkState(0)); + + //set state for parent + ui->treeMods->blockSignals(true); + if(item->checkState(0) == Qt::Checked) + { + while(item->parent()) + { + item->parent()->setCheckState(0, Qt::Checked); + item = item->parent(); + } + } + ui->treeMods->blockSignals(false); +} diff --git a/mapeditor/mapsettings/modsettings.h b/mapeditor/mapsettings/modsettings.h new file mode 100644 index 000000000..5a1f83064 --- /dev/null +++ b/mapeditor/mapsettings/modsettings.h @@ -0,0 +1,41 @@ +/* + * modsettings.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 "abstractsettings.h" + +namespace Ui { +class ModSettings; +} + +class ModSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit ModSettings(QWidget *parent = nullptr); + ~ModSettings(); + + void initialize(MapController & map) override; + void update() override; + +private slots: + void on_modResolution_map_clicked(); + + void on_modResolution_full_clicked(); + + void on_treeMods_itemChanged(QTreeWidgetItem *item, int column); + +private: + void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods); + +private: + Ui::ModSettings *ui; +}; diff --git a/mapeditor/mapsettings/modsettings.ui b/mapeditor/mapsettings/modsettings.ui new file mode 100644 index 000000000..0c3834d7a --- /dev/null +++ b/mapeditor/mapsettings/modsettings.ui @@ -0,0 +1,97 @@ + + + ModSettings + + + + 0 + 0 + 599 + 451 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Mandatory mods to play this map + + + + + + + QAbstractScrollArea::AdjustIgnored + + + 320 + + + + Mod name + + + + + Version + + + + + + + + + + Automatic assignment + + + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + Map objects mods + + + false + + + + + + + Set all mods having a game content as mandatory + + + Full content mods + + + false + + + + + + + + + + diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp new file mode 100644 index 000000000..7d47cc6c9 --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -0,0 +1,83 @@ +/* + * rumorsettings.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 "rumorsettings.h" +#include "ui_rumorsettings.h" +#include "../mapcontroller.h" + +RumorSettings::RumorSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::RumorSettings) +{ + ui->setupUi(this); +} + +RumorSettings::~RumorSettings() +{ + delete ui; +} + +void RumorSettings::initialize(MapController & c) +{ + AbstractSettings::initialize(c); + for(auto & rumor : controller->map()->rumors) + { + auto * item = new QListWidgetItem(QString::fromStdString(rumor.name)); + item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text.toString()))); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->rumors->addItem(item); + } +} + +void RumorSettings::update() +{ + controller->map()->rumors.clear(); + for(int i = 0; i < ui->rumors->count(); ++i) + { + Rumor rumor; + rumor.name = ui->rumors->item(i)->text().toStdString(); + rumor.text.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "rumor", i, "text"), ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString())); + controller->map()->rumors.push_back(rumor); + } +} + +void RumorSettings::on_message_textChanged() +{ + if(auto item = ui->rumors->currentItem()) + item->setData(Qt::UserRole, QVariant(ui->message->toPlainText())); +} + +void RumorSettings::on_add_clicked() +{ + auto * item = new QListWidgetItem(tr("New rumor")); + item->setData(Qt::UserRole, QVariant("")); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->rumors->addItem(item); + emit ui->rumors->itemActivated(item); +} + +void RumorSettings::on_remove_clicked() +{ + if(auto item = ui->rumors->currentItem()) + { + ui->message->setPlainText(""); + ui->rumors->takeItem(ui->rumors->row(item)); + } +} + + +void RumorSettings::on_rumors_itemSelectionChanged() +{ + if(auto item = ui->rumors->currentItem()) + ui->message->setPlainText(item->data(Qt::UserRole).toString()); +} + + + diff --git a/mapeditor/mapsettings/rumorsettings.h b/mapeditor/mapsettings/rumorsettings.h new file mode 100644 index 000000000..e31e9c08b --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.h @@ -0,0 +1,40 @@ +/* + * rumorsettings.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 "abstractsettings.h" + +namespace Ui { +class RumorSettings; +} + +class RumorSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit RumorSettings(QWidget *parent = nullptr); + ~RumorSettings(); + + void initialize(MapController & map) override; + void update() override; + +private slots: + void on_message_textChanged(); + + void on_add_clicked(); + + void on_remove_clicked(); + + void on_rumors_itemSelectionChanged(); + +private: + Ui::RumorSettings *ui; +}; diff --git a/mapeditor/mapsettings/rumorsettings.ui b/mapeditor/mapsettings/rumorsettings.ui new file mode 100644 index 000000000..67a66e670 --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.ui @@ -0,0 +1,105 @@ + + + RumorSettings + + + + 0 + 0 + 538 + 470 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + + + Tavern rumors + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + + 0 + 16777215 + + + + Remove + + + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::SelectRows + + + + + + + + + + + diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp new file mode 100644 index 000000000..a0aec1c17 --- /dev/null +++ b/mapeditor/mapsettings/timedevent.cpp @@ -0,0 +1,108 @@ +/* + * timedevent.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 "timedevent.h" +#include "ui_timedevent.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/constants/StringConstants.h" + +TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : + QDialog(parent), + target(t), + ui(new Ui::TimedEvent) +{ + ui->setupUi(this); + + + + const auto params = t->data(Qt::UserRole).toMap(); + 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->eventFirstOccurance->setValue(params.value("firstOccurence").toInt()); + ui->eventRepeatAfter->setValue(params.value("nextOccurence").toInt()); + + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + bool isAffected = (1 << i) & params.value("players").toInt(); + 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); + ui->playersAffected->addItem(item); + } + + ui->resources->setRowCount(GameConstants::RESOURCE_QUANTITY); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); + int val = params.value("resources").toMap().value(name).toInt(); + ui->resources->setItem(i, 0, new QTableWidgetItem(name)); + auto nval = new QTableWidgetItem(QString::number(val)); + nval->setFlags(nval->flags() | Qt::ItemIsEditable); + ui->resources->setItem(i, 1, nval); + } + + show(); +} + +TimedEvent::~TimedEvent() +{ + delete ui; +} + + +void TimedEvent::on_TimedEvent_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["firstOccurence"] = QVariant::fromValue(ui->eventFirstOccurance->value()); + descriptor["nextOccurence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); + + int players = 0; + for(int i = 0; i < ui->playersAffected->count(); ++i) + { + auto * item = ui->playersAffected->item(i); + if(item->checkState() == Qt::Checked) + players |= 1 << i; + } + descriptor["players"] = QVariant::fromValue(players); + + auto res = target->data(Qt::UserRole).toMap().value("resources").toMap(); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto * itemType = ui->resources->item(i, 0); + auto * itemQty = ui->resources->item(i, 1); + res[itemType->text()] = QVariant::fromValue(itemQty->text().toInt()); + } + descriptor["resources"] = res; + + target->setData(Qt::UserRole, descriptor); + target->setText(ui->eventNameText->text()); +} + + +void TimedEvent::on_pushButton_clicked() +{ + close(); +} + + +void TimedEvent::on_resources_itemDoubleClicked(QTableWidgetItem *item) +{ + if(item && item->column() == 1) + { + ui->resources->editItem(item); + } +} + diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h new file mode 100644 index 000000000..5aab63fc2 --- /dev/null +++ b/mapeditor/mapsettings/timedevent.h @@ -0,0 +1,37 @@ +/* + * timedevent.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 + +namespace Ui { +class TimedEvent; +} + +class TimedEvent : public QDialog +{ + Q_OBJECT + +public: + explicit TimedEvent(QListWidgetItem *, QWidget *parent = nullptr); + ~TimedEvent(); + +private slots: + + void on_TimedEvent_finished(int result); + + void on_pushButton_clicked(); + + void on_resources_itemDoubleClicked(QTableWidgetItem *item); + +private: + Ui::TimedEvent *ui; + QListWidgetItem * target; +}; diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui new file mode 100644 index 000000000..92ac597ab --- /dev/null +++ b/mapeditor/mapsettings/timedevent.ui @@ -0,0 +1,213 @@ + + + TimedEvent + + + Qt::NonModal + + + + 0 + 0 + 620 + 371 + + + + Timed event + + + true + + + + + + + + Event name + + + + + + + Type event message text + + + + + + + 0 + + + + + affects human + + + + + + + affects AI + + + + + + + + + 0 + + + + + + + Day of first occurance + + + + + + + + + + + + 0 + + + + + Repeat after (0 = no repeat) + + + + + + + + + + + + + + + + 0 + + + + + Affected players + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + + + + + Resources + + + + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + 2 + + + false + + + true + + + 20 + + + 60 + + + true + + + false + + + 16 + + + 16 + + + + type + + + + + qty + + + + + + + + Ok + + + + + + + + + + diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp new file mode 100644 index 000000000..dc20d4db0 --- /dev/null +++ b/mapeditor/mapsettings/translations.cpp @@ -0,0 +1,198 @@ +/* + * translations.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 "translations.h" +#include "ui_translations.h" +#include "../../lib/Languages.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/VCMI_Lib.h" + +void Translations::cleanupRemovedItems(CMap & map) +{ + std::set existingObjects{"map", "header"}; + for(auto object : map.objects) + existingObjects.insert(object->instanceName); + + for(auto & translations : map.translations.Struct()) + { + auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + for(auto & s : translations.second.Struct()) + { + for(auto part : QString::fromStdString(s.first).split('.')) + { + if(existingObjects.count(part.toStdString())) + { + updateTranslations.Struct()[s.first] = s.second; + break; + } + } + } + translations.second = updateTranslations; + } +} + +void Translations::cleanupRemovedItems(CMap & map, const std::string & pattern) +{ + for(auto & translations : map.translations.Struct()) + { + auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + for(auto & s : translations.second.Struct()) + { + if(s.first.find(pattern) == std::string::npos) + updateTranslations.Struct()[s.first] = s.second; + } + translations.second = updateTranslations; + } +} + +Translations::Translations(CMapHeader & mh, QWidget *parent) : + QDialog(parent), + ui(new Ui::Translations), + mapHeader(mh) +{ + setAttribute(Qt::WA_DeleteOnClose, true); + ui->setupUi(this); + + //fill languages list + std::set indexFoundLang; + int foundLang = -1; + ui->languageSelect->blockSignals(true); + for(auto & language : Languages::getLanguageList()) + { + ui->languageSelect->addItem(QString("%1 (%2)").arg(QString::fromStdString(language.nameEnglish), QString::fromStdString(language.nameNative))); + ui->languageSelect->setItemData(ui->languageSelect->count() - 1, QVariant(QString::fromStdString(language.identifier))); + if(mapHeader.translations.Struct().count(language.identifier) && !mapHeader.translations[language.identifier].Struct().empty()) + indexFoundLang.insert(ui->languageSelect->count() - 1); + if(language.identifier == VLC->generaltexth->getPreferredLanguage()) + foundLang = ui->languageSelect->count() - 1; + } + ui->languageSelect->blockSignals(false); + + if(foundLang >= 0 && !indexFoundLang.empty() && !indexFoundLang.count(foundLang)) + { + foundLang = *indexFoundLang.begin(); + mapPreferredLanguage = ui->languageSelect->itemData(foundLang).toString().toStdString(); + } + + if(foundLang >= 0) + ui->languageSelect->setCurrentIndex(foundLang); + + if(mapPreferredLanguage.empty()) + mapPreferredLanguage = VLC->generaltexth->getPreferredLanguage(); +} + +Translations::~Translations() +{ + mapHeader.registerMapStrings(); + delete ui; +} + +void Translations::fillTranslationsTable(const std::string & language) +{ + Translations::cleanupRemovedItems(dynamic_cast(mapHeader)); + auto & translation = mapHeader.translations[language]; + ui->translationsTable->blockSignals(true); + ui->translationsTable->setRowCount(0); + ui->translationsTable->setRowCount(translation.Struct().size()); + int i = 0; + for(auto & s : translation.Struct()) + { + auto textLines = QString::fromStdString(s.second.String()); + textLines = textLines.replace('\n', "\\n"); + + auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); + auto * wText = new QTableWidgetItem(textLines); + wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); + wText->setFlags(wId->flags() | Qt::ItemIsEditable); + ui->translationsTable->setItem(i, 0, wId); + ui->translationsTable->setItem(i++, 1, wText); + } + ui->translationsTable->resizeColumnToContents(0); + ui->translationsTable->blockSignals(false); +} + +void Translations::on_languageSelect_currentIndexChanged(int index) +{ + auto language = ui->languageSelect->currentData().toString().toStdString(); + bool hasLanguage = mapHeader.translations.Struct().count(language); + ui->supportedCheck->blockSignals(true); + ui->supportedCheck->setChecked(hasLanguage); + ui->supportedCheck->blockSignals(false); + ui->translationsTable->setEnabled(hasLanguage); + if(hasLanguage) + fillTranslationsTable(language); + else + ui->translationsTable->setRowCount(0); +} + + +void Translations::on_supportedCheck_toggled(bool checked) +{ + auto language = ui->languageSelect->currentData().toString().toStdString(); + auto & translation = mapHeader.translations[language]; + bool hasRecord = !translation.Struct().empty(); + + if(checked) + { + //copy from default language + translation = mapHeader.translations[mapPreferredLanguage]; + + fillTranslationsTable(language); + ui->translationsTable->setEnabled(true); + } + else + { + bool canRemove = language != mapPreferredLanguage; + if(!canRemove) + { + QMessageBox::information(this, tr("Remove translation"), tr("Default language cannot be removed")); + } + else if(hasRecord) + { + auto sure = QMessageBox::question(this, tr("Remove translation"), tr("All existing text records for this language will be removed. Continue?")); + canRemove = sure != QMessageBox::No; + } + + if(!canRemove) + { + ui->supportedCheck->blockSignals(true); + ui->supportedCheck->setChecked(true); + ui->supportedCheck->blockSignals(false); + return; + } + ui->translationsTable->blockSignals(true); + ui->translationsTable->setRowCount(0); + translation = JsonNode(JsonNode::JsonType::DATA_NULL); + ui->translationsTable->blockSignals(false); + ui->translationsTable->setEnabled(false); + } +} + + +void Translations::on_translationsTable_itemChanged(QTableWidgetItem * item) +{ + assert(item->column() == 1); + + auto language = ui->languageSelect->currentData().toString().toStdString(); + auto & translation = mapHeader.translations[language]; + + assert(!translation.isNull()); + + auto textId = ui->translationsTable->item(item->row(), 0)->text().toStdString(); + assert(!textId.empty()); + if(textId.empty()) + return; + + auto textLines = item->text(); + textLines = textLines.replace("\\n", "\n"); + translation[textId].String() = textLines.toStdString(); +} + diff --git a/mapeditor/mapsettings/translations.h b/mapeditor/mapsettings/translations.h new file mode 100644 index 000000000..85e70ba1b --- /dev/null +++ b/mapeditor/mapsettings/translations.h @@ -0,0 +1,45 @@ +/* + * translations.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/mapping/CMap.h" + +namespace Ui { +class Translations; +} + +class Translations : public QDialog +{ + Q_OBJECT + + void fillTranslationsTable(const std::string & language); + +public: + explicit Translations(CMapHeader & mapHeader, QWidget *parent = nullptr); + ~Translations(); + + //removes unused string IDs from map translations + static void cleanupRemovedItems(CMap & map); + static void cleanupRemovedItems(CMap & map, const std::string & pattern); + +private slots: + void on_languageSelect_currentIndexChanged(int index); + + void on_supportedCheck_toggled(bool checked); + + void on_translationsTable_itemChanged(QTableWidgetItem *item); + +private: + Ui::Translations *ui; + CMapHeader & mapHeader; + std::string mapPreferredLanguage; +}; diff --git a/mapeditor/mapsettings/translations.ui b/mapeditor/mapsettings/translations.ui new file mode 100644 index 000000000..41b6b8578 --- /dev/null +++ b/mapeditor/mapsettings/translations.ui @@ -0,0 +1,84 @@ + + + Translations + + + + 0 + 0 + 989 + 641 + + + + Map translations + + + true + + + + + + + + + 0 + 0 + + + + Language + + + + + + + + 0 + 0 + + + + + + + + Suppported + + + + + + + + + 240 + + + true + + + false + + + 24 + + + + String ID + + + + + Text + + + + + + + + + diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp new file mode 100644 index 000000000..5b4efe9e6 --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -0,0 +1,555 @@ +/* + * victoryconditions.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 "StdInc.h" +#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 + +VictoryConditions::VictoryConditions(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::VictoryConditions) +{ + ui->setupUi(this); +} + +void VictoryConditions::initialize(MapController & c) +{ + AbstractSettings::initialize(c); + + //victory message + ui->victoryMessageEdit->setText(QString::fromStdString(controller->map()->victoryMessage.toString())); + + //victory conditions + const std::array conditionStringsWin = { + QT_TR_NOOP("No special victory"), + QT_TR_NOOP("Capture artifact"), + QT_TR_NOOP("Hire creatures"), + QT_TR_NOOP("Accumulate resources"), + QT_TR_NOOP("Construct building"), + QT_TR_NOOP("Capture town"), + QT_TR_NOOP("Defeat hero"), + QT_TR_NOOP("Transport artifact"), + QT_TR_NOOP("Kill monster") + }; + + for(auto & s : conditionStringsWin) + { + ui->victoryComboBox->addItem(QString::fromStdString(s)); + } + ui->standardVictoryCheck->setChecked(false); + ui->onlyForHumansCheck->setChecked(false); + + for(auto & ev : controller->map()->triggeredEvents) + { + if(ev.effect.type == EventEffect::VICTORY) + { + if(ev.identifier == "standardVictory") + ui->standardVictoryCheck->setChecked(true); + + if(ev.identifier == "specialVictory") + { + auto readjson = ev.trigger.toJson(AbstractSettings::conditionToJson); + auto linearNodes = linearJsonArray(readjson); + + for(auto & json : linearNodes) + { + switch(json["condition"].Integer()) + { + case EventCondition::HAVE_ARTIFACT: { + ui->victoryComboBox->setCurrentIndex(1); + assert(victoryTypeWidget); + victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); + break; + } + + case EventCondition::HAVE_CREATURES: { + ui->victoryComboBox->setCurrentIndex(2); + assert(victoryTypeWidget); + assert(victoryValueWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + victoryValueWidget->setText(QString::number(json["value"].Integer())); + break; + } + + case EventCondition::HAVE_RESOURCES: { + ui->victoryComboBox->setCurrentIndex(3); + assert(victoryTypeWidget); + assert(victoryValueWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + victoryValueWidget->setText(QString::number(json["value"].Integer())); + break; + } + + case EventCondition::HAVE_BUILDING: { + ui->victoryComboBox->setCurrentIndex(4); + assert(victoryTypeWidget); + assert(victorySelectWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victorySelectWidget->findData(townIdx); + victorySelectWidget->setCurrentIndex(idx); + } + break; + } + + case EventCondition::CONTROL: { + ui->victoryComboBox->setCurrentIndex(5); + assert(victoryTypeWidget); + if(json["objectType"].Integer() == Obj::TOWN) + { + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victoryTypeWidget->findData(townIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } + //TODO: support control other objects (dwellings, mines) + break; + } + + case EventCondition::DESTROY: { + if(json["objectType"].Integer() == Obj::HERO) + { + ui->victoryComboBox->setCurrentIndex(6); + assert(victoryTypeWidget); + int heroIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(heroIdx >= 0) + { + auto idx = victoryTypeWidget->findData(heroIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } + if(json["objectType"].Integer() == Obj::MONSTER) + { + ui->victoryComboBox->setCurrentIndex(8); + assert(victoryTypeWidget); + int monsterIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(monsterIdx >= 0) + { + auto idx = victoryTypeWidget->findData(monsterIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } + break; + } + + case EventCondition::TRANSPORT: { + ui->victoryComboBox->setCurrentIndex(7); + assert(victoryTypeWidget); + assert(victorySelectWidget); + victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victorySelectWidget->findData(townIdx); + victorySelectWidget->setCurrentIndex(idx); + } + break; + } + + case EventCondition::IS_HUMAN: { + ui->onlyForHumansCheck->setChecked(true); + break; + } + }; + } + } + } + } +} + +void VictoryConditions::update() +{ + //victory messages + bool customMessage = true; + + //victory conditions + EventCondition victoryCondition(EventCondition::STANDARD_WIN); + + //Victory condition - defeat all + TriggeredEvent standardVictory; + standardVictory.effect.type = EventEffect::VICTORY; + standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + standardVictory.identifier = "standardVictory"; + standardVictory.description.clear(); // TODO: display in quest window + standardVictory.onFulfill.appendTextID("core.genrltxt.659"); + standardVictory.trigger = EventExpression(victoryCondition); + + //VICTORY + if(ui->victoryComboBox->currentIndex() == 0) + { + controller->map()->triggeredEvents.push_back(standardVictory); + controller->map()->victoryIconIndex = 11; + controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc.0"); + customMessage = false; + } + else + { + int vicCondition = ui->victoryComboBox->currentIndex() - 1; + + TriggeredEvent specialVictory; + specialVictory.effect.type = EventEffect::VICTORY; + specialVictory.identifier = "specialVictory"; + specialVictory.description.clear(); // TODO: display in quest window + + controller->map()->victoryIconIndex = vicCondition; + controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc." + std::to_string(vicCondition + 1)); + customMessage = false; + + switch(vicCondition) + { + case 0: { + EventCondition cond(EventCondition::HAVE_ARTIFACT); + assert(victoryTypeWidget); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); + specialVictory.onFulfill.appendTextID("core.genrltxt.280"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 1: { + EventCondition cond(EventCondition::HAVE_CREATURES); + assert(victoryTypeWidget); + cond.objectType = CreatureID(victoryTypeWidget->currentData().toInt()); + cond.value = victoryValueWidget->text().toInt(); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); + specialVictory.onFulfill.appendTextID("core.genrltxt.276"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 2: { + EventCondition cond(EventCondition::HAVE_RESOURCES); + assert(victoryTypeWidget); + cond.objectType = GameResID(victoryTypeWidget->currentData().toInt()); + cond.value = victoryValueWidget->text().toInt(); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); + specialVictory.onFulfill.appendTextID("core.genrltxt.278"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 3: { + EventCondition cond(EventCondition::HAVE_BUILDING); + assert(victoryTypeWidget); + cond.objectType = BuildingID(victoryTypeWidget->currentData().toInt()); + int townIdx = victorySelectWidget->currentData().toInt(); + if(townIdx > -1) + cond.position = controller->map()->objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); + specialVictory.onFulfill.appendTextID("core.genrltxt.282"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 4: { + EventCondition cond(EventCondition::CONTROL); + assert(victoryTypeWidget); + cond.objectType = Obj(Obj::TOWN); + int townIdx = victoryTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); + specialVictory.onFulfill.appendTextID("core.genrltxt.249"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 5: { + EventCondition cond(EventCondition::DESTROY); + assert(victoryTypeWidget); + cond.objectType = Obj(Obj::HERO); + int heroIdx = victoryTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[heroIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); + specialVictory.onFulfill.appendTextID("core.genrltxt.252"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 6: { + EventCondition cond(EventCondition::TRANSPORT); + assert(victoryTypeWidget); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); + int townIdx = victorySelectWidget->currentData().toInt(); + if(townIdx > -1) + cond.position = controller->map()->objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); + specialVictory.onFulfill.appendTextID("core.genrltxt.292"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 7: { + EventCondition cond(EventCondition::DESTROY); + assert(victoryTypeWidget); + cond.objectType = Obj(Obj::MONSTER); + int monsterIdx = victoryTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[monsterIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); + specialVictory.onFulfill.appendTextID("core.genrltxt.286"); + specialVictory.trigger = EventExpression(cond); + break; + } + + } + + // if condition is human-only turn it into following construction: AllOf(human, condition) + if(ui->onlyForHumansCheck->isChecked()) + { + EventExpression::OperatorAll oper; + EventCondition notAI(EventCondition::IS_HUMAN); + notAI.value = 1; + oper.expressions.push_back(notAI); + oper.expressions.push_back(specialVictory.trigger.get()); + specialVictory.trigger = EventExpression(oper); + } + + // if normal victory allowed - add one more quest + if(ui->standardVictoryCheck->isChecked()) + { + controller->map()->victoryMessage.appendRawString(" / "); + controller->map()->victoryMessage.appendTextID("core.vcdesc.0"); + controller->map()->triggeredEvents.push_back(standardVictory); + customMessage = false; + } + controller->map()->triggeredEvents.push_back(specialVictory); + } + + if(customMessage) + { + controller->map()->victoryMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "victoryMessage"), ui->victoryMessageEdit->text().toStdString())); + } +} + +VictoryConditions::~VictoryConditions() +{ + delete ui; +} + +void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) +{ + delete victoryTypeWidget; + delete victoryValueWidget; + delete victorySelectWidget; + delete pickObjectButton; + victoryTypeWidget = nullptr; + victoryValueWidget = nullptr; + victorySelectWidget = nullptr; + pickObjectButton = nullptr; + + if(index == 0) + { + ui->standardVictoryCheck->setChecked(true); + ui->standardVictoryCheck->setEnabled(false); + ui->onlyForHumansCheck->setChecked(false); + ui->onlyForHumansCheck->setEnabled(false); + return; + } + ui->onlyForHumansCheck->setEnabled(true); + ui->standardVictoryCheck->setEnabled(true); + + int vicCondition = index - 1; + switch(vicCondition) + { + case 0: { //EventCondition::HAVE_ARTIFACT + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < controller->map()->allowedArtifact.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); + break; + } + + case 1: { //EventCondition::HAVE_CREATURES + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < VLC->creh->objects.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->creh->objects[i]->getNamePluralTranslated()), QVariant::fromValue(i)); + + victoryValueWidget = new QLineEdit; + ui->victoryParamsLayout->addWidget(victoryValueWidget); + victoryValueWidget->setText("1"); + break; + } + + case 2: { //EventCondition::HAVE_RESOURCES + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + { + for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType) + { + auto resName = QString::fromStdString(GameConstants::RESOURCE_NAMES[resType]); + victoryTypeWidget->addItem(resName, QVariant::fromValue(resType)); + } + } + + victoryValueWidget = new QLineEdit; + ui->victoryParamsLayout->addWidget(victoryValueWidget); + victoryValueWidget->setText("1"); + break; + } + + case 3: { //EventCondition::HAVE_BUILDING + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + auto * ctown = VLC->townh->randomTown; + for(int bId : ctown->getAllBuildings()) + victoryTypeWidget->addItem(QString::fromStdString(defaultBuildingIdConversion(BuildingID(bId))), QVariant::fromValue(bId)); + + victorySelectWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victorySelectWidget); + victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); + for(int i : getObjectIndexes(*controller->map())) + victorySelectWidget->addItem(getTownName(*controller->map(), i).c_str(), QVariant::fromValue(i)); + + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); + break; + } + + case 4: { //EventCondition::CONTROL (Obj::TOWN) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); + break; + } + + case 5: { //EventCondition::DESTROY (Obj::HERO) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); + break; + } + + case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < controller->map()->allowedArtifact.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); + + victorySelectWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victorySelectWidget); + for(int i : getObjectIndexes(*controller->map())) + victorySelectWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); + break; + } + + case 7: { //EventCondition::DESTROY (Obj::MONSTER) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getMonsterName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); + break; + } + } +} + + +void VictoryConditions::onObjectSelect() +{ + int vicConditions = ui->victoryComboBox->currentIndex() - 1; + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + switch(vicConditions) + { + case 3: { //EventCondition::HAVE_BUILDING + l.highlight(); + break; + } + + case 4: { //EventCondition::CONTROL (Obj::TOWN) + l.highlight(); + break; + } + + case 5: { //EventCondition::DESTROY (Obj::HERO) + l.highlight(); + break; + } + + case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) + l.highlight(); + break; + } + + case 7: { //EventCondition::DESTROY (Obj::MONSTER) + l.highlight(); + break; + } + default: + return; + } + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &VictoryConditions::onObjectPicked); + } + + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); +} + +void VictoryConditions::onObjectPicked(const CGObjectInstance * obj) +{ + 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, &VictoryConditions::onObjectPicked); + } + + if(!obj) //discarded + return; + + int vicConditions = ui->victoryComboBox->currentIndex() - 1; + QComboBox * w = victoryTypeWidget; + if(vicConditions == 3 || vicConditions == 6) + w = victorySelectWidget; + + for(int i = 0; i < w->count(); ++i) + { + if(w->itemData(i).toInt() < 0) + continue; + + auto data = controller->map()->objects.at(w->itemData(i).toInt()); + if(data == obj) + { + w->setCurrentIndex(i); + break; + } + } +} diff --git a/mapeditor/mapsettings/victoryconditions.h b/mapeditor/mapsettings/victoryconditions.h new file mode 100644 index 000000000..5754b28ce --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.h @@ -0,0 +1,43 @@ +/* + * victoryconditions.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 "abstractsettings.h" + +namespace Ui { +class VictoryConditions; +} + +class VictoryConditions : public AbstractSettings +{ + Q_OBJECT + +public: + explicit VictoryConditions(QWidget *parent = nullptr); + ~VictoryConditions(); + + void initialize(MapController & map) override; + void update() override; + +public slots: + void onObjectSelect(); + void onObjectPicked(const CGObjectInstance *); + +private slots: + void on_victoryComboBox_currentIndexChanged(int index); + +private: + Ui::VictoryConditions *ui; + + QComboBox * victoryTypeWidget = nullptr; + QComboBox * victorySelectWidget = nullptr; + QLineEdit * victoryValueWidget = nullptr; + QToolButton * pickObjectButton = nullptr; +}; diff --git a/mapeditor/mapsettings/victoryconditions.ui b/mapeditor/mapsettings/victoryconditions.ui new file mode 100644 index 000000000..d1c86ea47 --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.ui @@ -0,0 +1,95 @@ + + + VictoryConditions + + + + 0 + 0 + 622 + 503 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + 0 + + + + + Victory message + + + + + + + true + + + + + + + + + + + + Only for human players + + + + + + + Allow standard victory + + + + + + + + 0 + 0 + + + + Parameters + + + + 12 + + + + + + + + + + + + diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 87775258a..52cb9f373 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -44,15 +44,9 @@ void MinimapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(!sc) return; - int w = sc->viewport.viewportWidth(); - int h = sc->viewport.viewportHeight(); auto pos = mapToScene(mouseEvent->pos()); - pos.setX(pos.x() - w / 2); - pos.setY(pos.y() - h / 2); - - QPointF point = pos * 32; - - emit cameraPositionChanged(point); + pos *= 32; + emit cameraPositionChanged(pos); } void MinimapView::mousePressEvent(QMouseEvent* event) @@ -68,8 +62,7 @@ MapView::MapView(QWidget * parent): void MapView::cameraChanged(const QPointF & pos) { - horizontalScrollBar()->setValue(pos.x()); - verticalScrollBar()->setValue(pos.y()); + centerOn(pos); } void MapView::setController(MapController * ctrl) @@ -96,10 +89,7 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(tile == tilePrev) //do not redraw return; - tilePrev = tile; - - //TODO: cast parent->parent to MainWindow in order to show coordinates or another way to do it? - //main->setStatusMessage(QString("x: %1 y: %2").arg(tile.x, tile.y)); + emit currentCoordinates(tile.x, tile.y); switch(selectionTool) { @@ -159,10 +149,78 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) sc->selectionTerrainView.draw(); break; + case MapView::SelectionTool::Line: + { + assert(tile.z == tileStart.z); + const auto diff = tile - tileStart; + if(diff == int3{}) + break; + + const int edge = std::max(abs(diff.x), abs(diff.y)); + int distMin = std::numeric_limits::max(); + int3 dir; + + for(auto & d : int3::getDirs()) + { + if(tile.dist2d(d * edge + tileStart) < distMin) + { + distMin = tile.dist2d(d * edge + tileStart); + dir = d; + } + } + + assert(dir != int3{}); + + if(mouseEvent->buttons() == Qt::LeftButton) + { + for(auto & ts : temporaryTiles) + sc->selectionTerrainView.erase(ts); + + for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) + { + if(!controller->map()->isInTheMap(ts)) + break; + if(!sc->selectionTerrainView.selection().count(ts)) + temporaryTiles.insert(ts); + sc->selectionTerrainView.select(ts); + } + } + if(mouseEvent->buttons() == Qt::RightButton) + { + for(auto & ts : temporaryTiles) + sc->selectionTerrainView.select(ts); + + for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) + { + if(!controller->map()->isInTheMap(ts)) + break; + if(sc->selectionTerrainView.selection().count(ts)) + temporaryTiles.insert(ts); + sc->selectionTerrainView.erase(ts); + } + } + sc->selectionTerrainView.draw(); + break; + } + case MapView::SelectionTool::Lasso: if(mouseEvent->buttons() == Qt::LeftButton) { - sc->selectionTerrainView.select(tile); + for(auto i = tilePrev; i != tile;) + { + int length = std::numeric_limits::max(); + int3 dir; + for(auto & d : int3::getDirs()) + { + if(tile.dist2dSQ(i + d) < length) + { + dir = d; + length = tile.dist2dSQ(i + d); + } + } + i += dir; + sc->selectionTerrainView.select(i); + } sc->selectionTerrainView.draw(); } break; @@ -189,6 +247,8 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) sc->selectionObjectsView.draw(); break; } + + tilePrev = tile; } void MapView::mousePressEvent(QMouseEvent *event) @@ -198,6 +258,9 @@ void MapView::mousePressEvent(QMouseEvent *event) auto * sc = static_cast(scene()); if(!sc || !controller->map()) return; + + if(sc->objectPickerView.isVisible()) + return; mouseStart = mapToScene(event->pos()); tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level); @@ -210,6 +273,7 @@ void MapView::mousePressEvent(QMouseEvent *event) switch(selectionTool) { case MapView::SelectionTool::Brush: + case MapView::SelectionTool::Line: sc->selectionObjectsView.clear(); sc->selectionObjectsView.draw(); @@ -267,6 +331,55 @@ void MapView::mousePressEvent(QMouseEvent *event) sc->selectionObjectsView.clear(); sc->selectionObjectsView.draw(); break; + + case MapView::SelectionTool::Fill: + { + if(event->button() != Qt::RightButton && event->button() != Qt::LeftButton) + break; + + std::vector queue; + queue.push_back(tileStart); + + const std::array dirs{ int3{1, 0, 0}, int3{-1, 0, 0}, int3{0, 1, 0}, int3{0, -1, 0} }; + + while(!queue.empty()) + { + auto tile = queue.back(); + queue.pop_back(); + if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(tile); + else + sc->selectionTerrainView.erase(tile); + for(auto & d : dirs) + { + auto tilen = tile + d; + if(!controller->map()->isInTheMap(tilen)) + continue; + if(event->button() == Qt::LeftButton) + { + if(controller->map()->getTile(tile).roadType + && controller->map()->getTile(tile).roadType != controller->map()->getTile(tilen).roadType) + continue; + 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) + continue; + } + if(event->button() == Qt::LeftButton && sc->selectionTerrainView.selection().count(tilen)) + continue; + if(event->button() == Qt::RightButton && !sc->selectionTerrainView.selection().count(tilen)) + continue; + queue.push_back(tilen); + } + } + + + sc->selectionTerrainView.draw(); + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + break; + } case MapView::SelectionTool::None: sc->selectionTerrainView.clear(); @@ -338,12 +451,45 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) if(rubberBand) rubberBand->hide(); + + if(sc->objectPickerView.isVisible()) + { + if(event->button() == Qt::RightButton) + sc->objectPickerView.discard(); + + if(event->button() == Qt::LeftButton) + { + mouseStart = mapToScene(event->pos()); + tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level); + if(auto * pickedObject = sc->selectionObjectsView.selectObjectAt(tileStart.x, tileStart.y)) + sc->objectPickerView.select(pickedObject); + } + + return; + } switch(selectionTool) { case MapView::SelectionTool::Lasso: { if(event->button() == Qt::RightButton) break; + + //connect with initial tile + for(auto i = tilePrev; i != tileStart;) + { + int length = std::numeric_limits::max(); + int3 dir; + for(auto & d : int3::getDirs()) + { + if(tileStart.dist2dSQ(i + d) < length) + { + dir = d; + length = tileStart.dist2dSQ(i + d); + } + } + i += dir; + sc->selectionTerrainView.select(i); + } //key: y position of tile //value.first: x position of left tile @@ -390,6 +536,10 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) break; } + case MapView::SelectionTool::Line: + temporaryTiles.clear(); + break; + case MapView::SelectionTool::None: if(event->button() == Qt::RightButton) break; @@ -397,6 +547,7 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) bool tab = false; if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::MOVEMENT) { + tab = sc->selectionObjectsView.shift.isNull(); controller->commitObjectShift(sc->level); } else @@ -405,7 +556,6 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) sc->selectionObjectsView.shift = QPoint(0, 0); sc->selectionObjectsView.draw(); tab = true; - //check if we have only one object } auto selection = sc->selectionObjectsView.getSelection(); if(selection.size() == 1) @@ -463,7 +613,9 @@ void MapView::dropEvent(QDropEvent * event) QString errorMsg; if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg)) { + auto * obj = sc->selectionObjectsView.newObject; controller->commitObjectCreate(sc->level); + emit openObjectProperties(obj, false); } else { @@ -545,6 +697,7 @@ MapScene::MapScene(int lvl): terrainView(this), objectsView(this), selectionObjectsView(this), + objectPickerView(this), isTerrainSelected(false), isObjectSelected(false) { @@ -560,6 +713,7 @@ std::list MapScene::getAbstractLayers() &objectsView, &gridView, &passabilityView, + &objectPickerView, &selectionTerrainView, &selectionObjectsView }; @@ -573,6 +727,7 @@ void MapScene::updateViews() objectsView.show(true); selectionTerrainView.show(true); selectionObjectsView.show(true); + objectPickerView.show(true); } void MapScene::terrainSelected(bool anythingSelected) diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index 009f40ac6..c99785647 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -66,6 +66,7 @@ public: TerrainLayer terrainView; ObjectsLayer objectsView; SelectionObjectsLayer selectionObjectsView; + ObjectPickerLayer objectPickerView; signals: void selected(bool anything); @@ -88,7 +89,7 @@ class MapView : public QGraphicsView public: enum class SelectionTool { - None, Brush, Brush2, Brush4, Area, Lasso + None, Brush, Brush2, Brush4, Area, Lasso, Line, Fill }; public: @@ -110,6 +111,7 @@ public slots: signals: void openObjectProperties(CGObjectInstance *, bool switchTab); + void currentCoordinates(int, int); //void viewportChanged(const QRectF & rect); protected: @@ -122,6 +124,8 @@ private: int3 tileStart; int3 tilePrev; bool pressedOnSelected; + + std::set temporaryTiles; }; class MinimapView : public QGraphicsView diff --git a/mapeditor/objectbrowser.cpp b/mapeditor/objectbrowser.cpp index c86b9aecc..beb9850be 100644 --- a/mapeditor/objectbrowser.cpp +++ b/mapeditor/objectbrowser.cpp @@ -76,8 +76,12 @@ bool ObjectBrowserProxyModel::filterAcceptsRowText(int source_row, const QModelI auto item = dynamic_cast(sourceModel())->itemFromIndex(index); if(!item) return false; + + auto data = item->data().toJsonObject(); - return (filter.isNull() || filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)); + return (filter.isNull() || filter.isEmpty() + || item->text().contains(filter, Qt::CaseInsensitive) + || data["typeName"].toString().contains(filter, Qt::CaseInsensitive)); } Qt::ItemFlags ObjectBrowserProxyModel::flags(const QModelIndex & index) const diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index ebf29c9b2..97a869f39 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -11,8 +11,9 @@ #include "StdInc.h" #include "playerparams.h" #include "ui_playerparams.h" +#include "mapsettings/abstractsettings.h" #include "../lib/CTownHandler.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" @@ -87,7 +88,8 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) { if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos) foundMainTown = townIndex; - const auto name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; + + const auto name = AbstractSettings::getTownName(*controller.map(), i); ui->mainTown->addItem(tr(name.c_str()), QVariant::fromValue(i)); ++townIndex; } @@ -149,8 +151,7 @@ void PlayerParams::allowedFactionsCheck(QListWidgetItem * item) playerInfo.allowedFactions.erase(FactionID(item->data(Qt::UserRole).toInt())); } - -void PlayerParams::on_mainTown_activated(int index) +void PlayerParams::on_mainTown_currentIndexChanged(int index) { if(index == 0) //default { @@ -171,7 +172,7 @@ void PlayerParams::on_mainTown_activated(int index) void PlayerParams::on_teamId_activated(int index) { - playerInfo.team = ui->teamId->currentData().toInt(); + playerInfo.team.setNum(ui->teamId->currentData().toInt()); } @@ -186,3 +187,50 @@ void PlayerParams::on_playerColorCombo_activated(int index) } } + +void PlayerParams::on_townSelect_clicked() +{ + auto pred = [this](const CGObjectInstance * obj) -> bool + { + if(auto town = dynamic_cast(obj)) + return town->getOwner().getNum() == playerColor; + return false; + }; + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(pred); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); + } + + dynamic_cast(parent()->parent()->parent()->parent())->hide(); +} + +void PlayerParams::onTownPicked(const CGObjectInstance * obj) +{ + dynamic_cast(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, &PlayerParams::onTownPicked); + } + + if(!obj) //discarded + return; + + for(int i = 0; i < ui->mainTown->count(); ++i) + { + auto town = controller.map()->objects.at(ui->mainTown->itemData(i).toInt()); + if(town == obj) + { + ui->mainTown->setCurrentIndex(i); + break; + } + } +} + diff --git a/mapeditor/playerparams.h b/mapeditor/playerparams.h index 2aaff5384..57601d2c3 100644 --- a/mapeditor/playerparams.h +++ b/mapeditor/playerparams.h @@ -28,13 +28,15 @@ public: PlayerInfo playerInfo; int playerColor; + + void onTownPicked(const CGObjectInstance *); private slots: void on_radioHuman_toggled(bool checked); void on_radioCpu_toggled(bool checked); - void on_mainTown_activated(int index); + void on_mainTown_currentIndexChanged(int index); void on_generateHero_stateChanged(int arg1); @@ -46,6 +48,8 @@ private slots: void on_playerColorCombo_activated(int index); + void on_townSelect_clicked(); + private: Ui::PlayerParams *ui; diff --git a/mapeditor/playerparams.ui b/mapeditor/playerparams.ui index 811fd626e..d2947a2a2 100644 --- a/mapeditor/playerparams.ui +++ b/mapeditor/playerparams.ui @@ -6,7 +6,7 @@ 0 0 - 505 + 614 160 @@ -25,7 +25,7 @@ - + 0 @@ -49,136 +49,180 @@ - - - - - - 0 - 0 - - - + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + CPU only + + + + + + + + 0 + 0 + + + + Human/CPU + + + + + + + Team + + + + + + + + 0 + 0 + + + + + - - - - - 0 - 0 - - - - Generate hero at main - - + + + + + + Color + + + + + + + + 0 + 0 + + + + + + + + Main town + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + (default) + + + + + + + + ... + + + + + + + + + + 0 + 0 + + + + Generate hero at main + + + + - - - - - 0 - 0 - - - - Random faction - - - - - - - Team - - - - - - - - 0 - 0 - - - - CPU only - - - - - - - - 0 - 0 - - - - Human/CPU - - - - - - - true - - - - 0 - 0 - - - - Qt::ClickFocus - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - - - - - - 0 - 0 - + + + + 0 - - (default) - + + + true + + + + 0 + 0 + + + + Qt::ClickFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + - - - - - - Main town - - - - - - - - 0 - 0 - - - - - - - - Color - - + + + + + 0 + 0 + + + + Random faction + + + +
    diff --git a/mapeditor/resourceExtractor/ResourceConverter.cpp b/mapeditor/resourceExtractor/ResourceConverter.cpp index 6cdecc345..a6481ed58 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.cpp +++ b/mapeditor/resourceExtractor/ResourceConverter.cpp @@ -19,13 +19,13 @@ #include "BitmapHandler.h" #include "Animation.h" -#include "boost/filesystem/path.hpp" -#include "boost/locale.hpp" +#include +#include void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversionOptions) { - bfs::path spritesPath = VCMIDirs::get().userExtractedPath() / "SPRITES"; - bfs::path imagesPath = VCMIDirs::get().userExtractedPath() / "IMAGES"; + boost::filesystem::path spritesPath = VCMIDirs::get().userExtractedPath() / "SPRITES"; + boost::filesystem::path imagesPath = VCMIDirs::get().userExtractedPath() / "IMAGES"; std::vector defFiles = { "TwCrPort.def", "CPRSMALL.def", "FlagPort.def", "ITPA.def", "ITPt.def", "Un32.def", "Un44.def" }; if(conversionOptions.splitDefs) @@ -35,16 +35,16 @@ void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversi doConvertPcxToPng(imagesPath, conversionOptions.deleteOriginals); } -void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::doConvertPcxToPng(const boost::filesystem::path & sourceFolder, bool deleteOriginals) { logGlobal->info("Converting .pcx to .png from folder: %s ...\n", sourceFolder); - for(const auto & directoryEntry : bfs::directory_iterator(sourceFolder)) + for(const auto & directoryEntry : boost::filesystem::directory_iterator(sourceFolder)) { const auto filename = directoryEntry.path().filename(); try { - if(!bfs::is_regular_file(directoryEntry)) + if(!boost::filesystem::is_regular_file(directoryEntry)) continue; std::string fileStem = directoryEntry.path().stem().string(); @@ -53,11 +53,11 @@ void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool d if(boost::algorithm::to_lower_copy(filename.extension().string()) == ".pcx") { auto img = BitmapHandler::loadBitmap(filenameLowerCase); - bfs::path pngFilePath = sourceFolder / (fileStem + ".png"); + boost::filesystem::path pngFilePath = sourceFolder / (fileStem + ".png"); img.save(pathToQString(pngFilePath), "PNG"); if(deleteOriginals) - bfs::remove(directoryEntry.path()); + boost::filesystem::remove(directoryEntry.path()); } } catch(const std::exception& ex) @@ -67,22 +67,22 @@ void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool d } } -void ResourceConverter::splitDefFile(const std::string & fileName, const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::splitDefFile(const std::string & fileName, const boost::filesystem::path & sourceFolder, bool deleteOriginals) { - if(CResourceHandler::get()->existsResource(ResourceID("SPRITES/" + fileName))) + if(CResourceHandler::get()->existsResource(ResourcePath("SPRITES/" + fileName))) { std::unique_ptr anim = std::make_unique(fileName); anim->preload(); anim->exportBitmaps(pathToQString(sourceFolder)); if(deleteOriginals) - bfs::remove(sourceFolder / fileName); + boost::filesystem::remove(sourceFolder / fileName); } else logGlobal->error("Def File Split error! " + fileName); } -void ResourceConverter::splitDefFiles(const std::vector & defFileNames, const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::splitDefFiles(const std::vector & defFileNames, const boost::filesystem::path & sourceFolder, bool deleteOriginals) { logGlobal->info("Splitting Def Files from folder: %s ...\n", sourceFolder); diff --git a/mapeditor/resourceExtractor/ResourceConverter.h b/mapeditor/resourceExtractor/ResourceConverter.h index cb1035562..faef5abdd 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.h +++ b/mapeditor/resourceExtractor/ResourceConverter.h @@ -9,8 +9,6 @@ */ #pragma once -namespace bfs = boost::filesystem; - // Struct for holding all Convertor Options struct ConversionOptions { @@ -45,14 +43,14 @@ public: private: // Converts all .pcx from extractedFolder/Images into .png - static void doConvertPcxToPng(const bfs::path & sourceFolder, bool deleteOriginals); + static void doConvertPcxToPng(const boost::filesystem::path & sourceFolder, bool deleteOriginals); // splits a .def file into individual images and converts the output to PNG format - static void splitDefFile(const std::string & fileName, const bfs::path & sourceFolder, bool deleteOriginals); + static void splitDefFile(const std::string & fileName, const boost::filesystem::path & sourceFolder, bool deleteOriginals); /// /// Splits the given .def files into individual images. /// For each .def file, the resulting images will be output in the same folder, in a subfolder (named just like the .def file) /// - static void splitDefFiles(const std::vector & defFileNames, const bfs::path & sourceFolder, bool deleteOriginals); + static void splitDefFiles(const std::vector & defFileNames, const boost::filesystem::path & sourceFolder, bool deleteOriginals); }; diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 5b4076608..33e8997e8 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -112,6 +112,86 @@ void PassabilityLayer::update() redraw(); } +ObjectPickerLayer::ObjectPickerLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void ObjectPickerLayer::highlight(std::function predicate) +{ + if(!map) + return; + + if(scene->level == 0 || map->twoLevel) + { + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + auto tl = map->getTile(int3(i, j, scene->level)); + auto * obj = tl.topVisitableObj(); + if(!obj && !tl.blockingObjects.empty()) + obj = tl.blockingObjects.front(); + + if(obj && predicate(obj)) + possibleObjects.insert(obj); + } + } + } + + isActive = true; +} + +bool ObjectPickerLayer::isVisible() const +{ + return isShown && isActive; +} + +void ObjectPickerLayer::clear() +{ + possibleObjects.clear(); + isActive = false; +} + +void ObjectPickerLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(Qt::transparent); + if(isActive) + pixmap->fill(QColor(255, 255, 255, 128)); + + + QPainter painter(pixmap.get()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + for(auto * obj : possibleObjects) + { + if(obj->pos.z != scene->level) + continue; + + for(auto & pos : obj->getBlockedPos()) + painter.fillRect(pos.x * 32, pos.y * 32, 32, 32, QColor(255, 211, 0, 64)); + } + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + redraw(); +} + +void ObjectPickerLayer::select(const CGObjectInstance * obj) +{ + if(obj && possibleObjects.count(obj)) + { + clear(); + emit selectionMade(obj); + } +} + +void ObjectPickerLayer::discard() +{ + clear(); + emit selectionMade(nullptr); +} + SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractLayer(s) { } @@ -297,8 +377,7 @@ void ObjectsLayer::draw(bool onlyDirty) return; QPainter painter(pixmap.get()); - std::set drawen; - + if(onlyDirty) { //objects could be modified @@ -312,7 +391,7 @@ void ObjectsLayer::draw(bool onlyDirty) painter.setCompositionMode(QPainter::CompositionMode_SourceOver); for(auto & p : dirty) - handler->drawObjects(painter, p.x, p.y, p.z); + handler->drawObjects(painter, p.x, p.y, p.z, lockedObjects); } else { @@ -321,7 +400,7 @@ void ObjectsLayer::draw(bool onlyDirty) { for(int i = 0; i < map->width; ++i) { - handler->drawObjects(painter, i, j, scene->level); + handler->drawObjects(painter, i, j, scene->level, lockedObjects); } } } @@ -350,6 +429,19 @@ void ObjectsLayer::setDirty(const CGObjectInstance * object) } } +void ObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) +{ + if(lock) + lockedObjects.insert(object); + else + lockedObjects.erase(object); +} + +void ObjectsLayer::unlockAll() +{ + lockedObjects.clear(); +} + SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractLayer(s), newObject(nullptr) { } @@ -421,36 +513,36 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO //visitable is most important for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->visitableAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } //if not visitable tile - try to get blocked for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->blockingAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } //finally, we can take any object for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->coveringAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } @@ -475,7 +567,8 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) if(map->isInTheMap(int3(i, j, scene->level))) { for(auto & o : handler->getObjects(i, j, scene->level)) - selectObject(o.obj, false); //do not inform about each object added + if(!lockedObjects.count(o.obj)) + selectObject(const_cast(o.obj), false); //do not inform about each object added } } } @@ -519,6 +612,19 @@ void SelectionObjectsLayer::onSelection() emit selectionMade(!selectedObjects.empty()); } +void SelectionObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) +{ + if(lock) + lockedObjects.insert(object); + else + lockedObjects.erase(object); +} + +void SelectionObjectsLayer::unlockAll() +{ + lockedObjects.clear(); +} + MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractLayer(s) { diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index 3d3cd6a3f..92add8ede 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -116,17 +116,52 @@ public: void update() override; - void draw(bool onlyDirty = true); //TODO: implement dirty + void draw(bool onlyDirty = true); void setDirty(int x, int y); void setDirty(const CGObjectInstance * object); + + void setLockObject(const CGObjectInstance * object, bool lock); + void unlockAll(); private: std::set objDirty; + std::set lockedObjects; std::set dirty; }; +class ObjectPickerLayer: public AbstractLayer +{ + Q_OBJECT +public: + ObjectPickerLayer(MapSceneBase * s); + + void update() override; + bool isVisible() const; + + template + void highlight() + { + highlight([](const CGObjectInstance * o){ return dynamic_cast(o); }); + } + + void highlight(std::function predicate); + + void clear(); + + void select(const CGObjectInstance *); + void discard(); + +signals: + void selectionMade(const CGObjectInstance *); + +private: + bool isActive = false; + std::set possibleObjects; +}; + + class SelectionObjectsLayer: public AbstractLayer { Q_OBJECT @@ -149,6 +184,9 @@ public: bool isSelected(const CGObjectInstance *) const; std::set getSelection() const; void clear(); + + void setLockObject(const CGObjectInstance * object, bool lock); + void unlockAll(); QPoint shift; CGObjectInstance * newObject; @@ -160,6 +198,7 @@ signals: private: std::set selectedObjects; + std::set lockedObjects; void onSelection(); }; diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 1c026f635..052ea7d2f 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings - + Wide formation - + Tight formation + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + + + + + Map description + + + + + Limit maximum heroes level + + + + + Difficulty + + + GeneratorProgress @@ -27,6 +83,95 @@ + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + + + + + 7 days without town + + + + + Parameters + + + + + No special loss + + + + + Lose castle + + + + + Lose hero + + + + + Time expired + + + + + Days without town + + + MainWindow @@ -40,546 +185,499 @@ - + Map - + Edit - + View - + Player - + Toolbar - + Minimap - + Map Objects View - + Browser - + Inspector - + Property - + Value - - Terrains View + + Tools - - Brush + + Painting - + Terrains - + Roads - + Rivers - + + Preview + + + + Open - + 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? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - - + Save map - - + VCMI maps (*.vmap) - + Type - + View surface - + No objects selected - + This operation is irreversible. Do you want to continue? - - Errors occured. %1 objects were not updated + + 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 + + MapSettings - + Map settings - + General - - Map name - - - - - Map description - - - - - Limit maximum heroes level - - - - - Difficulty - - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events - + Victory - - Victory message - - - - - Only for human players - - - - - Allow standard victory - - - - - - Parameters - - - - + Loss - - 7 days without town + + Timed - - Defeat message + + Rumors - + Abilities - + Spells - + Artifacts - + Heroes - + Ok - - - No special victory - - - - - Capture artifact - - - - - Hire creatures - - - - - Accumulate resources - - - - - Construct building - - - - - Capture town - - - - - Defeat hero - - - - - Transport artifact - - - - - No special loss - - - - - Lose castle - - - - - Lose hero - - - - - Time expired - - - - - Days without town - - MapView - + Can't place object @@ -587,55 +685,108 @@ MessageWidget - + Message + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + PlayerParams - + Human/CPU - + CPU only - + Team - + Main town - + Color - + + ... + + + + Random faction - + Generate hero at main - + (default) - + Player ID: %1 @@ -663,45 +814,634 @@ + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + + + + + Spells + + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + RewardsWidget - + Rewards - - Remove selected + + + + + Add - Delete all + + + + Remove - - Add or change + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + + + + + + Spells + + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + + + + + + Value + + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok TownBulidingsWidget - + Buildings + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ - + Map is not loaded - + 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + + + + + Only for human players + + + + + Allow standard victory + + + + + Parameters + + + + + No special victory + + + + + Capture artifact + + + + + Hire creatures + + + + + Accumulate resources + + + + + Construct building + + + + + Capture town + + + + + Defeat hero + + + + + Transport artifact + + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -990,28 +1803,28 @@ 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 splitted / converted. + + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 65c77b0fa..0d1218d96 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Paramètres de l'armée - + Wide formation Formation large - + Tight formation Formation serrée + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nom de la carte + + + + Map description + Description de la carte + + + + Limit maximum heroes level + + + + + Difficulty + Difficulté + + GeneratorProgress @@ -27,6 +83,95 @@ Générer une carte + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Message de défaite + + + + 7 days without town + 7 jours sans ville + + + + Parameters + Paramètres + + + + No special loss + Aucune perte spéciale + + + + Lose castle + Perdre un château + + + + Lose hero + Perdre un héros + + + + Time expired + Délai expiré + + + + Days without town + Jours sans ville + + MainWindow @@ -40,546 +185,499 @@ Fichier - + 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 - - Terrains View - Vue des terrains + + Tools + - - Brush - Pinceau + + Painting + - + Terrains Terrains - + Roads Routes - + Rivers Rivières - + + Preview + + + + Open Ouvrir - + 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 + + + + + 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 - + Confirmation - + Unsaved changes will be lost, are you sure? - + Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Ouvrir la carte - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - - + Save map Enregistrer la carte - - + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + View surface Afficher la surface - + No objects selected - + This operation is irreversible. Do you want to continue? - - Errors occured. %1 objects were not updated + + 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 + + MapSettings - + Map settings Paramètres de la carte - + General Général - - Map name - Nom de la carte - - - - Map description - Description de la carte - - - - Limit maximum heroes level - - - - - Difficulty - Difficulté - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Événements - + Victory Victoire - - Victory message - Message de victoire - - - - Only for human players - Uniquement pour les joueurs humains - - - - Allow standard victory - Autoriser la victoire standard - - - - - Parameters - Paramètres - - - + Loss Perte - - 7 days without town - 7 jours sans ville + + Timed + - - Defeat message - Message de défaite + + Rumors + - + Abilities Capacités - + Spells Sorts - + Artifacts Artefacts - + Heroes Héros - + Ok OK - - - 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 - - - - Construct building - Construire un bâtiment - - - - Capture town - Conquérir une ville - - - - Defeat hero - Battre un héros - - - - Transport artifact - Transporter un artefact - - - - No special loss - Aucune perte spéciale - - - - Lose castle - Perdre un château - - - - Lose hero - Perdre un héros - - - - Time expired - Délai expiré - - - - Days without town - Jours sans ville - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Message - PlayerParams + ModSettings - No team - Aucune équipe + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Human/Ordinateur - + CPU only Ordinateur uniquement - + Team Équipe - + Main town Ville principale - + Color - + + ... + + + + Random faction Faction aléatoire - + Generate hero at main Générer un héros dans le principal - + (default) (par défaut) - + Player ID: %1 Identifiant du joueur : %1 @@ -667,45 +814,634 @@ OK + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objectif de la mission + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefacts + + + + Spells + Sorts + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + RewardsWidget - + Rewards Récompenses - - Remove selected - Supprimer ce qui est sélectionné + + + + + Add + - Delete all - Tout supprimer + + + + Remove + - - Add or change - Ajouter ou modifier + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefacts + + + + + Spells + Sorts + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Type + + + + + Value + Valeur + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + OK TownBulidingsWidget - + Buildings Bâtiments + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Résultats de la validation de la carte - + Map is not loaded Aucune carte n'est chargée - + No factions allowed for player %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 - + 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + Message de victoire + + + + Only for human players + Uniquement pour les joueurs humains + + + + Allow standard victory + Autoriser la victoire standard + + + + Parameters + 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 + + + + 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 + + + WindowNewMap @@ -976,17 +1785,17 @@ Annuler - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,28 +1803,28 @@ 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. - + 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 splitted / converted. + + Delete original files, for the ones split / converted. Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 4da2aee66..5c87e7634 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Einstellungen der Armee - + Wide formation Breite Formation - + Tight formation Enge Formation + + EventSettings + + + Form + Formular + + + + Timed events + Zeitlich begrenzte Ereignisse + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New event + Neues Ereignis + + + + GeneralSettings + + + Form + Formular + + + + Map name + Kartenname + + + + Map description + Kartenbeschreibung + + + + Limit maximum heroes level + Maximales Level des Helden begrenzen + + + + Difficulty + Schwierigkeit + + GeneratorProgress @@ -27,6 +83,95 @@ Karte generieren + + HeroSkillsWidget + + + Hero skills + Helden-Fertigkeiten + + + + + + + TextLabel + TextLabel + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Skill + Fertigkeiten + + + + Level + Level + + + + Customize skills + Fertigkeiten anpassen + + + + LoseConditions + + + Form + Formular + + + + Defeat message + Niederlage-Nachricht + + + + 7 days without town + 7 Tage ohne Stadt + + + + Parameters + Parameter + + + + No special loss + Keine besondere Niederlage + + + + Lose castle + Schloss verlieren + + + + Lose hero + Held verlieren + + + + Time expired + Zeit abgelaufen + + + + Days without town + Tage ohne Stadt + + MainWindow @@ -40,546 +185,499 @@ Datei - + 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 - - Terrains View - Terrain-Ansicht + + Tools + Werkzeuge - - Brush - Pinsel + + Painting + Malen - + Terrains Terrains - + Roads Straßen - + Rivers Flüsse - + + Preview + Vorschau + + + Open Öffnen - + 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? - - Failed to open map - Öffnen der Karte fehlgeschlagen - - - - Cannot open map from this folder - Kann keine Karte aus diesem Ordner öffnen - - - + Open map Karte öffnen - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - - + Save map Karte speichern - - + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + View surface Oberfläche anzeigen - + No objects selected Keine Objekte selektiert - + This operation is irreversible. Do you want to continue? Diese Operation ist unumkehrbar. Möchten sie fortsetzen? - - Errors occured. %1 objects were not updated + + 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 + MapSettings - + Map settings Karteneinstellungen - + General Allgemein - - Map name - Kartenname - - - - Map description - Kartenbeschreibung - - - - Limit maximum heroes level - Maximales Level des Helden begrenzen - - - - Difficulty - Schwierigkeit - - - + Mods Mods - - Mandatory mods for playing this map - Notwendige Mods zum Spielen dieser Karte - - - - Mod name - Mod Name - - - - Version - Version - - - - Automatic assignment - Automatische Zuweisung - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Sie Belohnungen, Garnisonen usw. von Mods angepasst haben - - - - Map objects mods - Mods für Kartenobjekte - - - - Set all mods having a game content as mandatory - Alle Mods, die einen Spielinhalt haben, als notwendig festlegen - - - - Full content mods - Vollwertige Mods - - - + Events Ereignisse - + Victory Sieg - - Victory message - Sieg-Nachricht - - - - Only for human players - Nur für menschliche Spieler - - - - Allow standard victory - Standardsieg zulassen - - - - - Parameters - Parameter - - - + Loss Niederlage - - 7 days without town - 7 Tage ohne Stadt + + Timed + Zeitgesteuert - - Defeat message - Niederlage-Nachricht + + Rumors + Gerüchte - + Abilities Fähigkeiten - + Spells Zaubersprüche - + Artifacts Artefakte - + Heroes Helden - + Ok Ok - - - 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 - - - - No special loss - Keine besondere Niederlage - - - - Lose castle - Schloss verlieren - - - - Lose hero - Held verlieren - - - - Time expired - Zeit abgelaufen - - - - Days without town - Tage ohne Stadt - MapView - + Can't place object Objekt kann nicht platziert werden @@ -587,59 +685,108 @@ MessageWidget - + Message Nachricht - PlayerParams + ModSettings - No team - Kein Team + + Form + Formular - + + Mandatory mods to play this map + Benötigte Mods zum Spielen dieser Karte + + + + Mod name + Mod Name + + + + Version + Version + + + + Automatic assignment + Automatische Zuweisung + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Belohnungen, Garnisonen usw. von Mods angepasst wurden + + + + Map objects mods + Mods für Kartenobjekte + + + + Set all mods having a game content as mandatory + Alle Mods, die einen Spielinhalt haben, als notwendig festlegen + + + + Full content mods + Vollwertige Mods + + + + PlayerParams + + Human/CPU Mensch/CPU - + CPU only Nur CPU - + Team Team - + Main town Hauptstadt - + Color Farbe - + + ... + ... + + + Random faction Zufällige Fraktion - + Generate hero at main Held am Hauptplatz generieren - + (default) (Standard) - + Player ID: %1 Spieler-ID: %1 @@ -667,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + Porträt + + + + + ... + ... + + + + Default + Standard + + + + QObject + + + Beginner + Anfänger + + + + Advanced + Fortgeschrittene + + + + Expert + Experte + + + + Compliant + Konform + + + + Friendly + Freundlich + + + + Aggressive + Aggressiv + + + + Hostile + Feindlich + + + + Savage + Wild + + + + + neutral + neutral + + + + UNFLAGGABLE + UNFLAGGBAR + + QuestWidget - + Mission goal Missionsziel + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Hero level + Heldenstufe + + + + Hero experience + Heldenerfahrung + + + + Spell points + Zauberpunkte + + + + % + % + + + + Kill hero/monster + Held/Monster töten + + + + ... + ... + + + + Primary skills + Primäre Fähigkeiten + + + + Attack + Angriff + + + + Defence + Verteidigung + + + + Spell power + Zauberkraft + + + + Knowledge + Wissen + + + + Resources + Ressourcen + + + + Artifacts + Artefakte + + + + Spells + Zaubersprüche + + + + Skills + Fertigkeiten + + + + Creatures + Kreaturen + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + RewardsWidget - + Rewards Belohnungen - - Remove selected - Ausgewähltes entfernen + + + + + Add + Hinzufügen - Delete all - Alle löschen + + + + Remove + Entfernen - - Add or change - Hinzufügen oder ändern + + Visit mode + Besuchsmodus + + + + Select mode + Modus auswählen + + + + On select text + + + + + Can refuse + Kann ablehnen + + + + Reset parameters + Parameter zurücksetzen + + + + Period + Zeitraum + + + + days + Tage + + + + Reset visitors + Besucher zurücksetzen + + + + Reset rewards + Belohnungen zurücksetzen + + + + Window type + Fenstertyp + + + + Event info + Ereignis-Infos + + + + Message to be displayed on granting of this reward + Nachricht, die bei der Gewährung dieser Belohnung angezeigt wird + + + + Reward + Belohnung + + + + + Hero level + Heldenstufe + + + + + Hero experience + Heldenerfahrung + + + + + Spell points + Zauberpunkte + + + + + + + % + % + + + + Overflow + Überlauf + + + + Movement + Bewegung + + + + Remove object + Objekt entfernen + + + + + Primary skills + Primäre Fähigkeiten + + + + + Attack + Angriff + + + + + Defence + Verteidigung + + + + + Spell power + Zauberkraft + + + + + Knowledge + Wissen + + + + + Resources + Ressourcen + + + + + Artifacts + Artefakte + + + + + Spells + Zaubersprüche + + + + + Skills + Fertigkeiten + + + + + Creatures + Kreaturen + + + + Bonuses + Boni + + + + + Duration + Dauer + + + + + Type + Typ + + + + + Value + Wert + + + + Cast + Wirken + + + + Cast an adventure map spell + Einen Abenteuerkarten-Zauber wirken + + + + Spell + Zauberspruch + + + + Magic school level + Stufe der Zauberschule + + + + Limiter + Begrenzer + + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + + + + + Reward %1 + Belohnung %1 + + + + RumorSettings + + + Form + Formular + + + + Tavern rumors + Tavernen-Gerüchte + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New rumor + Neues Gerücht + + + + TimedEvent + + + Timed event + Zeitgesteuertes Ereignis + + + + Event name + Name des Ereignisses + + + + Type event message text + Ereignistext eingeben + + + + affects human + beeinflusst Menschen + + + + affects AI + beeinflusst KI + + + + Day of first occurance + Tag des ersten Auftretens + + + + Repeat after (0 = no repeat) + Wiederholung nach (0 = keine Wiederholung) + + + + Affected players + Betroffene Spieler + + + + Resources + Ressourcen + + + + type + Typ + + + + qty + anz. + + + + Ok + Ok TownBulidingsWidget - + Buildings Gebäude + + Translations + + + Map translations + Übersetzungen der Karte + + + + Language + Sprache + + + + Suppported + Unterstützt + + + + String ID + String-ID + + + + Text + Text + + + + + Remove translation + Übersetzung entfernen + + + + Default language cannot be removed + Standardsprache kann nicht entfernt werden + + + + All existing text records for this language will be removed. Continue? + Alle vorhandenen Textsätze für diese Sprache werden entfernt. Weiter? + + Validator @@ -714,116 +1450,189 @@ Ergebnisse der Kartenvalidierung - + Map is not loaded 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 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 + + VictoryConditions + + + Form + Formular + + + + Victory message + Siegesnachricht + + + + Only for human players + Nur für menschliche Spieler + + + + Allow standard victory + Standardsieg zulassen + + + + Parameters + 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 + + WindowNewMap @@ -976,17 +1785,17 @@ Abbrechen - + No template Kein Template - + No template for parameters scecified. Random map cannot be generated. Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden. - + RMG failure RMG-Fehler @@ -994,28 +1803,28 @@ 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 splitted / converted. + + 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 ef642f63b..cb68b9c49 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Ustawienia armii - + Wide formation Luźna formacja - + Tight formation Zwarta formacja + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nazwa mapy + + + + Map description + Opis mapy + + + + Limit maximum heroes level + Ogranicz maksymalny poziom bohaterów + + + + Difficulty + Poziom trudności + + GeneratorProgress @@ -27,6 +83,95 @@ Trwa generowanie mapy + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Komunikat o porażce + + + + 7 days without town + 7 dni bez miasta + + + + Parameters + Parametry + + + + No special loss + Bez specjalnych warunków porażki + + + + Lose castle + Utrata miasta + + + + Lose hero + Utrata bohatera + + + + Time expired + Upłynięcie czasu + + + + Days without town + Dni bez miasta + + MainWindow @@ -40,546 +185,499 @@ Plik - + 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ść - - Terrains View - Widok terenów + + Tools + - - Brush - Pędzel + + Painting + - + Terrains Tereny - + Roads Drogi - + Rivers Rzeki - + + Preview + + + + Open Otwórz - + 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 + + + + + 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 Potwierdzenie - + Unsaved changes will be lost, are you sure? Niezapisane zmiany zostaną utracone, jesteś pewny? - - Failed to open map - Nie udało się otworzyć mapy - - - - Cannot open map from this folder - Nie można otworzyć mapy z tego folderu - - - + Open map Otwórz mapę - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - - + Save map Zapisz mapę - - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + View surface Pokaż powierzchnię - + No objects selected Brak wybranych obiektów - + This operation is irreversible. Do you want to continue? Ta operacja jest nieodwracalna. Czy chcesz kontynuować? - - Errors occured. %1 objects were not updated + + 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 + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Ustawienia mapy - + General Ogólne - - Map name - Nazwa mapy - - - - Map description - Opis mapy - - - - Limit maximum heroes level - Ogranicz maksymalny poziom bohaterów - - - - Difficulty - Poziom trudności - - - + Mods Modyfikacje - - Mandatory mods for playing this map - Obowiązkowe modyfikacje do uruchomienia tej mapy - - - - Mod name - Nazwa modyfikacji - - - - Version - Wersja - - - - Automatic assignment - Automatyczne przypisanie - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji - - - - Map objects mods - Mody od nowych obiektów mapy - - - - Set all mods having a game content as mandatory - Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe - - - - Full content mods - Mody od złożonej zawartości - - - + Events Zdarzenia - + Victory Zwycięstwo - - Victory message - Komunikat zwycięstwa - - - - Only for human players - Dotyczy tylko graczy ludzkich - - - - Allow standard victory - Także standardowy warunek zwycięstwa - - - - - Parameters - Parametry - - - + Loss Porażka - - 7 days without town - 7 dni bez miasta + + Timed + - - Defeat message - Komunikat o porażce + + Rumors + - + Abilities Umiejętności - + Spells Zaklęcia - + Artifacts Artefakty - + Heroes Bohaterowie - + Ok Ok - - - 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 - - - - No special loss - Bez specjalnych warunków porażki - - - - Lose castle - Utrata miasta - - - - Lose hero - Utrata bohatera - - - - Time expired - Upłynięcie czasu - - - - Days without town - Dni bez miasta - MapView - + Can't place object Nie można umieścić obiektu @@ -587,59 +685,108 @@ MessageWidget - + Message Wiadomość - PlayerParams + ModSettings - No team - Brak drużyny + + Form + - + + Mandatory mods to play this map + + + + + Mod name + Nazwa modyfikacji + + + + Version + Wersja + + + + Automatic assignment + Automatyczne przypisanie + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji + + + + Map objects mods + Mody od nowych obiektów mapy + + + + Set all mods having a game content as mandatory + Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe + + + + Full content mods + Mody od złożonej zawartości + + + + PlayerParams + + Human/CPU Człowiek/Komputer - + CPU only Tylko komputer - + Team Drużyna - + Main town Główne miasto - + Color Kolor - + + ... + + + + Random faction Losowe miasto - + Generate hero at main Generuj bohatera w głównym - + (default) (domyślny) - + Player ID: %1 ID gracza: %1 @@ -667,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Cel misji + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefakty + + + + Spells + Zaklęcia + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + RewardsWidget - + Rewards Nagrody - - Remove selected - Usuń wybrane + + + + + Add + - Delete all - Usuń wszystkie + + + + Remove + - - Add or change - Dodaj lub zmień + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefakty + + + + + Spells + Zaklęcia + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Typ + + + + + Value + Wartość + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Ok TownBulidingsWidget - + Buildings Budynki + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Wynik sprawdzenia mapy - + Map is not loaded 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 - + 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + Komunikat zwycięstwa + + + + Only for human players + Dotyczy tylko graczy ludzkich + + + + Allow standard victory + Także standardowy warunek zwycięstwa + + + + Parameters + 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 + + + WindowNewMap @@ -976,17 +1785,17 @@ Anuluj - + No template Brak szablonu - + No template for parameters scecified. Random map cannot be generated. Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana. - + RMG failure Niepowodzenie generatora map losowych @@ -994,28 +1803,28 @@ 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 splitted / converted. + + Delete original files, for the ones split / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 6c5641347..4244e36fa 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Настройки армии - + Wide formation Расширенная формация - + Tight formation Суженная формация + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Название карты + + + + Map description + Описание карты + + + + Limit maximum heroes level + + + + + Difficulty + Сложность + + GeneratorProgress @@ -27,6 +83,95 @@ Создание карты + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Сообщение о поражении + + + + 7 days without town + 7 дней без городов + + + + Parameters + Параметры + + + + No special loss + Нет специального поражения + + + + Lose castle + Потерять город + + + + Lose hero + Потерять героя + + + + Time expired + Не успеть ко времени + + + + Days without town + Провести без городов + + MainWindow @@ -40,546 +185,499 @@ Файл - + Map Карта - + Edit Правка - + View Вид - + Player Игрок - + Toolbar Панель инструментов - + Minimap Мини-карта - + Map Objects View Объекты карты - + Browser Навигатор - + Inspector Инспектор - + Property Свойство - + Value Значение - - Terrains View - Кисти земель + + Tools + - - Brush - Кисть + + Painting + - + Terrains Земли - + Roads Дороги - + Rivers Реки - + + Preview + + + + Open Открыть - + 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? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Открыть карту - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - - + Save map Сохранить карту - - + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + View surface Вид на поверхность - + No objects selected - + This operation is irreversible. Do you want to continue? - - Errors occured. %1 objects were not updated + + 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 + + MapSettings - + Map settings Настройки карты - + General Общее - - Map name - Название карты - - - - Map description - Описание карты - - - - Limit maximum heroes level - - - - - Difficulty - Сложность - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events События - + Victory Победа - - Victory message - Сообщение о победе - - - - Only for human players - Только для игроков-людей - - - - Allow standard victory - Разрешить стандартную победу - - - - - Parameters - Параметры - - - + Loss Поражение - - 7 days without town - 7 дней без городов + + Timed + - - Defeat message - Сообщение о поражении + + Rumors + - + Abilities Способности - + Spells Заклинания - + Artifacts Артефакты - + Heroes Герои - + Ok ОК - - - No special victory - Нет специальной победы - - - - Capture artifact - Взять артефакт - - - - Hire creatures - Нанять существ - - - - Accumulate resources - Собрать ресурсы - - - - Construct building - Построить - - - - Capture town - Захватить город - - - - Defeat hero - Победить героя - - - - Transport artifact - Переместить артефакт - - - - No special loss - Нет специального поражения - - - - Lose castle - Потерять город - - - - Lose hero - Потерять героя - - - - Time expired - Не успеть ко времени - - - - Days without town - Провести без городов - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Сообщение - PlayerParams + ModSettings - No team - Без команды + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Человек/ИИ - + CPU only Только ИИ - + Team Команда - + Main town Главный город - + Color - + + ... + + + + Random faction Случайная фракция - + Generate hero at main Создать героя - + (default) (по умолчанию) - + Player ID: %1 Игрок: %1 @@ -667,45 +814,634 @@ ОК + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Цель миссии + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакты + + + + Spells + Заклинания + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + RewardsWidget - + Rewards Награды - - Remove selected - Удалить выбранное + + + + + Add + - Delete all - Удалить все + + + + Remove + - - Add or change - Добавить/Изменить + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакты + + + + + Spells + Заклинания + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значение + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + ОК TownBulidingsWidget - + Buildings Постройки + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Результаты проверки карты - + Map is not loaded - + 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + Сообщение о победе + + + + Only for human players + Только для игроков-людей + + + + Allow standard victory + Разрешить стандартную победу + + + + Parameters + Параметры + + + + No special victory + Нет специальной победы + + + + Capture artifact + Взять артефакт + + + + Hire creatures + Нанять существ + + + + Accumulate resources + Собрать ресурсы + + + + Construct building + Построить + + + + Capture town + Захватить город + + + + Defeat hero + Победить героя + + + + Transport artifact + Переместить артефакт + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Отмена - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,28 +1803,28 @@ 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 splitted / converted. + + Delete original files, for the ones split / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 73a9b60ed..8a33ffbc1 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Configuracion del Ejército - + Wide formation Formación amplia - + Tight formation Formación ajustada + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nombre del mapa + + + + Map description + Descripción del mapa + + + + Limit maximum heroes level + + + + + Difficulty + Dificultad + + GeneratorProgress @@ -27,6 +83,95 @@ Generando mapa + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Mensaje de derrota + + + + 7 days without town + 7 días sin ciudad + + + + Parameters + Parámetros + + + + No special loss + Sin pérdida especial + + + + Lose castle + Perder castillo + + + + Lose hero + Perder héroe + + + + Time expired + Expiró el tiempo + + + + Days without town + Días sin ciudad + + MainWindow @@ -40,546 +185,499 @@ Archivo - + 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 - - Terrains View - Vista de Terrenos + + Tools + - - Brush - Pincel + + Painting + - + Terrains Terrenos - + Roads Caminos - + Rivers Ríos - + + Preview + + + + Open Abrir - + 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 ? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - - + Save map Guardar mapa - - + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + View surface Ver superficie - + No objects selected - + This operation is irreversible. Do you want to continue? - - Errors occured. %1 objects were not updated + + 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 + + MapSettings - + Map settings Configuración del mapa - + General General - - Map name - Nombre del mapa - - - - Map description - Descripción del mapa - - - - Limit maximum heroes level - - - - - Difficulty - Dificultad - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Eventos - + Victory Victoria - - Victory message - Mensaje de victoria - - - - Only for human players - Solo para jugadores humanos - - - - Allow standard victory - Permitir victoria estándar - - - - - Parameters - Parámetros - - - + Loss Derrota - - 7 days without town - 7 días sin ciudad + + Timed + - - Defeat message - Mensaje de derrota + + Rumors + - + Abilities Habilidades - + Spells Hechizos - + Artifacts Artefactos - + Heroes Héroes - + Ok Aceptar - - - 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 - - - - No special loss - Sin pérdida especial - - - - Lose castle - Perder castillo - - - - Lose hero - Perder héroe - - - - Time expired - Expiró el tiempo - - - - Days without town - Días sin ciudad - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Mensaje - PlayerParams + ModSettings - No team - Sin equipo + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Humano/CPU - + CPU only Sólo CPU - + Team Equipo - + Main town Ciudad principal - + Color - + + ... + + + + Random faction Facción aleatoria - + Generate hero at main Generar héroe en la ciudad principal - + (default) (predeterminado) - + Player ID: %1 ID de jugador: %1 @@ -667,45 +814,634 @@ Aceptar + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objetivo de la misión + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefactos + + + + Spells + Hechizos + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + RewardsWidget - + Rewards Recompensas - - Remove selected - Eliminar seleccionado + + + + + Add + - Delete all - Borrar todo + + + + Remove + - - Add or change - Añadir o modificar + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefactos + + + + + Spells + Hechizos + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Tipo + + + + + Value + Valor + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Aceptar TownBulidingsWidget - + Buildings Edificios + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Resultados de la validación del mapa - + Map is not loaded 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + Mensaje de victoria + + + + Only for human players + Solo para jugadores humanos + + + + Allow standard victory + Permitir victoria estándar + + + + Parameters + 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 + + + WindowNewMap @@ -976,17 +1785,17 @@ Cancelar - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,28 +1803,28 @@ 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 splitted / converted. + + 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 8470bca90..0a62b7157 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Налаштування армії - + Wide formation Широка формація - + Tight formation Щільна формація + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Назва мапи + + + + Map description + Опис мапи + + + + Limit maximum heroes level + Обмежити максимальний рівень героїв + + + + Difficulty + Складність + + GeneratorProgress @@ -27,6 +83,95 @@ Побудова мапи + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Повідомлення про програш + + + + 7 days without town + 7 днів без міста + + + + Parameters + Параметри + + + + No special loss + Немає особливої поразки + + + + Lose castle + Втратити місто + + + + Lose hero + Втратити героя + + + + Time expired + Закінчився час + + + + Days without town + Дні без міста + + MainWindow @@ -40,546 +185,499 @@ Файл - + Map Мапа - + Edit Редагування - + View Вигляд - + Player Гравець - + Toolbar Панель інструментів - + Minimap Мінімапа - + Map Objects View Перегляд об'єктів мапи - + Browser Навігатор - + Inspector Інспектор - + Property Властивість - + Value Значення - - Terrains View - Перегляд поверхні + + Tools + - - Brush - Кисть + + Painting + - + Terrains Землі - + Roads Шляхи - + Rivers Річки - + + Preview + + + + Open Відкрити - + 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? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Відкрити мапу - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - - + Save map Зберегти мапу - - + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + View surface Дивитись поверхню - + No objects selected - + This operation is irreversible. Do you want to continue? - - Errors occured. %1 objects were not updated + + 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 + + MapSettings - + Map settings Налаштування мапи - + General Загальний - - Map name - Назва мапи - - - - Map description - Опис мапи - - - - Limit maximum heroes level - Обмежити максимальний рівень героїв - - - - Difficulty - Складність - - - + Mods Модифікації - - Mandatory mods for playing this map - Модифікації необхідні для гри на мапи - - - - Mod name - Назва модифікації - - - - Version - Версія - - - - Automatic assignment - Автоматичне визначення - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів - - - - Map objects mods - Моди з об'єктами мапи - - - - Set all mods having a game content as mandatory - Встановити усі моди з ігровим контентом як обов'язкові - - - - Full content mods - Усі модифікації - - - + Events Події - + Victory Перемога - - Victory message - Повідомлення про перемогу - - - - Only for human players - Тільки для гравців-людей - - - - Allow standard victory - Дозволити типову перемогу - - - - - Parameters - Параметри - - - + Loss Програш - - 7 days without town - 7 днів без міста + + Timed + - - Defeat message - Повідомлення про програш + + Rumors + - + Abilities Уміння - + Spells Закляття - + Artifacts Артефакти - + Heroes Герої - + Ok Підтвердити - - - No special victory - Немає особливої перемоги - - - - Capture artifact - Отримати артефакт - - - - Hire creatures - Найняти істот - - - - Accumulate resources - Накопичити ресурси - - - - Construct building - Побудувати будівлю - - - - Capture town - Захопити місто - - - - Defeat hero - Перемогти героя - - - - Transport artifact - Доставити артефакт - - - - No special loss - Немає особливої поразки - - - - Lose castle - Втратити місто - - - - Lose hero - Втратити героя - - - - Time expired - Закінчився час - - - - Days without town - Дні без міста - MapView - + Can't place object @@ -587,55 +685,108 @@ MessageWidget - + Message Повідомлення + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Назва модифікації + + + + Version + Версія + + + + Automatic assignment + Автоматичне визначення + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів + + + + Map objects mods + Моди з об'єктами мапи + + + + Set all mods having a game content as mandatory + Встановити усі моди з ігровим контентом як обов'язкові + + + + Full content mods + Усі модифікації + + PlayerParams - + Human/CPU Людина/Комп'ютер - + CPU only Тільки комп'ютер - + Team Команда - + Main town Головне місто - + Color Колір - + + ... + + + + Random faction Випадкова фракція - + Generate hero at main Згенерувати героя - + (default) (за замовчуванням) - + Player ID: %1 Гравець %1 @@ -663,45 +814,634 @@ Підтвердити + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Мета місії + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакти + + + + Spells + Закляття + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + RewardsWidget - + Rewards Винагороди - - Remove selected - Видалити вибране + + + + + Add + - Delete all - Видалити усі + + + + Remove + - - Add or change - Додати або змінити + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакти + + + + + Spells + Закляття + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значення + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Підтвердити TownBulidingsWidget - + Buildings Будівлі + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ Результати валідації карти - + Map is not loaded - + 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 - + 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 + + VictoryConditions + + + Form + + + + + Victory message + Повідомлення про перемогу + + + + Only for human players + Тільки для гравців-людей + + + + Allow standard victory + Дозволити типову перемогу + + + + Parameters + Параметри + + + + No special victory + Немає особливої перемоги + + + + Capture artifact + Отримати артефакт + + + + Hire creatures + Найняти істот + + + + Accumulate resources + Накопичити ресурси + + + + Construct building + Побудувати будівлю + + + + Capture town + Захопити місто + + + + Defeat hero + Перемогти героя + + + + Transport artifact + Доставити артефакт + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ Скасувати - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -990,28 +1803,28 @@ 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 splitted / converted. + + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts new file mode 100644 index 000000000..4794f75fd --- /dev/null +++ b/mapeditor/translation/vietnamese.ts @@ -0,0 +1,1831 @@ + + + + + ArmyWidget + + + Army settings + Cài đặt quân + + + + Wide formation + Đội hình rộng + + + + Tight formation + Đội hình kín + + + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Tên bản đồ + + + + Map description + Mô tả bản đồ + + + + Limit maximum heroes level + Giới hạn cấp tướng tối đa + + + + Difficulty + Độ khó + + + + GeneratorProgress + + + Generating map + Tạo bản đồ + + + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Thông báo thất bại + + + + 7 days without town + 7 ngày không có thành + + + + Parameters + Tham số + + + + No special loss + Không có thất bại đặc biệt + + + + Lose castle + Mất thành + + + + Lose hero + Mất tướng + + + + Time expired + Hết thời gian + + + + Days without town + Số ngày không có thành + + + + MainWindow + + + VCMI Map Editor + Bộ tạo bản đồ VCMI + + + + File + 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 + + + + + Painting + + + + + Preview + + + + + 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) + + + + Save map + Lưu bản đồ + + + + VCMI maps (*.vmap) + Bản đồ VCMI (*.vmap) + + + + Type + Loại + + + + View surface + Xem bề mặt + + + + No objects selected + Không mục tiêu được chọn + + + + This operation is irreversible. Do you want to continue? + Thao tác này không thể đảo ngược. Bạn muốn tiếp tục? + + + + Errors occurred. %1 objects were not updated + Xảy ra lỗi. %1 mục tiêu không được cập nhật + + + + Save to image + Lưu thành ảnh + + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + + + + MapSettings + + + Map settings + Cài đặt bản đồ + + + + General + Chung + + + + Mods + Bản sửa đổi + + + + Events + Sự kiện + + + + Victory + Chiến thắng + + + + Loss + Thất bại + + + + Timed + + + + + Rumors + + + + + Abilities + Năng lực + + + + Spells + Phép + + + + Artifacts + Vật phẩm + + + + Heroes + Tướng + + + + Ok + Đồng ý + + + + MapView + + + Can't place object + Không thể đặt vật thể + + + + MessageWidget + + + Message + Thông báo + + + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Tên bản sửa đổi + + + + Version + Phiên bản + + + + Automatic assignment + Gán tự động + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi + + + + Map objects mods + Bản sửa đổi vật thể trên bản đồ + + + + Set all mods having a game content as mandatory + Tập bản sửa đổi cần cho nội dung trò chơi + + + + Full content mods + Bản sửa đổi nội dung đầy đủ + + + + PlayerParams + + + Human/CPU + Người/Máy + + + + CPU only + Chỉ máy + + + + Team + Phe + + + + Main town + Thành chính + + + + Color + Màu + + + + ... + + + + + Random faction + Thành ngẫu nhiên + + + + Generate hero at main + Tạo tướng ban đầu + + + + (default) + (mặc định) + + + + Player ID: %1 + ID người chơi: %1 + + + + PlayerSettings + + + Player settings + Cài đặt người chơi + + + + Players + Người chơi + + + + 1 + 1 + + + + Ok + Đồng ý + + + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + + + QuestWidget + + + Mission goal + Mục tiêu nhiệm vụ + + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Vật phẩm + + + + Spells + Phép + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + + + + RewardsWidget + + + Rewards + Phần thưởng + + + + + + + Add + + + + + + + + Remove + + + + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Vật phẩm + + + + + Spells + Phép + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Loại + + + + + Value + Giá trị + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Đồng ý + + + + TownBulidingsWidget + + + Buildings + Công trình + + + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + + + Validator + + + Map validation results + Kết quả kiểm định bản đồ + + + + Map is not loaded + 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 + + + + 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 + + + + VictoryConditions + + + Form + + + + + Victory message + Thông báo chiến thắng + + + + Only for human players + Chỉ cho người + + + + Allow standard victory + Cho phép chiến thắng thông thường + + + + Parameters + 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 + + + + + WindowNewMap + + + Create new map + Tạo bản đồ mới + + + + Map size + Độ lớn bản đồ + + + + Two level map + Bản đồ 2 tầng + + + + Height + Cao + + + + Width + Rộng + + + + S (36x36) + Nhỏ (36x36) + + + + M (72x72) + Vừa (72x72) + + + + L (108x108) + Lớn (108x108) + + + + XL (144x144) + Rất lớn (144x144) + + + + Random map + Bản đồ ngẫu nhiên + + + + Players + Người chơi + + + + 0 + 0 + + + + Human/Computer + Người/Máy + + + + + + + Random + Ngẫu nhiên + + + + Computer only + Chỉ máy + + + + Human teams + Đội người + + + + Computer teams + Đội máy + + + + Monster strength + Sức mạnh quái + + + + Weak + Yếu + + + + + Normal + Trung bình + + + + Strong + Mạnh + + + + Water content + Có nước + + + + None + Không + + + + Islands + Các đảo + + + + 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. + 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 + + + + 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 d1ded12b0..1d7b8455d 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -14,8 +14,10 @@ #include "ui_validator.h" #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/MapObjects.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/CModInfo.h" +#include "../lib/spells/CSpellHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" Validator::Validator(const CMap * map, QWidget *parent) : QDialog(parent), @@ -96,14 +98,14 @@ std::list Validator::validate(const CMap * map) 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().getStr().c_str()), true); + 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); } //checking 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().getStr().c_str()), true); + 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()]; } @@ -123,13 +125,13 @@ std::list Validator::validate(const CMap * map) } if(ins->type) { - if(!map->allowedHeroes[ins->type->getId().getNum()]) + 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(!allHeroesOnMap.insert(ins->type).second) issues.emplace_back(QString(tr("Hero %1 has duplicate on map")).arg(ins->type->getNameTranslated().c_str()), false); } - else + 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); } @@ -140,15 +142,15 @@ std::list Validator::validate(const CMap * map) { if(ins->storedArtifact) { - if(!map->allowedSpells[ins->storedArtifact->getId().getNum()]) - issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); + 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); } else issues.emplace_back(QString(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[ins->subID]) + 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); } @@ -172,7 +174,7 @@ std::list Validator::validate(const CMap * 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).name)), true); + 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); } } } diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 6fdd404b8..bfe436e99 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -90,6 +90,11 @@ void WindowNewMap::loadUserSettings() { ui->heightTxt->setText(height.toString()); } + for(auto & sz : mapSizes) + { + if(sz.second.first == width.toInt() && sz.second.second == height.toInt()) + ui->sizeCombo->setCurrentIndex(sz.first); + } auto twoLevel = s.value(newMapTwoLevel); if (twoLevel.isValid()) { @@ -327,7 +332,7 @@ void WindowNewMap::on_humanCombo_activated(int index) ui->humanCombo->setCurrentIndex(humans); } - mapGenOptions.setPlayerCount(humans); + mapGenOptions.setHumanOrCpuPlayerCount(humans); int teams = mapGenOptions.getTeamCount(); if(teams > humans - 1) @@ -356,8 +361,10 @@ void WindowNewMap::on_humanCombo_activated(int index) void WindowNewMap::on_cpuCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int cpu = ui->cpuCombo->currentData().toInt(); + + // FIXME: Use mapGenOption method only to calculate actual number of players for current template if(cpu > PlayerColor::PLAYER_LIMIT_I - humans) { cpu = PlayerColor::PLAYER_LIMIT_I - humans; @@ -450,7 +457,7 @@ void WindowNewMap::on_checkSeed_toggled(bool checked) void WindowNewMap::on_humanTeamsCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int teams = ui->humanTeamsCombo->currentData().toInt(); if(teams >= humans) { diff --git a/scripting/erm/ERMInterpreter.cpp b/scripting/erm/ERMInterpreter.cpp index 663cb3572..757d0697d 100644 --- a/scripting/erm/ERMInterpreter.cpp +++ b/scripting/erm/ERMInterpreter.cpp @@ -1,1755 +1,1774 @@ -/* - * ERMInterpreter.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 "ERMInterpreter.h" - -#include - -namespace spirit = boost::spirit; -using ::scripting::ContextBase; -using namespace ::VERMInterpreter; - -typedef int TUnusedType; - -namespace ERMConverter -{ - //console printer - using namespace ERM; - - static const std::map CMP_OPERATION = - { - {"<", "<"}, - {">", ">"}, - {">=", ">="}, - {"=>", ">="}, - {"<=", "<="}, - {"=<", "<="}, - {"=", "=="}, - {"<>", "~="}, - {"><", "~="}, - }; - - - struct Variable - { - std::string name = ""; - std::string macro = ""; - int index = 0; - - Variable(const std::string & name_, int index_) - { - name = name_; - index = index_; - } - - Variable(const std::string & macro_) - { - macro = macro_; - } - - bool isEmpty() const - { - return (name == "") && (macro == ""); - } - - bool isMacro() const - { - return (name == "") && (macro != ""); - } - - bool isSpecial() const - { - return (name.size() > 0) && (name[0] == 'd'); - } - - std::string str() const - { - if(isEmpty()) - { - return std::to_string(index); - } - else if(isMacro()) - { - return boost::to_string(boost::format("M['%s']") % macro); - } - else if(isSpecial() && (name.size() == 1)) - { - boost::format fmt; - fmt.parse("{'d', %d}"); - fmt % index; - return fmt.str(); - } - else if(isSpecial() && (name.size() != 1)) - { - - std::string ret; - - { - boost::format fmt; - - if(index == 0) - { - fmt.parse("Q['%s']"); - fmt % name[name.size()-1]; - } - else - { - fmt.parse("%s['%d']"); - fmt % name[name.size()-1] % index; - } - ret = fmt.str(); - } - - for(int i = ((int) name.size())-2; i > 0; i--) - { - boost::format fmt("%s[tostring(%s)]"); - - fmt % name[i] % ret; - - ret = fmt.str(); - } - - { - boost::format fmt; - fmt.parse("{'d', %s}"); - fmt % ret; - return fmt.str(); - } - } - else - { - std::string ret; - { - boost::format fmt; - - if(index == 0) - { - fmt.parse("Q['%s']"); - fmt % name[name.size()-1]; - } - else - { - fmt.parse("%s['%d']"); - fmt % name[name.size()-1] % index; - } - ret = fmt.str(); - } - - for(int i = ((int) name.size())-2; i >= 0; i--) - { - boost::format fmt("%s[tostring(%s)]"); - - fmt % name[i] % ret; - - ret = fmt.str(); - } - - return ret; - } - } - }; - - struct LVL2IexpToVar - { - LVL2IexpToVar() = default; - - Variable operator()(const TVarExpNotMacro & val) const - { - if(val.val.has_value()) - return Variable(val.varsym, val.val.get()); - else - return Variable(val.varsym, 0); - } - - Variable operator()(const TMacroUsage & val) const - { - return Variable(val.macro); - } - }; - - struct LVL1IexpToVar - { - LVL1IexpToVar() = default; - - Variable operator()(const int & constant) const - { - return Variable("", constant); - } - - Variable operator()(const TVarExp & var) const - { - return std::visit(LVL2IexpToVar(), var); - } - }; - - struct Condition - { - std::string operator()(const TComparison & cmp) const - { - Variable lhs = std::visit(LVL1IexpToVar(), cmp.lhs); - Variable rhs = std::visit(LVL1IexpToVar(), cmp.rhs); - - auto sign = CMP_OPERATION.find(cmp.compSign); - if(sign == std::end(CMP_OPERATION)) - throw EScriptExecError(std::string("Wrong comparison sign: ") + cmp.compSign); - - boost::format fmt("(%s %s %s)"); - fmt % lhs.str() % sign->second % rhs.str(); - return fmt.str(); - } - std::string operator()(const int & flag) const - { - return boost::to_string(boost::format("F['%d']") % flag); - } - }; - - struct ParamIO - { - ParamIO() = default; - std::string name = ""; - bool isInput = false; - bool semi = false; - std::string semiCmpSign = ""; - }; - - struct Converter - { - mutable std::ostream * out; - Converter(std::ostream * out_) - : out(out_) - {} - protected: - - void put(const std::string & text) const - { - (*out) << text; - } - - void putLine(const std::string & line) const - { - (*out) << line << std::endl; - } - - void endLine() const - { - (*out) << std::endl; - } - }; - - struct GetBodyOption - { - virtual std::string operator()(const TVarConcatString & cmp) const - { - throw EScriptExecError("String concatenation not allowed in this receiver"); - } - virtual std::string operator()(const TStringConstant & cmp) const - { - throw EScriptExecError("String constant not allowed in this receiver"); - } - virtual std::string operator()(const TCurriedString & cmp) const - { - throw EScriptExecError("Curried string not allowed in this receiver"); - } - virtual std::string operator()(const TSemiCompare & cmp) const - { - throw EScriptExecError("Semi comparison not allowed in this receiver"); - } - virtual std::string operator()(const TMacroDef & cmp) const - { - throw EScriptExecError("Macro definition not allowed in this receiver"); - } - virtual std::string operator()(const TIexp & cmp) const - { - throw EScriptExecError("i-expression not allowed in this receiver"); - } - virtual std::string operator()(const TVarpExp & cmp) const - { - throw EScriptExecError("Varp expression not allowed in this receiver"); - } - virtual std::string operator()(const spirit::unused_type & cmp) const - { - throw EScriptExecError("\'Nothing\' not allowed in this receiver"); - } - }; - - struct BodyOption - { - ParamIO operator()(const TVarConcatString & cmp) const - { - throw EScriptExecError(std::string("String concatenation not allowed in this receiver|")+cmp.string.str+"|"); - } - - ParamIO operator()(const TStringConstant & cmp) const - { - boost::format fmt("[===[%s]===]"); - fmt % cmp.str; - - ParamIO ret; - ret.isInput = true; - ret.name = fmt.str(); - return ret; - } - - ParamIO operator()(const TCurriedString & cmp) const - { - throw EScriptExecError("Curried string not allowed in this receiver"); - } - - ParamIO operator()(const TSemiCompare & cmp) const - { - ParamIO ret; - ret.isInput = false; - ret.semi = true; - ret.semiCmpSign = cmp.compSign; - ret.name = (std::visit(LVL1IexpToVar(), cmp.rhs)).str(); - return ret; - } - - ParamIO operator()(const TMacroDef & cmp) const - { - throw EScriptExecError("Macro definition not allowed in this receiver"); - } - - ParamIO operator()(const TIexp & cmp) const - { - ParamIO ret; - ret.isInput = true; - ret.name = (std::visit(LVL1IexpToVar(), cmp)).str();; - return ret; - } - - ParamIO operator()(const TVarpExp & cmp) const - { - ParamIO ret; - ret.isInput = false; - - ret.name = (std::visit(LVL2IexpToVar(), cmp.var)).str(); - return ret; - } - - ParamIO operator()(const spirit::unused_type & cmp) const - { - throw EScriptExecError("\'Nothing\' not allowed in this receiver"); - } - }; - - struct Receiver : public Converter - { - Receiver(std::ostream * out_) - : Converter(out_) - {} - - virtual void operator()(const TVRLogic & trig) const - { - throw EInterpreterError("VR logic is not allowed in this receiver!"); - } - - virtual void operator()(const TVRArithmetic & trig) const - { - throw EInterpreterError("VR arithmetic is not allowed in this receiver!"); - } - - virtual void operator()(const TNormalBodyOption & trig) const - { - throw EInterpreterError("Normal body is not allowed in this receiver!"); - } - - }; - - struct GenericReceiver : public Receiver - { - std::string name; - bool specialSemiCompare = false; - - GenericReceiver(std::ostream * out_, const std::string & name_, bool specialSemiCompare_) - : Receiver(out_), - name(name_), - specialSemiCompare(specialSemiCompare_) - {} - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & trig) const override - { - std::string outParams; - std::string inParams; - - std::string semiCompareDecl; - - std::vector compares; - - bool hasOutput = false; - bool hasSemiCompare = false; - - std::vector optionParams; - - if(trig.params.has_value()) - { - for(auto & p : trig.params.get()) - optionParams.push_back(std::visit(BodyOption(), p)); - } - - int idx = 1; - int fidx = 1; - - for(const ParamIO & p : optionParams) - { - if(p.isInput) - { - if(outParams.empty()) - outParams = "_"; - else - outParams += ", _"; - - inParams += ", "; - inParams += p.name; - } - else if(p.semi) - { - hasOutput = true; - hasSemiCompare = true; - - std::string tempVar = std::string("s")+std::to_string(idx); - - if(semiCompareDecl.empty()) - { - semiCompareDecl = "local "+tempVar; - } - else - { - semiCompareDecl += ", "; - semiCompareDecl += tempVar; - } - - if(outParams.empty()) - { - outParams = tempVar; - } - else - { - outParams += ", "; - outParams += tempVar; - } - - inParams += ", nil"; - - - auto sign = CMP_OPERATION.find(p.semiCmpSign); - if(sign == std::end(CMP_OPERATION)) - throw EScriptExecError(std::string("Wrong comparison sign: ") + p.semiCmpSign); - - boost::format cmpFmt("F[%d] = (%s %s %s)"); - cmpFmt % fidx % p.name % sign->second % tempVar; - compares.push_back(cmpFmt.str()); - - fidx++; - } - else - { - hasOutput = true; - - if(outParams.empty()) - { - outParams = p.name; - } - else - { - outParams += ", "; - outParams += p.name; - } - - inParams += ", nil"; - } - - idx++; - } - - if(hasSemiCompare) - { - putLine(semiCompareDecl); - } - - boost::format callFormat; - - if(hasOutput) - { - callFormat.parse("%s = %s:%s(x%s)"); - callFormat % outParams; - } - else - { - callFormat.parse("%s:%s(x%s)"); - } - - callFormat % name; - callFormat % trig.optionCode; - callFormat % inParams; - - putLine(callFormat.str()); - - for(auto & str : compares) - putLine(str); - } - }; - - struct FU : public Receiver - { - Variable v; - - FU(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - FU(std::ostream * out_) - : Receiver(out_), - v("", 0) - { - } - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & trig) const override - { - switch(trig.optionCode) - { - case 'E': - { - putLine("do return end"); - } - break; - default: - throw EInterpreterError("Unknown opcode in FU receiver"); - break; - } - } - }; - - struct MC_S : public GetBodyOption - { - MC_S() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TMacroDef & cmp) const override - { - return cmp.macro; - } - }; - - struct MC : public Receiver - { - Variable v; - - MC(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - MC(std::ostream * out_) - : Receiver(out_), - v("", 0) - { - } - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & option) const override - { - switch(option.optionCode) - { - case 'S': - { - if(option.params.has_value()) - { - for(auto & p : option.params.get()) - { - std::string macroName = std::visit(MC_S(), p); - - boost::format callFormat; - - if(v.isEmpty()) - { - callFormat.parse("ERM:addMacro('%s', 'v', '%s')"); - callFormat % macroName % macroName; - } - else - { - callFormat.parse("ERM:addMacro('%s', '%s', '%d')"); - callFormat % macroName % v.name % v.index; - } - - putLine(callFormat.str()); - } - } - } - break; - default: - throw EInterpreterError("Unknown opcode in MC receiver"); - break; - } - } - }; - - struct VR_S : public GetBodyOption - { - VR_S() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - auto v = std::visit(LVL1IexpToVar(), cmp); - return v.str(); - } - std::string operator()(const TStringConstant & cmp) const override - { - boost::format fmt("[===[%s]===]"); - fmt % cmp.str; - return fmt.str(); - } - }; - - struct VR_H : public GetBodyOption - { - VR_H() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - Variable p = std::visit(LVL1IexpToVar(), cmp); - - if(p.index <= 0) - throw EScriptExecError("VR:H requires flag index"); - - if(p.name != "") - throw EScriptExecError("VR:H accept only flag index"); - - - boost::format fmt("'%d'"); - fmt % p.index; - return fmt.str(); - } - }; - - struct VR_X : public GetBodyOption - { - VR_X() - { - } - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - Variable p = std::visit(LVL1IexpToVar(), cmp); - - return p.str(); - } - }; - - struct VR : public Receiver - { - Variable v; - - VR(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - using Receiver::operator(); - - void operator()(const TVRLogic & trig) const override - { - Variable rhs = std::visit(LVL1IexpToVar(), trig.var); - - std::string opcode; - - switch (trig.opcode) - { - case '&': - opcode = "bit.band"; - break; - case '|': - opcode = "bit.bor"; - break; - default: - throw EInterpreterError("Wrong opcode in VR logic expression!"); - break; - } - - boost::format fmt("%s = %s(%s, %s)"); - fmt % v.str() % opcode % v.str() % rhs.str(); - putLine(fmt.str()); - } - - void operator()(const TVRArithmetic & trig) const override - { - Variable rhs = std::visit(LVL1IexpToVar(), trig.rhs); - - std::string opcode; - - switch (trig.opcode) - { - case '+': - opcode = v.name[0] == 'z' ? ".." : "+"; - break; - case '-': - case '*': - case '%': - opcode = trig.opcode; - break; - case ':': - opcode = "/"; - break; - default: - throw EInterpreterError("Wrong opcode in VR arithmetic!"); - break; - } - - boost::format fmt("%s = %s %s %s"); - fmt % v.str() % v.str() % opcode % rhs.str(); - putLine(fmt.str()); - } - - void operator()(const TNormalBodyOption & trig) const override - { - switch(trig.optionCode) - { - case 'C': //setting/checking v vars - { - if(v.index <= 0) - throw EScriptExecError("VR:C requires indexed variable"); - - std::vector optionParams; - - if(trig.params.has_value()) - { - for(auto & p : trig.params.get()) - optionParams.push_back(std::visit(BodyOption(), p)); - } - - auto index = v.index; - - for(auto & p : optionParams) - { - boost::format fmt; - if(p.isInput) - fmt.parse("%s['%d'] = %s") % v.name % index % p.name; - else - fmt.parse("%s = %s['%d']") % p.name % v.name % index; - putLine(fmt.str()); - index++; - } - } - break; - case 'H': //checking if string is empty - { - if(!trig.params.has_value() || trig.params.get().size() != 1) - throw EScriptExecError("VR:H option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_H(), trig.params.get()[0]); - boost::format fmt("ERM.VR(%s):H(%s)"); - fmt % v.str() % opt; - putLine(fmt.str()); - } - break; - case 'U': - { - if(!trig.params.has_value() || trig.params.get().size() != 1) - throw EScriptExecError("VR:H/U need 1 parameter!"); - - std::string opt = std::visit(VR_S(), trig.params.get()[0]); - boost::format fmt("ERM.VR(%s):%c(%s)"); - fmt % v.str() % (trig.optionCode) % opt; - putLine(fmt.str()); - } - break; - case 'M': //string operations - { - if(!trig.params.has_value() || trig.params.get().size() < 2) - throw EScriptExecError("VR:M needs at least 2 parameters!"); - - std::string opt = std::visit(VR_X(), trig.params.get()[0]); - int paramIndex = 1; - - if(opt == "3") - { - boost::format fmt("%s = ERM.VR(%s):M3("); - fmt % v.str() % v.str(); - put(fmt.str()); - } - else - { - auto target = std::visit(VR_X(), trig.params.get()[paramIndex++]); - - boost::format fmt("%s = ERM.VR(%s):M%s("); - fmt % target % v.str() % opt; - put(fmt.str()); - } - - for(int i = paramIndex; i < trig.params.get().size(); i++) - { - opt = std::visit(VR_X(), trig.params.get()[i]); - if(i > paramIndex) put(","); - put(opt); - } - - putLine(")"); - } - break; - case 'X': //bit xor - { - if(!trig.params.has_value() || trig.params.get().size() != 1) - throw EScriptExecError("VR:X option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_X(), trig.params.get()[0]); - - boost::format fmt("%s = bit.bxor(%s, %s)"); - fmt % v.str() % v.str() % opt;putLine(fmt.str()); - } - break; - case 'R': //random variables - { - //TODO - putLine("--VR:R not implemented"); - } - break; - case 'S': //setting variable - { - if(!trig.params.has_value() || trig.params.get().size() != 1) - throw EScriptExecError("VR:S option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_S(), trig.params.get()[0]); - put(v.str()); - put(" = "); - put(opt); - endLine(); - } - break; - case 'T': //random variables - { - //TODO - putLine("--VR:T not implemented"); - } - break; - case 'V': //convert string to value - { - if(!trig.params.has_value() || trig.params.get().size() != 1) - throw EScriptExecError("VR:V option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_X(), trig.params.get()[0]); - boost::format fmt("%s = tostring(%s)"); - fmt % v.str() % opt; - putLine(fmt.str()); - } - break; - default: - throw EScriptExecError("Wrong VR receiver option!"); - break; - } - } - }; - - - struct ERMExp : public Converter - { - ERMExp(std::ostream * out_) - : Converter(out_) - {} - - template - void performBody(const std::optional & body, const Visitor & visitor) const - { - if(body.has_value()) - { - const ERM::Tbody & bo = body.get(); - for(int g=0; g & identifier, const std::optional & body) const - { - if(name == "VR") - { - if(!identifier.has_value()) - throw EScriptExecError("VR receiver requires arguments"); - - ERM::Tidentifier tid = identifier.value(); - if(tid.size() != 1) - throw EScriptExecError("VR receiver takes exactly 1 argument"); - - performBody(body, VR(out, tid[0])); - } - else if(name == "re") - { - if(!identifier.has_value()) - throw EScriptExecError("re receiver requires arguments"); - - ERM::Tidentifier tid = identifier.value(); - - auto argc = tid.size(); - - if(argc > 0) - { - std::string loopCounter = (std::visit(LVL1IexpToVar(), tid.at(0))).str(); - - std::string startVal = argc > 1 ? (std::visit(LVL1IexpToVar(), tid.at(1))).str() : loopCounter; - std::string stopVal = argc > 2 ? (std::visit(LVL1IexpToVar(), tid.at(2))).str() : loopCounter; - std::string increment = argc > 3 ? (std::visit(LVL1IexpToVar(), tid.at(3))).str() : "1"; - - boost::format fmt("for __iter = %s, %s, %s do"); - - - fmt % startVal % stopVal % increment; - putLine(fmt.str()); - fmt.parse("%s = __iter"); - fmt % loopCounter; - putLine(fmt.str()); - } - else - { - throw EScriptExecError("re receiver requires arguments"); - } - } - else if(name == "FU" && !identifier.has_value()) - { - performBody(body, FU(out)); //assume FU:E - } - else if(name == "MC") - { - if(identifier.has_value()) - { - ERM::Tidentifier tid = identifier.value(); - if(tid.size() != 1) - throw EScriptExecError("MC receiver takes no more than 1 argument"); - - performBody(body, MC(out, tid[0])); - } - else - { - performBody(body, MC(out)); - } - } - else - { - std::vector identifiers; - - if(identifier.has_value()) - { - for(const auto & id : identifier.value()) - { - Variable v = std::visit(LVL1IexpToVar(), id); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in receiver identifier"); - identifiers.push_back(v.str()); - } - } - - std::string params; - - for(auto iter = std::begin(identifiers); iter != std::end(identifiers); ++iter) - { - if(!params.empty()) - params += ", "; - params += *iter; - } - - if(body.has_value()) - { - const ERM::Tbody & bo = body.get(); - if(bo.size() == 1) - { - boost::format fmt("ERM.%s(%s)"); - fmt % name; - fmt % params; - - GenericReceiver gr(out, fmt.str(), (name == "DO")); - bo[0].apply_visitor(gr); - } - else - { - putLine("do"); - boost::format fmt("local %s = ERM.%s(%s)"); - fmt % name; - fmt % name; - fmt % params; - - putLine(fmt.str()); - - performBody(body, GenericReceiver(out, name, (name=="DO") )); - - putLine("end"); - } - } - else - { - //is it an error? - logMod->warn("ERM receiver '%s %s' w/o body", name, params); - } - - - } - } - - void convertConditionInner(const Tcondition & cond, char op) const - { - std::string lhs = std::visit(Condition(), cond.cond); - - if(cond.ctype != '/') - op = cond.ctype; - - switch (op) - { - case '&': - put(" and "); - break; - case '|': - put(" or "); - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - put(lhs); - - if(cond.rhs.has_value()) - { - switch (op) - { - case '&': - case '|': - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - convertConditionInner(cond.rhs.get().get(), op); - } - } - - void convertCondition(const Tcondition & cond) const - { - //&c1/c2/c3|c4/c5/c6 -> (c1 & c2 & c3) | c4 | c5 | c6 - std::string lhs = std::visit(Condition(), cond.cond); - put("if "); - put(lhs); - - if(cond.rhs.has_value()) - { - switch (cond.ctype) - { - case '&': - case '|': - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - convertConditionInner(cond.rhs.get().get(), cond.ctype); - } - - putLine(" then "); - } - - void convertReceiverOrInstruction(const std::optional & condition, - const std::string & name, - const std::optional & identifier, - const std::optional & body) const - { - if(name=="if") - { - if(condition.has_value()) - convertCondition(condition.get()); - else - putLine("if true then"); - } - else if(name=="el") - { - putLine("else"); - } - else if(name=="en") - { - putLine("end"); - } - else - { - if(condition.has_value()) - { - convertCondition(condition.get()); - convert(name, identifier, body); - putLine("end"); - } - else - { - convert(name, identifier, body); - } - } - } - - void operator()(const Ttrigger & trig) const - { - throw EInterpreterError("Triggers cannot be executed!"); - } - - void operator()(const TPostTrigger & trig) const - { - throw EInterpreterError("Post-triggers cannot be executed!"); - } - - void operator()(const Tinstruction & trig) const - { - convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, std::make_optional(trig.body)); - } - - void operator()(const Treceiver & trig) const - { - convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, trig.body); - } - }; - - struct Command : public Converter - { - Command(std::ostream * out_) - : Converter(out_) - {} - - void operator()(const Tcommand & cmd) const - { - std::visit(ERMExp(out), cmd.cmd); - } - void operator()(const std::string & comment) const - { - (*out) << "-- " << comment; - endLine(); - } - - void operator()(const spirit::unused_type &) const - { - } - }; - - struct TLiteralEval - { - - std::string operator()(const char & val) - { - return "{\"'\",'"+ std::to_string(val) +"'}"; - } - std::string operator()(const double & val) - { - return std::to_string(val); - } - std::string operator()(const int & val) - { - return std::to_string(val); - } - std::string operator()(const std::string & val) - { - return "{\"'\",[===[" + val + "]===]}"; - } - }; - - struct VOptionEval : public Converter - { - VOptionEval(std::ostream * out_) - : Converter(out_) - {} - - void operator()(VNIL const & opt) const - { - (*out) << "{}"; - } - void operator()(const VNode & opt) const; - - void operator()(const VSymbol & opt) const - { - (*out) << "\"" << opt.text << "\""; - } - void operator()(const TLiteral & opt) const - { - TLiteralEval tmp; - (*out) << std::visit(tmp, opt); - } - void operator()(ERM const ::Tcommand & opt) const - { - //this is how FP works, evaluation == producing side effects - //TODO: can we evaluate to smth more useful? - //??? - throw EVermScriptExecError("Using ERM options in VERM expression is not (yet) allowed"); -// std::visit(ERMExp(out), opt.cmd); - } - }; - - void VOptionEval::operator()(const VNode & opt) const - { - VNode tmpn(opt); - - (*out) << "{"; - - for(VOption & op : tmpn.children) - { - std::visit(VOptionEval(out), op); - (*out) << ","; - } - (*out) << "}"; - } - - struct Line : public Converter - { - Line(std::ostream * out_) - : Converter(out_) - {} - - void operator()(const TVExp & cmd) const - { - put("VERM:E"); - - VNode line(cmd); - - VOptionEval eval(out); - eval(line); - - - endLine(); - } - void operator()(const TERMline & cmd) const - { - std::visit(Command(out), cmd); - } - }; - - void convertInstructions(std::ostream & out, ERMInterpreter * owner) - { - out << "local function instructions()" << std::endl; - out << "local e, x, y = {}, {}, {}" << std::endl; - - Line lineConverter(&out); - - for(const LinePointer & lp : owner->instructions) - { - ERM::TLine & line = owner->retrieveLine(lp); - - std::visit(lineConverter, line); - } - - out << "end" << std::endl; - } - - void convertFunctions(std::ostream & out, ERMInterpreter * owner, const std::vector & triggers) - { - Line lineConverter(&out); - - for(const VERMInterpreter::Trigger & trigger : triggers) - { - ERM::TLine & firstLine = owner->retrieveLine(trigger.line); - - const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); - - //TODO: condition - - out << "ERM:addTrigger({" << std::endl; - - if(!trig.identifier.has_value()) - throw EInterpreterError("Function must have identifier"); - - ERM::Tidentifier tid = trig.identifier.value(); - - if(tid.empty()) - throw EInterpreterError("Function must have identifier"); - - Variable v = std::visit(LVL1IexpToVar(), tid[0]); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in function definition"); - - out << "id = {" << v.str() << "}," << std::endl; - out << "name = 'FU'," << std::endl; - out << "fn = function (e, y, x)" << std::endl; - out << "local _" << std::endl; - - LinePointer lp = trigger.line; - ++lp; - - for(; lp.isValid(); ++lp) - { - ERM::TLine curLine = owner->retrieveLine(lp); - if(owner->isATrigger(curLine)) - break; - - std::visit(lineConverter, curLine); - } - - out << "end," << std::endl; - out << "})" << std::endl; - } - } - - void convertTriggers(std::ostream & out, ERMInterpreter * owner, const VERMInterpreter::TriggerType & type, const std::vector & triggers) - { - Line lineConverter(&out); - - for(const VERMInterpreter::Trigger & trigger : triggers) - { - ERM::TLine & firstLine = owner->retrieveLine(trigger.line); - - const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); - - //TODO: condition - - out << "ERM:addTrigger({" << std::endl; - - std::vector identifiers; - - if(trig.identifier.has_value()) - { - for(const auto & id : trig.identifier.value()) - { - Variable v = std::visit(LVL1IexpToVar(), id); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in trigger definition"); - identifiers.push_back(v.str()); - } - } - - out << "id = {"; - for(const auto & id : identifiers) - out << "'" << id << "',"; - out << "}," << std::endl; - - out << "name = '" << trig.name << "'," << std::endl; - out << "fn = function (e, y)" << std::endl; - out << "local _" << std::endl; - - LinePointer lp = trigger.line; - ++lp; - - for(; lp.isValid(); ++lp) - { - ERM::TLine curLine = owner->retrieveLine(lp); - if(owner->isATrigger(curLine)) - break; - - std::visit(lineConverter, curLine); - } - - out << "end," << std::endl; - out << "})" << std::endl; - } - } -} - -struct ScriptScanner -{ - ERMInterpreter * interpreter; - LinePointer lp; - - ScriptScanner(ERMInterpreter * interpr, const LinePointer & _lp) : interpreter(interpr), lp(_lp) - {} - - void operator()(const TVExp & cmd) const - { - // - } - void operator()(const TERMline & cmd) const - { - if(cmd.which() == 0) //TCommand - { - Tcommand tcmd = std::get(cmd); - switch (tcmd.cmd.which()) - { - case 0: //trigger - { - Trigger trig; - trig.line = lp; - interpreter->triggers[ TriggerType(std::get(tcmd.cmd).name) ].push_back(trig); - } - break; - case 1: //instruction - { - interpreter->instructions.push_back(lp); - } - break; - case 3: //post trigger - { - Trigger trig; - trig.line = lp; - interpreter->postTriggers[ TriggerType(std::get(tcmd.cmd).name) ].push_back(trig); - } - break; - default: - break; - } - } - - } -}; - - -ERMInterpreter::ERMInterpreter(vstd::CLoggerBase * logger_) - : logger(logger_) -{ - -} - -ERMInterpreter::~ERMInterpreter() -{ - -} - -bool ERMInterpreter::isATrigger( const ERM::TLine & line ) -{ - switch(line.which()) - { - case 0: //v-exp - { - TVExp vexp = std::get(line); - if(vexp.children.empty()) - return false; - - switch (getExpType(vexp.children[0])) - { - case SYMBOL: - return false; - break; - case TCMD: - return isCMDATrigger( std::get(vexp.children[0]) ); - break; - default: - return false; - break; - } - } - break; - case 1: //erm - { - TERMline ermline = std::get(line); - switch(ermline.which()) - { - case 0: //tcmd - return isCMDATrigger( std::get(ermline) ); - break; - default: - return false; - break; - } - } - break; - default: - assert(0); //it should never happen - break; - } - assert(0); - return false; -} - -ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) -{ - //MAINTENANCE: keep it correct! - return static_cast(opt.which()); -} - -bool ERMInterpreter::isCMDATrigger(const ERM::Tcommand & cmd) -{ - switch (cmd.cmd.which()) - { - case 0: //trigger - case 3: //post trigger - return true; - break; - default: - return false; - break; - } -} - -ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) -{ - return scripts.find(linePtr)->second; -} - -ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) -{ - if(line.which() == 1) - { - ERM::TERMline &tl = std::get(line); - if(tl.which() == 0) - { - ERM::Tcommand &tcm = std::get(tl); - if(tcm.cmd.which() == 0) - { - return std::get(tcm.cmd); - } - else if(tcm.cmd.which() == 3) - { - return std::get(tcm.cmd); - } - throw ELineProblem("Given line is not a trigger!"); - } - throw ELineProblem("Given line is not a command!"); - } - throw ELineProblem("Given line is not an ERM trigger!"); -} - -std::string ERMInterpreter::loadScript(const std::string & name, const std::string & source) -{ - CERMPreprocessor preproc(source); - - const bool isVERM = preproc.version == CERMPreprocessor::Version::VERM; - - ERMParser ep; - - std::vector buf = ep.parseFile(preproc); - - for(int g=0; g(buf.size()), g, buf[g].realLineNum)] = buf[g].tl; - - for(auto p : scripts) - std::visit(ScriptScanner(this, p.first), p.second); - - std::stringstream out; - - out << "local ERM = require(\"core:erm\")" << std::endl; - - if(isVERM) - { - out << "local VERM = require(\"core:verm\")" << std::endl; - } - - out << "local _" << std::endl; - out << "local v, w, z, F, M, Q = ERM.v, ERM.w, ERM.z, ERM.F, ERM.M, ERM.Q" << std::endl; - - ERMConverter::convertInstructions(out, this); - - for(const auto & p : triggers) - { - const VERMInterpreter::TriggerType & tt = p.first; - - if(tt.type == VERMInterpreter::TriggerType::FU) - { - ERMConverter::convertFunctions(out, this, p.second); - } - else - { - ERMConverter::convertTriggers(out, this, tt, p.second); - } - } - - for(const auto & p : postTriggers) - ;//TODO:postTriggers - - out << "ERM:callInstructions(instructions)" << std::endl; - - return out.str(); -} - -namespace VERMInterpreter -{ - VOption convertToVOption(const ERM::TVOption & tvo) - { - return std::visit(OptionConverterVisitor(), tvo); - } - - VNode::VNode( const ERM::TVExp & exp ) - { - for(int i=0; i & modifierList, bool asSymbol ) - { - for(int g=0; g0; i--) - { - children[i] = children[i-1]; - } - } - else - { - children.cdr() = VNode(children); - } - - if(modifierList[g] == "`") - { - children.car() = VSymbol("`"); - } - else if(modifierList[g] == ",!") - { - children.car() = VSymbol("comma-unlist"); - } - else if(modifierList[g] == ",") - { - children.car() = VSymbol(","); - } - else if(modifierList[g] == "#'") - { - children.car() = VSymbol("get-func"); - } - else if(modifierList[g] == "'") - { - children.car() = VSymbol("'"); - } - else - throw EInterpreterError("Incorrect value of modifier!"); - } - } - - VermTreeIterator & VermTreeIterator::operator=( const VOption & opt ) - { - switch (state) - { - case CAR: - if(parent->size() <= basePos) - parent->push_back(opt); - else - (*parent)[basePos] = opt; - break; - case NORM: - parent->resize(basePos+1); - (*parent)[basePos] = opt; - break; - default://should never happen - break; - } - return *this; - } - - VermTreeIterator & VermTreeIterator::operator=( const std::vector & opt ) - { - switch (state) - { - case CAR: - //TODO: implement me - break; - case NORM: - parent->resize(basePos+1); - parent->insert(parent->begin()+basePos, opt.begin(), opt.end()); - break; - default://should never happen - break; - } - return *this; - } - VermTreeIterator & VermTreeIterator::operator=( const VOptionList & opt ) - { - return *this = opt; - } - VOption & VermTreeIterator::getAsItem() - { - if(state == CAR) - return (*parent)[basePos]; - else - throw EInterpreterError("iterator is not in car state, cannot get as list"); - } - - size_t VermTreeIterator::size() const - { - return parent->size() - basePos; - } - - VERMInterpreter::VOptionList VermTreeIterator::getAsList() - { - VOptionList ret; - for(int g = basePos; gsize(); ++g) - { - ret.push_back((*parent)[g]); - } - return ret; - } - - VOption OptionConverterVisitor::operator()(ERM const ::TVExp & cmd) const - { - return VNode(cmd); - } - VOption OptionConverterVisitor::operator()(ERM const ::TSymbol & cmd) const - { - if(cmd.symModifier.empty()) - return VSymbol(cmd.sym); - else - return VNode(cmd); - } - VOption OptionConverterVisitor::operator()(const char & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(const double & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(const int & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(ERM const ::Tcommand & cmd) const - { - return cmd; - } - VOption OptionConverterVisitor::operator()(ERM const ::TStringConstant & cmd) const - { - return TLiteral(cmd.str); - } - - VermTreeIterator VOptionList::cdr() - { - VermTreeIterator ret(*this); - ret.basePos = 1; - return ret; - } - - VermTreeIterator VOptionList::car() - { - VermTreeIterator ret(*this); - ret.state = VermTreeIterator::CAR; - return ret; - } - -} +/* + * ERMInterpreter.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 "ERMInterpreter.h" + +#include + +namespace spirit = boost::spirit; +using ::scripting::ContextBase; +using namespace ::VERMInterpreter; + +typedef int TUnusedType; + +namespace ERMConverter +{ + //console printer + using namespace ERM; + + static const std::map CMP_OPERATION = + { + {"<", "<"}, + {">", ">"}, + {">=", ">="}, + {"=>", ">="}, + {"<=", "<="}, + {"=<", "<="}, + {"=", "=="}, + {"<>", "~="}, + {"><", "~="}, + }; + + + struct Variable + { + std::string name = ""; + std::string macro = ""; + int index = 0; + + Variable(const std::string & name_, int index_) + { + name = name_; + index = index_; + } + + Variable(const std::string & macro_) + { + macro = macro_; + } + + bool isEmpty() const + { + return (name == "") && (macro == ""); + } + + bool isMacro() const + { + return (name == "") && (macro != ""); + } + + bool isSpecial() const + { + return (name.size() > 0) && (name[0] == 'd'); + } + + std::string str() const + { + if(isEmpty()) + { + return std::to_string(index); + } + else if(isMacro()) + { + return boost::to_string(boost::format("M['%s']") % macro); + } + else if(isSpecial() && (name.size() == 1)) + { + boost::format fmt; + fmt.parse("{'d', %d}"); + fmt % index; + return fmt.str(); + } + else if(isSpecial() && (name.size() != 1)) + { + + std::string ret; + + { + boost::format fmt; + + if(index == 0) + { + fmt.parse("Q['%s']"); + fmt % name[name.size()-1]; + } + else + { + fmt.parse("%s['%d']"); + fmt % name[name.size()-1] % index; + } + ret = fmt.str(); + } + + for(int i = ((int) name.size())-2; i > 0; i--) + { + boost::format fmt("%s[tostring(%s)]"); + + fmt % name[i] % ret; + + ret = fmt.str(); + } + + { + boost::format fmt; + fmt.parse("{'d', %s}"); + fmt % ret; + return fmt.str(); + } + } + else + { + std::string ret; + { + boost::format fmt; + + if(index == 0) + { + fmt.parse("Q['%s']"); + fmt % name[name.size()-1]; + } + else + { + fmt.parse("%s['%d']"); + fmt % name[name.size()-1] % index; + } + ret = fmt.str(); + } + + for(int i = ((int) name.size())-2; i >= 0; i--) + { + boost::format fmt("%s[tostring(%s)]"); + + fmt % name[i] % ret; + + ret = fmt.str(); + } + + return ret; + } + } + }; + + struct LVL2IexpToVar + { + LVL2IexpToVar() = default; + + Variable operator()(const TVarExpNotMacro & val) const + { + if(val.val.has_value()) + return Variable(val.varsym, *val.val); + else + return Variable(val.varsym, 0); + } + + Variable operator()(const TMacroUsage & val) const + { + return Variable(val.macro); + } + }; + + struct LVL1IexpToVar + { + LVL1IexpToVar() = default; + + Variable operator()(const int & constant) const + { + return Variable("", constant); + } + + Variable operator()(const TVarExp & var) const + { + return std::visit(LVL2IexpToVar(), var); + } + }; + + struct Condition + { + std::string operator()(const TComparison & cmp) const + { + Variable lhs = std::visit(LVL1IexpToVar(), cmp.lhs); + Variable rhs = std::visit(LVL1IexpToVar(), cmp.rhs); + + auto sign = CMP_OPERATION.find(cmp.compSign); + if(sign == std::end(CMP_OPERATION)) + throw EScriptExecError(std::string("Wrong comparison sign: ") + cmp.compSign); + + boost::format fmt("(%s %s %s)"); + fmt % lhs.str() % sign->second % rhs.str(); + return fmt.str(); + } + std::string operator()(const int & flag) const + { + return boost::to_string(boost::format("F['%d']") % flag); + } + }; + + struct ParamIO + { + ParamIO() = default; + std::string name = ""; + bool isInput = false; + bool semi = false; + std::string semiCmpSign = ""; + }; + + struct Converter + { + mutable std::ostream * out; + Converter(std::ostream * out_) + : out(out_) + {} + protected: + + void put(const std::string & text) const + { + (*out) << text; + } + + void putLine(const std::string & line) const + { + (*out) << line << std::endl; + } + + void endLine() const + { + (*out) << std::endl; + } + }; + + struct GetBodyOption + { + virtual std::string operator()(const TVarConcatString & cmp) const + { + throw EScriptExecError("String concatenation not allowed in this receiver"); + } + virtual std::string operator()(const TStringConstant & cmp) const + { + throw EScriptExecError("String constant not allowed in this receiver"); + } + virtual std::string operator()(const TCurriedString & cmp) const + { + throw EScriptExecError("Curried string not allowed in this receiver"); + } + virtual std::string operator()(const TSemiCompare & cmp) const + { + throw EScriptExecError("Semi comparison not allowed in this receiver"); + } + virtual std::string operator()(const TMacroDef & cmp) const + { + throw EScriptExecError("Macro definition not allowed in this receiver"); + } + virtual std::string operator()(const TIexp & cmp) const + { + throw EScriptExecError("i-expression not allowed in this receiver"); + } + virtual std::string operator()(const TVarpExp & cmp) const + { + throw EScriptExecError("Varp expression not allowed in this receiver"); + } + virtual std::string operator()(const spirit::unused_type & cmp) const + { + throw EScriptExecError("\'Nothing\' not allowed in this receiver"); + } + }; + + struct BodyOption + { + ParamIO operator()(const TVarConcatString & cmp) const + { + throw EScriptExecError(std::string("String concatenation not allowed in this receiver|")+cmp.string.str+"|"); + } + + ParamIO operator()(const TStringConstant & cmp) const + { + boost::format fmt("[===[%s]===]"); + fmt % cmp.str; + + ParamIO ret; + ret.isInput = true; + ret.name = fmt.str(); + return ret; + } + + ParamIO operator()(const TCurriedString & cmp) const + { + throw EScriptExecError("Curried string not allowed in this receiver"); + } + + ParamIO operator()(const TSemiCompare & cmp) const + { + ParamIO ret; + ret.isInput = false; + ret.semi = true; + ret.semiCmpSign = cmp.compSign; + ret.name = (std::visit(LVL1IexpToVar(), cmp.rhs)).str(); + return ret; + } + + ParamIO operator()(const TMacroDef & cmp) const + { + throw EScriptExecError("Macro definition not allowed in this receiver"); + } + + ParamIO operator()(const TIexp & cmp) const + { + ParamIO ret; + ret.isInput = true; + ret.name = (std::visit(LVL1IexpToVar(), cmp)).str();; + return ret; + } + + ParamIO operator()(const TVarpExp & cmp) const + { + ParamIO ret; + ret.isInput = false; + + ret.name = (std::visit(LVL2IexpToVar(), cmp.var)).str(); + return ret; + } + + ParamIO operator()(const spirit::unused_type & cmp) const + { + throw EScriptExecError("\'Nothing\' not allowed in this receiver"); + } + }; + + struct Receiver : public Converter + { + Receiver(std::ostream * out_) + : Converter(out_) + {} + + virtual void operator()(const TVRLogic & trig) const + { + throw EInterpreterError("VR logic is not allowed in this receiver!"); + } + + virtual void operator()(const TVRArithmetic & trig) const + { + throw EInterpreterError("VR arithmetic is not allowed in this receiver!"); + } + + virtual void operator()(const TNormalBodyOption & trig) const + { + throw EInterpreterError("Normal body is not allowed in this receiver!"); + } + + }; + + struct GenericReceiver : public Receiver + { + std::string name; + bool specialSemiCompare = false; + + GenericReceiver(std::ostream * out_, const std::string & name_, bool specialSemiCompare_) + : Receiver(out_), + name(name_), + specialSemiCompare(specialSemiCompare_) + {} + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & trig) const override + { + std::string outParams; + std::string inParams; + + std::string semiCompareDecl; + + std::vector compares; + + bool hasOutput = false; + bool hasSemiCompare = false; + + std::vector optionParams; + + if(trig.params.has_value()) + { + for(auto & p : *trig.params) + optionParams.push_back(std::visit(BodyOption(), p)); + } + + int idx = 1; + int fidx = 1; + + for(const ParamIO & p : optionParams) + { + if(p.isInput) + { + if(outParams.empty()) + outParams = "_"; + else + outParams += ", _"; + + inParams += ", "; + inParams += p.name; + } + else if(p.semi) + { + hasOutput = true; + hasSemiCompare = true; + + std::string tempVar = std::string("s")+std::to_string(idx); + + if(semiCompareDecl.empty()) + { + semiCompareDecl = "local "+tempVar; + } + else + { + semiCompareDecl += ", "; + semiCompareDecl += tempVar; + } + + if(outParams.empty()) + { + outParams = tempVar; + } + else + { + outParams += ", "; + outParams += tempVar; + } + + inParams += ", nil"; + + + auto sign = CMP_OPERATION.find(p.semiCmpSign); + if(sign == std::end(CMP_OPERATION)) + throw EScriptExecError(std::string("Wrong comparison sign: ") + p.semiCmpSign); + + boost::format cmpFmt("F[%d] = (%s %s %s)"); + cmpFmt % fidx % p.name % sign->second % tempVar; + compares.push_back(cmpFmt.str()); + + fidx++; + } + else + { + hasOutput = true; + + if(outParams.empty()) + { + outParams = p.name; + } + else + { + outParams += ", "; + outParams += p.name; + } + + inParams += ", nil"; + } + + idx++; + } + + if(hasSemiCompare) + { + putLine(semiCompareDecl); + } + + boost::format callFormat; + + if(hasOutput) + { + callFormat.parse("%s = %s:%s(x%s)"); + callFormat % outParams; + } + else + { + callFormat.parse("%s:%s(x%s)"); + } + + callFormat % name; + callFormat % trig.optionCode; + callFormat % inParams; + + putLine(callFormat.str()); + + for(auto & str : compares) + putLine(str); + } + }; + + struct FU : public Receiver + { + Variable v; + + FU(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + FU(std::ostream * out_) + : Receiver(out_), + v("", 0) + { + } + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & trig) const override + { + switch(trig.optionCode) + { + case 'E': + { + putLine("do return end"); + } + break; + default: + throw EInterpreterError("Unknown opcode in FU receiver"); + break; + } + } + }; + + struct MC_S : public GetBodyOption + { + MC_S() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TMacroDef & cmp) const override + { + return cmp.macro; + } + }; + + struct MC : public Receiver + { + Variable v; + + MC(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + MC(std::ostream * out_) + : Receiver(out_), + v("", 0) + { + } + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & option) const override + { + switch(option.optionCode) + { + case 'S': + { + if(option.params.has_value()) + { + for(auto & p : *option.params) + { + std::string macroName = std::visit(MC_S(), p); + + boost::format callFormat; + + if(v.isEmpty()) + { + callFormat.parse("ERM:addMacro('%s', 'v', '%s')"); + callFormat % macroName % macroName; + } + else + { + callFormat.parse("ERM:addMacro('%s', '%s', '%d')"); + callFormat % macroName % v.name % v.index; + } + + putLine(callFormat.str()); + } + } + } + break; + default: + throw EInterpreterError("Unknown opcode in MC receiver"); + break; + } + } + }; + + struct VR_S : public GetBodyOption + { + VR_S() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + auto v = std::visit(LVL1IexpToVar(), cmp); + return v.str(); + } + std::string operator()(const TStringConstant & cmp) const override + { + boost::format fmt("[===[%s]===]"); + fmt % cmp.str; + return fmt.str(); + } + }; + + struct VR_H : public GetBodyOption + { + VR_H() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + Variable p = std::visit(LVL1IexpToVar(), cmp); + + if(p.index <= 0) + throw EScriptExecError("VR:H requires flag index"); + + if(p.name != "") + throw EScriptExecError("VR:H accept only flag index"); + + + boost::format fmt("'%d'"); + fmt % p.index; + return fmt.str(); + } + }; + + struct VR_X : public GetBodyOption + { + VR_X() + { + } + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + Variable p = std::visit(LVL1IexpToVar(), cmp); + + return p.str(); + } + }; + + struct VR : public Receiver + { + Variable v; + + VR(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + using Receiver::operator(); + + void operator()(const TVRLogic & trig) const override + { + Variable rhs = std::visit(LVL1IexpToVar(), trig.var); + + std::string opcode; + + switch (trig.opcode) + { + case '&': + opcode = "bit.band"; + break; + case '|': + opcode = "bit.bor"; + break; + default: + throw EInterpreterError("Wrong opcode in VR logic expression!"); + break; + } + + boost::format fmt("%s = %s(%s, %s)"); + fmt % v.str() % opcode % v.str() % rhs.str(); + putLine(fmt.str()); + } + + void operator()(const TVRArithmetic & trig) const override + { + Variable rhs = std::visit(LVL1IexpToVar(), trig.rhs); + + std::string opcode; + + switch (trig.opcode) + { + case '+': + opcode = v.name[0] == 'z' ? ".." : "+"; + break; + case '-': + case '*': + case '%': + opcode = trig.opcode; + break; + case ':': + opcode = "/"; + break; + default: + throw EInterpreterError("Wrong opcode in VR arithmetic!"); + break; + } + + boost::format fmt("%s = %s %s %s"); + fmt % v.str() % v.str() % opcode % rhs.str(); + putLine(fmt.str()); + } + + void operator()(const TNormalBodyOption & trig) const override + { + switch(trig.optionCode) + { + case 'C': //setting/checking v vars + { + if(v.index <= 0) + throw EScriptExecError("VR:C requires indexed variable"); + + std::vector optionParams; + + if(trig.params.has_value()) + { + for(auto & p : *trig.params) + optionParams.push_back(std::visit(BodyOption(), p)); + } + + auto index = v.index; + + for(auto & p : optionParams) + { + boost::format fmt; + if(p.isInput) + fmt.parse("%s['%d'] = %s") % v.name % index % p.name; + else + fmt.parse("%s = %s['%d']") % p.name % v.name % index; + putLine(fmt.str()); + index++; + } + } + break; + case 'H': //checking if string is empty + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:H option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_H(), (*trig.params)[0]); + boost::format fmt("ERM.VR(%s):H(%s)"); + fmt % v.str() % opt; + putLine(fmt.str()); + } + break; + case 'U': + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:H/U need 1 parameter!"); + + std::string opt = std::visit(VR_S(), (*trig.params)[0]); + boost::format fmt("ERM.VR(%s):%c(%s)"); + fmt % v.str() % (trig.optionCode) % opt; + putLine(fmt.str()); + } + break; + case 'M': //string operations + { + if(!trig.params.has_value() || trig.params->size() < 2) + throw EScriptExecError("VR:M needs at least 2 parameters!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + int paramIndex = 1; + + if(opt == "3") + { + boost::format fmt("%s = ERM.VR(%s):M3("); + fmt % v.str() % v.str(); + put(fmt.str()); + } + else + { + auto target = std::visit(VR_X(), (*trig.params)[paramIndex++]); + + boost::format fmt("%s = ERM.VR(%s):M%s("); + fmt % target % v.str() % opt; + put(fmt.str()); + } + + for(int i = paramIndex; i < trig.params->size(); i++) + { + opt = std::visit(VR_X(), (*trig.params)[i]); + if(i > paramIndex) put(","); + put(opt); + } + + putLine(")"); + } + break; + case 'X': //bit xor + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:X option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + + boost::format fmt("%s = bit.bxor(%s, %s)"); + fmt % v.str() % v.str() % opt;putLine(fmt.str()); + } + break; + case 'R': //random variables + { + //TODO + putLine("--VR:R not implemented"); + } + break; + case 'S': //setting variable + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:S option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_S(), (*trig.params)[0]); + put(v.str()); + put(" = "); + put(opt); + endLine(); + } + break; + case 'T': //random variables + { + //TODO + putLine("--VR:T not implemented"); + } + break; + case 'V': //convert string to value + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:V option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + boost::format fmt("%s = tostring(%s)"); + fmt % v.str() % opt; + putLine(fmt.str()); + } + break; + default: + throw EScriptExecError("Wrong VR receiver option!"); + break; + } + } + }; + + + struct ERMExp : public Converter + { + ERMExp(std::ostream * out_) + : Converter(out_) + {} + + template + void performBody(const std::optional & body, const Visitor & visitor) const + { + if(body.has_value()) + { + const ERM::Tbody & bo = *body; + for(int g=0; g & identifier, const std::optional & body) const + { + if(name == "VR") + { + if(!identifier.has_value()) + throw EScriptExecError("VR receiver requires arguments"); + + ERM::Tidentifier tid = identifier.value(); + if(tid.size() != 1) + throw EScriptExecError("VR receiver takes exactly 1 argument"); + + performBody(body, VR(out, tid[0])); + } + else if(name == "re") + { + if(!identifier.has_value()) + throw EScriptExecError("re receiver requires arguments"); + + ERM::Tidentifier tid = identifier.value(); + + auto argc = tid.size(); + + if(argc > 0) + { + std::string loopCounter = (std::visit(LVL1IexpToVar(), tid.at(0))).str(); + + std::string startVal = argc > 1 ? (std::visit(LVL1IexpToVar(), tid.at(1))).str() : loopCounter; + std::string stopVal = argc > 2 ? (std::visit(LVL1IexpToVar(), tid.at(2))).str() : loopCounter; + std::string increment = argc > 3 ? (std::visit(LVL1IexpToVar(), tid.at(3))).str() : "1"; + + boost::format fmt("for __iter = %s, %s, %s do"); + + + fmt % startVal % stopVal % increment; + putLine(fmt.str()); + fmt.parse("%s = __iter"); + fmt % loopCounter; + putLine(fmt.str()); + } + else + { + throw EScriptExecError("re receiver requires arguments"); + } + } + else if(name == "FU" && !identifier.has_value()) + { + performBody(body, FU(out)); //assume FU:E + } + else if(name == "MC") + { + if(identifier.has_value()) + { + ERM::Tidentifier tid = identifier.value(); + if(tid.size() != 1) + throw EScriptExecError("MC receiver takes no more than 1 argument"); + + performBody(body, MC(out, tid[0])); + } + else + { + performBody(body, MC(out)); + } + } + else + { + std::vector identifiers; + + if(identifier.has_value()) + { + for(const auto & id : identifier.value()) + { + Variable v = std::visit(LVL1IexpToVar(), id); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in receiver identifier"); + identifiers.push_back(v.str()); + } + } + + std::string params; + + for(auto iter = std::begin(identifiers); iter != std::end(identifiers); ++iter) + { + if(!params.empty()) + params += ", "; + params += *iter; + } + + if(body.has_value()) + { + const ERM::Tbody & bo = *body; + if(bo.size() == 1) + { + boost::format fmt("ERM.%s(%s)"); + fmt % name; + fmt % params; + + GenericReceiver gr(out, fmt.str(), (name == "DO")); + std::visit(gr,bo[0]); + } + else + { + putLine("do"); + boost::format fmt("local %s = ERM.%s(%s)"); + fmt % name; + fmt % name; + fmt % params; + + putLine(fmt.str()); + + performBody(body, GenericReceiver(out, name, (name=="DO") )); + + putLine("end"); + } + } + else + { + //is it an error? + logMod->warn("ERM receiver '%s %s' w/o body", name, params); + } + + + } + } + + void convertConditionInner(const Tcondition & cond, char op) const + { + std::string lhs = std::visit(Condition(), cond.cond); + + if(cond.ctype != '/') + op = cond.ctype; + + switch (op) + { + case '&': + put(" and "); + break; + case '|': + put(" or "); + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + put(lhs); + + if(cond.rhs.has_value()) + { + switch (op) + { + case '&': + case '|': + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + convertConditionInner(cond.rhs->get(), op); + } + } + + void convertCondition(const Tcondition & cond) const + { + //&c1/c2/c3|c4/c5/c6 -> (c1 & c2 & c3) | c4 | c5 | c6 + std::string lhs = std::visit(Condition(), cond.cond); + put("if "); + put(lhs); + + if(cond.rhs.has_value()) + { + switch (cond.ctype) + { + case '&': + case '|': + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + convertConditionInner(cond.rhs->get(), cond.ctype); + } + + putLine(" then "); + } + + void convertReceiverOrInstruction(const std::optional & condition, + const std::string & name, + const std::optional & identifier, + const std::optional & body) const + { + if(name=="if") + { + if(condition.has_value()) + convertCondition(*condition); + else + putLine("if true then"); + } + else if(name=="el") + { + putLine("else"); + } + else if(name=="en") + { + putLine("end"); + } + else + { + if(condition.has_value()) + { + convertCondition(*condition); + convert(name, identifier, body); + putLine("end"); + } + else + { + convert(name, identifier, body); + } + } + } + + void operator()(const Ttrigger & trig) const + { + throw EInterpreterError("Triggers cannot be executed!"); + } + + void operator()(const TPostTrigger & trig) const + { + throw EInterpreterError("Post-triggers cannot be executed!"); + } + + void operator()(const Tinstruction & trig) const + { + convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, std::make_optional(trig.body)); + } + + void operator()(const Treceiver & trig) const + { + convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, trig.body); + } + }; + + struct Command : public Converter + { + Command(std::ostream * out_) + : Converter(out_) + {} + + void operator()(const Tcommand & cmd) const + { + std::visit(ERMExp(out), cmd.cmd); + } + void operator()(const std::string & comment) const + { + (*out) << "-- " << comment; + endLine(); + } + + void operator()(const spirit::unused_type &) const + { + } + }; + + struct TLiteralEval + { + + std::string operator()(const char & val) + { + return "{\"'\",'"+ std::to_string(val) +"'}"; + } + std::string operator()(const double & val) + { + return std::to_string(val); + } + std::string operator()(const int & val) + { + return std::to_string(val); + } + std::string operator()(const std::string & val) + { + return "{\"'\",[===[" + val + "]===]}"; + } + }; + + struct VOptionEval : public Converter + { + VOptionEval(std::ostream * out_) + : Converter(out_) + {} + + void operator()(VNIL const & opt) const + { + (*out) << "{}"; + } + void operator()(const boost::recursive_wrapper & opt) const; + + void operator()(const VSymbol & opt) const + { + (*out) << "\"" << opt.text << "\""; + } + void operator()(const TLiteral & opt) const + { + TLiteralEval tmp; + (*out) << std::visit(tmp, opt); + } + void operator()(const ERM::Tcommand & opt) const + { + //this is how FP works, evaluation == producing side effects + //TODO: can we evaluate to smth more useful? + //??? + throw EVermScriptExecError("Using ERM options in VERM expression is not (yet) allowed"); +// std::visit(ERMExp(out), opt.cmd); + } + }; + + void VOptionEval::operator()(const boost::recursive_wrapper & opt) const + { + VNode tmpn(opt.get()); + + (*out) << "{"; + + for(VOption & op : tmpn.children) + { + std::visit(VOptionEval(out), op); + (*out) << ","; + } + (*out) << "}"; + } + + struct Line : public Converter + { + Line(std::ostream * out_) + : Converter(out_) + {} + + void operator()(const TVExp & cmd) const + { + put("VERM:E"); + + VNode line(cmd); + + VOptionEval eval(out); + eval(line); + + + endLine(); + } + void operator()(const TERMline & cmd) const + { + std::visit(Command(out), cmd); + } + }; + + void convertInstructions(std::ostream & out, ERMInterpreter * owner) + { + out << "local function instructions()" << std::endl; + out << "local e, x, y = {}, {}, {}" << std::endl; + + Line lineConverter(&out); + + for(const LinePointer & lp : owner->instructions) + { + ERM::TLine & line = owner->retrieveLine(lp); + + std::visit(lineConverter, line); + } + + out << "end" << std::endl; + } + + void convertFunctions(std::ostream & out, ERMInterpreter * owner, const std::vector & triggers) + { + Line lineConverter(&out); + + for(const VERMInterpreter::Trigger & trigger : triggers) + { + ERM::TLine & firstLine = owner->retrieveLine(trigger.line); + + const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); + + //TODO: condition + + out << "ERM:addTrigger({" << std::endl; + + if(!trig.identifier.has_value()) + throw EInterpreterError("Function must have identifier"); + + ERM::Tidentifier tid = trig.identifier.value(); + + if(tid.empty()) + throw EInterpreterError("Function must have identifier"); + + Variable v = std::visit(LVL1IexpToVar(), tid[0]); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in function definition"); + + out << "id = {" << v.str() << "}," << std::endl; + out << "name = 'FU'," << std::endl; + out << "fn = function (e, y, x)" << std::endl; + out << "local _" << std::endl; + + LinePointer lp = trigger.line; + ++lp; + + for(; lp.isValid(); ++lp) + { + ERM::TLine curLine = owner->retrieveLine(lp); + if(owner->isATrigger(curLine)) + break; + + std::visit(lineConverter, curLine); + } + + out << "end," << std::endl; + out << "})" << std::endl; + } + } + + void convertTriggers(std::ostream & out, ERMInterpreter * owner, const VERMInterpreter::TriggerType & type, const std::vector & triggers) + { + Line lineConverter(&out); + + for(const VERMInterpreter::Trigger & trigger : triggers) + { + ERM::TLine & firstLine = owner->retrieveLine(trigger.line); + + const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); + + //TODO: condition + + out << "ERM:addTrigger({" << std::endl; + + std::vector identifiers; + + if(trig.identifier.has_value()) + { + for(const auto & id : trig.identifier.value()) + { + Variable v = std::visit(LVL1IexpToVar(), id); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in trigger definition"); + identifiers.push_back(v.str()); + } + } + + out << "id = {"; + for(const auto & id : identifiers) + out << "'" << id << "',"; + out << "}," << std::endl; + + out << "name = '" << trig.name << "'," << std::endl; + out << "fn = function (e, y)" << std::endl; + out << "local _" << std::endl; + + LinePointer lp = trigger.line; + ++lp; + + for(; lp.isValid(); ++lp) + { + ERM::TLine curLine = owner->retrieveLine(lp); + if(owner->isATrigger(curLine)) + break; + + std::visit(lineConverter, curLine); + } + + out << "end," << std::endl; + out << "})" << std::endl; + } + } +} + +struct ScriptScanner +{ + ERMInterpreter * interpreter; + LinePointer lp; + + ScriptScanner(ERMInterpreter * interpr, const LinePointer & _lp) : interpreter(interpr), lp(_lp) + {} + + void operator()(const TVExp & cmd) const + { + // + } + void operator()(const TERMline & cmd) const + { + if(std::holds_alternative(cmd)) //TCommand + { + Tcommand tcmd = std::get(cmd); + struct Visitor + { + void operator()(const ERM::Ttrigger& t) const + { + Trigger trig; + trig.line = l; + i->triggers[ TriggerType(t.name) ].push_back(trig); + } + void operator()(const ERM::Tinstruction&) const + { + i->instructions.push_back(l); + } + void operator()(const ERM::Treceiver&) const {} + void operator()(const ERM::TPostTrigger& pt) const + { + Trigger trig; + trig.line = l; + i->postTriggers[ TriggerType(pt.name) ].push_back(trig); + } + const decltype(interpreter)& i; + const LinePointer& l; + }; + + Visitor v{interpreter, lp}; + std::visit(v, tcmd.cmd); + } + } +}; + + +ERMInterpreter::ERMInterpreter(vstd::CLoggerBase * logger_) + : logger(logger_) +{ + +} + +ERMInterpreter::~ERMInterpreter() +{ + +} + +bool ERMInterpreter::isATrigger( const ERM::TLine & line ) +{ + if(std::holds_alternative(line)) + { + TVExp vexp = std::get(line); + if(vexp.children.empty()) + return false; + + switch (getExpType(vexp.children[0])) + { + case SYMBOL: + return false; + break; + case TCMD: + return isCMDATrigger( std::get(vexp.children[0]) ); + break; + default: + return false; + break; + } + } + else if(std::holds_alternative(line)) + { + TERMline ermline = std::get(line); + return std::holds_alternative(ermline) && isCMDATrigger( std::get(ermline) ); + } + else + { + assert(0); + } + return false; +} + +ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) +{ + struct Visitor + { + ERM::EVOtions operator()(const boost::recursive_wrapper&) const + { + return ERM::EVOtions::VEXP; + } + ERM::EVOtions operator()(const ERM::TSymbol&) const + { + return ERM::EVOtions::SYMBOL; + } + ERM::EVOtions operator()(char) const + { + return ERM::EVOtions::CHAR; + } + ERM::EVOtions operator()(double) const + { + return ERM::EVOtions::DOUBLE; + } + ERM::EVOtions operator()(int) const + { + return ERM::EVOtions::INT; + } + ERM::EVOtions operator()(const ERM::Tcommand&) const + { + return ERM::EVOtions::TCMD; + } + ERM::EVOtions operator()(const ERM::TStringConstant&) const + { + return ERM::EVOtions::STRINGC; + } + }; + const Visitor v; + return std::visit(v, opt); +} + +bool ERMInterpreter::isCMDATrigger(const ERM::Tcommand & cmd) +{ + struct Visitor + { + bool operator()(const ERM::Ttrigger&) const { return true; } + bool operator()(const ERM::TPostTrigger&) const { return true; } + bool operator()(const ERM::Tinstruction&) const { return false; } + bool operator()(const ERM::Treceiver&) const { return false; } + }; + const Visitor v; + return std::visit(v, cmd.cmd); +} + +ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) +{ + return scripts.find(linePtr)->second; +} + +ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) +{ + if(std::holds_alternative(line)) + { + ERM::TERMline &tl = std::get(line); + if(std::holds_alternative(tl)) + { + ERM::Tcommand &tcm = std::get(tl); + if(std::holds_alternative(tcm.cmd)) + { + return std::get(tcm.cmd); + } + else if(std::holds_alternative(tcm.cmd)) + { + return std::get(tcm.cmd); + } + throw ELineProblem("Given line is not a trigger!"); + } + throw ELineProblem("Given line is not a command!"); + } + throw ELineProblem("Given line is not an ERM trigger!"); +} + +std::string ERMInterpreter::loadScript(const std::string & name, const std::string & source) +{ + CERMPreprocessor preproc(source); + + const bool isVERM = preproc.version == CERMPreprocessor::Version::VERM; + + ERMParser ep; + + std::vector buf = ep.parseFile(preproc); + + for(int g=0; g(buf.size()), g, buf[g].realLineNum)] = buf[g].tl; + + for(auto p : scripts) + std::visit(ScriptScanner(this, p.first), p.second); + + std::stringstream out; + + out << "local ERM = require(\"core:erm\")" << std::endl; + + if(isVERM) + { + out << "local VERM = require(\"core:verm\")" << std::endl; + } + + out << "local _" << std::endl; + out << "local v, w, z, F, M, Q = ERM.v, ERM.w, ERM.z, ERM.F, ERM.M, ERM.Q" << std::endl; + + ERMConverter::convertInstructions(out, this); + + for(const auto & p : triggers) + { + const VERMInterpreter::TriggerType & tt = p.first; + + if(tt.type == VERMInterpreter::TriggerType::FU) + { + ERMConverter::convertFunctions(out, this, p.second); + } + else + { + ERMConverter::convertTriggers(out, this, tt, p.second); + } + } + + for(const auto & p : postTriggers) + ;//TODO:postTriggers + + out << "ERM:callInstructions(instructions)" << std::endl; + + return out.str(); +} + +namespace VERMInterpreter +{ + VOption convertToVOption(const ERM::TVOption & tvo) + { + struct OptionConverterVisitor + { + VOption operator()(const boost::recursive_wrapper& cmd) const + { + return boost::recursive_wrapper(VNode(cmd.get())); + } + VOption operator()(const ERM::TSymbol & cmd) const + { + if(cmd.symModifier.empty()) + return VSymbol(cmd.sym); + else + return boost::recursive_wrapper(VNode(cmd)); + } + VOption operator()(const char & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const double & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const int & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const ERM::Tcommand & cmd) const + { + return cmd; + } + VOption operator()(const ERM::TStringConstant & cmd) const + { + return TLiteral(cmd.str); + } + }; + return std::visit(OptionConverterVisitor(), tvo); + } + + VNode::VNode( const ERM::TVExp & exp ) + { + for(int i=0; i & modifierList, bool asSymbol ) + { + for(int g=0; g0; i--) + { + children[i] = children[i-1]; + } + } + else + { + children.cdr() = VNode(children); + } + + if(modifierList[g] == "`") + { + children.car() = VSymbol("`"); + } + else if(modifierList[g] == ",!") + { + children.car() = VSymbol("comma-unlist"); + } + else if(modifierList[g] == ",") + { + children.car() = VSymbol(","); + } + else if(modifierList[g] == "#'") + { + children.car() = VSymbol("get-func"); + } + else if(modifierList[g] == "'") + { + children.car() = VSymbol("'"); + } + else + throw EInterpreterError("Incorrect value of modifier!"); + } + } + + VermTreeIterator & VermTreeIterator::operator=( const VOption & opt ) + { + switch (state) + { + case CAR: + if(parent->size() <= basePos) + parent->push_back(opt); + else + (*parent)[basePos] = opt; + break; + case NORM: + parent->resize(basePos+1); + (*parent)[basePos] = opt; + break; + default://should never happen + break; + } + return *this; + } + + VermTreeIterator & VermTreeIterator::operator=( const std::vector & opt ) + { + switch (state) + { + case CAR: + //TODO: implement me + break; + case NORM: + parent->resize(basePos+1); + parent->insert(parent->begin()+basePos, opt.begin(), opt.end()); + break; + default://should never happen + break; + } + return *this; + } + VermTreeIterator & VermTreeIterator::operator=( const VOptionList & opt ) + { + return *this = opt; + } + VOption & VermTreeIterator::getAsItem() + { + if(state == CAR) + return (*parent)[basePos]; + else + throw EInterpreterError("iterator is not in car state, cannot get as list"); + } + + size_t VermTreeIterator::size() const + { + return parent->size() - basePos; + } + + VERMInterpreter::VOptionList VermTreeIterator::getAsList() + { + VOptionList ret; + for(int g = basePos; gsize(); ++g) + { + ret.push_back((*parent)[g]); + } + return ret; + } + + VermTreeIterator VOptionList::cdr() + { + VermTreeIterator ret(*this); + ret.basePos = 1; + return ret; + } + + VermTreeIterator VOptionList::car() + { + VermTreeIterator ret(*this); + ret.state = VermTreeIterator::CAR; + return ret; + } + +} diff --git a/scripting/erm/ERMInterpreter.h b/scripting/erm/ERMInterpreter.h index baf3d317e..748b632e9 100644 --- a/scripting/erm/ERMInterpreter.h +++ b/scripting/erm/ERMInterpreter.h @@ -1,329 +1,318 @@ -/* - * ERMInterpreter.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 "ERMParser.h" -#include "ERMScriptModule.h" - -class ERMInterpreter; - -namespace VERMInterpreter -{ - using namespace ERM; - - //different exceptions that can be thrown during interpreting - class EInterpreterProblem : public std::exception - { - std::string problem; - public: - const char * what() const throw() override - { - return problem.c_str(); - } - ~EInterpreterProblem() throw() - {} - EInterpreterProblem(const std::string & problemDesc) : problem(problemDesc) - {} - }; - - struct ESymbolNotFound : public EInterpreterProblem - { - ESymbolNotFound(const std::string & sym) : - EInterpreterProblem(std::string("Symbol \"") + sym + std::string("\" not found!")) - {} - }; - - struct EInvalidTrigger : public EInterpreterProblem - { - EInvalidTrigger(const std::string & sym) : - EInterpreterProblem(std::string("Trigger \"") + sym + std::string("\" is invalid!")) - {} - }; - - struct EUsageOfUndefinedMacro : public EInterpreterProblem - { - EUsageOfUndefinedMacro(const std::string & macro) : - EInterpreterProblem(std::string("Macro ") + macro + " is undefined") - {} - }; - - struct EIexpProblem : public EInterpreterProblem - { - EIexpProblem(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - struct ELineProblem : public EInterpreterProblem - { - ELineProblem(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - struct EExecutionError : public EInterpreterProblem - { - EExecutionError(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - //internal interpreter error related to execution - struct EInterpreterError : public EExecutionError - { - EInterpreterError(const std::string & desc) : - EExecutionError(desc) - {} - }; - - //wrong script - struct EScriptExecError : public EExecutionError - { - EScriptExecError(const std::string & desc) : - EExecutionError(desc) - {} - }; - - //wrong script - struct EVermScriptExecError : public EScriptExecError - { - EVermScriptExecError(const std::string & desc) : - EScriptExecError(desc) - {} - }; - - // All numeric variables are integer variables and have a range of -2147483647...+2147483647 - // c stores game active day number //indirect variable - // d current value //not an actual variable but a modifier - // e1..e100 Function floating point variables //local - // e-1..e-100 Trigger local floating point variables //local - // 'f'..'t' Standard variables ('quick variables') //global - // v1..v1000 Standard variables //global - // w1..w100 Hero variables - // w101..w200 Hero variables - // x1..x16 Function parameters //local - // y1..y100 Function local variables //local - // y-1..y-100 Trigger-based local integer variables //local - // z1..z1000 String variables //global - // z-1..z-10 Function local string variables //local - - - struct TriggerType - { - //the same order of trigger types in this enum and in validTriggers array is obligatory! - enum ETrigType - { - AE, BA, BF, BG, BR, CM, CO, FU, GE, GM, HE, HL, HM, IP, LE, MF, MG, MM, MR, MW, OB, PI, SN, TH, TM - }; - - ETrigType type; - - static ETrigType convertTrigger(const std::string & trig) - { - static const std::string validTriggers[] = - { - "AE", "BA", "BF", "BG", "BR", "CM", "CO", "FU", - "GE", "GM", "HE", "HL", "HM", "IP", "LE", "MF", "MG", "MM", "MR", "MW", "OB", "PI", "SN", - "TH", "TM" - }; - - for(int i=0; i(i); - } - throw EInvalidTrigger(trig); - } - - bool operator<(const TriggerType & t2) const - { - return type < t2.type; - } - - TriggerType(const std::string & sym) - { - type = convertTrigger(sym); - } - - }; - - struct LinePointer - { - int lineNum; - int realLineNum; - int fileLength; - - LinePointer() - : fileLength(-1) - {} - - LinePointer(int _fileLength, int line, int _realLineNum) - : fileLength(_fileLength), - lineNum(line), - realLineNum(_realLineNum) - {} - - bool operator<(const LinePointer & rhs) const - { - return lineNum < rhs.lineNum; - } - - bool operator!=(const LinePointer & rhs) const - { - return lineNum != rhs.lineNum; - } - LinePointer & operator++() - { - ++lineNum; - return *this; - } - bool isValid() const - { - return fileLength > 0 && lineNum < fileLength; - } - }; - - struct Trigger - { - LinePointer line; - Trigger() - {} - }; - - - //verm goodies - struct VSymbol - { - std::string text; - VSymbol(const std::string & txt) : text(txt) - {} - }; - - struct VNode; - struct VOptionList; - - struct VNIL - {}; - - - typedef std::variant TLiteral; - - typedef std::variant, VSymbol, TLiteral, ERM::Tcommand> VOption; //options in v-expression, VNIl should be the default - - template - T& getAs(SecType & opt) - { - if(opt.type() == typeid(T)) - return std::get(opt); - else - throw EVermScriptExecError("Wrong type!"); - } - - template - bool isA(const SecType & opt) - { - if(opt.type() == typeid(T)) - return true; - else - return false; - } - - struct VermTreeIterator - { - private: - friend struct VOptionList; - VOptionList * parent; - enum Estate {NORM, CAR} state; - int basePos; //car/cdr offset - public: - VermTreeIterator(VOptionList & _parent) : parent(&_parent), state(NORM), basePos(0) - {} - VermTreeIterator() : parent(nullptr), state(NORM) - {} - - VermTreeIterator & operator=(const VOption & opt); - VermTreeIterator & operator=(const std::vector & opt); - VermTreeIterator & operator=(const VOptionList & opt); - VOption & getAsItem(); - VOptionList getAsList(); - size_t size() const; - - VermTreeIterator& operator=(const VermTreeIterator & rhs) - { - if(this == &rhs) - { - return *this; - } - parent = rhs.parent; - state = rhs.state; - basePos = rhs.basePos; - - return *this; - } - }; - - struct VOptionList : public std::vector - { - private: - friend struct VermTreeIterator; - public: - VermTreeIterator car(); - VermTreeIterator cdr(); - }; - - struct OptionConverterVisitor - { - VOption operator()(ERM const ::TVExp & cmd) const; - VOption operator()(ERM const ::TSymbol & cmd) const; - VOption operator()(const char & cmd) const; - VOption operator()(const double & cmd) const; - VOption operator()(const int & cmd) const; - VOption operator()(ERM const ::Tcommand & cmd) const; - VOption operator()(ERM const ::TStringConstant & cmd) const; - }; - - struct VNode - { - private: - void processModifierList(const std::vector & modifierList, bool asSymbol); - public: - VOptionList children; - VNode( const ERM::TVExp & exp); - VNode( const VOptionList & cdren ); - VNode( const ERM::TSymbol & sym ); //only in case sym has modifiers! - VNode( const VOption & first, const VOptionList & rest); //merges given arguments into [a, rest]; - void setVnode( const VOption & first, const VOptionList & rest); - }; -} - -class ERMInterpreter -{ -/*not so*/ public: - - std::map scripts; - - typedef std::map > TtriggerListType; - TtriggerListType triggers; - TtriggerListType postTriggers; - std::vector instructions; - - static bool isCMDATrigger(const ERM::Tcommand & cmd); - static bool isATrigger(const ERM::TLine & line); - static ERM::EVOtions getExpType(const ERM::TVOption & opt); - ERM::TLine & retrieveLine(const VERMInterpreter::LinePointer & linePtr); - static ERM::TTriggerBase & retrieveTrigger(ERM::TLine & line); -public: - vstd::CLoggerBase * logger; - - ERMInterpreter(vstd::CLoggerBase * logger_); - virtual ~ERMInterpreter(); - - std::string loadScript(const std::string & name, const std::string & source); -}; +/* + * ERMInterpreter.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 "ERMParser.h" +#include "ERMScriptModule.h" + +class ERMInterpreter; + +namespace VERMInterpreter +{ + using namespace ERM; + + //different exceptions that can be thrown during interpreting + class EInterpreterProblem : public std::exception + { + std::string problem; + public: + const char * what() const throw() override + { + return problem.c_str(); + } + ~EInterpreterProblem() throw() + {} + EInterpreterProblem(const std::string & problemDesc) : problem(problemDesc) + {} + }; + + struct ESymbolNotFound : public EInterpreterProblem + { + ESymbolNotFound(const std::string & sym) : + EInterpreterProblem(std::string("Symbol \"") + sym + std::string("\" not found!")) + {} + }; + + struct EInvalidTrigger : public EInterpreterProblem + { + EInvalidTrigger(const std::string & sym) : + EInterpreterProblem(std::string("Trigger \"") + sym + std::string("\" is invalid!")) + {} + }; + + struct EUsageOfUndefinedMacro : public EInterpreterProblem + { + EUsageOfUndefinedMacro(const std::string & macro) : + EInterpreterProblem(std::string("Macro ") + macro + " is undefined") + {} + }; + + struct EIexpProblem : public EInterpreterProblem + { + EIexpProblem(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + struct ELineProblem : public EInterpreterProblem + { + ELineProblem(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + struct EExecutionError : public EInterpreterProblem + { + EExecutionError(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + //internal interpreter error related to execution + struct EInterpreterError : public EExecutionError + { + EInterpreterError(const std::string & desc) : + EExecutionError(desc) + {} + }; + + //wrong script + struct EScriptExecError : public EExecutionError + { + EScriptExecError(const std::string & desc) : + EExecutionError(desc) + {} + }; + + //wrong script + struct EVermScriptExecError : public EScriptExecError + { + EVermScriptExecError(const std::string & desc) : + EScriptExecError(desc) + {} + }; + + // All numeric variables are integer variables and have a range of -2147483647...+2147483647 + // c stores game active day number //indirect variable + // d current value //not an actual variable but a modifier + // e1..e100 Function floating point variables //local + // e-1..e-100 Trigger local floating point variables //local + // 'f'..'t' Standard variables ('quick variables') //global + // v1..v1000 Standard variables //global + // w1..w100 Hero variables + // w101..w200 Hero variables + // x1..x16 Function parameters //local + // y1..y100 Function local variables //local + // y-1..y-100 Trigger-based local integer variables //local + // z1..z1000 String variables //global + // z-1..z-10 Function local string variables //local + + + struct TriggerType + { + //the same order of trigger types in this enum and in validTriggers array is obligatory! + enum ETrigType + { + AE, BA, BF, BG, BR, CM, CO, FU, GE, GM, HE, HL, HM, IP, LE, MF, MG, MM, MR, MW, OB, PI, SN, TH, TM + }; + + ETrigType type; + + static ETrigType convertTrigger(const std::string & trig) + { + static const std::string validTriggers[] = + { + "AE", "BA", "BF", "BG", "BR", "CM", "CO", "FU", + "GE", "GM", "HE", "HL", "HM", "IP", "LE", "MF", "MG", "MM", "MR", "MW", "OB", "PI", "SN", + "TH", "TM" + }; + + for(int i=0; i(i); + } + throw EInvalidTrigger(trig); + } + + bool operator<(const TriggerType & t2) const + { + return type < t2.type; + } + + TriggerType(const std::string & sym) + { + type = convertTrigger(sym); + } + + }; + + struct LinePointer + { + int lineNum; + int realLineNum; + int fileLength; + + LinePointer() + : fileLength(-1) + {} + + LinePointer(int _fileLength, int line, int _realLineNum) + : fileLength(_fileLength), + lineNum(line), + realLineNum(_realLineNum) + {} + + bool operator<(const LinePointer & rhs) const + { + return lineNum < rhs.lineNum; + } + + bool operator!=(const LinePointer & rhs) const + { + return lineNum != rhs.lineNum; + } + LinePointer & operator++() + { + ++lineNum; + return *this; + } + bool isValid() const + { + return fileLength > 0 && lineNum < fileLength; + } + }; + + struct Trigger + { + LinePointer line; + Trigger() + {} + }; + + + //verm goodies + struct VSymbol + { + std::string text; + VSymbol(const std::string & txt) : text(txt) + {} + }; + + struct VNode; + struct VOptionList; + + struct VNIL + {}; + + + typedef std::variant TLiteral; + + typedef std::variant, VSymbol, TLiteral, ERM::Tcommand> VOption; //options in v-expression, VNIl should be the default + + template + T& getAs(SecType & opt) + { + if(opt.type() == typeid(T)) + return std::get(opt); + else + throw EVermScriptExecError("Wrong type!"); + } + + template + bool isA(const SecType & opt) + { + if(opt.type() == typeid(T)) + return true; + else + return false; + } + + struct VermTreeIterator + { + private: + friend struct VOptionList; + VOptionList * parent; + enum Estate {NORM, CAR} state; + int basePos; //car/cdr offset + public: + VermTreeIterator(VOptionList & _parent) : parent(&_parent), state(NORM), basePos(0) + {} + VermTreeIterator() : parent(nullptr), state(NORM) + {} + + VermTreeIterator & operator=(const VOption & opt); + VermTreeIterator & operator=(const std::vector & opt); + VermTreeIterator & operator=(const VOptionList & opt); + VOption & getAsItem(); + VOptionList getAsList(); + size_t size() const; + + VermTreeIterator& operator=(const VermTreeIterator & rhs) + { + if(this == &rhs) + { + return *this; + } + parent = rhs.parent; + state = rhs.state; + basePos = rhs.basePos; + + return *this; + } + }; + + struct VOptionList : public std::vector + { + private: + friend struct VermTreeIterator; + public: + VermTreeIterator car(); + VermTreeIterator cdr(); + }; + + struct VNode + { + private: + void processModifierList(const std::vector & modifierList, bool asSymbol); + public: + VOptionList children; + VNode( const ERM::TVExp & exp); + VNode( const VOptionList & cdren ); + VNode( const ERM::TSymbol & sym ); //only in case sym has modifiers! + VNode( const VOption & first, const VOptionList & rest); //merges given arguments into [a, rest]; + void setVnode( const VOption & first, const VOptionList & rest); + }; +} + +class ERMInterpreter +{ +/*not so*/ public: + + std::map scripts; + + typedef std::map > TtriggerListType; + TtriggerListType triggers; + TtriggerListType postTriggers; + std::vector instructions; + + static bool isCMDATrigger(const ERM::Tcommand & cmd); + static bool isATrigger(const ERM::TLine & line); + static ERM::EVOtions getExpType(const ERM::TVOption & opt); + ERM::TLine & retrieveLine(const VERMInterpreter::LinePointer & linePtr); + static ERM::TTriggerBase & retrieveTrigger(ERM::TLine & line); +public: + vstd::CLoggerBase * logger; + + ERMInterpreter(vstd::CLoggerBase * logger_); + virtual ~ERMInterpreter(); + + std::string loadScript(const std::string & name, const std::string & source); +}; diff --git a/scripting/erm/ERMParser.cpp b/scripting/erm/ERMParser.cpp index e13d0a9a5..1747d0f03 100644 --- a/scripting/erm/ERMParser.cpp +++ b/scripting/erm/ERMParser.cpp @@ -1,529 +1,529 @@ -/* - * ERMParser.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 "ERMParser.h" - - -#include -#include -#include -#include -#include -#include -#include - - -namespace qi = boost::spirit::qi; -namespace ascii = spirit::ascii; -namespace phoenix = boost::phoenix; - - -//Greenspun's Tenth Rule of Programming: -//Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, -//bug-ridden, slow implementation of half of Common Lisp. -//actually these macros help in dealing with std::variant - - -CERMPreprocessor::CERMPreprocessor(const std::string & source) - : sourceStream(source), - lineNo(0), - version(Version::INVALID) -{ - //check header - std::string header; - getline(header); - - if(header == "ZVSE") - version = Version::ERM; - else if(header == "VERM") - version = Version::VERM; - else - logGlobal->error("File %s has wrong header", fname); -} - -class ParseErrorException : public std::exception -{ - -}; - -std::string CERMPreprocessor::retrieveCommandLine() -{ - std::string wholeCommand; - - //parse file - bool verm = false; - bool openedString = false; - int openedBraces = 0; - - while(sourceStream.good()) - { - std::string line ; - getline(line); //reading line - - size_t dash = line.find_first_of('^'); - bool inTheMiddle = openedBraces || openedString; - - if(!inTheMiddle) - { - if(line.size() < 2) - continue; - if(line[0] != '!' ) //command lines must begin with ! -> otherwise treat as comment - continue; - verm = line[1] == '['; - } - - if(openedString) - { - wholeCommand += "\n"; - if(dash != std::string::npos) - { - wholeCommand += line.substr(0, dash + 1); - line.erase(0,dash + 1); - openedString = false; - } - else //no closing marker -> the whole line is further part of string - { - wholeCommand += line; - continue; - } - } - - int i = 0; - for(; i < line.length(); i++) - { - char c = line[i]; - if(!openedString) - { - if(c == '[') - openedBraces++; - else if(c == ']') - { - openedBraces--; - if(!openedBraces) //the last brace has been matched -> stop "parsing", everything else in the line is comment - { - i++; - break; - } - } - else if(c == '^') - openedString = true; - else if(c == ';' && !verm) //do not allow comments inside VExp for now - { - line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands - break; - } -// else if(c == ';') // a ';' that is in command line (and not in string) ends the command -> throw away rest -// { -// line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands -// break; -// } - } - else if(c == '^') - openedString = false; - } - - if(verm && !openedBraces && i < line.length()) - { - line.erase(i, line.length() - i); - } - - if(wholeCommand.size()) //separate lines with a space - wholeCommand += " "; - - wholeCommand += line; - if(!openedBraces && !openedString) - return wholeCommand; - - //loop end - } - - if(openedBraces || openedString) - { - logGlobal->error("Ill-formed file: %s", fname); - throw ParseErrorException(); - } - return ""; -} - -void CERMPreprocessor::getline(std::string &ret) -{ - lineNo++; - std::getline(sourceStream, ret); - boost::trim(ret); //get rid of wspace -} - -ERMParser::ERMParser() -{ - ERMgrammar = std::make_shared>(); - -} - -ERMParser::~ERMParser() = default; - -std::vector ERMParser::parseFile(CERMPreprocessor & preproc) -{ - std::vector ret; - try - { - while(1) - { - std::string command = preproc.retrieveCommandLine(); - if(command.length() == 0) - break; - - repairEncoding(command); - LineInfo li; - li.realLineNum = preproc.getCurLineNo(); - li.tl = parseLine(command, li.realLineNum); - ret.push_back(li); - } - } - catch (ParseErrorException & e) - { - logGlobal->error("ERM Parser Error. File: '%s' Line: %d Exception: '%s'" - , preproc.getCurFileName(), preproc.getCurLineNo(), e.what()); - throw; - } - return ret; -} - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TStringConstant, - (std::string, str) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TMacroUsage, - (std::string, macro) - ) - -// BOOST_FUSION_ADAPT_STRUCT( -// ERM::TQMacroUsage, -// (std::string, qmacro) -// ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TMacroDef, - (std::string, macro) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarExpNotMacro, - (std::optional, questionMark) - (std::string, varsym) - (ERM::TVarExpNotMacro::Tval, val) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TArithmeticOp, - (ERM::TIexp, lhs) - (char, opcode) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarpExp, - (ERM::TVarExp, var) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVRLogic, - (char, opcode) - (ERM::TIexp, var) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVRArithmetic, - (char, opcode) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TNormalBodyOption, - (char, optionCode) - (std::optional, params) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Ttrigger, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TComparison, - (ERM::TIexp, lhs) - (std::string, compSign) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TSemiCompare, - (std::string, compSign) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TCurriedString, - (ERM::TIexp, iexp) - (ERM::TStringConstant, string) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarConcatString, - (ERM::TVarExp, var) - (ERM::TStringConstant, string) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tcondition, - (char, ctype) - (ERM::Tcondition::Tcond, cond) - (ERM::TconditionNode, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tinstruction, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - (ERM::Tbody, body) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Treceiver, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - (std::optional, body) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TPostTrigger, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - ) - -//BOOST_FUSION_ADAPT_STRUCT( -// ERM::Tcommand, -// (ERM::Tcommand::Tcmd, cmd) -// (std::string, comment) -// ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tcommand, - (ERM::Tcommand::Tcmd, cmd) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVExp, - (std::vector, modifier) - (std::vector, children) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TSymbol, - (std::vector, symModifier) - (std::string, sym) - ) - -namespace ERM -{ - template - struct ERM_grammar : qi::grammar - { - ERM_grammar() : ERM_grammar::base_type(vline, "VERM script line") - { - //do not build too complicated expressions, e.g. (a >> b) | c, qi has problems with them - ERMmacroUsage %= qi::lexeme[qi::lit('$') >> *(qi::char_ - '$') >> qi::lit('$')]; - ERMmacroDef %= qi::lexeme[qi::lit('@') >> *(qi::char_ - '@') >> qi::lit('@')]; - varExpNotMacro %= -qi::char_("?") >> (+(qi::char_("a-z") - 'u')) >> -qi::int_; - //TODO: mixed var/macro expressions like in !!HE-1&407:Id$cost$; [script 13] - /*qERMMacroUsage %= qi::lexeme[qi::lit("?$") >> *(qi::char_ - '$') >> qi::lit('$')];*/ - varExp %= varExpNotMacro | ERMmacroUsage; - iexp %= varExp | qi::int_; - varp %= qi::lit("?") >> varExp; - comment %= *qi::char_; - commentLine %= (~qi::char_("!") >> comment | (qi::char_('!') >> (~qi::char_("?!$#[")) >> comment )); - cmdName %= qi::lexeme[qi::repeat(2)[qi::char_]]; - arithmeticOp %= iexp >> qi::char_ >> iexp; - //??? - //identifier is usually a vector of i-expressions but VR receiver performs arithmetic operations on it - - //identifier %= (iexp | arithmeticOp) % qi::lit('/'); - identifier %= iexp % qi::lit('/'); - - comparison %= iexp >> (*qi::char_("<=>")) >> iexp; - condition %= qi::char_("&|/") >> (comparison | qi::int_) >> -condition; - - trigger %= cmdName >> -identifier >> -condition > qi::lit(";"); ///// - string %= qi::lexeme['^' >> *(qi::char_ - '^') >> '^']; - - VRLogic %= qi::char_("&|") >> iexp; - VRarithmetic %= qi::char_("+*:/%-") >> iexp; - semiCompare %= +qi::char_("<=>") >> iexp; - curStr %= iexp >> string; - varConcatString %= varExp >> qi::lit("+") >> string; - bodyOptionItem %= varConcatString | curStr | string | semiCompare | ERMmacroDef | varp | iexp ; - exactBodyOptionList %= (bodyOptionItem % qi::lit("/")); - normalBodyOption = qi::char_("A-Z") > -(exactBodyOptionList); - bodyOption %= VRLogic | VRarithmetic | normalBodyOption; - body %= qi::lit(":") >> *(bodyOption) > qi::lit(";"); - - instruction %= cmdName >> -identifier >> -condition >> body; - receiver %= cmdName >> -identifier >> -condition >> body; - postTrigger %= cmdName >> -identifier >> -condition > qi::lit(";"); - - command %= (qi::lit("!") >> - ( - (qi::lit("?") >> trigger) | - (qi::lit("!") >> receiver) | - (qi::lit("#") >> instruction) | - (qi::lit("$") >> postTrigger) - ) //>> comment - ); - - rline %= - ( - command | commentLine | spirit::eps - ); - - vmod %= qi::string("`") | qi::string(",!") | qi::string(",") | qi::string("#'") | qi::string("'"); - vsym %= *vmod >> qi::lexeme[+qi::char_("+*/$%&_=<>~a-zA-Z0-9-")]; - - qi::real_parser > strict_double; - vopt %= qi::lexeme[(qi::lit("!") >> qi::char_ >> qi::lit("!"))] | qi::lexeme[strict_double] | qi::lexeme[qi::int_] | command | vexp | string | vsym; - vexp %= *vmod >> qi::lit("[") >> *(vopt) >> qi::lit("]"); - - vline %= (( qi::lit("!") >>vexp) | rline ) > spirit::eoi; - - //error handling - - string.name("string constant"); - ERMmacroUsage.name("macro usage"); - /*qERMMacroUsage.name("macro usage with ?");*/ - ERMmacroDef.name("macro definition"); - varExpNotMacro.name("variable expression (not macro)"); - varExp.name("variable expression"); - iexp.name("i-expression"); - comment.name("comment"); - commentLine.name("comment line"); - cmdName.name("name of a command"); - identifier.name("identifier"); - condition.name("condition"); - trigger.name("trigger"); - body.name("body"); - instruction.name("instruction"); - receiver.name("receiver"); - postTrigger.name("post trigger"); - command.name("command"); - rline.name("ERM script line"); - vsym.name("V symbol"); - vopt.name("V option"); - vexp.name("V expression"); - vline.name("VERM line"); - - qi::on_error - ( - vline - , std::cout //or phoenix::ref(std::count), is there any difference? - << phoenix::val("Error! Expecting ") - << qi::_4 // what failed? - << phoenix::val(" here: \"") - << phoenix::construct(qi::_3, qi::_2) // iterators to error-pos, end - << phoenix::val("\"") - ); - - } - - qi::rule string; - - qi::rule ERMmacroUsage; - /*qi::rule qERMMacroUsage;*/ - qi::rule ERMmacroDef; - qi::rule varExpNotMacro; - qi::rule varExp; - qi::rule iexp; - qi::rule varp; - qi::rule arithmeticOp; - qi::rule comment; - qi::rule commentLine; - qi::rule cmdName; - qi::rule identifier; - qi::rule comparison; - qi::rule condition; - qi::rule VRLogic; - qi::rule VRarithmetic; - qi::rule semiCompare; - qi::rule curStr; - qi::rule varConcatString; - qi::rule bodyOptionItem; - qi::rule exactBodyOptionList; - qi::rule normalBodyOption; - qi::rule bodyOption; - qi::rule trigger; - qi::rule body; - qi::rule instruction; - qi::rule receiver; - qi::rule postTrigger; - qi::rule command; - qi::rule rline; - qi::rule vsym; - qi::rule vmod; - qi::rule vopt; - qi::rule vexp; - qi::rule vline; - }; -} - -ERM::TLine ERMParser::parseLine(const std::string & line, int realLineNo) -{ - try - { - return parseLine(line); - } - catch(...) - { - //logGlobal->error("Parse error occurred in file %s (line %d): %s", fname, realLineNo, line); - throw; - } -} - -ERM::TLine ERMParser::parseLine(const std::string & line) -{ - auto beg = line.begin(); - auto end = line.end(); - - ERM::TLine AST; - - bool r = qi::phrase_parse(beg, end, *ERMgrammar.get(), ascii::space, AST); - if(!r || beg != end) - { - logGlobal->error("Parse error: cannot parse: %s", std::string(beg, end)); - throw ParseErrorException(); - } - return AST; -} - -void ERMParser::repairEncoding(std::string & str) const -{ - for(int g=0; g +#include +#include +#include +#include +#include +#include + + +namespace qi = boost::spirit::qi; +namespace ascii = spirit::ascii; +namespace phoenix = boost::phoenix; + + +//Greenspun's Tenth Rule of Programming: +//Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, +//bug-ridden, slow implementation of half of Common Lisp. +//actually these macros help in dealing with std::variant + + +CERMPreprocessor::CERMPreprocessor(const std::string & source) + : sourceStream(source), + lineNo(0), + version(Version::INVALID) +{ + //check header + std::string header; + getline(header); + + if(header == "ZVSE") + version = Version::ERM; + else if(header == "VERM") + version = Version::VERM; + else + logGlobal->error("File %s has wrong header", fname); +} + +class ParseErrorException : public std::exception +{ + +}; + +std::string CERMPreprocessor::retrieveCommandLine() +{ + std::string wholeCommand; + + //parse file + bool verm = false; + bool openedString = false; + int openedBraces = 0; + + while(sourceStream.good()) + { + std::string line ; + getline(line); //reading line + + size_t dash = line.find_first_of('^'); + bool inTheMiddle = openedBraces || openedString; + + if(!inTheMiddle) + { + if(line.size() < 2) + continue; + if(line[0] != '!' ) //command lines must begin with ! -> otherwise treat as comment + continue; + verm = line[1] == '['; + } + + if(openedString) + { + wholeCommand += "\n"; + if(dash != std::string::npos) + { + wholeCommand += line.substr(0, dash + 1); + line.erase(0,dash + 1); + openedString = false; + } + else //no closing marker -> the whole line is further part of string + { + wholeCommand += line; + continue; + } + } + + int i = 0; + for(; i < line.length(); i++) + { + char c = line[i]; + if(!openedString) + { + if(c == '[') + openedBraces++; + else if(c == ']') + { + openedBraces--; + if(!openedBraces) //the last brace has been matched -> stop "parsing", everything else in the line is comment + { + i++; + break; + } + } + else if(c == '^') + openedString = true; + else if(c == ';' && !verm) //do not allow comments inside VExp for now + { + line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands + break; + } +// else if(c == ';') // a ';' that is in command line (and not in string) ends the command -> throw away rest +// { +// line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands +// break; +// } + } + else if(c == '^') + openedString = false; + } + + if(verm && !openedBraces && i < line.length()) + { + line.erase(i, line.length() - i); + } + + if(wholeCommand.size()) //separate lines with a space + wholeCommand += " "; + + wholeCommand += line; + if(!openedBraces && !openedString) + return wholeCommand; + + //loop end + } + + if(openedBraces || openedString) + { + logGlobal->error("Ill-formed file: %s", fname); + throw ParseErrorException(); + } + return ""; +} + +void CERMPreprocessor::getline(std::string &ret) +{ + lineNo++; + std::getline(sourceStream, ret); + boost::trim(ret); //get rid of wspace +} + +ERMParser::ERMParser() +{ + ERMgrammar = std::make_shared>(); + +} + +ERMParser::~ERMParser() = default; + +std::vector ERMParser::parseFile(CERMPreprocessor & preproc) +{ + std::vector ret; + try + { + while(1) + { + std::string command = preproc.retrieveCommandLine(); + if(command.length() == 0) + break; + + repairEncoding(command); + LineInfo li; + li.realLineNum = preproc.getCurLineNo(); + li.tl = parseLine(command, li.realLineNum); + ret.push_back(li); + } + } + catch (ParseErrorException & e) + { + logGlobal->error("ERM Parser Error. File: '%s' Line: %d Exception: '%s'" + , preproc.getCurFileName(), preproc.getCurLineNo(), e.what()); + throw; + } + return ret; +} + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TStringConstant, + (std::string, str) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TMacroUsage, + (std::string, macro) + ) + +// BOOST_FUSION_ADAPT_STRUCT( +// ERM::TQMacroUsage, +// (std::string, qmacro) +// ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TMacroDef, + (std::string, macro) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarExpNotMacro, + (std::optional, questionMark) + (std::string, varsym) + (ERM::TVarExpNotMacro::Tval, val) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TArithmeticOp, + (ERM::TIexp, lhs) + (char, opcode) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarpExp, + (ERM::TVarExp, var) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVRLogic, + (char, opcode) + (ERM::TIexp, var) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVRArithmetic, + (char, opcode) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TNormalBodyOption, + (char, optionCode) + (std::optional, params) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Ttrigger, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TComparison, + (ERM::TIexp, lhs) + (std::string, compSign) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TSemiCompare, + (std::string, compSign) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TCurriedString, + (ERM::TIexp, iexp) + (ERM::TStringConstant, string) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarConcatString, + (ERM::TVarExp, var) + (ERM::TStringConstant, string) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tcondition, + (char, ctype) + (ERM::Tcondition::Tcond, cond) + (ERM::TconditionNode, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tinstruction, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + (ERM::Tbody, body) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Treceiver, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + (std::optional, body) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TPostTrigger, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + ) + +//BOOST_FUSION_ADAPT_STRUCT( +// ERM::Tcommand, +// (ERM::Tcommand::Tcmd, cmd) +// (std::string, comment) +// ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tcommand, + (ERM::Tcommand::Tcmd, cmd) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVExp, + (std::vector, modifier) + (std::vector, children) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TSymbol, + (std::vector, symModifier) + (std::string, sym) + ) + +namespace ERM +{ + template + struct ERM_grammar : qi::grammar + { + ERM_grammar() : ERM_grammar::base_type(vline, "VERM script line") + { + //do not build too complicated expressions, e.g. (a >> b) | c, qi has problems with them + ERMmacroUsage %= qi::lexeme[qi::lit('$') >> *(qi::char_ - '$') >> qi::lit('$')]; + ERMmacroDef %= qi::lexeme[qi::lit('@') >> *(qi::char_ - '@') >> qi::lit('@')]; + varExpNotMacro %= -qi::char_("?") >> (+(qi::char_("a-z") - 'u')) >> -qi::int_; + //TODO: mixed var/macro expressions like in !!HE-1&407:Id$cost$; [script 13] + /*qERMMacroUsage %= qi::lexeme[qi::lit("?$") >> *(qi::char_ - '$') >> qi::lit('$')];*/ + varExp %= varExpNotMacro | ERMmacroUsage; + iexp %= varExp | qi::int_; + varp %= qi::lit("?") >> varExp; + comment %= *qi::char_; + commentLine %= (~qi::char_("!") >> comment | (qi::char_('!') >> (~qi::char_("?!$#[")) >> comment )); + cmdName %= qi::lexeme[qi::repeat(2)[qi::char_]]; + arithmeticOp %= iexp >> qi::char_ >> iexp; + //??? + //identifier is usually a vector of i-expressions but VR receiver performs arithmetic operations on it + + //identifier %= (iexp | arithmeticOp) % qi::lit('/'); + identifier %= iexp % qi::lit('/'); + + comparison %= iexp >> (*qi::char_("<=>")) >> iexp; + condition %= qi::char_("&|/") >> (comparison | qi::int_) >> -condition; + + trigger %= cmdName >> -identifier >> -condition > qi::lit(";"); ///// + string %= qi::lexeme['^' >> *(qi::char_ - '^') >> '^']; + + VRLogic %= qi::char_("&|") >> iexp; + VRarithmetic %= qi::char_("+*:/%-") >> iexp; + semiCompare %= +qi::char_("<=>") >> iexp; + curStr %= iexp >> string; + varConcatString %= varExp >> qi::lit("+") >> string; + bodyOptionItem %= varConcatString | curStr | string | semiCompare | ERMmacroDef | varp | iexp ; + exactBodyOptionList %= (bodyOptionItem % qi::lit("/")); + normalBodyOption = qi::char_("A-Z") > -(exactBodyOptionList); + bodyOption %= VRLogic | VRarithmetic | normalBodyOption; + body %= qi::lit(":") >> *(bodyOption) > qi::lit(";"); + + instruction %= cmdName >> -identifier >> -condition >> body; + receiver %= cmdName >> -identifier >> -condition >> body; + postTrigger %= cmdName >> -identifier >> -condition > qi::lit(";"); + + command %= (qi::lit("!") >> + ( + (qi::lit("?") >> trigger) | + (qi::lit("!") >> receiver) | + (qi::lit("#") >> instruction) | + (qi::lit("$") >> postTrigger) + ) //>> comment + ); + + rline %= + ( + command | commentLine | spirit::eps + ); + + vmod %= qi::string("`") | qi::string(",!") | qi::string(",") | qi::string("#'") | qi::string("'"); + vsym %= *vmod >> qi::lexeme[+qi::char_("+*/$%&_=<>~a-zA-Z0-9-")]; + + qi::real_parser > strict_double; + vopt %= qi::lexeme[(qi::lit("!") >> qi::char_ >> qi::lit("!"))] | qi::lexeme[strict_double] | qi::lexeme[qi::int_] | command | vexp | string | vsym; + vexp %= *vmod >> qi::lit("[") >> *(vopt) >> qi::lit("]"); + + vline %= (( qi::lit("!") >>vexp) | rline ) > spirit::eoi; + + //error handling + + string.name("string constant"); + ERMmacroUsage.name("macro usage"); + /*qERMMacroUsage.name("macro usage with ?");*/ + ERMmacroDef.name("macro definition"); + varExpNotMacro.name("variable expression (not macro)"); + varExp.name("variable expression"); + iexp.name("i-expression"); + comment.name("comment"); + commentLine.name("comment line"); + cmdName.name("name of a command"); + identifier.name("identifier"); + condition.name("condition"); + trigger.name("trigger"); + body.name("body"); + instruction.name("instruction"); + receiver.name("receiver"); + postTrigger.name("post trigger"); + command.name("command"); + rline.name("ERM script line"); + vsym.name("V symbol"); + vopt.name("V option"); + vexp.name("V expression"); + vline.name("VERM line"); + + qi::on_error + ( + vline + , std::cout //or phoenix::ref(std::count), is there any difference? + << phoenix::val("Error! Expecting ") + << qi::_4 // what failed? + << phoenix::val(" here: \"") + << phoenix::construct(qi::_3, qi::_2) // iterators to error-pos, end + << phoenix::val("\"") + ); + + } + + qi::rule string; + + qi::rule ERMmacroUsage; + /*qi::rule qERMMacroUsage;*/ + qi::rule ERMmacroDef; + qi::rule varExpNotMacro; + qi::rule varExp; + qi::rule iexp; + qi::rule varp; + qi::rule arithmeticOp; + qi::rule comment; + qi::rule commentLine; + qi::rule cmdName; + qi::rule identifier; + qi::rule comparison; + qi::rule condition; + qi::rule VRLogic; + qi::rule VRarithmetic; + qi::rule semiCompare; + qi::rule curStr; + qi::rule varConcatString; + qi::rule bodyOptionItem; + qi::rule exactBodyOptionList; + qi::rule normalBodyOption; + qi::rule bodyOption; + qi::rule trigger; + qi::rule body; + qi::rule instruction; + qi::rule receiver; + qi::rule postTrigger; + qi::rule command; + qi::rule rline; + qi::rule vsym; + qi::rule vmod; + qi::rule vopt; + qi::rule vexp; + qi::rule vline; + }; +} + +ERM::TLine ERMParser::parseLine(const std::string & line, int realLineNo) +{ + try + { + return parseLine(line); + } + catch(...) + { + //logGlobal->error("Parse error occurred in file %s (line %d): %s", fname, realLineNo, line); + throw; + } +} + +ERM::TLine ERMParser::parseLine(const std::string & line) +{ + auto beg = line.begin(); + auto end = line.end(); + + ERM::TLine AST; + + bool r = qi::phrase_parse(beg, end, *ERMgrammar.get(), ascii::space, AST); + if(!r || beg != end) + { + logGlobal->error("Parse error: cannot parse: %s", std::string(beg, end)); + throw ParseErrorException(); + } + return AST; +} + +void ERMParser::repairEncoding(std::string & str) const +{ + for(int g=0; g - -namespace spirit = boost::spirit; - -class CERMPreprocessor -{ - std::string fname; - std::stringstream sourceStream; - int lineNo; - - - void getline(std::string &ret); - -public: - enum class Version : ui8 - { - INVALID, - ERM, - VERM - }; - Version version; - - CERMPreprocessor(const std::string & source); - std::string retrieveCommandLine(); - int getCurLineNo() const - { - return lineNo; - } - - const std::string& getCurFileName() const - { - return fname; - } -}; - -//various classes that represent ERM/VERM AST -namespace ERM -{ - using ValType = int; //todo: set to int64_t - using IType = int; //todo: set to int32_t - - struct TStringConstant - { - std::string str; - }; - struct TMacroUsage - { - std::string macro; - }; - -// //macro with '?', for write only -// struct TQMacroUsage -// { -// std::string qmacro; -// }; - - //definition of a macro - struct TMacroDef - { - std::string macro; - }; - typedef std::string TCmdName; - - struct TVarExpNotMacro - { - typedef std::optional Tval; - std::optional questionMark; - std::string varsym; - Tval val; - }; - - typedef std::variant TVarExp; - - //write-only variable expression - struct TVarpExp - { - TVarExp var; - }; - - //i-expression (identifier expression) - an integral constant, variable symbol or array symbol - typedef std::variant TIexp; - - struct TArithmeticOp - { - TIexp lhs, rhs; - char opcode; - }; - - struct TVRLogic - { - char opcode; - TIexp var; - }; - - struct TVRArithmetic - { - char opcode; - TIexp rhs; - }; - - struct TSemiCompare - { - std::string compSign; - TIexp rhs; - }; - - struct TCurriedString - { - TIexp iexp; - TStringConstant string; - }; - - struct TVarConcatString - { - TVarExp var; - TStringConstant string; - }; - - typedef std::variant TBodyOptionItem; - - typedef std::vector TNormalBodyOptionList; - - struct TNormalBodyOption - { - char optionCode; - std::optional params; - }; - typedef std::variant TBodyOption; - -// typedef std::variant TIdentifierInternal; - typedef std::vector< TIexp > Tidentifier; - - struct TComparison - { - std::string compSign; - TIexp lhs, rhs; - }; - - struct Tcondition; - typedef std::optional> TconditionNode; - - struct Tcondition - { - typedef std::variant< - TComparison, - int> - Tcond; //comparison or condition flag - char ctype; - Tcond cond; - TconditionNode rhs; - }; - - struct TTriggerBase - { - bool pre; //if false it's !$ post-trigger, elsewise it's !# (pre)trigger - TCmdName name; - std::optional identifier; - std::optional condition; - }; - - struct Ttrigger : TTriggerBase - { - Ttrigger() - { - pre = true; - } - }; - - struct TPostTrigger : TTriggerBase - { - TPostTrigger() - { - pre = false; - } - }; - - //a dirty workaround for preprocessor magic that prevents the use types with comma in it in BOOST_FUSION_ADAPT_STRUCT - //see http://comments.gmane.org/gmane.comp.lib.boost.user/62501 for some info - // - //moreover, I encountered a quite serious bug in boost: http://boost.2283326.n4.nabble.com/container-hpp-111-error-C2039-value-type-is-not-a-member-of-td3352328.html - //not sure how serious it is... - - //typedef std::variant bodyItem; - typedef std::vector Tbody; - - struct Tinstruction - { - TCmdName name; - std::optional identifier; - std::optional condition; - Tbody body; - }; - - struct Treceiver - { - TCmdName name; - std::optional identifier; - std::optional condition; - std::optional body; - }; - - struct Tcommand - { - typedef std::variant< - Ttrigger, - Tinstruction, - Treceiver, - TPostTrigger - > - Tcmd; - Tcmd cmd; - //std::string comment; - }; - - //vector expression - - - typedef std::variant TERMline; - - typedef std::string TVModifier; //'`', ',', ',@', '#'' - - struct TSymbol - { - std::vector symModifier; - std::string sym; - }; - - //for #'symbol expression - - enum EVOtions{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 - struct TVExp - { - std::vector modifier; - std::vector children; - }; - - //script line - typedef std::variant TLine; - - template struct ERM_grammar; -} - -struct LineInfo -{ - ERM::TLine tl; - int realLineNum; -}; - -class ERMParser -{ -public: - std::shared_ptr> ERMgrammar; - - ERMParser(); - virtual ~ERMParser(); - - std::vector parseFile(CERMPreprocessor & preproc); -private: - void repairEncoding(char * str, int len) const; //removes nonstandard ascii characters from string - void repairEncoding(std::string & str) const; //removes nonstandard ascii characters from string - ERM::TLine parseLine(const std::string & line, int realLineNo); - ERM::TLine parseLine(const std::string & line); -}; +/* + * ERMParser.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 + +namespace spirit = boost::spirit; + +class CERMPreprocessor +{ + std::string fname; + std::stringstream sourceStream; + int lineNo; + + + void getline(std::string &ret); + +public: + enum class Version : ui8 + { + INVALID, + ERM, + VERM + }; + Version version; + + CERMPreprocessor(const std::string & source); + std::string retrieveCommandLine(); + int getCurLineNo() const + { + return lineNo; + } + + const std::string& getCurFileName() const + { + return fname; + } +}; + +//various classes that represent ERM/VERM AST +namespace ERM +{ + using ValType = int; //todo: set to int64_t + using IType = int; //todo: set to int32_t + + struct TStringConstant + { + std::string str; + }; + struct TMacroUsage + { + std::string macro; + }; + +// //macro with '?', for write only +// struct TQMacroUsage +// { +// std::string qmacro; +// }; + + //definition of a macro + struct TMacroDef + { + std::string macro; + }; + typedef std::string TCmdName; + + struct TVarExpNotMacro + { + typedef std::optional Tval; + std::optional questionMark; + std::string varsym; + Tval val; + }; + + typedef std::variant TVarExp; + + //write-only variable expression + struct TVarpExp + { + TVarExp var; + }; + + //i-expression (identifier expression) - an integral constant, variable symbol or array symbol + typedef std::variant TIexp; + + struct TArithmeticOp + { + TIexp lhs, rhs; + char opcode; + }; + + struct TVRLogic + { + char opcode; + TIexp var; + }; + + struct TVRArithmetic + { + char opcode; + TIexp rhs; + }; + + struct TSemiCompare + { + std::string compSign; + TIexp rhs; + }; + + struct TCurriedString + { + TIexp iexp; + TStringConstant string; + }; + + struct TVarConcatString + { + TVarExp var; + TStringConstant string; + }; + + typedef std::variant TBodyOptionItem; + + typedef std::vector TNormalBodyOptionList; + + struct TNormalBodyOption + { + char optionCode; + std::optional params; + }; + typedef std::variant TBodyOption; + +// typedef std::variant TIdentifierInternal; + typedef std::vector< TIexp > Tidentifier; + + struct TComparison + { + std::string compSign; + TIexp lhs, rhs; + }; + + struct Tcondition; + typedef std::optional> TconditionNode; + + struct Tcondition + { + typedef std::variant< + TComparison, + int> + Tcond; //comparison or condition flag + char ctype; + Tcond cond; + TconditionNode rhs; + }; + + struct TTriggerBase + { + bool pre; //if false it's !$ post-trigger, elsewise it's !# (pre)trigger + TCmdName name; + std::optional identifier; + std::optional condition; + }; + + struct Ttrigger : TTriggerBase + { + Ttrigger() + { + pre = true; + } + }; + + struct TPostTrigger : TTriggerBase + { + TPostTrigger() + { + pre = false; + } + }; + + //a dirty workaround for preprocessor magic that prevents the use types with comma in it in BOOST_FUSION_ADAPT_STRUCT + //see http://comments.gmane.org/gmane.comp.lib.boost.user/62501 for some info + // + //moreover, I encountered a quite serious bug in boost: http://boost.2283326.n4.nabble.com/container-hpp-111-error-C2039-value-type-is-not-a-member-of-td3352328.html + //not sure how serious it is... + + //typedef std::variant bodyItem; + typedef std::vector Tbody; + + struct Tinstruction + { + TCmdName name; + std::optional identifier; + std::optional condition; + Tbody body; + }; + + struct Treceiver + { + TCmdName name; + std::optional identifier; + std::optional condition; + std::optional body; + }; + + struct Tcommand + { + typedef std::variant< + Ttrigger, + Tinstruction, + Treceiver, + TPostTrigger + > + Tcmd; + Tcmd cmd; + //std::string comment; + }; + + //vector expression + + + typedef std::variant TERMline; + + typedef std::string TVModifier; //'`', ',', ',@', '#'' + + struct TSymbol + { + std::vector symModifier; + std::string sym; + }; + + //for #'symbol expression + + enum EVOtions{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 + struct TVExp + { + std::vector modifier; + std::vector children; + }; + + //script line + typedef std::variant TLine; + + template struct ERM_grammar; +} + +struct LineInfo +{ + ERM::TLine tl; + int realLineNum; +}; + +class ERMParser +{ +public: + std::shared_ptr> ERMgrammar; + + ERMParser(); + virtual ~ERMParser(); + + std::vector parseFile(CERMPreprocessor & preproc); +private: + void repairEncoding(char * str, int len) const; //removes nonstandard ascii characters from string + void repairEncoding(std::string & str) const; //removes nonstandard ascii characters from string + ERM::TLine parseLine(const std::string & line, int realLineNo); + ERM::TLine parseLine(const std::string & line); +}; diff --git a/scripting/erm/ERMScriptModule.cpp b/scripting/erm/ERMScriptModule.cpp index 2aa9d12b3..d82e7cd9f 100644 --- a/scripting/erm/ERMScriptModule.cpp +++ b/scripting/erm/ERMScriptModule.cpp @@ -1,68 +1,68 @@ -/* - * ERMScriptModule.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 "ERMScriptModule.h" - -#include "ERMInterpreter.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -const char *g_cszAiName = "(V)ERM interpreter"; - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewModule(std::shared_ptr &out) -{ - out = std::make_shared(); -} - -ERMScriptModule::ERMScriptModule() -{ - -} - -std::string ERMScriptModule::compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const -{ - std::shared_ptr interp = std::make_shared(logger); - - try - { - return interp->loadScript(name, source); - } - catch(const std::exception & ex) - { - logger->error(ex.what()); - } - catch(const std::string & ex) - { - logger->error(ex); - } - catch(...) - { - logger->error("Sorry, caught unknown exception type. No more info available."); - } - - return ""; -} - -std::shared_ptr ERMScriptModule::createContextFor(const scripting::Script * source, const Environment * env) const -{ - throw std::runtime_error("ERM context creation is not possible"); -} - -void ERMScriptModule::registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const -{ - throw std::runtime_error("ERM spell effect registration is not possible"); -} +/* + * ERMScriptModule.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 "ERMScriptModule.h" + +#include "ERMInterpreter.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +const char *g_cszAiName = "(V)ERM interpreter"; + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewModule(std::shared_ptr &out) +{ + out = std::make_shared(); +} + +ERMScriptModule::ERMScriptModule() +{ + +} + +std::string ERMScriptModule::compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const +{ + std::shared_ptr interp = std::make_shared(logger); + + try + { + return interp->loadScript(name, source); + } + catch(const std::exception & ex) + { + logger->error(ex.what()); + } + catch(const std::string & ex) + { + logger->error(ex); + } + catch(...) + { + logger->error("Sorry, caught unknown exception type. No more info available."); + } + + return ""; +} + +std::shared_ptr ERMScriptModule::createContextFor(const scripting::Script * source, const Environment * env) const +{ + throw std::runtime_error("ERM context creation is not possible"); +} + +void ERMScriptModule::registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const +{ + throw std::runtime_error("ERM spell effect registration is not possible"); +} diff --git a/scripting/erm/ERMScriptModule.h b/scripting/erm/ERMScriptModule.h index c62b1813d..2b64471a9 100644 --- a/scripting/erm/ERMScriptModule.h +++ b/scripting/erm/ERMScriptModule.h @@ -1,25 +1,25 @@ -/* - * ERMScriptModule.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/CScriptingModule.h" - -class ERMScriptModule : public scripting::Module -{ -public: - ERMScriptModule(); - - std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const override; - - std::shared_ptr createContextFor(const scripting::Script * source, const Environment * env) const override; - - void registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const override; -}; - +/* + * ERMScriptModule.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/CScriptingModule.h" + +class ERMScriptModule : public scripting::Module +{ +public: + ERMScriptModule(); + + std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const override; + + std::shared_ptr createContextFor(const scripting::Script * source, const Environment * env) const override; + + void registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const override; +}; + diff --git a/scripting/erm/StdInc.cpp b/scripting/erm/StdInc.cpp index c8f4ddf05..c17377322 100644 --- a/scripting/erm/StdInc.cpp +++ b/scripting/erm/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" \ No newline at end of file diff --git a/scripting/erm/StdInc.h b/scripting/erm/StdInc.h index 02b2c08f3..020481377 100644 --- a/scripting/erm/StdInc.h +++ b/scripting/erm/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 38bed3222..2eda9ac47 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -20,11 +20,10 @@ #include "api/Registry.h" #include "../../lib/JsonNode.h" -#include "../../lib/NetPacks.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/battle/IBattleInfoCallback.h" #include "../../lib/CGameInfoCallback.h" -#include "../../lib/CModHandler.h" +#include "../../lib/modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -75,7 +74,7 @@ LuaContext::LuaContext(const Script * source, const Environment * env_): S.push(env->game()); lua_setglobal(L, "GAME"); - S.push(env->battle()); + S.push(env->battle(BattleID::NONE)); lua_setglobal(L, "BATTLE"); S.push(env->eventBus()); @@ -511,18 +510,18 @@ int LuaContext::loadModule() registar->pushMetatable(L); } - else if(scope == CModHandler::scopeBuiltin()) + else if(scope == ModScope::scopeBuiltin()) { // boost::algorithm::replace_all(modulePath, boost::is_any_of("\\/ "), ""); boost::algorithm::replace_all(modulePath, ".", "/"); - auto *loader = CResourceHandler::get(CModHandler::scopeBuiltin()); + auto *loader = CResourceHandler::get(ModScope::scopeBuiltin()); modulePath = "scripts/lib/" + modulePath; - ResourceID id(modulePath, EResType::LUA); + ResourcePath id(modulePath, EResType::LUA); if(!loader->existsResource(id)) return errorRetVoid("Module not found: "+modulePath); diff --git a/scripting/lua/LuaStack.h b/scripting/lua/LuaStack.h index 880536e6b..7963c1d77 100644 --- a/scripting/lua/LuaStack.h +++ b/scripting/lua/LuaStack.h @@ -26,13 +26,13 @@ namespace detail template struct IsRegularClass { - static constexpr auto value = std::is_class::value && !std::is_base_of::value; + static constexpr auto value = std::is_class::value && !std::is_base_of::value; }; template struct IsIdClass { - static constexpr auto value = std::is_class::value && std::is_base_of::value; + static constexpr auto value = std::is_class::value && std::is_base_of::value; }; } @@ -78,7 +78,7 @@ public: template::value, int>::type = 0> void push(const T & value) { - pushInteger(static_cast(value.toEnum())); + pushInteger(static_cast(value.getNum())); } template::value, int>::type = 0> diff --git a/scripting/lua/api/BonusSystem.cpp b/scripting/lua/api/BonusSystem.cpp index be0d981a0..2ce613ab0 100644 --- a/scripting/lua/api/BonusSystem.cpp +++ b/scripting/lua/api/BonusSystem.cpp @@ -62,7 +62,7 @@ int BonusProxy::getSubtype(lua_State * L) std::shared_ptr object; if(!S.tryGet(1, object)) return S.retNil(); - return LuaStack::quickRetInt(L, object->subtype); + return LuaStack::quickRetInt(L, object->subtype.getNum()); } int BonusProxy::getDuration(lua_State * L) @@ -116,7 +116,7 @@ int BonusProxy::getSourceID(lua_State * L) std::shared_ptr object; if(!S.tryGet(1, object)) return S.retNil(); - return LuaStack::quickRetInt(L, object->sid); + return LuaStack::quickRetInt(L, object->sid.getNum()); } int BonusProxy::getEffectRange(lua_State * L) diff --git a/scripting/lua/api/GameCb.cpp b/scripting/lua/api/GameCb.cpp index 5bf19b3df..0aa3cd0e6 100644 --- a/scripting/lua/api/GameCb.cpp +++ b/scripting/lua/api/GameCb.cpp @@ -29,8 +29,6 @@ VCMI_REGISTER_CORE_SCRIPT_API(GameCbProxy, "Game"); const std::vector GameCbProxy::REGISTER_CUSTOM = { {"getDate", LuaMethodWrapper::invoke, false}, - {"isAllowed", LuaMethodWrapper::invoke, false}, - {"getCurrentPlayer", LuaMethodWrapper::invoke, false}, {"getPlayer", LuaMethodWrapper::invoke, false}, {"getHero", LuaMethodWrapper::invoke, false}, diff --git a/scripting/lua/api/ServerCb.cpp b/scripting/lua/api/ServerCb.cpp index 1d0f9d501..aa257c6e9 100644 --- a/scripting/lua/api/ServerCb.cpp +++ b/scripting/lua/api/ServerCb.cpp @@ -14,7 +14,7 @@ #include "Registry.h" #include "../LuaStack.h" -#include "../../../lib/NetPacks.h" +#include "../../../lib/networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/scripting/lua/api/netpacks/BattleLogMessage.h b/scripting/lua/api/netpacks/BattleLogMessage.h index ebe6f049e..62ed18bc7 100644 --- a/scripting/lua/api/netpacks/BattleLogMessage.h +++ b/scripting/lua/api/netpacks/BattleLogMessage.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/BattleStackMoved.h b/scripting/lua/api/netpacks/BattleStackMoved.h index 3926a737e..a0fc9a9e4 100644 --- a/scripting/lua/api/netpacks/BattleStackMoved.h +++ b/scripting/lua/api/netpacks/BattleStackMoved.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/BattleUnitsChanged.h b/scripting/lua/api/netpacks/BattleUnitsChanged.h index 2344076e3..83837f814 100644 --- a/scripting/lua/api/netpacks/BattleUnitsChanged.h +++ b/scripting/lua/api/netpacks/BattleUnitsChanged.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/EntitiesChanged.h b/scripting/lua/api/netpacks/EntitiesChanged.h index 4b7e0f0b8..062edb979 100644 --- a/scripting/lua/api/netpacks/EntitiesChanged.h +++ b/scripting/lua/api/netpacks/EntitiesChanged.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/InfoWindow.h b/scripting/lua/api/netpacks/InfoWindow.h index f941761f8..d841a342c 100644 --- a/scripting/lua/api/netpacks/InfoWindow.h +++ b/scripting/lua/api/netpacks/InfoWindow.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/PackForClient.h b/scripting/lua/api/netpacks/PackForClient.h index c81732ce3..4ff676473 100644 --- a/scripting/lua/api/netpacks/PackForClient.h +++ b/scripting/lua/api/netpacks/PackForClient.h @@ -12,7 +12,7 @@ #include "../../LuaWrapper.h" -#include "../../../../lib/NetPacks.h" +#include "../../../../lib/networkPacks/NetPacksBase.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/scripting/lua/api/netpacks/SetResources.cpp b/scripting/lua/api/netpacks/SetResources.cpp index 2d8b5c569..4338fa987 100644 --- a/scripting/lua/api/netpacks/SetResources.cpp +++ b/scripting/lua/api/netpacks/SetResources.cpp @@ -12,6 +12,7 @@ #include "SetResources.h" #include "../../LuaStack.h" +#include "../../../../lib/networkPacks/PacksForClient.h" #include "../Registry.h" @@ -102,7 +103,7 @@ int SetResourcesProxy::getAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); @@ -122,7 +123,7 @@ int SetResourcesProxy::setAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); diff --git a/scripting/lua/api/netpacks/SetResources.h b/scripting/lua/api/netpacks/SetResources.h index d58908240..4b35ef6f6 100644 --- a/scripting/lua/api/netpacks/SetResources.h +++ b/scripting/lua/api/netpacks/SetResources.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index f9650493d..4d989108f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -10,74 +10,70 @@ #include "StdInc.h" #include "CGameHandler.h" -#include "HeroPoolProcessor.h" +#include "CVCMIServer.h" #include "ServerNetPackVisitors.h" #include "ServerSpellCastEnvironment.h" -#include "CVCMIServer.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "processors/TurnOrderProcessor.h" +#include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" -#include "PlayerMessageProcessor.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileInfo.h" -#include "../lib/int3.h" #include "../lib/ArtifactUtils.h" -#include "../lib/StartInfo.h" -#include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CBuildingHandler.h" +#include "../lib/CCreatureHandler.h" +#include "../lib/CCreatureSet.h" +#include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.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/ScriptHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/TerrainHandler.h" +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/int3.h" + +#include "../lib/battle/BattleInfo.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/CGMarket.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/spells/AbilityCaster.h" -#include "../lib/spells/BonusCaster.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/spells/ObstacleCasterProxy.h" -#include "../lib/spells/Problem.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CTownHandler.h" -#include "../lib/CCreatureHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CStack.h" -#include "../lib/UnlockGuard.h" -#include "../lib/GameSettings.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/CondSh.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/mapping/CMap.h" -#include "../lib/mapping/CMapService.h" + +#include "../lib/registerTypes/RegisterTypesServerPacks.h" + #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/VCMIDirs.h" -#include "../lib/ScopeGuard.h" -#include "../lib/CSoundBase.h" -#include "../lib/TerrainHandler.h" -#include "../lib/CCreatureSet.h" -#include "../lib/CThreadHelper.h" -#include "../lib/GameConstants.h" -#include "../lib/registerTypes/RegisterTypes.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/Connection.h" -#include "../lib/serializer/Cast.h" -#include "../lib/serializer/JsonSerializer.h" -#include "../lib/ScriptHandler.h" + +#include "../lib/serializer/CSaveFile.h" +#include "../lib/serializer/CLoadFile.h" + +#include "../lib/spells/CSpellHandler.h" + #include "vstd/CLoggerBase.h" -#include #include #include #include -#ifndef _MSC_VER -#include -#endif - #define COMPLAIN_RET_IF(cond, txt) do {if (cond){complain(txt); return;}} while(0) #define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){complain(txt); return false;}} while(0) #define COMPLAIN_RET(txt) {complain(txt); return false;} #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} -CondSh battleMadeAction(false); -CondSh battleResult(nullptr); template class CApplyOnGH; class CBaseForGHApply @@ -99,7 +95,7 @@ public: T *ptr = static_cast(pack); try { - ApplyGhNetPackVisitor applier(*gh, *gs); + ApplyGhNetPackVisitor applier(*gh); ptr->visit(applier); @@ -129,140 +125,6 @@ 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)); } -static void giveExp(BattleResult &r) -{ - if (r.winner > 1) - { - // 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[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; - } -} - -static void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard -{ - int x = targetPosition.getX(); - int y = targetPosition.getY(); - - const bool targetIsAttacker = side == BattleSide::ATTACKER; - - if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - - //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's - if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) - { - if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } - else - { //add back-side guardians for two-hex target, side guardians for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); - - if (!targetIsTwoHex && x > 2) //back guard for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); - else if (targetIsTwoHex)//front-side guardians for two-hex target - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - if (x > 3) //back guard for two-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - } - } - - } - - else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) - { - if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - else - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); - - if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else if (targetIsTwoHex) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - if (x < GameConstants::BFIELD_WIDTH - 4) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - } - } - } - - else if (!targetIsAttacker && y % 2 == 0) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - - else if (targetIsAttacker && y % 2 == 1) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } -} - -PlayerStatus PlayerStatuses::operator[](PlayerColor player) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - return players.at(player); - } - else - { - throw std::runtime_error("No such player!"); - } -} -void PlayerStatuses::addPlayer(PlayerColor player) -{ - boost::unique_lock l(mx); - players[player]; -} - -bool PlayerStatuses::checkFlag(PlayerColor player, bool PlayerStatus::*flag) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - return players[player].*flag; - } - else - { - throw std::runtime_error("No such player!"); - } -} - -void PlayerStatuses::setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - players[player].*flag = val; - } - else - { - throw std::runtime_error("No such player!"); - } - cv.notify_all(); -} template void callWith(std::vector args, std::function fun, ui32 which) @@ -275,9 +137,9 @@ const Services * CGameHandler::services() const return VLC; } -const CGameHandler::BattleCb * CGameHandler::battle() const +const CGameHandler::BattleCb * CGameHandler::battle(const BattleID & battleID) const { - return this; + return gs->getBattle(battleID); } const CGameHandler::GameCb * CGameHandler::game() const @@ -325,31 +187,27 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) sps.val = 1; sendAndApply(&sps); - PrepareHeroLevelUp pre; - pre.heroId = hero->id; - sendAndApply(&pre); - HeroLevelUp hlu; hlu.player = hero->tempOwner; hlu.heroId = hero->id; hlu.primskill = primarySkill; - hlu.skills = pre.skills; + hlu.skills = hero->getLevelUpProposedSecondarySkills(heroPool->getHeroSkillsRandomGenerator(hero->getHeroType())); if (hlu.skills.size() == 0) { sendAndApply(&hlu); levelUpHero(hero); } - else if (hlu.skills.size() == 1) + else if (hlu.skills.size() == 1 || !hero->getOwner().isValidPlayer()) { sendAndApply(&hlu); - levelUpHero(hero, pre.skills.front()); + levelUpHero(hero, hlu.skills.front()); } else if (hlu.skills.size() > 1) { auto levelUpQuery = std::make_shared(this, hlu, hero); hlu.queryID = levelUpQuery->queryID; - queries.addQuery(levelUpQuery); + queries->addQuery(levelUpQuery); sendAndApply(&hlu); //level up will be called on query reply } @@ -368,7 +226,6 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) return; } - scp.accumulatedBonus.subtype = 0; scp.accumulatedBonus.additionalInfo = 0; scp.accumulatedBonus.duration = BonusDuration::PERMANENT; scp.accumulatedBonus.turnsRemain = 0; @@ -388,11 +245,11 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) { case ECommander::ATTACK: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::ATTACK; + scp.accumulatedBonus.subtype = BonusSubtypeID(PrimarySkill::ATTACK); break; case ECommander::DEFENSE: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::DEFENSE; + scp.accumulatedBonus.subtype = BonusSubtypeID(PrimarySkill::DEFENSE); break; case ECommander::HEALTH: scp.accumulatedBonus.type = BonusType::STACK_HEALTH; @@ -400,7 +257,6 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) break; case ECommander::DAMAGE: scp.accumulatedBonus.type = BonusType::CREATURE_DAMAGE; - scp.accumulatedBonus.subtype = 0; scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE; break; case ECommander::SPEED: @@ -490,7 +346,7 @@ void CGameHandler::levelUpCommander(const CCommanderInstance * c) { auto commanderLevelUp = std::make_shared(this, clu, hero); clu.queryID = commanderLevelUp->queryID; - queries.addQuery(commanderLevelUp); + queries->addQuery(commanderLevelUp); sendAndApply(&clu); } } @@ -508,7 +364,7 @@ void CGameHandler::expGiven(const CGHeroInstance *hero) // levelUpHero(hero); } -void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs) +void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) { if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached { @@ -574,652 +430,9 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh giveSpells(hero->visitedTown, hero); } -void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) -{ - LOG_TRACE(logGlobal); - - //Fill BattleResult structure with exp info - giveExp(*battleResult.data); - - if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped - { - if(heroAttacker) - battleResult.data->exp[1] += 500; - if(heroDefender) - battleResult.data->exp[0] += 500; - } - - // Give 500 exp to winner if a town was conquered during the battle - const auto * defendedTown = battleGetDefendedTown(); - if (defendedTown && battleResult.data->winner == BattleSide::ATTACKER) - battleResult.data->exp[BattleSide::ATTACKER] += 500; - - if(heroAttacker) - battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill - if(heroDefender) - battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - if (!battleQuery) - { - logGlobal->error("Cannot find battle query!"); - complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " has no battle query at the top!"); - return; - } - - battleQuery->result = std::make_optional(*battleResult.data); - - //Check how many battle queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; - finishingBattle = std::make_unique(battleQuery, queriedPlayers); - - // in battles against neutrals, 1st player can ask to replay battle manually - if (!gs->curB->sides[1].color.isValidPlayer()) - { - auto battleDialogQuery = std::make_shared(this, gs->curB); - battleResult.data->queryID = battleDialogQuery->queryID; - queries.addQuery(battleDialogQuery); - } - else - battleResult.data->queryID = -1; - - //set same battle result for all queries - for(auto q : queries.allQueries()) - { - auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery) - otherBattleQuery->result = battleQuery->result; - } - - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - - if (battleResult.data->queryID == -1) - endBattleConfirm(gs->curB); -} - -void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) -{ - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(battleInfo->sides.at(0).color)); - if(!battleQuery) - { - logGlobal->trace("No battle query, battle end was confirmed by another player"); - return; - } - - const BattleResult::EResult result = battleResult.get()->result; - - CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle - ChangeSpells cs; //for Eagle Eye - - if(!finishingBattle->isDraw() && finishingBattle->winnerHero) - { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) - { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) - { - auto spell = spellId.toSpell(VLC->spells()); - if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && getRandomGenerator().nextInt(99) < eagleEyeChance) - cs.spells.insert(spell->getId()); - } - } - } - std::vector arts; //display them in window - - if(result == BattleResult::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, slot); - if(ArtifactUtils::isSlotBackpack(slot)) - ma->askAssemble = false; - 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, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - 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, - ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); - 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->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - 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? - { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); - if (iw.components.size() >= 14) - { - sendAndApply(&iw); - iw.components.clear(); - } - } - if (iw.components.size()) - { - 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.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); - if (i == cs.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); - } - sendAndApply(&iw); - sendAndApply(&cs); - } - cab1.updateArmy(this); - cab2.updateArmy(this); //take casualties after battle is deleted - - if(finishingBattle->loserHero) //remove beaten hero - { - RemoveObject ro(finishingBattle->loserHero->id); - sendAndApply(&ro); - } - if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - } - - if(battleResult.data->winner == BattleSide::DEFENDER - && finishingBattle->winnerHero - && finishingBattle->winnerHero->visitedTown - && !finishingBattle->winnerHero->inTownGarrison - && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) - { - swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place - } - //give exp - if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - - BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); - raccepted.heroResult[0].exp = battleResult.data->exp[0]; - raccepted.heroResult[1].exp = battleResult.data->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; - sendAndApply(&raccepted); - - queries.popIfTop(battleQuery); - //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query -} - -void CGameHandler::battleAfterLevelUp(const BattleResult &result) -{ - LOG_TRACE(logGlobal); - - if(!finishingBattle) - return; - - finishingBattle->remainingBattleQueriesCount--; - logGlobal->trace("Decremented queries count to %d", finishingBattle->remainingBattleQueriesCount); - - if (finishingBattle->remainingBattleQueriesCount > 0) - //Battle results will be handled when all battle queries are closed - return; - - //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible - // but the battle consequences are applied after final player is unblocked. Hard to abuse... - // Still, it looks like a hole. - - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : 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(); - - if (necroSlot != SlotID()) - { - finishingBattle->winnerHero->showNecromancyDialog(raisedStack, getRandomGenerator()); - addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); - } - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - sendAndApply(&resultsApplied); - - setBattle(nullptr); - - if (visitObjectAfterVictory && result.winner==0 && !finishingBattle->winnerHero->stacks.empty()) - { - logGlobal->trace("post-victory visit"); - visitObjectOnTile(*getTile(finishingBattle->winnerHero->visitablePos()), finishingBattle->winnerHero); - } - visitObjectAfterVictory = false; - - //handle victory/loss of engaged players - std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; - checkVictoryLossConditions(playerColors); - - if (result.result == BattleResult::SURRENDER) - heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); - - if (result.result == BattleResult::ESCAPE) - heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); - - if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() - && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - - if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) - heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); - } - - finishingBattle.reset(); -} - -void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) -{ - if(first && !counter) - handleAttackBeforeCasting(ranged, attacker, defender); - - FireShieldInfo fireShield; - BattleAttack bat; - BattleLogMessage blm; - bat.stackAttacking = attacker->unitId(); - bat.tile = targetHex; - - std::shared_ptr attackerState = attacker->acquireState(); - - if(ranged) - bat.flags |= BattleAttack::SHOT; - if(counter) - bat.flags |= BattleAttack::COUNTER; - - const int attackerLuck = attacker->luckVal(); - - if(attackerLuck > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::LUCKY; - } - - if(attackerLuck < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::UNLUCKY; - } - - if (getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) - { - bat.flags |= BattleAttack::DEATH_BLOW; - } - - const auto * owner = gs->curB->getHero(attacker->unitOwner()); - if(owner) - { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); - if (chance > getRandomGenerator().nextInt(99)) - bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; - } - - int64_t drainedLife = 0; - - // only primary target - if(defender->alive()) - drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); - - //multiple-hex normal attack - std::set attackedCreatures = gs->curB->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(bat, attackerState, fireShield, stack, distance, true); - } - - std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? - { - //this is need for displaying hit animation - bat.flags |= BattleAttack::SPELL_LIKE; - bat.spellID = SpellID(bonus->subtype); - - //TODO: should spell override creature`s projectile? - - auto spell = bat.spellID.toSpell(); - - battle::Target target; - target.emplace_back(defender, targetHex); - - spells::BattleCast event(gs->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); - event.setSpellLevel(bonus->val); - - auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); - - //TODO: get exact attacked hex for defender - - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - { - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - } - - //now add effect info for all attacked stacks - for (BattleStackAttacked & bsa : bat.bsa) - { - if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield - { - //this is need for displaying affect animation - bsa.flags |= BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID(bonus->subtype); - } - } - } - - attackerState->afterAttack(ranged, counter); - - { - UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); - attackerState->save(info.data); - bat.attackerChanges.changedStacks.push_back(info); - } - - if (drainedLife > 0) - bat.flags |= BattleAttack::LIFE_DRAIN; - - sendAndApply(&bat); - - { - const bool multipleTargets = bat.bsa.size() > 1; - - int64_t totalDamage = 0; - int32_t totalKills = 0; - - for(const BattleStackAttacked & bsa : bat.bsa) - { - totalDamage += bsa.damageAmount; - totalKills += bsa.killedAmount; - } - - { - MetaString text; - attacker->addText(text, EMetaText::GENERAL_TXT, 376); - attacker->addNameReplacement(text); - text.replaceNumber(totalDamage); - blm.lines.push_back(text); - } - - addGenericKilledLog(blm, defender, totalKills, multipleTargets); - } - - // drain life effect (as well as log entry) must be applied after the attack - if(drainedLife > 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)); - } - - if(!fireShield.empty()) - { - //todo: this should be "virtual" spell instead, we only need fire spell school bonus here - const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); - int64_t totalDamage = 0; - - for(const auto & item : fireShield) - { - const CStack * actor = item.first; - int64_t rawDamage = item.second; - - const CGHeroInstance * actorOwner = gs->curB->getHero(actor->unitOwner()); - - if(actorOwner) - { - rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); - } - else - { - rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); - } - - totalDamage+=rawDamage; - //FIXME: add custom effect on actor - } - - if (totalDamage > 0) - { - BattleStackAttacked bsa; - - bsa.flags |= BattleStackAttacked::FIRE_SHIELD; - bsa.stackAttacked = attacker->unitId(); //invert - bsa.attackerID = defender->unitId(); - bsa.damageAmount = totalDamage; - attacker->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured pack; - pack.stacks.push_back(bsa); - sendAndApply(&pack); - - // TODO: this is already implemented in Damage::describeEffect() - { - MetaString text; - text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); - text.replaceNumber(totalDamage); - blm.lines.push_back(std::move(text)); - } - addGenericKilledLog(blm, attacker, bsa.killedAmount, false); - } - } - - sendAndApply(&blm); - - handleAfterAttackCasting(ranged, attacker, defender); -} - -int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) -{ - BattleStackAttacked bsa; - if(secondary) - bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities - - bsa.attackerID = attackerState->unitId(); - bsa.stackAttacked = def->unitId(); - { - BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); - - bai.deathBlow = bat.deathBlow(); - bai.doubleDamage = bat.ballistaDoubleDmg(); - bai.luckyStrike = bat.lucky(); - bai.unluckyStrike = bat.unlucky(); - - auto range = gs->curB->calculateDmgRange(bai); - bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator()); - CStack::prepareAttacked(bsa, 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; - } - - //soul steal handling - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) - { - //we can have two bonuses - one with subtype 0 and another with subtype 1 - //try to use permanent first, use only one of two - for(si32 subtype = 1; subtype >= 0; subtype--) - { - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) - { - int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); - drainedLife += toHeal; - break; - } - } - } - bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated - - //fire shield handling - if(!bat.shot() && - !def->isClone() && - def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && - CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) - ) - { - //TODO: use damage with bonus but without penalties - 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 CGameHandler::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - BattleLogMessage blm; - addGenericKilledLog(blm, defender, killed, multiple); - sendAndApply(&blm); - } -} - -void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - const int32_t txtIndex = (killed > 1) ? 379 : 378; - std::string formatString = VLC->generaltexth->allTexts[txtIndex]; - - // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); - boost::algorithm::trim(formatString); - - boost::format txt(formatString); - if(killed > 1) - { - txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish - } - else //killed == 1 - { - txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes - } - MetaString line; - line.appendRawString(txt.str()); - blm.lines.push_back(std::move(line)); - } -} - void CGameHandler::handleClientDisconnection(std::shared_ptr c) { - if(lobby->state == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) + if(lobby->getState() == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) return; for(auto & playerConnections : connections) @@ -1246,12 +459,12 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) PackageApplied applied; applied.player = pack->player; applied.result = succesfullyApplied; - applied.packType = typeList.getTypeID(pack); + applied.packType = CTypeList::getInstance().getTypeID(pack); applied.requestID = pack->requestID; pack->c->sendPack(&applied); }; - CBaseForGHApply * apply = applier->getApplier(typeList.getTypeID(pack)); //and appropriate applier object + CBaseForGHApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //and appropriate applier object if(isBlockedByQueries(pack, pack->player)) { sendPackageResponse(false); @@ -1276,322 +489,33 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) vstd::clear_pointer(pack); } -int CGameHandler::moveStack(int stack, BattleHex dest) -{ - int ret = 0; - const CStack *curStack = gs->curB->battleGetStackByID(stack), - *stackAtEnd = gs->curB->battleGetStackByPos(dest); - - assert(curStack); - assert(dest < GameConstants::BFIELD_SIZE); - - if (gs->curB->tacticDistance) - { - assert(gs->curB->isInTacticRange(dest)); - } - - auto start = curStack->getPosition(); - if (start == dest) - return 0; - - //initing necessary tables - auto accessibility = getAccesibility(curStack); - std::set passed; - //Ignore obstacles on starting position - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - - //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) - if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) - { - BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); - - if(accessibility.accessible(shifted, curStack)) - dest = shifted; - } - - if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) - { - complain("Given destination is not accessible!"); - return 0; - } - - bool canUseGate = false; - auto dbState = gs->curB->si.gateState; - if(battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && - dbState != EGateState::DESTROYED && - dbState != EGateState::BLOCKED) - { - canUseGate = true; - } - - std::pair< std::vector, int > path = gs->curB->getPath(start, dest, curStack); - - ret = path.second; - - int creSpeed = curStack->speed(0, true); - - if (gs->curB->tacticDistance > 0 && creSpeed > 0) - creSpeed = GameConstants::BFIELD_SIZE; - - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_OUTER) - return true; - if (hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (isGateDrawbridgeHex(hex)) - return true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) - return true; - } - - return false; - }; - - if (curStack->hasBonusOfType(BonusType::FLYING)) - { - if (path.second <= creSpeed && path.first.size() > 0) - { - if (canUseGate && dbState != EGateState::OPENED && - occupyGateDrawbridgeHex(dest)) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - - //inform clients about move - BattleStackMoved sm; - sm.stack = curStack->unitId(); - std::vector tiles; - tiles.push_back(path.first[0]); - sm.tilesToMove = tiles; - sm.distance = path.second; - sm.teleporting = false; - sendAndApply(&sm); - } - } - else //for non-flying creatures - { - std::vector tiles; - const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); - int v = (int)path.first.size()-1; - path.first.push_back(start); - - // check if gate need to be open or closed at some point - BattleHex openGateAtHex, gateMayCloseAtHex; - if (canUseGate) - { - for (int i = (int)path.first.size()-1; i >= 0; i--) - { - auto needOpenGates = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) - return true; - else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto hex = path.first[i]; - if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) - { - if (needOpenGates(hex)) - openGateAtHex = path.first[i+1]; - - //TODO we need find batter way to handle double-wide stacks - //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && needOpenGates(otherHex)) - openGateAtHex = path.first[i+2]; - } - - //gate may be opened and then closed during stack movement, but not other way around - if (openGateAtHex.isValid()) - dbState = EGateState::OPENED; - } - - if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) - { - if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - if (hasWideMoat) - { - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && - path.first[i-1] != ESiegeHex::GATE_INNER && - path.first[i-1] != ESiegeHex::GATE_BRIDGE) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - } - } - - bool stackIsMoving = true; - - while(stackIsMoving) - { - if (verror("Movement terminated abnormally"); - break; - } - - bool gateStateChanging = false; - //special handling for opening gate on from starting hex - if (openGateAtHex.isValid() && openGateAtHex == start) - gateStateChanging = true; - else - { - for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) - { - BattleHex hex = path.first[v]; - tiles.push_back(hex); - - if ((openGateAtHex.isValid() && openGateAtHex == hex) || - (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) - { - gateStateChanging = true; - } - - //if we walked onto something, finalize this portion of stack movement check into obstacle - if(!battleGetAllObstaclesOnPos(hex, false).empty()) - obstacleHit = true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - //two hex creature hit obstacle by backside - auto obstacle2 = battleGetAllObstaclesOnPos(otherHex, false); - if(otherHex.isValid() && !obstacle2.empty()) - obstacleHit = true; - } - if(!obstacleHit) - passed.insert(hex); - } - } - - if (!tiles.empty()) - { - //commit movement - BattleStackMoved sm; - sm.stack = curStack->unitId(); - sm.distance = path.second; - sm.teleporting = false; - sm.tilesToMove = tiles; - sendAndApply(&sm); - tiles.clear(); - } - - //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end - if (curStack->getPosition() != dest) - { - if(stackIsMoving && start != curStack->getPosition()) - { - stackIsMoving = handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - } - if (gateStateChanging) - { - if (curStack->getPosition() == openGateAtHex) - { - openGateAtHex = BattleHex(); - //only open gate if stack is still alive - if (curStack->alive()) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - } - else if (curStack->getPosition() == gateMayCloseAtHex) - { - gateMayCloseAtHex = BattleHex(); - updateGateState(); - } - } - } - else - //movement finished normally: we reached destination - stackIsMoving = false; - } - } - //handle last hex separately for deviation - if (VLC->settings()->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())) - passed.clear(); //Just empty passed, obstacles will handled automatically - } - //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - - return ret; -} +CGameHandler::CGameHandler() + : turnTimerHandler(*this) +{} CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) + , battles(std::make_unique(this)) + , turnOrder(std::make_unique(this)) + , queries(std::make_unique()) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") , complainInvalidSlot("Invalid slot accessed!") + , turnTimerHandler(*this) { QID = 1; IObjectInterface::cb = this; applier = std::make_shared>(); registerTypesServerPacks(*applier); - visitObjectAfterVictory = false; spellEnv = new ServerSpellCastEnvironment(this); } CGameHandler::~CGameHandler() { - if (battleThread) - { - //Setting battleMadeAction is needed because battleThread waits for the action to continue the main loop - battleMadeAction.setn(true); - battleThread->join(); - } delete spellEnv; delete gs; } @@ -1604,7 +528,7 @@ void CGameHandler::reinitScripting() #endif } -void CGameHandler::init(StartInfo *si) +void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTracking) { if (si->seedToBeUsed == 0) { @@ -1614,15 +538,19 @@ void CGameHandler::init(StartInfo *si) gs = new CGameState(); gs->preInit(VLC); logGlobal->info("Gamestate created!"); - gs->init(&mapService, si); + 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) { - states.addPlayer(elem.first); + if(elem) + heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed } reinitScripting(); @@ -1675,7 +603,45 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa } } -void CGameHandler::newTurn() +void CGameHandler::onPlayerTurnStarted(PlayerColor which) +{ + events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which); + turnTimerHandler.onPlayerGetTurn(which); +} + +void CGameHandler::onPlayerTurnEnded(PlayerColor which) +{ + const auto * playerState = gs->getPlayerState(which); + assert(playerState->status == EPlayerStatus::INGAME); + + if (playerState->towns.empty()) + { + DaysWithoutTown pack; + pack.player = which; + pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; + sendAndApply(&pack); + } + 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; @@ -1764,13 +730,13 @@ void CGameHandler::newTurn() { if (elem.first == PlayerColor::NEUTRAL) continue; - else if (elem.first >= PlayerColor::PLAYER_LIMIT) - assert(0); //illegal player number! + + assert(elem.first.isValidPlayer());//illegal player number! std::pair playerGold(elem.first, elem.second.resources[EGameResID::GOLD]); hadGold.insert(playerGold); - if (newWeek) //new heroes in tavern + if (firstTurn) heroPool->onNewWeek(elem.first); n.res[elem.first] = elem.second.resources; @@ -1823,9 +789,9 @@ void CGameHandler::newTurn() if (!firstTurn) //not first day { - for (int k = 0; k < GameConstants::RESOURCE_QUANTITY; k++) + for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) { - n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, k); + n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)); } } } @@ -1840,7 +806,7 @@ void CGameHandler::newTurn() setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning if (!firstTurn) - if (t->hasBuilt(BuildingSubID::TREASURY) && player < PlayerColor::PLAYER_LIMIT) + 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)) @@ -1881,7 +847,7 @@ void CGameHandler::newTurn() } } } - if (!firstTurn && player < PlayerColor::PLAYER_LIMIT)//not the first day and town not neutral + if (!firstTurn && player.isValidPlayer())//not the first day and town not neutral { n.res[player] = n.res[player] + t->dailyIncome(); } @@ -1893,10 +859,10 @@ void CGameHandler::newTurn() if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player { FoWChange fw; - fw.mode = 1; + fw.mode = ETileVisibility::REVEALED; fw.player = player; // find all hidden tiles - const auto fow = getPlayerTeam(player)->fogOfWarMap; + const auto & fow = getPlayerTeam(player)->fogOfWarMap; auto shape = fow->shape(); for(size_t z = 0; z < shape[0]; z++) @@ -1914,7 +880,7 @@ void CGameHandler::newTurn() { if (getPlayerStatus(player.first) == EPlayerStatus::INGAME && getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES) - changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, true); + changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); } } } @@ -1922,7 +888,7 @@ void CGameHandler::newTurn() if (newMonth) { SetAvailableArtifacts saa; - saa.id = -1; + saa.id = ObjectInstanceID::NONE; pickAllowedArtsSet(saa.arts, getRandomGenerator()); sendAndApply(&saa); } @@ -1944,24 +910,24 @@ void CGameHandler::newTurn() { case NewTurn::DOUBLE_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + 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.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + iw.text.replaceNameSingular(n.creatureid); + iw.text.replaceNameSingular(n.creatureid); break; case NewTurn::DEITYOFFIRE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replacePositiveNumber(15); //%+d 15 - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 43); //%s familiar - iw.text.replacePositiveNumber(15); //%+d 15 + 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) @@ -1983,6 +949,9 @@ void CGameHandler::newTurn() } } + if (!firstTurn) + checkVictoryLossConditionsForAll(); // check for map turn limit + logGlobal->trace("Info about turn %d has been sent!", n.day); handleTimeEvents(); //call objects @@ -1994,11 +963,11 @@ void CGameHandler::newTurn() synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that } + void CGameHandler::run(bool resume) { LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); - using namespace boost::posix_time; for (auto cc : lobby->connections) { auto players = lobby->getAllClientPlayers(cc->connectionID); @@ -2019,140 +988,28 @@ void CGameHandler::run(bool resume) services()->scripts()->run(serverScripts); #endif - if(resume) + if (!resume) + { + onNewTurn(); + events::TurnStarted::defaultExecute(serverEventBus.get()); + for(auto & player : gs->players) + turnTimerHandler.onGameplayStart(player.first); + } + else events::GameResumed::defaultExecute(serverEventBus.get()); - auto playerTurnOrder = generatePlayerTurnOrder(); + turnOrder->onGameStarted(); - while(lobby->state == EServerState::GAMEPLAY) + //wait till game is done + auto clockLast = std::chrono::steady_clock::now(); + while(lobby->getState() == EServerState::GAMEPLAY) { - if(!resume) - { - newTurn(); - events::TurnStarted::defaultExecute(serverEventBus.get()); - } + const auto clockDuration = std::chrono::steady_clock::now() - clockLast; + const int timePassed = std::chrono::duration_cast(clockDuration).count(); + clockLast += clockDuration; + turnTimerHandler.update(timePassed); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); - std::list::iterator it; - if (resume) - { - it = std::find(playerTurnOrder.begin(), playerTurnOrder.end(), gs->currentPlayer); - } - else - { - it = playerTurnOrder.begin(); - } - - resume = false; - for (; (it != playerTurnOrder.end()) && (lobby->state == EServerState::GAMEPLAY) ; it++) - { - auto playerColor = *it; - - auto onGetTurn = [&](events::PlayerGotTurn & event) - { - //if player runs out of time, he shouldn't get the turn (especially AI) - //pre-trigger may change anything, should check before each player - //TODO: is it enough to check only one player? - checkVictoryLossConditionsForAll(); - - auto player = event.getPlayer(); - - const PlayerState * playerState = &gs->players[player]; - - if(playerState->status != EPlayerStatus::INGAME) - { - event.setPlayer(PlayerColor::CANNOT_DETERMINE); - } - else - { - states.setFlag(player, &PlayerStatus::makingTurn, true); - - YourTurn yt; - yt.player = player; - //Change local daysWithoutCastle counter for local interface message //TODO: needed? - yt.daysWithoutCastle = playerState->daysWithoutCastle; - applyAndSend(&yt); - } - }; - - events::PlayerGotTurn::defaultExecute(serverEventBus.get(), onGetTurn, playerColor); - - if(playerColor != PlayerColor::CANNOT_DETERMINE) - { - //wait till turn is done - boost::unique_lock lock(states.mx); - while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) - { - static time_duration p = milliseconds(100); - states.cv.timed_wait(lock, p); - } - } - } - //additional check that game is not finished - bool activePlayer = false; - for (auto player : playerTurnOrder) - { - if (gs->players[player].status == EPlayerStatus::INGAME) - activePlayer = true; - } - if(!activePlayer) - lobby->state = EServerState::GAMEPLAY_ENDED; - } -} - -std::list CGameHandler::generatePlayerTurnOrder() const -{ - // Generate player turn order - std::list playerTurnOrder; - - for (const auto & player : gs->players) // add human players first - { - if (player.second.human) - playerTurnOrder.push_back(player.first); - } - for (const auto & player : gs->players) // then add non-human players - { - if (!player.second.human) - playerTurnOrder.push_back(player.first); - } - return playerTurnOrder; -} - -void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) -{ - battleResult.set(nullptr); - - const auto & t = *getTile(tile); - TerrainId terrain = t.terType->getId(); - if (gs->map->isCoastalTile(tile)) //coastal tile is always ground - terrain = ETerrainId::SAND; - - BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); - if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield.ship_to_ship")); - - //send info about battles - BattleStart bs; - bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); - - engageIntoBattle(bs.info->sides[0].color); - engageIntoBattle(bs.info->sides[1].color); - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(bs.info->sides[0].color)); - bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); - - sendAndApply(&bs); -} - -void CGameHandler::checkBattleStateChanges() -{ - //check if drawbridge state need to be changes - if (battleGetSiegeLevel() > 0) - updateGateState(); - - //check if battle ended - if (auto result = battleIsFinished()) - { - setBattleResult(BattleResult::NORMAL, *result); } } @@ -2189,7 +1046,7 @@ void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) sendAndApply(&cs); } -bool CGameHandler::removeObject(const CGObjectInstance * obj) +bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) { if (!obj || !getObj(obj->id)) { @@ -2198,7 +1055,8 @@ bool CGameHandler::removeObject(const CGObjectInstance * obj) } RemoveObject ro; - ro.id = obj->id; + ro.objectID = obj->id; + ro.initiator = initiator; sendAndApply(&ro); checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) @@ -2209,13 +1067,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { const CGHeroInstance *h = getHero(hid); // not turn of that hero or player can't simply teleport hero (at least not with this function) - if (!h || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer))) + if(!h || (asker != PlayerColor::NEUTRAL && teleporting)) { + if(h && getStartInfo()->turnTimerInfo.isEnabled() && gs->players[h->getOwner()].turnTimer.turnTimer == 0) + return true; //timer expired, no error + logGlobal->error("Illegal call to move hero!"); return false; } - logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.getStr(), 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->pos.toString(), dst.toString()); const int3 hmpos = h->convertToVisitablePos(dst); if (!gs->map->isInTheMap(hmpos)) @@ -2226,8 +1087,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const TerrainTile t = *getTile(hmpos); const int3 guardPos = gs->guardingCreaturePosition(hmpos); + CGObjectInstance * objectToVisit = nullptr; + CGObjectInstance * guardian = nullptr; - const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT; + if (!t.visitableObjects.empty()) + objectToVisit = t.visitableObjects.back(); + + if (isInTheMap(guardPos)) + guardian = getTile(guardPos)->visitableObjects.back(); + + const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT; const bool disembarking = h->boat && t.terType->isLand() && (dst == h->pos @@ -2249,29 +1118,53 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo 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()); - //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() || (t.blocked && !t.visitable && !canFly)) - && complain("Cannot move hero, destination tile is blocked!")) - || ((!h->boat && !canWalkOnSea && !canFly && t.terType->isWater() && (t.visitableObjects.size() < 1 || !t.visitableObjects.back()->isCoastVisitable())) //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) - && complain("Cannot move hero, destination tile is on water!")) - || ((h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) - && complain("Cannot disembark hero, tile is blocked!")) - || ((distance(h->pos, dst) >= 1.5 && !teleporting) - && complain("Tiles are not neighboring!")) - || ((h->inTownGarrison) - && complain("Can not move garrisoned hero!")) - || (((int)h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) - && complain("Hero doesn't have any movement points left!")) - || ((transit && !canFly && !CGTeleport::isTeleport(t.topVisitableObj())) - && complain("Hero cannot transit over this tile!")) - /*|| (states.checkFlag(h->tempOwner, &PlayerStatus::engagedIntoBattle) - && complain("Cannot move hero during the battle"))*/) + const bool movingOntoObstacle = t.blocked && !t.visitable; + const bool objectCoastVisitable = objectToVisit && objectToVisit->isCoastVisitable(); + const bool movingOntoWater = !h->boat && t.terType->isWater() && !objectCoastVisitable; + + const auto complainRet = [&](const std::string & message) { //send info about movement failure + complain(message); sendAndApply(&tmh); return false; - } + }; + + if (guardian && getVisitingHero(guardian) != nullptr) + return complainRet("Cannot move hero, destination monster is busy!"); + + if (objectToVisit && getVisitingHero(objectToVisit) != nullptr) + return complainRet("Cannot move hero, destination object is busy!"); + + if (objectToVisit && + objectToVisit->getOwner().isValidPlayer() && + getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES && + !turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner())) + return complainRet("Cannot move hero, destination player is busy!"); + + //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)) + 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) + return complainRet("Cannot disembark hero, tile is blocked!"); + + if(distance(h->pos, dst) >= 1.5 && !teleporting) + return complainRet("Tiles are not neighboring!"); + + if(h->inTownGarrison) + return complainRet("Can not move garrisoned hero!"); + + if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) + return complainRet("Hero doesn't have any movement points left!"); + + if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()) && !CGTeleport::isTeleport(objectToVisit)) + return complainRet("Hero cannot transit over this tile!"); //several generic blocks of code @@ -2282,7 +1175,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { obj->onHeroLeave(h); } - this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), h->tempOwner, 1); + this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), ETileVisibility::HIDDEN, h->tempOwner); }; auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards, @@ -2291,7 +1184,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo LOG_TRACE_PARAMS(logGlobal, "Hero %s starts movement from %s to %s", h->getNameTranslated() % tmh.start.toString() % tmh.end.toString()); auto moveQuery = std::make_shared(this, tmh, h); - queries.addQuery(moveQuery); + queries->addQuery(moveQuery); if (leavingTile == LEAVING_TILE) leaveTile(); @@ -2302,14 +1195,13 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo tmh.result = result; sendAndApply(&tmh); - if (visitDest == VISIT_DEST && t.topVisitableObj() && t.topVisitableObj()->id == h->id) + if (visitDest == VISIT_DEST && objectToVisit && objectToVisit->id == h->id) { // Hero should be always able to visit any object he staying on even if there guards around visitObjectOnTile(t, h); } else if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos)) { - const TerrainTile &guardTile = *gs->getTile(guardPos); - objectVisited(guardTile.visitableObjects.back(), h); + objectVisited(guardian, h); moveQuery->visitDestAfterVictory = visitDest==VISIT_DEST; } @@ -2318,7 +1210,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - queries.popIfTop(moveQuery); + queries->popIfTop(moveQuery); logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; }; @@ -2367,9 +1259,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo // visit town for town portal \ castle gates // do not use generic visitObjectOnTile to avoid double-teleporting // if this moveHero call was triggered by teleporter - if (!t.visitableObjects.empty()) + if (objectToVisit) { - if (CGTownInstance * town = dynamic_cast(t.visitableObjects.back())) + if (CGTownInstance * town = dynamic_cast(objectToVisit)) town->onHeroVisit(h); } @@ -2387,10 +1279,10 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo EVisitDest visitDest = VISIT_DEST; if (transit) { - if (CGTeleport::isTeleport(t.topVisitableObj())) + if (CGTeleport::isTeleport(objectToVisit)) visitDest = DONT_VISIT_DEST; - if (canFly) + if (canFly || (canWalkOnSea && t.terType->isWater())) { lookForGuards = IGNORE_GUARDS; visitDest = DONT_VISIT_DEST; @@ -2400,8 +1292,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo return true; if(h->boat && !h->boat->onboardAssaultAllowed) - lookForGuards = IGNORE_GUARDS; + lookForGuards = IGNORE_GUARDS; + turnTimerHandler.setEndTurnAllowed(h->getOwner(), !movingOntoWater && !movingOntoObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); return true; } @@ -2412,7 +1305,7 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui const CGHeroInstance *h = getHero(hid); const CGTownInstance *t = getTown(dstid); - if (!h || !t || h->getOwner() != gs->currentPlayer) + if (!h || !t) COMPLAIN_RET("Invalid call to teleportHero!"); const CGTownInstance *from = h->visitedTown; @@ -2437,8 +1330,8 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owner) { PlayerColor oldOwner = getOwner(obj->id); - SetObjectProperty sop(obj->id, ObjProperty::OWNER, owner.getNum()); - sendAndApply(&sop); + + setObjPropertyID(obj->id, ObjProperty::OWNER, owner); std::set playerColors = {owner, oldOwner}; checkVictoryLossConditions(playerColors); @@ -2446,20 +1339,20 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne const CGTownInstance * town = dynamic_cast(obj); if (town) //town captured { - if (owner < PlayerColor::PLAYER_LIMIT) //new owner is real player + if (owner.isValidPlayer()) //new owner is real player { if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) setPortalDwelling(town, true, false); } - if (oldOwner < PlayerColor::PLAYER_LIMIT) //old owner is real player + if (oldOwner.isValidPlayer()) //old owner is real player { if (getPlayerState(oldOwner)->towns.empty() && getPlayerState(oldOwner)->status != EPlayerStatus::LOSER) //previous player lost last 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.replaceLocalString(EMetaText::COLOR, oldOwner.getNum()); + iw.text.replaceName(oldOwner); sendAndApply(&iw); } } @@ -2480,7 +1373,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne void CGameHandler::showBlockingDialog(BlockingDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -2488,7 +1381,7 @@ void CGameHandler::showBlockingDialog(BlockingDialog *iw) void CGameHandler::showTeleportDialog(TeleportDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -2592,69 +1485,6 @@ void CGameHandler::removeArtifact(const ArtifactLocation &al) ea.al = al; sendAndApply(&ea); } -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) //use hero=nullptr for no hero -{ - if(gs->curB) - gs->curB.dellNull(); - - static const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - static const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; - - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - - //existing battle query for retying auto-combat - if(lastBattleQuery) - { - for(int i : {0, 1}) - { - if(heroes[i]) - { - SetMana restoreInitialMana; - restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; - restoreInitialMana.hid = heroes[i]->id; - sendAndApply(&restoreInitialMana); - } - } - - lastBattleQuery->bi = gs->curB; - lastBattleQuery->result = std::nullopt; - lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject; - lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject; - } - - auto nextBattleQuery = std::make_shared(this, gs->curB); - for(int i : {0, 1}) - { - if(heroes[i]) - { - nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; - } - } - queries.addQuery(nextBattleQuery); - - this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) -{ - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) { @@ -2694,14 +1524,15 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta //Reveal fow around new hero, especially released from Prison auto h = getHero(id); - changeFogOfWar(h->pos, h->getSightRadius(), player, false); + changeFogOfWar(h->pos, h->getSightRadius(), player, ETileVisibility::REVEALED); } -void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos) +void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) { ChangeObjPos cop; cop.objid = objid; cop.nPos = newPos; + cop.initiator = initiator; sendAndApply(&cop); } @@ -2709,8 +1540,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t { const CGHeroInstance * h1 = getHero(fromHero); const CGHeroInstance * h2 = getHero(toHero); - int h1_scholarSpellLevel = h1->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT, -1); - int h2_scholarSpellLevel = h2->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT, -1); + int h1_scholarSpellLevel = h1->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT); + int h2_scholarSpellLevel = h2->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT); if (h1_scholarSpellLevel < h2_scholarSpellLevel) { @@ -2729,7 +1560,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t cs1.learn = true; cs1.hid = toHero;//giving spells to first hero for (auto it : h1->getSpellsInSpellbook()) - if (h2Lvl >= it.toSpell()->level && !h2->spellbookContainsSpell(it))//hero can learn it and don't have it yet + if (h2Lvl >= it.toSpell()->getLevel() && !h2->spellbookContainsSpell(it))//hero can learn it and don't have it yet cs1.spells.insert(it);//spell to learn ChangeSpells cs2; @@ -2737,16 +1568,17 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t cs2.hid = fromHero; for (auto it : h2->getSpellsInSpellbook()) - if (h1Lvl >= it.toSpell()->level && !h1->spellbookContainsSpell(it)) + if (h1Lvl >= it.toSpell()->getLevel() && !h1->spellbookContainsSpell(it)) cs2.spells.insert(it); if (!cs1.spells.empty() || !cs2.spells.empty())//create a message { - int ScholarSkillLevel = std::max(h1->getSecSkillLevel(SecondarySkill::SCHOLAR), - h2->getSecSkillLevel(SecondarySkill::SCHOLAR)); + SecondarySkill scholarSkill = SecondarySkill::SCHOLAR; + + int scholarSkillLevel = std::max(h1->getSecSkillLevel(scholarSkill), h2->getSecSkillLevel(scholarSkill)); InfoWindow iw; iw.player = h1->tempOwner; - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, 18, ScholarSkillLevel, 0); + iw.components.emplace_back(ComponentType::SEC_SKILL, scholarSkill, scholarSkillLevel); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 139);//"%s, who has studied magic extensively, iw.text.replaceRawString(h1->getNameTranslated()); @@ -2757,8 +1589,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs2.spells.size()); for (auto it : cs2.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.components.emplace_back(ComponentType::SPELL, it); + iw.text.appendName(it); switch (size--) { case 2: @@ -2785,8 +1617,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs1.spells.size()); for (auto it : cs1.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.components.emplace_back(ComponentType::SPELL, it); + iw.text.appendName(it); switch (size--) { case 2: @@ -2809,7 +1641,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) { auto h1 = getHero(hero1), h2 = getHero(hero2); - if (getPlayerRelations(h1->getOwner(), h2->getOwner())) + if (getPlayerRelations(h1->getOwner(), h2->getOwner()) != PlayerRelations::ENEMIES) { auto exchange = std::make_shared(this, h1, h2); ExchangeDialog hex; @@ -2820,7 +1652,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) sendAndApply(&hex); useScholarSkill(hero1,hero2); - queries.addQuery(exchange); + queries->addQuery(exchange); } } @@ -2843,12 +1675,6 @@ void CGameHandler::sendAndApply(CPackForClient * pack) logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); } -void CGameHandler::applyAndSend(CPackForClient * pack) -{ - gs->apply(pack); - sendToAllClients(pack); -} - void CGameHandler::sendAndApply(CGarrisonOperationPack * pack) { sendAndApply(static_cast(pack)); @@ -2869,7 +1695,7 @@ void CGameHandler::sendAndApply(NewStructures * pack) bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) { - return getPlayerAt(pack->c) == getOwner(id); + return pack->player == getOwner(id) && hasPlayerAt(getOwner(id), pack->c); } void CGameHandler::throwNotAllowedAction(CPackForServer * pack) @@ -2884,14 +1710,14 @@ void CGameHandler::throwNotAllowedAction(CPackForServer * pack) void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer) { std::ostringstream oss; - oss << "You were identified as player " << getPlayerAt(pack->c) << " while expecting " << expectedplayer; + oss << "You were identified as player " << pack->player << " while expecting " << expectedplayer; logNetwork->error(oss.str()); if(pack->c) playerMessages->sendSystemMessage(pack->c, oss.str()); } -void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id) +void CGameHandler::throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id) { if(!isPlayerOwns(pack, id)) { @@ -2900,9 +1726,14 @@ void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id) } } -void CGameHandler::throwOnWrongPlayer(CPackForServer * pack, PlayerColor player) +void CGameHandler::throwIfWrongPlayer(CPackForServer * pack) { - if(!hasPlayerAt(player, pack->c) && player != getPlayerAt(pack->c)) + throwIfWrongPlayer(pack, pack->player); +} + +void CGameHandler::throwIfWrongPlayer(CPackForServer * pack, PlayerColor player) +{ + if(!hasPlayerAt(player, pack->c) || pack->player != player) { wrongPlayerMessage(pack, player); throwNotAllowedAction(pack); @@ -2920,12 +1751,13 @@ void CGameHandler::save(const std::string & filename) logGlobal->info("Saving to %s", filename); const auto stem = FileInfo::GetPathStem(filename); const auto savefname = stem.to_string() + ".vsgm1"; + ResourcePath savePath(stem.to_string(), EResType::SAVEGAME); CResourceHandler::get("local")->createResource(savefname); try { { - CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME))); + CSaveFile save(*CResourceHandler::get("local")->getResourceName(savePath)); saveCommonState(save); logGlobal->info("Saving server state"); save << *this; @@ -2948,24 +1780,44 @@ bool CGameHandler::load(const std::string & filename) try { { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); loadCommonState(lf); logGlobal->info("Loading server state"); lf >> *this; } logGlobal->info("Game has been successfully loaded!"); } - catch(const CModHandler::Incompatibility & e) + catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } lobby->announceMessage(errorMsg); return false; } + catch(const IdentifierResolutionException & e) + { + logGlobal->error("Failed to load game: %s", e.what()); + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + 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()); return false; } gs->preInit(VLC); @@ -3228,10 +2080,18 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player) { - const CArmedInstance * s1 = static_cast(getObjInstance(id1)), - * s2 = static_cast(getObjInstance(id2)); - const CCreatureSet &S1 = *s1, &S2 = *s2; + 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), sl2(s2, p2); + + if (s1 == nullptr || s2 == nullptr) + { + complain("Cannot exchange stacks between non-existing objects!!\n"); + return false; + } + if (!sl1.slot.validSlot() || !sl2.slot.validSlot()) { complain(complainInvalidSlot); @@ -3370,28 +2230,9 @@ bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr return connections.at(player).count(c); } -PlayerColor CGameHandler::getPlayerAt(std::shared_ptr c) const +bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const { - std::set all; - for (auto i=connections.cbegin(); i!=connections.cend(); i++) - if(vstd::contains(i->second, c)) - all.insert(i->first); - - switch(all.size()) - { - case 0: - return PlayerColor::NEUTRAL; - case 1: - return *all.begin(); - default: - { - //if we have more than one player at this connection, try to pick active one - if (vstd::contains(all, gs->currentPlayer)) - return gs->currentPlayer; - else - return PlayerColor::CANNOT_DETERMINE; //cannot say which player is it - } - } + return connections.at(left) == connections.at(right); } bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos) @@ -3443,7 +2284,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, if(!t->visitingHero || !t->visitingHero->hasArt(ArtifactID::GRAIL)) COMPLAIN_RET("Cannot build this without grail!") else - removeArtifact(ArtifactLocation(t->visitingHero, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); + removeArtifact(ArtifactLocation(t->visitingHero->id, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); } break; } @@ -3488,7 +2329,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, auto isLibrary = isMageGuild ? false : t->town->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; - if(isMageGuild || isLibrary || (t->subID == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) + if(isMageGuild || isLibrary || (t->getFaction() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) { if(t->visitingHero) giveSpells(t,t->visitingHero); @@ -3559,11 +2400,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, processAfterBuiltStructure(builtID); // now when everything is built - reveal tiles for lookout tower - FoWChange fw; - fw.player = t->tempOwner; - fw.mode = 1; - getTilesInRange(fw.tiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, 1); - sendAndApply(&fw); + changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); if(t->visitingHero) visitCastleObjects(t, t->visitingHero); @@ -3597,28 +2434,41 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) return true; } -bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl) +bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl, PlayerColor player) { - const CGDwelling * dw = static_cast(getObj(objid)); - const CArmedInstance *dst = nullptr; - const CCreature *c = VLC->creh->objects.at(crid); + const CGDwelling * dwelling = dynamic_cast(getObj(objid)); + const CGTownInstance * town = dynamic_cast(getObj(objid)); + const CArmedInstance * army = dynamic_cast(getObj(dstid)); + const CGHeroInstance * hero = dynamic_cast(getObj(dstid)); + const CCreature * c = VLC->creh->objects.at(crid); + const bool warMachine = c->warMachine != ArtifactID::NONE; - //TODO: test for owning - //TODO: check if dst can recruit objects (e.g. hero is actually visiting object, town and source are same, etc) - dst = dynamic_cast(getObj(dstid)); + //TODO: check if hero is actually visiting object - assert(dw && dst); + COMPLAIN_RET_FALSE_IF(!dwelling || !army, "Cannot recruit: invalid object!"); + COMPLAIN_RET_FALSE_IF(dwelling->getOwner() != player && dwelling->getOwner() != PlayerColor::UNFLAGGABLE, "Cannot recruit: dwelling not owned!"); + + if (town) + { + COMPLAIN_RET_FALSE_IF(town != army && !hero, "Cannot recruit: invalid destination!"); + COMPLAIN_RET_FALSE_IF(hero != town->garrisonHero && hero != town->visitingHero, "Cannot recruit: can only recruit to town or hero in town!!"); + } + else + { + COMPLAIN_RET_FALSE_IF(getVisitingHero(dwelling) != hero, "Cannot recruit: can only recruit by visiting hero!"); + COMPLAIN_RET_FALSE_IF(!hero || hero->getOwner() != player, "Cannot recruit: can only recruit to owned hero!"); + } //verify bool found = false; int level = 0; - for (; level < dw->creatures.size(); level++) //iterate through all levels + for (; level < dwelling->creatures.size(); level++) //iterate through all levels { if ((fromLvl != -1) && (level !=fromLvl)) continue; - const auto &cur = dw->creatures.at(level); //current level info + const auto &cur = dwelling->creatures.at(level); //current level info int i = 0; for (; i < cur.second.size(); i++) //look for crid among available creatures list on current level if (cur.second.at(i) == crid) @@ -3631,10 +2481,10 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst break; } } - SlotID slot = dst->getSlotFor(crid); + SlotID slot = army->getSlotFor(crid); if ((!found && complain("Cannot recruit: no such creatures!")) - || ((si32)cram > VLC->creh->objects.at(crid)->maxAmount(getPlayerState(dst->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) + || ((si32)cram > VLC->creh->objects.at(crid)->maxAmount(getPlayerState(army->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) || (cram<=0 && complain("Cannot recruit: cram <= 0!")) || (!slot.validSlot() && !warMachine && complain("Cannot recruit: no available slot!"))) { @@ -3642,33 +2492,28 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } //recruit - giveResources(dst->tempOwner, -(c->getFullRecruitCost() * cram)); + giveResources(army->tempOwner, -(c->getFullRecruitCost() * cram)); SetAvailableCreatures sac; sac.tid = objid; - sac.creatures = dw->creatures; + sac.creatures = dwelling->creatures; sac.creatures[level].first -= cram; sendAndApply(&sac); if (warMachine) { - const CGHeroInstance *h = dynamic_cast(dst); - - COMPLAIN_RET_FALSE_IF(!h, "Only hero can buy war machines"); - ArtifactID artId = c->warMachine; - - COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!"); - const CArtifact * art = artId.toArtifact(); + COMPLAIN_RET_FALSE_IF(!hero, "Only hero can buy war machines"); + COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!"); COMPLAIN_RET_FALSE_IF(nullptr == art, "Invalid war machine artifact"); - return giveHeroNewArtifact(h, art); + return giveHeroNewArtifact(hero, art); } else { - addToSlot(StackLocation(dst, slot), c, cram); + addToSlot(StackLocation(army, slot), c, cram); } return true; } @@ -3678,7 +2523,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI const CArmedInstance * obj = static_cast(getObjInstance(objid)); if (!obj->hasStackAtSlot(pos)) { - COMPLAIN_RET("Cannot upgrade, no stack at slot " + boost::to_string(pos)); + COMPLAIN_RET("Cannot upgrade, no stack at slot " + std::to_string(pos)); } UpgradeInfo ui; fillUpgradeInfo(obj, pos, ui); @@ -3688,7 +2533,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI int newIDpos= vstd::find_pos(ui.newID, upgID);//get position of new id in UpgradeInfo //check if upgrade is possible - if ((ui.oldID<0 || newIDpos == -1) && complain("That upgrade is not possible!")) + if ((ui.oldID == CreatureID::NONE || newIDpos == -1) && complain("That upgrade is not possible!")) { return false; } @@ -3833,81 +2678,73 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) // With the amount of changes done to the function, it's more like transferArtifacts. // Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. -bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) +bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) { - ArtifactLocation src = al1, dst = al2; - const PlayerColor srcPlayer = src.owningPlayer(), dstPlayer = dst.owningPlayer(); - const CArmedInstance *srcObj = src.relatedObj(), *dstObj = dst.relatedObj(); + const auto srcArtSet = getArtSet(src); + const auto dstArtSet = getArtSet(dst); + assert(srcArtSet); + assert(dstArtSet); // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcObj->id, dstObj->id)) + if(!isAllowedExchange(src.artHolder, dst.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); - const CArtifactInstance *srcArtifact = src.getArt(); - const CArtifactInstance *destArtifact = dst.getArt(); - const bool isDstSlotBackpack = ArtifactUtils::isSlotBackpack(dst.slot); + const auto srcArtifact = srcArtSet->getArt(src.slot); + const auto dstArtifact = dstArtSet->getArt(dst.slot); + const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dst.slot) : false; if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); - if(destArtifact && srcPlayer != dstPlayer && !isDstSlotBackpack) + if(dstArtifact && getHero(src.artHolder)->getOwner() != getHero(dst.artHolder)->getOwner() && !isDstSlotBackpack) COMPLAIN_RET("Can't touch artifact on hero of another player!"); // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) - && srcArtifact && !srcArtifact->canBePutAt(dst, true)) + if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstArtSet, dst.slot, true)) COMPLAIN_RET("Cannot move artifact!"); - auto srcSlot = src.getSlot(); - auto dstSlot = dst.getSlot(); + auto srcSlotInfo = srcArtSet->getSlot(src.slot); + auto dstSlotInfo = dstArtSet->getSlot(dst.slot); - if((srcSlot && srcSlot->locked) || (dstSlot && dstSlot->locked)) + if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); if(isDstSlotBackpack && srcArtifact->artType->isBig()) COMPLAIN_RET("Cannot put big artifacts in backpack!"); if(src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); + if(isDstSlotBackpack && !ArtifactUtils::isBackpackFreeSlots(dstArtSet)) + COMPLAIN_RET("Backpack is full!"); - if(isDstSlotBackpack) + auto dstSlot = std::min(dst.slot, 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!"); + + BulkMoveArtifacts ma(src.artHolder, dst.artHolder, false); + ma.srcCreature = src.creature; + ma.dstCreature = dst.creature; + + // Check if dst slot is occupied + if(!isDstSlotBackpack && dstArtifact) { - if(!ArtifactUtils::isBackpackFreeSlots(dst.getHolderArtSet())) - COMPLAIN_RET("Backpack is full!"); - vstd::amin(dst.slot, GameConstants::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size()); + // Previous artifact must be removed + ma.artsPack1.push_back(BulkMoveArtifacts::LinkedSlots(dstSlot, src.slot)); + ma.swap = true; } - if(!(src.slot == ArtifactPosition::TRANSITION_POS && dst.slot == ArtifactPosition::TRANSITION_POS)) - { - if(src.slot == dst.slot && src.artHolder == dst.artHolder) - COMPLAIN_RET("Won't move artifact: Dest same as source!"); + auto hero = getHero(dst.artHolder); + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) + giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - // Check if dst slot is occupied - if(!isDstSlotBackpack && destArtifact) - { - // Previous artifact must be removed first - moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition::TRANSITION_POS)); - } - - try - { - auto hero = std::get>(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dst.slot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - } - catch(const std::bad_variant_access &) - { - // object other than hero received an art - ignore - } - - MoveArtifact ma(&src, &dst); - if(dst.slot == ArtifactPosition::TRANSITION_POS) - ma.askAssemble = false; - sendAndApply(&ma); - } + ma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(src.slot, dstSlot)); + if(src.artHolder != dst.artHolder) + ma.askAssemble = true; + sendAndApply(&ma); return true; } -bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) +bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) { // Make sure exchange is even possible between the two heroes. if(!isAllowedExchange(srcHero, dstHero)) @@ -3918,8 +2755,7 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID if((!psrcHero) || (!pdstHero)) COMPLAIN_RET("bulkMoveArtifacts: wrong hero's ID"); - BulkMoveArtifacts ma(static_cast>(psrcHero), - static_cast>(pdstHero), swap); + BulkMoveArtifacts ma(srcHero, dstHero, swap); auto & slotsSrcDst = ma.artsPack0; auto & slotsDstSrc = ma.artsPack1; @@ -3962,34 +2798,45 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID slots.push_back(BulkMoveArtifacts::LinkedSlots(slot, slot)); } }; - // Move over artifacts that are worn srcHero -> dstHero - moveArtsWorn(psrcHero, pdstHero, slotsSrcDst); - artFittingSet.artifactsWorn.clear(); - // Move over artifacts that are worn dstHero -> srcHero - moveArtsWorn(pdstHero, psrcHero, slotsDstSrc); - // Move over artifacts that are in backpack srcHero -> dstHero - moveArtsInBackpack(psrcHero, slotsSrcDst); - // Move over artifacts that are in backpack dstHero -> srcHero - moveArtsInBackpack(pdstHero, slotsDstSrc); + if(equipped) + { + // Move over artifacts that are worn srcHero -> dstHero + moveArtsWorn(psrcHero, pdstHero, slotsSrcDst); + artFittingSet.artifactsWorn.clear(); + // Move over artifacts that are worn dstHero -> srcHero + moveArtsWorn(pdstHero, psrcHero, slotsDstSrc); + } + if(backpack) + { + // Move over artifacts that are in backpack srcHero -> dstHero + moveArtsInBackpack(psrcHero, slotsSrcDst); + // Move over artifacts that are in backpack dstHero -> srcHero + moveArtsInBackpack(pdstHero, slotsDstSrc); + } } else { artFittingSet.artifactsInBackpack = pdstHero->artifactsInBackpack; artFittingSet.artifactsWorn = pdstHero->artifactsWorn; - - // Move over artifacts that are worn - for(auto & artInfo : psrcHero->artifactsWorn) + if(equipped) { - if(ArtifactUtils::isArtRemovable(artInfo)) + // Move over artifacts that are worn + for(auto & artInfo : psrcHero->artifactsWorn) { - moveArtifact(psrcHero->getArt(artInfo.first), artInfo.first, pdstHero, slotsSrcDst); + if(ArtifactUtils::isArtRemovable(artInfo)) + { + moveArtifact(psrcHero->getArt(artInfo.first), artInfo.first, pdstHero, slotsSrcDst); + } } } - // Move over artifacts that are in backpack - for(auto & slotInfo : psrcHero->artifactsInBackpack) + if(backpack) { - moveArtifact(psrcHero->getArt(psrcHero->getArtPos(slotInfo.artifact)), - psrcHero->getArtPos(slotInfo.artifact), pdstHero, slotsSrcDst); + // Move over artifacts that are in backpack + for(auto & slotInfo : psrcHero->artifactsInBackpack) + { + moveArtifact(psrcHero->getArt(psrcHero->getArtPos(slotInfo.artifact)), + psrcHero->getArtPos(slotInfo.artifact), pdstHero, slotsSrcDst); + } } } sendAndApply(&ma); @@ -4004,7 +2851,7 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID * @param assembleTo If assemble is true, this represents the artifact ID of the combination * artifact to assemble to. Otherwise it's not used. */ -bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { const CGHeroInstance * hero = getHero(heroID); const CArtifactInstance * destArtifact = hero->getArt(artifactSlot); @@ -4012,23 +2859,27 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition if(!destArtifact) COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); + const auto dstLoc = ArtifactLocation(hero->id, artifactSlot); if(assemble) { CArtifact * combinedArt = VLC->arth->objects[assembleTo]; if(!combinedArt->isCombined()) COMPLAIN_RET("assembleArtifacts: Artifact being attempted to assemble is not a combined artifacts!"); - if (!vstd::contains(ArtifactUtils::assemblyPossibilities(hero, destArtifact->getTypeId(), - ArtifactUtils::isSlotEquipment(artifactSlot)), combinedArt)) + if(!vstd::contains(ArtifactUtils::assemblyPossibilities(hero, destArtifact->getTypeId()), combinedArt)) { COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); } - + if(!destArtifact->canBePutAt(hero, artifactSlot) + && !destArtifact->canBePutAt(hero, ArtifactPosition::BACKPACK_START)) + { + COMPLAIN_RET("assembleArtifacts: It's impossible to give the artholder requested artifact!"); + } if(ArtifactUtils::checkSpellbookIsNeeded(hero, assembleTo, artifactSlot)) giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); AssembledArtifact aa; - aa.al = ArtifactLocation(hero, artifactSlot); + aa.al = dstLoc; aa.builtArt = combinedArt; sendAndApply(&aa); } @@ -4042,7 +2893,7 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble but backpack is full!"); DisassembledArtifact da; - da.al = ArtifactLocation(hero, artifactSlot); + da.al = dstLoc; sendAndApply(&da); } @@ -4051,15 +2902,15 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition bool CGameHandler::eraseArtifactByClient(const ArtifactLocation & al) { - const auto * hero = getHero(al.relatedObj()->id); + const auto * hero = getHero(al.artHolder); if(hero == nullptr) COMPLAIN_RET("eraseArtifactByClient: wrong hero's ID"); - const auto * art = al.getArt(); + const auto * art = hero->getArt(al.slot); if(art == nullptr) COMPLAIN_RET("Cannot remove artifact!"); - if(al.getArt()->artType->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) + if(art->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) COMPLAIN_RET("Illegal artifact removal request"); removeArtifact(al); @@ -4126,12 +2977,12 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe SetAvailableArtifacts saa; if(dynamic_cast(m)) { - saa.id = -1; + saa.id = ObjectInstanceID::NONE; saa.arts = CGTownInstance::merchantArtifacts; } else if(const CGBlackMarket *bm = dynamic_cast(m)) //black market { - saa.id = bm->id.getNum(); + saa.id = bm->id; saa.arts = bm->artifacts; } else @@ -4166,7 +3017,7 @@ bool CGameHandler::sellArtifact(const IMarket *m, const CGHeroInstance *h, Artif int resVal = 0, dump = 1; m->getOffer(art->artType->getId(), rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); - removeArtifact(ArtifactLocation(h, h->getArtPos(art))); + removeArtifact(ArtifactLocation(h->id, h->getArtPos(art))); giveResource(h->tempOwner, rid, resVal); return true; } @@ -4197,23 +3048,23 @@ bool CGameHandler::buySecSkill(const IMarket *m, const CGHeroInstance *h, Second return true; } -bool CGameHandler::tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2) +bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy) { - TResourceCap r1 = getPlayerState(player)->resources[id1]; + TResourceCap haveToSell = getPlayerState(player)->resources[toSell]; - vstd::amin(val, r1); //can't trade more resources than have + vstd::amin(amountToSell, haveToSell); //can't trade more resources than have int b1, b2; //base quantities for trade - market->getOffer(id1, id2, b1, b2, EMarketMode::RESOURCE_RESOURCE); - int units = val / b1; //how many base quantities we trade + market->getOffer(toSell, toBuy, b1, b2, EMarketMode::RESOURCE_RESOURCE); + int amountToBoy = amountToSell / b1; //how many base quantities we trade - if (val%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error + if (amountToSell % b1 != 0) //all offered units of resource should be used, if not -> somewhere in calculations must be an error { COMPLAIN_RET("Invalid deal, not all offered units of resource were used."); } - giveResource(player, GameResID(id1), - b1 * units); - giveResource(player, GameResID(id2), b2 * units); + giveResource(player, toSell, -b1 * amountToBoy); + giveResource(player, toBuy, b2 * amountToBoy); return true; } @@ -4297,7 +3148,7 @@ bool CGameHandler::sendResources(ui32 val, PlayerColor player, GameResID r1, Pla return true; } -bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) +bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) { const CGHeroInstance *h = getHero(hid); if (!h) @@ -4314,20 +3165,21 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) return true; } -bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor player) +bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { boost::unique_lock lock(gsm); logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); - logGlobal->trace(answer.toJson()); + if (answer) + logGlobal->trace("%d", *answer); - auto topQuery = queries.topQuery(player); + auto topQuery = queries->topQuery(player); COMPLAIN_RET_FALSE_IF(!topQuery, "This player doesn't have any queries!"); if(topQuery->queryID != qid) { - auto currentQuery = queries.getQuery(qid); + auto currentQuery = queries->getQuery(qid); if(currentQuery != nullptr && currentQuery->endsByPlayerAnswer()) currentQuery->setReply(answer); @@ -4337,712 +3189,10 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor COMPLAIN_RET_FALSE_IF(!topQuery->endsByPlayerAnswer(), "This query cannot be ended by player's answer!"); topQuery->setReply(answer); - queries.popQuery(topQuery); + queries->popQuery(topQuery); return true; } -static EndAction end_action; - -void CGameHandler::updateGateState() -{ - // GATE_BRIDGE - leftmost tile, located over moat - // GATE_OUTER - central tile, mostly covered by gate image - // GATE_INNER - rightmost tile, inside the walls - - // GATE_OUTER or GATE_INNER: - // - if defender moves unit on these tiles, bridge will open - // - if there is a creature (dead or alive) on these tiles, bridge will always remain open - // - blocked to attacker if bridge is closed - - // GATE_BRIDGE - // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) - // - if Force Field is cast here, bridge can't open (but can close, in any town) - // - deals moat damage to attacker if bridge is closed (fortress only) - - bool hasForceFieldOnBridge = !battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); - bool hasStackAtGateInner = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; - bool hasStackAtGateOuter = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; - bool hasStackAtGateBridge = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - BattleUpdateGateState db; - db.state = gs->curB->si.gateState; - if (gs->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) - { - db.state = EGateState::DESTROYED; - } - else if (db.state == EGateState::OPENED) - { - bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; - bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; - - if (gateCanClose) - db.state = EGateState::CLOSED; - else - db.state = EGateState::OPENED; - } - else // CLOSED or BLOCKED - { - bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; - - if (gateBlocked) - db.state = EGateState::BLOCKED; - else - db.state = EGateState::CLOSED; - } - - if (db.state != gs->curB->si.gateState) - sendAndApply(&db); -} - -bool CGameHandler::makeBattleAction(BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - bool ok = true; - - battle::Target target = ba.getTarget(gs->curB); - - const CStack * stack = battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack - - const bool isAboutActiveStack = stack && (ba.stackNumber == gs->curB->getActiveStackID()); - - logGlobal->trace("Making action: %s", ba.toString()); - - switch(ba.actionType) - { - case EActionType::WALK: //walk - case EActionType::DEFEND: //defend - case EActionType::WAIT: //wait - case EActionType::WALK_AND_ATTACK: //walk or attack - case EActionType::SHOOT: //shoot - case EActionType::CATAPULT: //catapult - case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::MONSTER_SPELL: - - if (!stack) - { - complain("No such stack!"); - return false; - } - if (!stack->alive()) - { - complain("This stack is dead: " + stack->nodeName()); - return false; - } - - if (battleTacticDist()) - { - if (stack && stack->unitSide() != battleGetTacticsSide()) - { - complain("This is not a stack of side that has tactics!"); - return false; - } - } - else if (!isAboutActiveStack) - { - complain("Action has to be about active stack!"); - return false; - } - } - - auto wrapAction = [this](BattleAction &ba) - { - StartAction startAction(ba); - sendAndApply(&startAction); - - return vstd::makeScopeGuard([&]() - { - sendAndApply(&end_action); - }); - }; - - switch(ba.actionType) - { - case EActionType::END_TACTIC_PHASE: //wait - case EActionType::BAD_MORALE: - case EActionType::NO_ACTION: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::WALK: - { - auto wrapper = wrapAction(ba); - if(target.size() < 1) - { - complain("Destination required for move action."); - ok = false; - break; - } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move - if (!walkedTiles) - complain("Stack failed movement!"); - break; - } - case EActionType::DEFEND: - { - //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) - SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); - int oldDefenceValue = defence.totalValue(); - - defence.push_back(std::make_shared(defenseBonusToAdd)); - defence.push_back(std::make_shared(bonus2)); - - int difference = defence.totalValue() - oldDefenceValue; - std::vector buffer; - if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) - { - difference = 1; - buffer.push_back(alternativeWeakCreatureBonus); - } - else - { - buffer.push_back(defenseBonusToAdd); - } - - buffer.push_back(bonus2); - - sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - sendAndApply(&sse); - - BattleLogMessage message; - - MetaString text; - stack->addText(text, EMetaText::GENERAL_TXT, 120); - stack->addNameReplacement(text); - text.replaceNumber(difference); - - message.lines.push_back(text); - - sendAndApply(&message); - //don't break - we share code with next case - } - [[fallthrough]]; - case EActionType::WAIT: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::RETREAT: //retreat/flee - { - if (!gs->curB->battleCanFlee(gs->curB->sides.at(ba.side).color)) - complain("Cannot retreat!"); - else - setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case EActionType::SURRENDER: - { - PlayerColor player = gs->curB->sides.at(ba.side).color; - int cost = gs->curB->battleGetSurrenderCost(player); - if (cost < 0) - complain("Cannot surrender!"); - else if (getResource(player, EGameResID::GOLD) < cost) - complain("Not enough gold to surrender!"); - else - { - giveResource(player, EGameResID::GOLD, -cost); - setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case EActionType::WALK_AND_ATTACK: //walk or attack - { - auto wrapper = wrapAction(ba); - - if(!stack) - { - complain("No attacker"); - ok = false; - break; - } - - if(target.size() < 2) - { - complain("Two destinations required for attack action."); - ok = false; - break; - } - - BattleHex attackPos = target.at(0).hexValue; - BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gs->curB->battleGetStackByPos(destinationTile, true); - - if(!destinationStack) - { - complain("Invalid target to attack"); - ok = false; - break; - } - - BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); - - logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - - if(stack->getPosition() != attackPos - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) - ) - { - // we were not able to reach destination tile, nor occupy specified hex - // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine - break; - } - - if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - complain("Unit can not attack itself"); - ok = false; - break; - } - - if(!CStack::isMeleeAttackPossible(stack, destinationStack)) - { - complain("Attack cannot be performed!"); - ok = false; - break; - } - - //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); - const bool retaliation = destinationStack->ableToRetaliate(); - for (int i = 0; i < totalAttacks; ++i) - { - //first strike - if(i == 0 && firstStrike && retaliation) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - - //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification - if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) - { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - } - - //counterattack - //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) - && (i == 0 && !firstStrike) - && retaliation && destinationStack->ableToRetaliate()) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - } - - //return - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) - && target.size() == 3 - && startingPos != stack->getPosition() - && startingPos == target.at(2).hexValue - && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) - } - break; - } - case EActionType::SHOOT: - { - if(target.size() < 1) - { - complain("Destination required for shot action."); - ok = false; - break; - } - - auto destination = target.at(0).hexValue; - - const CStack * destinationStack = gs->curB->battleGetStackByPos(destination); - - if (!gs->curB->battleCanShoot(stack, destination)) - { - complain("Cannot shoot!"); - break; - } - if (!destinationStack) - { - complain("No target to shoot!"); - break; - } - - auto wrapper = wrapAction(ba); - - makeAttack(stack, destinationStack, 0, destination, true, true, false); - - //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) - && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) - && destinationStack->ableToRetaliate() - && gs->curB->battleCanShoot(destinationStack, stack->getPosition()) - && stack->alive()) //attacker may have died (fire shield) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); - } - //allow more than one additional attack - - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - for(int i = 1; i < totalRangedAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) - { - makeAttack(stack, destinationStack, 0, destination, false, true, false); - } - } - break; - } - case EActionType::CATAPULT: - { - auto wrapper = wrapAction(ba); - const CStack * shooter = gs->curB->battleGetStackByID(ba.stackNumber); - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) - { - complain("We do not know how to shoot :P"); - } - else - { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult - auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); - parameters.setSpellLevel(shotLevel); - parameters.cast(spellEnv, target); - } - //finish by scope guard - break; - } - case EActionType::STACK_HEAL: //healing with First Aid Tent - { - auto wrapper = wrapAction(ba); - const CStack * healer = gs->curB->battleGetStackByID(ba.stackNumber); - - if(target.size() < 1) - { - complain("Destination required for heal action."); - ok = false; - break; - } - - const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); - - if(target.at(0).unitValue) - destStack = target.at(0).unitValue; - else - destStack = gs->curB->battleGetUnitByPos(target.at(0).hexValue); - - if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) - { - complain("There is either no healer, no destination, or healer cannot heal :P"); - } - else - { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent - auto dest = battle::Destination(destStack, target.at(0).hexValue); - parameters.setSpellLevel(0); - parameters.cast(spellEnv, {dest}); - } - break; - } - case EActionType::MONSTER_SPELL: - { - auto wrapper = wrapAction(ba); - - const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.actionSubtype); - - std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if (randSpellcaster && battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID < 0) - complain("That stack can't cast spells!"); - else - { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gs->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(spellEnv, target); - } - break; - } - } - if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND - || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) - handleObstacleTriggersForUnit(*spellEnv, *stack); - if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished - battleMadeAction.setn(true); - return ok; -} - -bool CGameHandler::makeCustomAction(BattleAction & ba) -{ - boost::unique_lock lock(battleActionMutex); - - switch(ba.actionType) - { - case EActionType::HERO_SPELL: - { - COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); - - const CGHeroInstance *h = gs->curB->battleGetFightingHero(ba.side); - COMPLAIN_RET_FALSE_IF((!h), "Wrong caster!"); - - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); - if (!s) - { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); - return false; - } - - spells::BattleCast parameters(gs->curB, h, spells::Mode::HERO, s); - - spells::detail::ProblemImpl problem; - - auto m = s->battleMechanics(¶meters); - - if(!m->canBeCast(problem))//todo: should we check aimed cast? - { - logGlobal->warn("Spell cannot be cast!"); - std::vector texts; - problem.getAll(texts); - for(auto s : texts) - logGlobal->warn(s); - return false; - } - - StartAction start_action(ba); - sendAndApply(&start_action); //start spell casting - - parameters.cast(spellEnv, ba.getTarget(gs->curB)); - - sendAndApply(&end_action); - if (!gs->curB->battleGetStackByID(gs->curB->activeStack)) - { - battleMadeAction.setn(true); - } - checkBattleStateChanges(); - if (battleResult.get()) - { - battleMadeAction.setn(true); - //battle will be ended by startBattle function - //endBattle(gs->curB->tile, gs->curB->heroes[0], gs->curB->heroes[1]); - } - - return true; - - } - } - return false; -} - -void CGameHandler::stackEnchantedTrigger(const CStack * st) -{ - auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); - for(auto b : bl) - { - const CSpell * sp = SpellID(b->subtype).toSpell(); - if(!sp) - continue; - - const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); - const int32_t level = ((val > 3) ? (val - 3) : val); - - spells::BattleCast battleCast(gs->curB, st, spells::Mode::PASSIVE, sp); - //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle - battleCast.setEffectDuration(50); - battleCast.setSpellLevel(level); - spells::Target target; - - if(val > 3) - { - for(auto s : gs->curB->battleGetAllStacks()) - if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied - target.emplace_back(s); - } - else - { - target.emplace_back(st); - } - battleCast.applyEffects(spellEnv, target, false, true); - } -} - -void CGameHandler::stackTurnTrigger(const CStack *st) -{ - BattleTriggerEffect bte; - bte.stackID = st->unitId(); - bte.effect = -1; - bte.val = 0; - bte.additionalInfo = 0; - if (st->alive()) - { - //unbind - if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) - { - bool unbind = true; - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); - auto adjacent = gs->curB->battleAdjacentUnits(st); - - for (auto b : bl) - { - if(b->additionalInfo != CAddInfo::NONE) - { - const CStack * stack = gs->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent - if(stack) - { - if(vstd::contains(adjacent, stack)) //binding stack is still present - unbind = false; - } - } - else - { - unbind = false; - } - } - if (unbind) - { - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::UNBIND; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - - if (st->hasBonusOfType(BonusType::POISON)) - { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) //TODO: what if not?... - { - bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); - if (bte.val < b->val) //(negative) poison effect increases - update it - { - bte.effect = vstd::to_underlying(BonusType::POISON); - sendAndApply(&bte); - } - } - } - if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) - { - const PlayerColor opponent = gs->curB->otherPlayer(gs->curB->battleGetOwner(st)); - const CGHeroInstance * opponentHero = gs->curB->getHero(opponent); - if(opponentHero) - { - ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); - vstd::amin(manaDrained, opponentHero->mana); - if(manaDrained) - { - bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); - bte.val = manaDrained; - bte.additionalInfo = opponentHero->id.getNum(); //for sanity - sendAndApply(&bte); - } - } - } - if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) - { - bool fearsomeCreature = false; - for (CStack * stack : gs->curB->stacks) - { - if (battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) - { - fearsomeCreature = true; - break; - } - } - if (fearsomeCreature) - { - if (getRandomGenerator().nextInt(99) < 10) //fixed 10% - { - bte.effect = vstd::to_underlying(BonusType::FEAR); - sendAndApply(&bte); - } - } - } - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = gs->curB->whatSide(st->unitOwner()); - if(st->canCast() && gs->curB->battleGetEnchanterCounter(side) == 0) - { - bool cast = false; - while(!bl.empty() && !cast) - { - auto bonus = *RandomGeneratorUtil::nextItem(bl, getRandomGenerator()); - auto spellID = SpellID(bonus->subtype); - const CSpell * spell = SpellID(spellID).toSpell(); - bl.remove_if([&bonus](const Bonus * b) - { - return b == bonus.get(); - }); - spells::BattleCast parameters(gs->curB, st, spells::Mode::ENCHANTER, spell); - parameters.setSpellLevel(bonus->val); - parameters.massive = true; - parameters.smart = true; - //todo: recheck effect level - if(parameters.castIfPossible(spellEnv, spells::Target(1, spells::Destination()))) - { - cast = true; - - int cooldown = bonus->additionalInfo[0]; - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; - ssp.absolute = false; - ssp.val = cooldown; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - } - } -} - void CGameHandler::handleTimeEvents() { gs->map->events.sort(evntCmp); @@ -5069,12 +3219,12 @@ void CGameHandler::handleTimeEvents() //prepare dialog InfoWindow iw; iw.player = color; - iw.text.appendRawString(ev.message); + iw.text = ev.message; - for (int i=0; iresources[i] != n.res.at(player)[i]) //if resource had changed, we add it to the dialog - iw.components.emplace_back(Component::EComponentType::RESOURCE,i,n.res.at(player)[i]-was[i],0); - + iw.components.emplace_back(ComponentType::RESOURCE, i, n.res.at(player)[i] - was[i]); } for (auto & i : ev.buildings) @@ -5139,7 +3288,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->hasBuilt(i)) { buildStructure(town->id, i, true); - iw.components.emplace_back(Component::EComponentType::BUILDING, town->subID, i, 0); + iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); } } @@ -5155,8 +3304,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->creatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling { sac.creatures[i].first += ev.creatures.at(i); - iw.components.emplace_back(Component::EComponentType::CREATURE, - town->creatures.at(i).second.back(), ev.creatures.at(i), 0); + iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), ev.creatures.at(i)); } } sendAndApply(&iw); //show dialog @@ -5202,7 +3350,7 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h assert(upperArmy); auto garrisonQuery = std::make_shared(this, upperArmy, lowerArmy); - queries.addQuery(garrisonQuery); + queries->addQuery(garrisonQuery); GarrisonDialog gd; gd.hid = hid; @@ -5212,13 +3360,20 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h sendAndApply(&gd); } -void CGameHandler::showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) +void CGameHandler::showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) { - OpenWindow ow; - ow.window = EOpenWindowMode::THIEVES_GUILD; - ow.id1 = player.getNum(); - ow.id2 = requestingObjId.getNum(); - sendAndApply(&ow); + OpenWindow pack; + pack.window = window; + pack.object = object->id; + pack.visitor = visitor->id; + + if (addQuery) + { + auto windowQuery = std::make_shared(this, visitor, window); + pack.queryID = windowQuery->queryID; + queries->addQuery(windowQuery); + } + sendAndApply(&pack); } bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) @@ -5256,10 +3411,10 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) } //Ongoing garrison exchange - usually picking from top garison (from o1 to o2), but who knows - auto dialog = std::dynamic_pointer_cast(queries.topQuery(o1->tempOwner)); + auto dialog = std::dynamic_pointer_cast(queries->topQuery(o1->tempOwner)); if (!dialog) { - dialog = std::dynamic_pointer_cast(queries.topQuery(o2->tempOwner)); + dialog = std::dynamic_pointer_cast(queries->topQuery(o2->tempOwner)); } if (dialog) { @@ -5278,7 +3433,13 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta { using events::ObjectVisitStarted; - logGlobal->debug("%s visits %s (%d:%d)", h->nodeName(), obj->getObjectName(), obj->ID, obj->subID); + logGlobal->debug("%s visits %s (%d)", h->nodeName(), obj->getObjectName(), obj->ID); + + if (getVisitingHero(obj) != nullptr) + { + logGlobal->error("Attempt to visit object that is being visited by another hero!"); + throw std::runtime_error("Can not visit object that is being visited"); + } std::shared_ptr visitQuery; @@ -5300,7 +3461,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta } } visitQuery = std::make_shared(this, visitedObject, h, visitedObject->visitablePos()); - queries.addQuery(visitQuery); //TODO real visit pos + queries->addQuery(visitQuery); //TODO real visit pos HeroVisit hv; hv.objId = obj->id; @@ -5315,7 +3476,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta ObjectVisitStarted::defaultExecute(serverEventBus.get(), startVisit, h->tempOwner, h->id, obj->id); if(visitQuery) - queries.popIfTop(visitQuery); //visit ends here if no queries were created + queries->popIfTop(visitQuery); //visit ends here if no queries were created } void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) @@ -5366,20 +3527,10 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) } giveResources(playerID, -boatCost); - createObject(tile, Obj::BOAT, obj->getBoatType().getNum()); + createObject(tile, playerID, Obj::BOAT, obj->getBoatType().getNum()); return true; } -void CGameHandler::engageIntoBattle(PlayerColor player) -{ - //notify interfaces - PlayerBlocked pb; - pb.player = player; - pb.reason = PlayerBlocked::UPCOMING_BATTLE; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - sendAndApply(&pb); -} - void CGameHandler::checkVictoryLossConditions(const std::set & playerColors) { for (auto playerColor : playerColors) @@ -5418,6 +3569,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) peg.victoryLossCheckResult = victoryLossCheckResult; sendAndApply(&peg); + turnOrder->onPlayerEndsGame(player); + if (victoryLossCheckResult.victory()) { //one player won -> all enemies lost @@ -5440,7 +3593,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) if(p->human) { - lobby->state = EServerState::GAMEPLAY_ENDED; + lobby->setState(EServerState::GAMEPLAY_ENDED); } } else @@ -5450,7 +3603,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) for (auto h : hlp) //eliminate heroes { if (h.get()) - removeObject(h); + removeObject(h, player); } //player lost -> all his objects become unflagged (neutral) @@ -5483,14 +3636,6 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) } checkVictoryLossConditions(playerColors); } - - auto playerInfo = getPlayerState(gs->currentPlayer, false); - // If we are called before the actual game start, there might be no current player - if (playerInfo && playerInfo->status != EPlayerStatus::INGAME) - { - // If player making turn has lost his turn must be over as well - states.setFlag(gs->currentPlayer, &PlayerStatus::makingTurn, false); - } } } @@ -5498,16 +3643,16 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC { out.player = player; out.text = victoryLossCheckResult.messageToSelf; - out.text.replaceLocalString(EMetaText::COLOR, player.getNum()); - out.components.emplace_back(Component::EComponentType::FLAG, player.getNum(), 0, 0); + out.text.replaceName(player); + out.components.emplace_back(ComponentType::FLAG, player); } 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)!", h->diggingStatus()); + COMPLAIN_RETF("Hero cannot dig (error code %d)!", static_cast(h->diggingStatus())); - createObject(h->visitablePos(), Obj::HOLE, 0 ); + createObject(h->visitablePos(), h->getOwner(), Obj::HOLE, 0 ); //take MPs SetMovePoints smp; @@ -5520,16 +3665,18 @@ bool CGameHandler::dig(const CGHeroInstance *h) iw.player = h->tempOwner; if (gs->map->grailPos == h->visitablePos()) { + ArtifactID grail = ArtifactID::GRAIL; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " - iw.text.appendLocalString(EMetaText::ART_NAMES, ArtifactID::GRAIL); + iw.text.replaceName(grail); iw.soundID = soundBase::ULTIMATEARTIFACT; - giveHeroNewArtifact(h, VLC->arth->objects[ArtifactID::GRAIL], ArtifactPosition::FIRST_AVAILABLE); //give grail + giveHeroNewArtifact(h, grail.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); //give grail sendAndApply(&iw); iw.soundID = soundBase::invalid; - iw.components.emplace_back(Component::EComponentType::ARTIFACT, ArtifactID::GRAIL, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, grail); iw.text.clear(); - iw.text.appendLocalString(EMetaText::ART_DESCR, ArtifactID::GRAIL); + iw.text.appendTextID(grail.toArtifact()->getDescriptionTextID()); sendAndApply(&iw); } else @@ -5542,239 +3689,6 @@ bool CGameHandler::dig(const CGHeroInstance *h) return true; } -void CGameHandler::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) -{ - if(attacker->hasBonusOfType(attackMode)) - { - std::set spellsToCast; - TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - spellsToCast.insert(SpellID(sf->subtype)); - } - for(SpellID spellID : spellsToCast) - { - bool castMe = false; - if(!defender->alive()) - { - logGlobal->debug("attackCasting: all attacked creatures have been killed"); - return; - } - int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); - for(const auto & sf : *spellsByType) - { - int meleeRanged; - if(sf->additionalInfo.size() < 2) - { - // legacy format - vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); - meleeRanged = sf->additionalInfo[0] / 1000; - } - else - { - vstd::amax(spellLevel, sf->additionalInfo[0]); - meleeRanged = sf->additionalInfo[1]; - } - if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) - castMe = true; - } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); - vstd::amin(chance, 100); - - const CSpell * spell = SpellID(spellID).toSpell(); - spells::AbilityCaster caster(attacker, spellLevel); - - spells::Target target; - target.emplace_back(defender); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - - auto m = spell->battleMechanics(¶meters); - - spells::detail::ProblemImpl ignored; - - if(!m->canBeCastAt(target, ignored)) - continue; - - //check if spell should be cast (probability handling) - if(getRandomGenerator().nextInt(99) >= chance) - continue; - - //casting - if(castMe) - { - parameters.cast(spellEnv, target); - } - } - } -} - -void CGameHandler::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? -} - -void CGameHandler::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - if(!attacker->alive() || !defender->alive()) // can be already dead - return; - - attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); - - if(!defender->alive()) - { - //don't try death stare or acid breath on dead stack (crash!) - return; - } - - if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - - int staredCreatures = distribution(getRandomGenerator().getStdGenerator()); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); - if(staredCreatures) - { - //TODO: death stare was not originally available for multiple-hex attacks, but... - const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - parameters.setEffectValue(staredCreatures); - parameters.cast(spellEnv, target); - } - } - - if(!defender->alive()) - return; - - int64_t acidDamage = 0; - TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); - for(const auto & b : *acidBreath) - { - if(b->additionalInfo[0] > getRandomGenerator().nextInt(99)) - acidDamage += b->val; - } - - if(acidDamage > 0) - { - const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - - parameters.setEffectValue(acidDamage * attacker->getCount()); - parameters.cast(spellEnv, target); - } - - - if(!defender->alive()) - return; - - if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability - { - double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; - vstd::amin(chanceToTrigger, 1); //cap at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; - - if(defender->unitType()->getId() == bonusAdditionalInfo || - (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) - return; - - battle::UnitInfo resurrectInfo; - resurrectInfo.id = gs->curB->battleNextUnitId(); - resurrectInfo.summoned = false; - resurrectInfo.position = defender->getPosition(); - resurrectInfo.side = defender->unitSide(); - - if(bonusAdditionalInfo != CAddInfo::NONE) - resurrectInfo.type = CreatureID(bonusAdditionalInfo); - else - resurrectInfo.type = attacker->creatureId(); - - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) - resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) - resurrectInfo.count = defender->getCount(); - else - return; //wrong subtype - - BattleUnitsChanged addUnits; - addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); - resurrectInfo.save(addUnits.changedStacks.back().data); - - BattleUnitsChanged removeUnits; - removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); - sendAndApply(&removeUnits); - sendAndApply(&addUnits); - - // send empty event to client - // temporary(?) workaround to force animations to trigger - StacksInjured fakeEvent; - sendAndApply(&fakeEvent); - } - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) - { - double chanceToTrigger = 0; - int amountToDie = 0; - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; - amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); - } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; - } - - vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - BattleStackAttacked bsa; - bsa.attackerID = -1; - bsa.stackAttacked = defender->unitId(); - bsa.damageAmount = amountToDie * defender->getMaxHealth(); - bsa.flags = BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID::SLAYER; - defender->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured si; - si.stacks.push_back(bsa); - - sendAndApply(&si); - sendGenericKilledLog(defender, bsa.killedAmount, false); - } -} - void CGameHandler::visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h) { if (!t.visitableObjects.empty()) @@ -5841,8 +3755,8 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h for(int i = 0; i < slot.size(); ++i) { - ArtifactLocation al(hero, slot[i]); - const CArtifactInstance * a = al.getArt(); + ArtifactLocation al(hero->id, slot[i]); + const CArtifactInstance * a = hero->getArt(al.slot); if(!a) { @@ -5873,16 +3787,6 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h return true; } -void CGameHandler::makeStackDoNothing(const CStack * next) -{ - BattleAction doNothing; - doNothing.actionType = EActionType::NO_ACTION; - doNothing.side = next->unitSide(); - doNothing.stackNumber = next->unitId(); - - makeAutomaticAction(next, doNothing); -} - bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) { if (sl.army->hasStackAtSlot(sl.slot)) @@ -6061,468 +3965,46 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s } } -void CGameHandler::runBattle() +bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) { - boost::unique_lock lock(battleActionMutex); + assert(art && art->artType); + ArtifactLocation dst(al.artHolder, ArtifactPosition::PRE_FIRST); + dst.creature = al.creature; + auto putTo = getArtSet(al); + assert(putTo); - setBattle(gs->curB); - assert(gs->curB); - //TODO: pre-tactic stuff, call scripts etc. - - //Moat should be initialized here, because only here we can use spellcasting - if (gs->curB->town && gs->curB->town->fortLevel() >= CGTownInstance::CITADEL) + if(al.slot == ArtifactPosition::FIRST_AVAILABLE) { - const auto * h = gs->curB->battleGetFightingHero(BattleSide::DEFENDER); - const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(gs->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(gs->curB, &moatCaster, spells::Mode::PASSIVE, gs->curB->town->town->moatAbility.toSpell()); - auto target = spells::Target(); - cast.cast(spellEnv, target); + dst.slot = ArtifactUtils::getArtAnyPosition(putTo, art->getTypeId()); } - - //tactic round + else if(ArtifactUtils::isSlotBackpack(al.slot) && !al.creature.has_value()) { - while ((lobby->state != EServerState::SHUTDOWN) && gs->curB->tacticDistance && !battleResult.get()) - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - } - - //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = gs->curB->stacks; //use temporary variable to outclude summoned stacks added to gs->curB->stacks from processing - - for (CStack * stack : initialStacks) - { - if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) - { - std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); - std::vector targetHexes; - const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard - const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); - - /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. - For one-hex targets there are four guardians - front, back and one per side (up + down). - Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front - Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ - if (!guardianIsBig) - targetHexes = stack->getSurroundingHexes(); - else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); - - for(auto hex : targetHexes) - { - if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex - { - battle::UnitInfo info; - info.id = gs->curB->battleNextUnitId(); - info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); - info.type = creatureData; - info.side = stack->unitSide(); - info.position = hex; - info.summoned = true; - - BattleUnitsChanged pack; - pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(pack.changedStacks.back().data); - sendAndApply(&pack); - } - } - - // send empty event to client - // temporary(?) workaround to force animations to trigger - StacksInjured fakeEvent; - sendAndApply(&fakeEvent); - - } - - stackEnchantedTrigger(stack); - } - - //spells opening battle - for (int i = 0; i < 2; ++i) - { - auto h = gs->curB->battleGetFightingHero(i); - if (h) - { - TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); - - for (auto b : *bl) - { - spells::BonusCaster caster(h, b); - - const CSpell * spell = SpellID(b->subtype).toSpell(); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - parameters.setSpellLevel(3); - parameters.setEffectDuration(b->val); - parameters.massive = true; - parameters.castIfPossible(spellEnv, spells::Target()); - } - } - } - // it is possible that due to opening spells one side was eliminated -> check for end of battle - checkBattleStateChanges(); - - bool firstRound = true;//FIXME: why first round is -1? - - //main loop - while ((lobby->state != EServerState::SHUTDOWN) && !battleResult.get()) //till the end of the battle ;] - { - BattleNextRound bnr; - bnr.round = gs->curB->round + 1; - logGlobal->debug("Round %d", bnr.round); - sendAndApply(&bnr); - - auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it - for (auto &obstPtr : obstacles) - { - if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) - if (sco->turnsRemaining == 0) - removeObstacle(*obstPtr); - } - - const BattleInfo & curB = *gs->curB; - - for(auto stack : curB.stacks) - { - if(stack->alive() && !firstRound) - stackEnchantedTrigger(stack); - } - - //stack loop - - auto getNextStack = [this]() -> const CStack * - { - if(battleResult.get()) - return nullptr; - - std::vector q; - gs->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" - - if(!q.empty()) - { - if(!q.front().empty()) - { - auto next = q.front().front(); - const auto stack = dynamic_cast(next); - - // regeneration takes place before everything else but only during first turn attempt in each round - // also works under blind and similar effects - if(stack && stack->alive() && !stack->waiting) - { - BattleTriggerEffect bte; - bte.stackID = stack->unitId(); - bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); - - const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); - if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) - bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); - - if(bte.val) // anything to heal - sendAndApply(&bte); - } - - if(next->willMove()) - return stack; - } - } - - return nullptr; - }; - - const CStack * next = nullptr; - while((lobby->state != EServerState::SHUTDOWN) && (next = getNextStack())) - { - BattleUnitsChanged removeGhosts; - for(auto stack : curB.stacks) - { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } - - if(!removeGhosts.changedStacks.empty()) - sendAndApply(&removeGhosts); - - // check for bad morale => freeze - int nextStackMorale = next->moraleVal(); - if(!next->hadMorale && !next->waited() && nextStackMorale < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - //unit loses its turn - empty freeze action - BattleAction ba; - ba.actionType = EActionType::BAD_MORALE; - ba.side = next->unitSide(); - ba.stackNumber = next->unitId(); - - makeAutomaticAction(next, ba); - continue; - } - } - - if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk - { - logGlobal->trace("Handle Berserk effect"); - std::pair attackInfo = curB.getNearestStack(next); - if (attackInfo.first != nullptr) - { - BattleAction attack; - attack.actionType = EActionType::WALK_AND_ATTACK; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - attack.aimToHex(attackInfo.second); - attack.aimToUnit(attackInfo.first); - - makeAutomaticAction(next, attack); - logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); - } - else - { - makeStackDoNothing(next); - logGlobal->trace("No target found"); - } - continue; - } - - const CGHeroInstance * curOwner = battleGetOwnerHero(next); - const int stackCreatureId = next->unitType()->getId(); - - if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) - { - BattleAction attack; - attack.actionType = EActionType::SHOOT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - //TODO: select target by priority - - const battle::Unit * target = nullptr; - - for(auto & elem : gs->curB->stacks) - { - if(elem->unitType()->getId() != CreatureID::CATAPULT - && elem->unitOwner() != next->unitOwner() - && elem->isValidTarget() - && gs->curB->battleCanShoot(next, elem->getPosition())) - { - target = elem; - break; - } - } - - if(target == nullptr) - { - makeStackDoNothing(next); - } - else - { - attack.aimToUnit(target); - makeAutomaticAction(next, attack); - } - continue; - } - - if (next->unitType()->getId() == CreatureID::CATAPULT) - { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); - - if (attackableBattleHexes.empty()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) - { - BattleAction attack; - attack.actionType = EActionType::CATAPULT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - makeAutomaticAction(next, attack); - continue; - } - } - - if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) - { - TStacks possibleStacks = battleGetStacksIf([=](const CStack * s) - { - return s->unitOwner() == next->unitOwner() && s->canBeHealed(); - }); - - if (!possibleStacks.size()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) - { - RandomGeneratorUtil::randomShuffle(possibleStacks, getRandomGenerator()); - const CStack * toBeHealed = possibleStacks.front(); - - BattleAction heal; - heal.actionType = EActionType::STACK_HEAL; - heal.aimToUnit(toBeHealed); - heal.side = next->unitSide(); - heal.stackNumber = next->unitId(); - - makeAutomaticAction(next, heal); - continue; - } - } - - int numberOfAsks = 1; - bool breakOuter = false; - do - {//ask interface and wait for answer - if (!battleResult.get()) - { - stackTurnTrigger(next); //various effects - - if(next->fear) - { - makeStackDoNothing(next); //end immediately if stack was affected by fear - } - else - { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - sendAndApply(&sas); - - auto actionWasMade = [&]() -> bool - { - if (battleMadeAction.data)//active stack has made its action - return true; - if (battleResult.get())// battle is finished - return true; - if (next == nullptr)//active stack was been removed - return true; - return !next->alive();//active stack is dead - }; - - boost::unique_lock lock(battleMadeAction.mx); - battleMadeAction.data = false; - while ((lobby->state != EServerState::SHUTDOWN) && !actionWasMade()) - { - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - battleMadeAction.cond.wait(lock); - } - if (battleGetStackByID(nextId, false) != next) - next = nullptr; //it may be removed, while we wait - } - } - } - - if (battleResult.get()) //don't touch it, battle could be finished while waiting got action - { - breakOuter = true; - break; - } - //we're after action, all results applied - checkBattleStateChanges(); //check if this action ended the battle - - if(next != nullptr) - { - //check for good morale - nextStackMorale = next->moraleVal(); - if( !battleResult.get() - && !next->hadMorale - && !next->defending - && !next->waited() - && !next->fear - && next->alive() - && nextStackMorale > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - BattleTriggerEffect bte; - bte.stackID = next->unitId(); - bte.effect = vstd::to_underlying(BonusType::MORALE); - bte.val = 1; - bte.additionalInfo = 0; - sendAndApply(&bte); //play animation - - ++numberOfAsks; //move this stack once more - } - } - } - --numberOfAsks; - } while (numberOfAsks > 0); - - if (breakOuter) - { - break; - } - - } - firstRound = false; - } - - if (lobby->state != EServerState::SHUTDOWN) - endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1)); -} - -bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - BattleSetActiveStack bsa; - bsa.stack = stack->unitId(); - bsa.askPlayerInterface = false; - sendAndApply(&bsa); - - bool ret = makeBattleAction(ba); - checkBattleStateChanges(); - return ret; -} - -bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) -{ - assert(a->artType); - ArtifactLocation al(h, ArtifactPosition::PRE_FIRST); - - if(pos == ArtifactPosition::FIRST_AVAILABLE) - { - al.slot = ArtifactUtils::getArtAnyPosition(h, a->getTypeId()); - } - else if(ArtifactUtils::isSlotBackpack(pos)) - { - al.slot = ArtifactUtils::getArtBackpackPosition(h, a->getTypeId()); + dst.slot = ArtifactUtils::getArtBackpackPosition(putTo, art->getTypeId()); } else { - al.slot = pos; + dst.slot = al.slot; } - if(a->canBePutAt(al)) - putArtifact(al, a); + if(!askAssemble.has_value()) + { + if(!dst.creature.has_value() && ArtifactUtils::isSlotEquipment(dst.slot)) + askAssemble = true; + else + askAssemble = false; + } + + if(art->canBePutAt(putTo, dst.slot)) + { + PutArtifact pa(dst, askAssemble.value()); + pa.art = art; + sendAndApply(&pa); + return true; + } else + { return false; - - return true; -} - -void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) -{ - PutArtifact pa; - pa.art = a; - pa.al = al; - sendAndApply(&pa); + } } bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) @@ -6551,28 +4033,12 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact na.art = newArtInst; sendAndApply(&na); // -> updates newArtInst!!! - if(giveHeroArtifact(h, newArtInst, pos)) + if(putArtifact(ArtifactLocation(h->id, pos), newArtInst, false)) return true; else return false; } -void CGameHandler::setBattleResult(BattleResult::EResult resultType, int victoriusSide) -{ - boost::unique_lock guard(battleResult.mx); - if (battleResult.data) - { - complain((boost::format("The battle result has been already set (to %d, asked to %d)") - % battleResult.data->result % resultType).str()); - return; - } - auto br = new BattleResult(); - br->result = resultType; - br->winner = victoriusSide; //surrendering side loses - gs->curB->calculateCasualties(br->casualties); - battleResult.data = br; -} - void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { std::vector::iterator tile; @@ -6590,30 +4056,20 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { auto count = cre->getRandomAmount(std::rand); - createObject(*tile, Obj::MONSTER, creatureID); + createObject(*tile, PlayerColor::NEUTRAL, Obj::MONSTER, creatureID); auto monsterId = getTopObj(*tile)->id; - setObjProperty(monsterId, ObjProperty::MONSTER_COUNT, count); - setObjProperty(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_COUNT, count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); } tiles.erase(tile); //not use it again } } -void CGameHandler::removeObstacle(const CObstacleInstance & obstacle) -{ - BattleObstaclesChanged obsRem; - obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); - sendAndApply(&obsRem); -} - void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; - uahl.treasures = VLC->arth->treasures; - uahl.minors = VLC->arth->minors; - uahl.majors = VLC->arth->majors; - uahl.relics = VLC->arth->relics; + uahl.allocatedArtifacts = gs->allocatedArtifacts; sendAndApply(&uahl); } @@ -6627,12 +4083,12 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) if (!strcmp(typeid(*pack).name(), typeid(PlayerMessage).name())) return false; - auto query = queries.topQuery(player); + auto query = queries->topQuery(player); if (query && query->blocksPack(pack)) { complain(boost::str(boost::format( "\r\n| Player \"%s\" has to answer queries before attempting any further actions.\r\n| Top Query: \"%s\"\r\n") - % boost::to_upper_copy(player.getStr()) + % boost::to_upper_copy(player.toString()) % query->toString() )); return true; @@ -6644,7 +4100,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) void CGameHandler::removeAfterVisit(const CGObjectInstance *object) { //If the object is being visited, there must be a matching query - for (const auto &query : queries.allQueries()) + for (const auto &query : queries->allQueries()) { if (auto someVistQuery = std::dynamic_pointer_cast(query)) { @@ -6660,52 +4116,87 @@ void CGameHandler::removeAfterVisit(const CGObjectInstance *object) assert("This function needs to be called during the object visit!"); } -void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) { std::unordered_set tiles; - getTilesInRange(tiles, center, radius, player, hide? -1 : 1); - if (hide) + + if (mode == ETileVisibility::HIDDEN) { + getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player); + std::unordered_set observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems auto p = getPlayerState(player); for (auto h : p->heroes) { - getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), h->tempOwner, -1); + getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner); } for (auto t : p->towns) { - getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, -1); + getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner); } for (auto tile : observedTiles) vstd::erase_if_present (tiles, tile); } - changeFogOfWar(tiles, player, hide); + else + { + getTilesInRange(tiles, center, radius, ETileVisibility::HIDDEN, player); + } + changeFogOfWar(tiles, player, mode); } -void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) { FoWChange fow; fow.tiles = tiles; fow.player = player; - fow.mode = hide? 0 : 1; + fow.mode = mode; sendAndApply(&fow); } +const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj) +{ + assert(obj); + + for(const auto & query : queries->allQueries()) + { + auto visit = std::dynamic_pointer_cast(query); + if (visit && visit->visitedObject == obj) + return visit->visitingHero; + } + return nullptr; +} + bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { - if (auto topQuery = queries.topQuery(hero->getOwner())) + assert(obj); + assert(hero); + assert(getVisitingHero(obj) == hero); + // Check top query of targeted player: + // If top query is NOT visit to targeted object then we assume that + // 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)) return !(visit->visitedObject == obj && visit->visitingHero == hero); return true; } -void CGameHandler::setObjProperty(ObjectInstanceID objid, int prop, si64 val) +void CGameHandler::setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) { SetObjectProperty sob; sob.id = objid; sob.what = prop; - sob.val = static_cast(val); + sob.identifier = NumericID(value); + sendAndApply(&sob); +} + +void CGameHandler::setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) +{ + SetObjectProperty sob; + sob.id = objid; + sob.what = prop; + sob.identifier = identifier; sendAndApply(&sob); } @@ -6722,167 +4213,6 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): - army(battleSide.armyObject) -{ - heroWithDeadCommander = ObjectInstanceID(); - - PlayerColor color = battleSide.color; - - for(CStack * st : bat->stacks) - { - if(st->summoned) //don't take into account temporary summoned stacks - continue; - if(st->unitOwner() != color) //remove only our stacks - continue; - - logGlobal->debug("Calculating casualties for %s", st->nodeName()); - - st->health.takeResurrected(); - - if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) - { - logGlobal->debug("Ignored arrow towers stack."); - } - else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) - { - auto warMachine = st->unitType()->warMachine; - - if(warMachine == ArtifactID::NONE) - { - logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); - } - //catapult artifact remain even if "creature" killed in siege - else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) - { - logGlobal->debug("War machine has been destroyed"); - auto hero = dynamic_ptr_cast (army); - if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); - else - logGlobal->error("War machine in army without hero"); - } - } - else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) - { - if(st->alive() && st->getCount() > 0) - { - logGlobal->debug("Permanently summoned %d units.", st->getCount()); - const CreatureID summonedType = st->creatureId(); - summoned[summonedType] += st->getCount(); - } - } - else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - if (nullptr == st->base) - { - logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); - } - else - { - auto c = dynamic_cast (st->base); - if(c) - { - auto h = dynamic_cast (army); - if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) - { - logGlobal->debug("Commander is dead."); - heroWithDeadCommander = army->id; //TODO: unify commander handling - } - } - else - logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); - } - } - else if(st->base && !army->slotEmpty(st->unitSlot())) - { - logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); - if(st->getCount() == 0 || !st->alive()) - { - logGlobal->debug("Stack has been destroyed."); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); - } - else if(st->getCount() < army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - else if(st->getCount() > army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - } - else - { - logGlobal->warn("Unable to process stack: %s", st->nodeName()); - } - } -} - -void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) -{ - for (TStackAndItsNewCount &ncount : newStackCounts) - { - if (ncount.second > 0) - gh->changeStackCount(ncount.first, ncount.second, true); - else - gh->eraseStack(ncount.first, true); - } - for (auto summoned_iter : summoned) - { - SlotID slot = army->getSlotFor(summoned_iter.first); - if (slot.validSlot()) - { - StackLocation location(army, slot); - gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); - } - else - { - //even if it will be possible to summon anything permanently it should be checked for free slot - //necromancy is handled separately - gh->complain("No free slot to put summoned creature"); - } - } - for (auto al : removedWarMachines) - { - gh->removeArtifact(al); - } - if (heroWithDeadCommander != ObjectInstanceID()) - { - SetCommanderProperty scp; - scp.heroid = heroWithDeadCommander; - scp.which = SetCommanderProperty::ALIVE; - scp.amount = 0; - gh->sendAndApply(&scp); - } -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount) -{ - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; - winnerSide = result.winner; - remainingBattleQueriesCount = RemainingBattleQueriesCount; -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; - winnerSide = 0; - remainingBattleQueriesCount = 0; -} - CRandomGenerator & CGameHandler::getRandomGenerator() { return CRandomGenerator::getDefault(); @@ -6894,25 +4224,33 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const return serverScripts.get(); } -scripting::Pool * CGameHandler::getContextPool() const -{ - return serverScripts.get(); -} +//scripting::Pool * CGameHandler::getContextPool() const +//{ +// return serverScripts.get(); +//} #endif -void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_t subtype) +void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) { NewObject no; no.ID = type; - no.subID= subtype; + no.subID = subtype; + no.initiator = initiator; no.targetPos = visitablePosition; sendAndApply(&no); } -void CGameHandler::deserializationFix() +void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) { - //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization - // restore any places that requires such pointer manually - heroPool->gameHandler = this; - playerMessages->gameHandler = this; + battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +{ + battles->startBattleI(army1, army2, tile, creatureBank); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank ) +{ + battles->startBattleI(army1, army2, creatureBank); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 49a6dfcd6..9d15d33a8 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -1,399 +1,298 @@ -/* - * CGameHandler.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/FunctionList.h" -#include "../lib/IGameCallback.h" -#include "../lib/battle/CBattleInfoCallback.h" -#include "../lib/battle/BattleAction.h" -#include "../lib/ScriptHandler.h" -#include "CQuery.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGameState; -struct StartInfo; -struct BattleResult; -struct SideInBattle; -struct BattleAttack; -struct BattleStackAttacked; -struct CPack; -struct Query; -struct SetResources; -struct NewStructures; -class CGHeroInstance; -class IMarket; - -class SpellCastEnvironment; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class PoolImpl; -} -#endif - - -template class CApplier; - -VCMI_LIB_NAMESPACE_END - -class HeroPoolProcessor; -class CGameHandler; -class CVCMIServer; -class CBaseForGHApply; -class PlayerMessageProcessor; - -struct PlayerStatus -{ - bool makingTurn; - - PlayerStatus():makingTurn(false){}; - template void serialize(Handler &h, const int version) - { - h & makingTurn; - } -}; -class PlayerStatuses -{ -public: - std::map players; - boost::mutex mx; - boost::condition_variable cv; //notifies when any changes are made - - void addPlayer(PlayerColor player); - PlayerStatus operator[](PlayerColor player); - bool checkFlag(PlayerColor player, bool PlayerStatus::*flag); - void setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val); - template void serialize(Handler &h, const int version) - { - h & players; - } -}; - -struct CasualtiesAfterBattle -{ - using TStackAndItsNewCount = std::pair; - using TSummoned = std::map; - enum {ERASE = -1}; - const CArmedInstance * army; - std::vector newStackCounts; - std::vector removedWarMachines; - TSummoned summoned; - ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - - CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); - void updateArmy(CGameHandler *gh); -}; - -class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment -{ - CVCMIServer * lobby; - std::shared_ptr> applier; - std::unique_ptr battleThread; - -public: - boost::recursive_mutex battleActionMutex; - - std::unique_ptr heroPool; - - using FireShieldInfo = std::vector>; - //use enums as parameters, because doMove(sth, true, false, true) is not readable - enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; - enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; - enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; - - std::unique_ptr playerMessages; - - std::map>> connections; //player color -> connection to client with interface of that player - PlayerStatuses states; //player color -> player state - - //queries stuff - boost::recursive_mutex gsm; - ui32 QID; - Queries queries; - - SpellCastEnvironment * spellEnv; - - const Services * services() const override; - const BattleCb * battle() const override; - const GameCb * game() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - CVCMIServer * gameLobby() const; - - bool isValidObject(const CGObjectInstance *obj) const; - bool isBlockedByQueries(const CPack *pack, PlayerColor player); - bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); - void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); - int moveStack(int stack, BattleHex dest); //returned value - travelled distance - void runBattle(); - - ////used only in endBattle - don't touch elsewhere - bool visitObjectAfterVictory; - // - void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle - void endBattleConfirm(const BattleInfo * battleInfo); - - void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - - // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); - - void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); - void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); - - void checkBattleStateChanges(); - void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - void setBattleResult(BattleResult::EResult resultType, int victoriusSide); - - CGameHandler() = default; - CGameHandler(CVCMIServer * lobby); - ~CGameHandler(); - - ////////////////////////////////////////////////////////////////////////// - //from IGameCallback - //do sth - void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; - bool removeObject(const CGObjectInstance * obj) override; - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override; - void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::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 showTeleportDialog(TeleportDialog *iw) override; - void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override; - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override; - void giveResource(PlayerColor player, GameResID which, int val) override; - void giveResources(PlayerColor player, TResources resources) override; - - void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override; - void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) override; - bool changeStackType(const StackLocation &sl, const CCreature *c) override; - bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override; - bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override; - bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override; - bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override; - bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override; - void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override; - bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count = -1) override; - - void removeAfterVisit(const CGObjectInstance *object) override; - - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; - void removeArtifact(const ArtifactLocation &al) override; - bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; - bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap); - bool eraseArtifactByClient(const ArtifactLocation & al); - void synchronizeArtifactHandlerLists(); - - 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 - bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; - void giveHeroBonus(GiveBonus * bonus) override; - void setMovePoints(SetMovePoints * smp) override; - void setManaPoints(ObjectInstanceID hid, int val) override; - void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override; - void changeObjPos(ObjectInstanceID objid, int3 newPos) override; - void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; - - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override; - - void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; - - bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; - void showInfoDialog(InfoWindow * iw) override; - void showInfoDialog(const std::string & msg, PlayerColor player) override; - - ////////////////////////////////////////////////////////////////////////// - void useScholarSkill(ObjectInstanceID hero1, ObjectInstanceID hero2); - void setPortalDwelling(const CGTownInstance * town, bool forced, bool clear); - 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 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 - void levelUpCommander (const CCommanderInstance * c); - - void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero - ////////////////////////////////////////////////////////////////////////// - - void init(StartInfo *si); - void handleClientDisconnection(std::shared_ptr c); - void handleReceivedPack(CPackForServer * pack); - PlayerColor getPlayerAt(std::shared_ptr c) const; - bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; - - void updateGateState(); - bool makeBattleAction(BattleAction &ba); - bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) - bool makeCustomAction(BattleAction &ba); - void stackEnchantedTrigger(const CStack * stack); - void stackTurnTrigger(const CStack *stack); - - void removeObstacle(const CObstacleInstance &obstacle); - bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); - bool buildBoat( ObjectInstanceID objid, PlayerColor player ); - bool setFormation( ObjectInstanceID hid, ui8 formation ); - bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); - bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); - bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); - bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); - bool transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot); - bool assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo); - bool buyArtifact( ObjectInstanceID hid, ArtifactID aid ); //for blacksmith and mage guild only -> buying for gold in common buildings - bool buyArtifact( const IMarket *m, const CGHeroInstance *h, GameResID rid, ArtifactID aid); //for artifact merchant and black market -> buying for any resource in special building / advobject - bool sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, GameResID rid); //for artifact merchant selling - //void lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts); //after battle - move al arts to winer - bool buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill); - bool garrisonSwap(ObjectInstanceID tid); - bool swapGarrisonOnSiege(ObjectInstanceID tid) override; - bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ); - bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level); - bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings - bool razeStructure(ObjectInstanceID tid, BuildingID bid); - 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); - bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany); - bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner); - bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner); - void save(const std::string &fname); - bool load(const std::string &fname); - - 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 engageIntoBattle( PlayerColor player ); - bool dig(const CGHeroInstance *h); - void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); - - template void serialize(Handler &h, const int version) - { - h & QID; - h & states; - h & finishingBattle; - h & heroPool; - h & getRandomGenerator(); - h & playerMessages; - - if (!h.saving) - deserializationFix(); - -#if SCRIPTING_ENABLED - JsonNode scriptsState; - if(h.saving) - serverScripts->serializeState(h.saving, scriptsState); - h & scriptsState; - if(!h.saving) - serverScripts->serializeState(h.saving, scriptsState); -#endif - } - - void sendToAllClients(CPackForClient * pack); - void sendAndApply(CPackForClient * pack) override; - void applyAndSend(CPackForClient * pack); - void sendAndApply(CGarrisonOperationPack * pack); - void sendAndApply(SetResources * pack); - void sendAndApply(NewStructures * pack); - - void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); - void throwNotAllowedAction(CPackForServer * pack); - void throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id); - void throwOnWrongPlayer(CPackForServer * pack, PlayerColor player); - void throwAndComplain(CPackForServer * pack, std::string txt); - bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - - struct FinishingBattleHelper - { - FinishingBattleHelper(); - FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); - - inline bool isDraw() const {return winnerSide == 2;} - - const CGHeroInstance *winnerHero, *loserHero; - PlayerColor victor, loser; - ui8 winnerSide; - - int remainingBattleQueriesCount; - - template void serialize(Handler &h, const int version) - { - h & winnerHero; - h & loserHero; - h & victor; - h & loser; - h & winnerSide; - h & remainingBattleQueriesCount; - } - }; - - std::unique_ptr finishingBattle; - - void battleAfterLevelUp(const BattleResult &result); - - void run(bool resume); - void newTurn(); - void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); - bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); - void spawnWanderingMonsters(CreatureID creatureID); - - // Check for victory and loss conditions - void checkVictoryLossConditionsForPlayer(PlayerColor player); - void checkVictoryLossConditions(const std::set & playerColors); - void checkVictoryLossConditionsForAll(); - - CRandomGenerator & getRandomGenerator(); - -#if SCRIPTING_ENABLED - scripting::Pool * getGlobalContextPool() const override; - scripting::Pool * getContextPool() const override; -#endif - - std::list generatePlayerTurnOrder() const; - - friend class CVCMIServer; -private: - std::unique_ptr serverEventBus; -#if SCRIPTING_ENABLED - std::shared_ptr serverScripts; -#endif - - void reinitScripting(); - void deserializationFix(); - - - void makeStackDoNothing(const CStack * next); - void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; - - const std::string complainNoCreatures; - const std::string complainNotEnoughCreatures; - const std::string complainInvalidSlot; -}; - -class ExceptionNotAllowedAction : public std::exception -{ - -}; +/* + * CGameHandler.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/IGameCallback.h" +#include "../lib/LoadProgress.h" +#include "../lib/ScriptHandler.h" +#include "TurnTimerHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct SideInBattle; +class IMarket; +class SpellCastEnvironment; +class CConnection; +class CCommanderInstance; +class EVictoryLossCheckResult; + +struct CPack; +struct CPackForServer; +struct NewTurn; +struct CGarrisonOperationPack; +struct SetResources; +struct NewStructures; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class PoolImpl; +} +#endif + +template class CApplier; + +VCMI_LIB_NAMESPACE_END + +class HeroPoolProcessor; +class CVCMIServer; +class CBaseForGHApply; +class PlayerMessageProcessor; +class BattleProcessor; +class TurnOrderProcessor; +class QueriesProcessor; +class CObjectVisitQuery; + +class CGameHandler : public IGameCallback, public Environment +{ + CVCMIServer * lobby; + std::shared_ptr> applier; + +public: + std::unique_ptr heroPool; + std::unique_ptr battles; + std::unique_ptr queries; + std::unique_ptr turnOrder; + + //use enums as parameters, because doMove(sth, true, false, true) is not readable + enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; + enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; + enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; + + std::unique_ptr playerMessages; + + std::map>> connections; //player color -> connection to client with interface of that player + + //queries stuff + boost::recursive_mutex gsm; + ui32 QID; + + SpellCastEnvironment * spellEnv; + + TurnTimerHandler turnTimerHandler; + + const Services * services() const override; + const BattleCb * battle(const BattleID & battleID) const override; + const GameCb * game() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + CVCMIServer * gameLobby() const; + + bool isValidObject(const CGObjectInstance *obj) const; + bool isBlockedByQueries(const CPack *pack, PlayerColor player); + bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); + void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); + + CGameHandler(); + CGameHandler(CVCMIServer * lobby); + ~CGameHandler(); + + ////////////////////////////////////////////////////////////////////////// + //from IGameCallback + //do sth + void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) 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 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 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; + void giveResource(PlayerColor player, GameResID which, int val) override; + void giveResources(PlayerColor player, TResources resources) override; + + void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override; + void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) override; + bool changeStackType(const StackLocation &sl, const CCreature *c) override; + bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override; + bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override; + bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override; + bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override; + bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override; + void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override; + bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count = -1) override; + + 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; + void removeArtifact(const ArtifactLocation &al) override; + bool moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) override; + bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); + bool eraseArtifactByClient(const ArtifactLocation & al); + void synchronizeArtifactHandlerLists(); + + 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 + bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; + void giveHeroBonus(GiveBonus * bonus) override; + void setMovePoints(SetMovePoints * smp) override; + 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 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 castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; + + /// Returns hero that is currently visiting this object, or nullptr if no visit is active + const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); + 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 showInfoDialog(InfoWindow * iw) override; + void showInfoDialog(const std::string & msg, PlayerColor player) override; + + ////////////////////////////////////////////////////////////////////////// + void useScholarSkill(ObjectInstanceID hero1, ObjectInstanceID hero2); + void setPortalDwelling(const CGTownInstance * town, bool forced, bool clear); + 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 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 + void levelUpCommander (const CCommanderInstance * c); + + void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero + ////////////////////////////////////////////////////////////////////////// + + void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); + void handleClientDisconnection(std::shared_ptr c); + void handleReceivedPack(CPackForServer * pack); + bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; + bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; + + bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); + bool buildBoat( ObjectInstanceID objid, PlayerColor player ); + bool setFormation( ObjectInstanceID hid, EArmyFormation formation ); + bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy); + bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); + bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); + bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); + bool transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot); + bool assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo); + bool buyArtifact( ObjectInstanceID hid, ArtifactID aid ); //for blacksmith and mage guild only -> buying for gold in common buildings + bool buyArtifact( const IMarket *m, const CGHeroInstance *h, GameResID rid, ArtifactID aid); //for artifact merchant and black market -> buying for any resource in special building / advobject + bool sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, GameResID rid); //for artifact merchant selling + //void lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts); //after battle - move al arts to winer + bool buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill); + bool garrisonSwap(ObjectInstanceID tid); + bool swapGarrisonOnSiege(ObjectInstanceID tid) override; + 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 razeStructure(ObjectInstanceID tid, BuildingID bid); + 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); + bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany); + bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner); + bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner); + void save(const std::string &fname); + bool load(const std::string &fname); + + void onPlayerTurnStarted(PlayerColor which); + void onPlayerTurnEnded(PlayerColor which); + void onNewTurn(); + + 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); + bool dig(const CGHeroInstance *h); + void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); + + template void serialize(Handler &h, const int version) + { + h & QID; + h & getRandomGenerator(); + h & *battles; + h & *heroPool; + h & *playerMessages; + h & *turnOrder; + +#if SCRIPTING_ENABLED + JsonNode scriptsState; + if(h.saving) + serverScripts->serializeState(h.saving, scriptsState); + h & scriptsState; + if(!h.saving) + serverScripts->serializeState(h.saving, scriptsState); +#endif + } + + 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 + void throwNotAllowedAction(CPackForServer * pack); + /// Throws if player stated in pack is not making turn right now + void throwIfPlayerNotActive(CPackForServer * pack); + /// Throws if object is not owned by pack sender + void throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id); + /// Throws if player is not present on connection of this pack + void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player); + void throwIfWrongPlayer(CPackForServer * pack); + void throwAndComplain(CPackForServer * pack, std::string txt); + + bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); + + void run(bool resume); + bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); + void spawnWanderingMonsters(CreatureID creatureID); + + // Check for victory and loss conditions + void checkVictoryLossConditionsForPlayer(PlayerColor player); + void checkVictoryLossConditions(const std::set & playerColors); + void checkVictoryLossConditionsForAll(); + + CRandomGenerator & getRandomGenerator(); + +#if SCRIPTING_ENABLED + scripting::Pool * getGlobalContextPool() const override; +// scripting::Pool * getContextPool() const override; +#endif + + friend class CVCMIServer; +private: + std::unique_ptr serverEventBus; +#if SCRIPTING_ENABLED + std::shared_ptr serverScripts; +#endif + + void reinitScripting(); + + void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; + + const std::string complainNoCreatures; + const std::string complainNotEnoughCreatures; + const std::string complainInvalidSlot; +}; + +class ExceptionNotAllowedAction : public std::exception +{ + +}; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4239d07ba..0263fef96 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,27 +1,51 @@ set(server_SRCS StdInc.cpp + battles/BattleActionProcessor.cpp + battles/BattleFlowProcessor.cpp + battles/BattleProcessor.cpp + battles/BattleResultProcessor.cpp + + queries/BattleQueries.cpp + queries/CQuery.cpp + queries/MapQueries.cpp + queries/QueriesProcessor.cpp + + processors/HeroPoolProcessor.cpp + processors/PlayerMessageProcessor.cpp + processors/TurnOrderProcessor.cpp + CGameHandler.cpp - HeroPoolProcessor.cpp - PlayerMessageProcessor.cpp ServerSpellCastEnvironment.cpp - CQuery.cpp CVCMIServer.cpp NetPacksServer.cpp NetPacksLobbyServer.cpp + TurnTimerHandler.cpp ) set(server_HEADERS StdInc.h + battles/BattleActionProcessor.h + battles/BattleFlowProcessor.h + battles/BattleProcessor.h + battles/BattleResultProcessor.h + + queries/BattleQueries.h + queries/CQuery.h + queries/MapQueries.h + queries/QueriesProcessor.h + + processors/HeroPoolProcessor.h + processors/PlayerMessageProcessor.h + processors/TurnOrderProcessor.h + CGameHandler.h - HeroPoolProcessor.h - PlayerMessageProcessor.h ServerSpellCastEnvironment.h - CQuery.h CVCMIServer.h LobbyNetPackVisitors.h ServerNetPackVisitors.h + TurnTimerHandler.h ) assign_source_group(${server_SRCS} ${server_HEADERS}) diff --git a/server/CQuery.cpp b/server/CQuery.cpp deleted file mode 100644 index 1d0a7d7b3..000000000 --- a/server/CQuery.cpp +++ /dev/null @@ -1,584 +0,0 @@ -/* - * CQuery.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 "CQuery.h" -#include "CGameHandler.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/mapObjects/MiscObjects.h" -#include "../lib/serializer/Cast.h" - -boost::mutex Queries::mx; - -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(); -} - -std::ostream & operator<<(std::ostream & out, QueryPtr query) -{ - return out << "[" << query.get() << "] " << query->toString(); -} - -CQuery::CQuery(Queries * Owner): - owner(Owner) -{ - boost::unique_lock l(Queries::mx); - - static QueryID QID = QueryID(0); - - queryID = ++QID; - logGlobal->trace("Created a new query with id %d", queryID); -} - - -CQuery::~CQuery() -{ - logGlobal->trace("Destructed the query with id %d", queryID); -} - -void CQuery::addPlayer(PlayerColor color) -{ - if(color.isValidPlayer()) - players.push_back(color); -} - -std::string CQuery::toString() const -{ - const auto size = players.size(); - const std::string plural = size > 1 ? "s" : ""; - std::string names; - - for(size_t i = 0; i < size; i++) - { - names += boost::to_upper_copy(players[i].getStr()); - - if(i < size - 2) - names += ", "; - else if(size > 1 && i == size - 2) - names += " and "; - } - std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") - % typeid(*this).name() - % queryID - % plural - % names - ); - return ret; -} - -bool CQuery::endsByPlayerAnswer() const -{ - return false; -} - -void CQuery::onRemoval(PlayerColor color) -{ - -} - -bool CQuery::blocksPack(const CPack * pack) const -{ - return false; -} - -void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - -} - -void CQuery::onExposure(QueryPtr topQuery) -{ - logGlobal->trace("Exposed query with id %d", queryID); - owner->popQuery(*this); -} - -void CQuery::onAdding(PlayerColor color) -{ - -} - -void CQuery::onAdded(PlayerColor color) -{ - -} - -void CQuery::setReply(const JsonNode & reply) -{ - -} - -bool CQuery::blockAllButReply(const CPack * pack) const -{ - //We accept only query replies from correct player - if(auto reply = dynamic_ptr_cast(pack)) - return !vstd::contains(players, reply->player); - - return true; -} - -CGhQuery::CGhQuery(CGameHandler * owner): - CQuery(&owner->queries), gh(owner) -{ - -} - -CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): - CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) -{ - 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); -} - -void CObjectVisitQuery::onExposure(QueryPtr topQuery) -{ - //Object may have been removed and deleted. - if(gh->isValidObject(visitedObject)) - topQuery->notifyObjectAboutRemoval(*this); - - owner->popIfTop(*this); -} - -void Queries::popQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); - if(topQuery(player) != query) - { - logGlobal->trace("Cannot remove, not a top!"); - return; - } - - queries[player] -= query; - auto nextQuery = topQuery(player); - - query->onRemoval(player); - - //Exposure on query below happens only if removal didn't trigger any new query - if(nextQuery && nextQuery == topQuery(player)) - nextQuery->onExposure(query); -} - -void Queries::popQuery(const CQuery &query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); - - assert(query.players.size()); - for(auto player : query.players) - { - auto top = topQuery(player); - if(top.get() == &query) - popQuery(top); - else - { - logGlobal->trace("Cannot remove query %s", query.toString()); - logGlobal->trace("Queries found:"); - for(auto q : queries[player]) - { - logGlobal->trace(q->toString()); - } - } - } -} - -void Queries::popQuery(QueryPtr query) -{ - for(auto player : query->players) - popQuery(player, query); -} - -void Queries::addQuery(QueryPtr query) -{ - for(auto player : query->players) - addQuery(player, query); - - for(auto player : query->players) - query->onAdded(player); -} - -void Queries::addQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); - query->onAdding(player); - queries[player].push_back(query); -} - -QueryPtr Queries::topQuery(PlayerColor player) -{ - return vstd::backOrNull(queries[player]); -} - -void Queries::popIfTop(QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); - if(!query) - logGlobal->error("The query is nullptr! Ignoring."); - - popIfTop(*query); -} - -void Queries::popIfTop(const CQuery & query) -{ - for(PlayerColor color : query.players) - if(topQuery(color).get() == &query) - popQuery(color, topQuery(color)); -} - -std::vector> Queries::allQueries() const -{ - std::vector> ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -std::vector Queries::allQueries() -{ - //TODO code duplication with const function :( - std::vector ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -QueryPtr Queries::getQuery(QueryID queryID) -{ - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - if(query->queryID == queryID) - return query; - return nullptr; -} - -void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - if(result) - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): - CGhQuery(owner) -{ - belligerents[0] = Bi->sides[0].armyObject; - belligerents[1] = Bi->sides[1].armyObject; - - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner): - CGhQuery(owner), bi(nullptr) -{ - belligerents[0] = belligerents[1] = nullptr; -} - -bool CBattleQuery::blocksPack(const CPack * pack) const -{ - const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name()); -} - -void CBattleQuery::onRemoval(PlayerColor color) -{ - if(result) - gh->battleAfterLevelUp(*result); -} - -void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); -} - -CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): - CDialogQuery(owner) -{ - exchangingArmies[0] = up; - exchangingArmies[1] = down; - - addPlayer(up->tempOwner); - addPlayer(down->tempOwner); -} - -bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const -{ - std::set ourIds; - ourIds.insert(this->exchangingArmies[0]->id); - ourIds.insert(this->exchangingArmies[1]->id); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); - - if(auto arts = dynamic_ptr_cast(pack)) - { - if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) - if(!vstd::contains(ourIds, *id1)) - return true; - - if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) - if(!vstd::contains(ourIds, *id2)) - return true; - return false; - } - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->id); - - if(auto arts = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); - - if(auto art = dynamic_ptr_cast(pack)) - { - if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) - return !vstd::contains(ourIds, *id); - } - - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->heroID); - - if(auto upgrade = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, upgrade->id); - - if(auto formation = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, formation->hid); - - return CDialogQuery::blocksPack(pack); -} - -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): - CDialogQuery(owner) -{ - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -void CBattleDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - if(*answer == 1) - { - gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); - } - else - { - gh->endBattleConfirm(bi); - } -} - -void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - assert(answer); - objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); -} - -CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): - CDialogQuery(owner) -{ - this->bd = bd; - addPlayer(bd.player); -} - -void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - // do not change to dynamic_ptr_cast - SIGSEGV! - auto obj = dynamic_cast(objectVisit.visitedObject); - if(obj) - obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); - else - logGlobal->error("Invalid instance in teleport query"); -} - -CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): - CDialogQuery(owner) -{ - this->td = td; - addPlayer(td.player); -} - -CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - hlu = Hlu; - addPlayer(hero->tempOwner); -} - -void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); - gh->levelUpHero(hero, hlu.skills[*answer]); -} - -void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - -CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - clu = Clu; - addPlayer(hero->tempOwner); -} - -void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); - gh->levelUpCommander(hero->commander, clu.skills[*answer]); -} - -void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - -CDialogQuery::CDialogQuery(CGameHandler * owner): - CGhQuery(owner) -{ - -} - -bool CDialogQuery::endsByPlayerAnswer() const -{ - return true; -} - -bool CDialogQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -void CDialogQuery::setReply(const JsonNode & reply) -{ - if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) - answer = reply.Integer(); -} - -CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): - CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) -{ - players.push_back(hero->tempOwner); -} - -void CHeroMovementQuery::onExposure(QueryPtr topQuery) -{ - assert(players.size() == 1); - - if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard - //TODO what if there were H4-like escape? we should also check pos - { - logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); - //finish movement - visitDestAfterVictory = false; - gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); - } - - owner->popIfTop(*this); -} - -void CHeroMovementQuery::onRemoval(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; - gh->sendAndApply(&pb); -} - -void CHeroMovementQuery::onAdding(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - gh->sendAndApply(&pb); -} - -CGenericQuery::CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback): - CQuery(Owner), callback(Callback) -{ - addPlayer(color); -} - -bool CGenericQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -bool CGenericQuery::endsByPlayerAnswer() const -{ - return true; -} - -void CGenericQuery::onExposure(QueryPtr topQuery) -{ - //do nothing -} - -void CGenericQuery::setReply(const JsonNode & reply) -{ - this->reply = reply; -} - -void CGenericQuery::onRemoval(PlayerColor color) -{ - callback(reply); -} diff --git a/server/CQuery.h b/server/CQuery.h deleted file mode 100644 index 089e1c256..000000000 --- a/server/CQuery.h +++ /dev/null @@ -1,242 +0,0 @@ -/* - * CQuery.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/GameConstants.h" -#include "../lib/int3.h" -#include "../lib/NetPacks.h" -#include "JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CArmedInstance; - -VCMI_LIB_NAMESPACE_END - -class CGameHandler; -class CObjectVisitQuery; -class CQuery; -class Queries; - -using QueryPtr = std::shared_ptr; - -// This class represents any kind of prolonged interaction that may need to do something special after it is over. -// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. -// Examples: -// - all kinds of blocking dialog windows -// - battle -// - object visit -// - hero movement -// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. -class CQuery -{ -public: - std::vector players; //players that are affected (often "blocked") by query - QueryID queryID; - - CQuery(Queries * Owner); - - - virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. - - 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; - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; - - virtual void setReply(const JsonNode & reply); - - virtual ~CQuery(); -protected: - Queries * owner; - void addPlayer(PlayerColor color); - bool blockAllButReply(const CPack * pack) const; -}; - -std::ostream &operator<<(std::ostream &out, const CQuery &query); -std::ostream &operator<<(std::ostream &out, QueryPtr query); - -class CGhQuery : public CQuery -{ -public: - CGhQuery(CGameHandler * owner); -protected: - CGameHandler * gh; -}; - -//Created when hero visits object. -//Removed when query above is resolved (or immediately after visit if no queries were created) -class CObjectVisitQuery : public CGhQuery -{ -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); - - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; -}; - -class CBattleQuery : public CGhQuery -{ -public: - std::array belligerents; - std::array initialHeroMana; - - const BattleInfo *bi; - std::optional result; - - CBattleQuery(CGameHandler * owner); - CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; -}; - -//Created when hero attempts move and something happens -//(not necessarily position change, could be just an object interaction). -class CHeroMovementQuery : public CGhQuery -{ -public: - TryMoveHero tmh; - bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated - const CGHeroInstance *hero; - - virtual void onExposure(QueryPtr topQuery) override; - - CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); - virtual void onAdding(PlayerColor color) override; - virtual void onRemoval(PlayerColor color) override; -}; - -class CDialogQuery : public CGhQuery -{ -public: - CDialogQuery(CGameHandler * owner); - virtual bool endsByPlayerAnswer() const override; - virtual bool blocksPack(const CPack *pack) const override; - void setReply(const JsonNode & reply) override; -protected: - std::optional answer; -}; - -class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs -{ -public: - std::array exchangingArmies; - - CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; -}; - -class CBattleDialogQuery : public CDialogQuery -{ -public: - CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); - - const BattleInfo * bi; - - virtual void onRemoval(PlayerColor color) override; -}; - -//yes/no and component selection dialogs -class CBlockingDialogQuery : public CDialogQuery -{ -public: - BlockingDialog bd; //copy of pack... debug purposes - - CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CTeleportDialogQuery : public CDialogQuery -{ -public: - TeleportDialog td; //copy of pack... debug purposes - - CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CHeroLevelUpDialogQuery : public CDialogQuery -{ -public: - CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - HeroLevelUp hlu; - const CGHeroInstance * hero; -}; - -class CCommanderLevelUpDialogQuery : public CDialogQuery -{ -public: - CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - CommanderLevelUp clu; - const CGHeroInstance * hero; -}; - -class CGenericQuery : public CQuery -{ -public: - CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback); - - bool blocksPack(const CPack * pack) const override; - bool endsByPlayerAnswer() const override; - void onExposure(QueryPtr topQuery) override; - void setReply(const JsonNode & reply) override; - void onRemoval(PlayerColor color) override; -private: - std::function callback; - JsonNode reply; -}; - -class Queries -{ -private: - void addQuery(PlayerColor player, QueryPtr query); - void popQuery(PlayerColor player, QueryPtr query); - - std::map> queries; //player => stack of queries - -public: - static boost::mutex mx; - - void addQuery(QueryPtr query); - void popQuery(const CQuery &query); - void popQuery(QueryPtr query); - void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) - void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) - - QueryPtr topQuery(PlayerColor player); - - std::vector> allQueries() const; - std::vector allQueries(); - QueryPtr getQuery(QueryID queryID); - //void removeQuery -}; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 234ee09ab..f7d894f33 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -14,7 +14,6 @@ #include "../lib/campaign/CampaignState.h" #include "../lib/CThreadHelper.h" #include "../lib/serializer/Connection.h" -#include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" @@ -27,19 +26,16 @@ #include "../lib/StartInfo.h" #include "../lib/mapping/CMapHeader.h" #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/NetPackVisitor.h" #include "LobbyNetPackVisitors.h" #ifdef VCMI_ANDROID #include #include #include "lib/CAndroidVMHelper.h" -#elif !defined(VCMI_IOS) -#include "../lib/Interprocess.h" #endif #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "CGameHandler.h" -#include "PlayerMessageProcessor.h" +#include "processors/PlayerMessageProcessor.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/GameConstants.h" #include "../lib/logging/CBasicLogConfigurator.h" @@ -51,7 +47,7 @@ #include "../lib/UnlockGuard.h" // for applier -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" // UUID generation #include @@ -142,9 +138,9 @@ CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) catch(...) { logNetwork->info("Port %d is busy, trying to use random port instead", port); - if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm")) + if(cmdLineOptions.count("run-by-client")) { - logNetwork->error("Cant pass port number to client without shared memory!", port); + logNetwork->error("Port must be specified when run-by-client is used!!"); exit(0); } acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); @@ -161,22 +157,21 @@ CVCMIServer::~CVCMIServer() announceLobbyThread->join(); } +void CVCMIServer::setState(EServerState value) +{ + state.store(value); +} + +EServerState CVCMIServer::getState() const +{ + return state.load(); +} + void CVCMIServer::run() { if(!restartGameplay) { this->announceLobbyThread = std::make_unique(&CVCMIServer::threadAnnounceLobby, this); -#if !defined(VCMI_MOBILE) - if(cmdLineOptions.count("enable-shm")) - { - std::string sharedMemoryName = "vcmi_memory"; - if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid")) - { - sharedMemoryName += "_" + cmdLineOptions["uuid"].as(); - } - shm = std::make_shared(sharedMemoryName); - } -#endif startAsyncAccept(); if(!remoteConnectionsThread && cmdLineOptions.count("lobby")) @@ -189,16 +184,11 @@ void CVCMIServer::run() CAndroidVMHelper vmHelper; vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady"); #endif -#elif !defined(VCMI_IOS) - if(shm) - { - shm->sr->setToReadyAndNotify(port); - } #endif } while(state == EServerState::LOBBY || state == EServerState::GAMEPLAY_STARTING) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); logNetwork->info("Thread handling connections ended"); @@ -207,32 +197,33 @@ void CVCMIServer::run() gh->run(si->mode == StartInfo::LOAD_GAME); } while(state == EServerState::GAMEPLAY_ENDED) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } void CVCMIServer::establishRemoteConnections() { + setThreadName("establishConnection"); + //wait for host connection while(connections.empty()) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); uuid = cmdLineOptions["lobby-uuid"].as(); int numOfConnections = cmdLineOptions["connections"].as(); - auto address = cmdLineOptions["lobby"].as(); - int port = cmdLineOptions["lobby-port"].as(); - logGlobal->info("Server is connecting to remote at %s:%d with uuid %s %d times", address, port, uuid, numOfConnections); - for(int i = 0; i < numOfConnections; ++i) - connectToRemote(address, port); + connectToRemote(); } -void CVCMIServer::connectToRemote(const std::string & addr, int port) +void CVCMIServer::connectToRemote() { std::shared_ptr c; try { - logNetwork->info("Establishing connection..."); - c = std::make_shared(addr, port, SERVER_NAME, uuid); + auto address = cmdLineOptions["lobby"].as(); + int port = cmdLineOptions["lobby-port"].as(); + + logNetwork->info("Establishing connection to remote at %s:%d with uuid %s", address, port, uuid); + c = std::make_shared(address, port, SERVER_NAME, uuid); } catch(...) { @@ -242,12 +233,14 @@ void CVCMIServer::connectToRemote(const std::string & addr, int port) if(c) { connections.insert(c); + remoteConnections.insert(c); c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); } } void CVCMIServer::threadAnnounceLobby() { + setThreadName("announceLobby"); while(state != EServerState::SHUTDOWN) { { @@ -265,7 +258,7 @@ void CVCMIServer::threadAnnounceLobby() } } - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } } @@ -284,7 +277,7 @@ void CVCMIServer::prepareToRestart() campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); } // FIXME: dirry hack to make sure old CGameHandler::run is finished - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } for(auto c : connections) @@ -298,25 +291,53 @@ void CVCMIServer::prepareToRestart() bool CVCMIServer::prepareToStartGame() { + Load::ProgressAccumulator progressTracking; + Load::Progress current(1); + progressTracking.include(current); + Load::Type currentProgress = std::numeric_limits::max(); + + auto progressTrackingThread = boost::thread([this, &progressTracking, ¤tProgress]() + { + while(!progressTracking.finished()) + { + if(progressTracking.get() != currentProgress) + { + currentProgress = progressTracking.get(); + std::unique_ptr loadProgress(new LobbyLoadProgress); + loadProgress->progress = currentProgress; + addToAnnounceQueue(std::move(loadProgress)); + } + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + }); + gh = std::make_shared(this); switch(si->mode) { case StartInfo::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); + si->fileURI = mi->fileURI; si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); - gh->init(si.get()); + gh->init(si.get(), progressTracking); break; case StartInfo::NEW_GAME: logNetwork->info("Preparing to start new game"); - gh->init(si.get()); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); + si->fileURI = mi->fileURI; + gh->init(si.get(), progressTracking); break; case StartInfo::LOAD_GAME: logNetwork->info("Preparing to start loaded game"); if(!gh->load(si->mapname)) + { + current.finish(); + progressTrackingThread.join(); return false; + } break; default: logNetwork->error("Wrong mode in StartInfo!"); @@ -324,7 +345,9 @@ bool CVCMIServer::prepareToStartGame() break; } - state = EServerState::GAMEPLAY_STARTING; + current.finish(); + progressTrackingThread.join(); + return true; } @@ -412,7 +435,7 @@ public: void CVCMIServer::threadHandleClient(std::shared_ptr c) { - setThreadName("CVCMIServer::handleConnection"); + setThreadName("handleClient"); c->enterLobbyConnectionMode(); while(c->connected) @@ -422,6 +445,7 @@ void CVCMIServer::threadHandleClient(std::shared_ptr c) try { pack = c->retrievePack(); + pack->c = c; } catch(boost::system::system_error & e) { @@ -462,7 +486,7 @@ void CVCMIServer::threadHandleClient(std::shared_ptr c) void CVCMIServer::handleReceivedPack(std::unique_ptr pack) { - CBaseForServerApply * apply = applier->getApplier(typeList.getTypeID(pack.get())); + CBaseForServerApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack.get())); if(apply->applyOnServerBefore(this, pack.get())) addToAnnounceQueue(std::move(pack)); } @@ -479,7 +503,7 @@ void CVCMIServer::announcePack(std::unique_ptr pack) c->sendPack(pack.get()); } - applier->getApplier(typeList.getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); + applier->getApplier(CTypeList::getInstance().getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); } void CVCMIServer::announceMessage(const std::string & txt) @@ -705,10 +729,10 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; - pset.heroName = pinfo.mainCustomHeroName; + pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; pset.heroPortrait = pinfo.mainCustomHeroPortrait; } @@ -811,17 +835,36 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor) } } +void CVCMIServer::setPlayerName(PlayerColor color, std::string name) +{ + if(color == PlayerColor::CANNOT_DETERMINE) + return; + + PlayerSettings & player = si->playerInfos.at(color); + + if(!player.isControlledByHuman()) + return; + + if(player.connectedPlayerIDs.empty()) + return; + + int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + + playerNames[nameID].name = name; + setPlayerConnectedId(player, nameID); +} + void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; FactionID & cur = s.castle; - auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; - const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom; + auto & allowed = getPlayerInfo(player).allowedFactions; + const bool allowRandomTown = getPlayerInfo(player).isFactionRandom; - if(cur == PlayerSettings::NONE) //no change + if(cur == FactionID::NONE) //no change return; - if(cur == PlayerSettings::RANDOM) //first/last available + if(cur == FactionID::RANDOM) //first/last available { if(dir > 0) cur = *allowed.begin(); //id of first town @@ -835,7 +878,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { if(allowRandomTown) { - cur = PlayerSettings::RANDOM; + cur = FactionID::RANDOM; } else { @@ -854,12 +897,34 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) } } - if(s.hero >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { - s.hero = PlayerSettings::RANDOM; + s.hero = HeroTypeID::RANDOM; } - if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) - s.bonus = PlayerSettings::RANDOM; + if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE) + s.bonus = PlayerStartingBonus::RANDOM; +} + +void CVCMIServer::optionSetCastle(PlayerColor player, FactionID id) +{ + PlayerSettings & s = si->playerInfos[player]; + FactionID & cur = s.castle; + auto & allowed = getPlayerInfo(player).allowedFactions; + + if(cur == FactionID::NONE) //no change + return; + + if(allowed.find(id) == allowed.end() && id != FactionID::RANDOM) // valid id + return; + + cur = static_cast(id); + + if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + { + s.hero = HeroTypeID::RANDOM; + } + if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE) + s.bonus = PlayerStartingBonus::RANDOM; } void CVCMIServer::setCampaignMap(CampaignScenarioID mapId) @@ -891,89 +956,123 @@ void CVCMIServer::setCampaignBonus(int bonusId) void CVCMIServer::optionNextHero(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle < 0 || s.hero == PlayerSettings::NONE) + if(!s.castle.isValid() || s.hero == HeroTypeID::NONE) return; - if(s.hero == PlayerSettings::RANDOM) // first/last available + if(s.hero == HeroTypeID::RANDOM) // first/last available { - int max = static_cast(VLC->heroh->size()), - min = 0; - s.hero = nextAllowedHero(player, min, max, 0, dir); + if (dir > 0) + s.hero = nextAllowedHero(player, HeroTypeID(-1), dir); + else + s.hero = nextAllowedHero(player, HeroTypeID(VLC->heroh->size()), dir); } else { - if(dir > 0) - s.hero = nextAllowedHero(player, s.hero, (int)VLC->heroh->size(), 1, dir); - else - s.hero = nextAllowedHero(player, -1, s.hero, 1, dir); // min needs to be -1 -- hero at index 0 would be skipped otherwise + s.hero = nextAllowedHero(player, s.hero, dir); } } -int CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) +void CVCMIServer::optionSetHero(PlayerColor player, HeroTypeID id) { - if(dir > 0) + PlayerSettings & s = si->playerInfos[player]; + if(!s.castle.isValid() || s.hero == HeroTypeID::NONE) + return; + + if(id == HeroTypeID::RANDOM) { - for(int i = min + incl; i <= max - incl; i++) + s.hero = HeroTypeID::RANDOM; + } + if(canUseThisHero(player, id)) + s.hero = static_cast(id); +} + +HeroTypeID CVCMIServer::nextAllowedHero(PlayerColor player, HeroTypeID initial, int direction) +{ + HeroTypeID first(initial.getNum() + direction); + + if(direction > 0) + { + for (auto i = first; i.getNum() < VLC->heroh->size(); ++i) if(canUseThisHero(player, i)) return i; } else { - for(int i = max - incl; i >= min + incl; i--) + for (auto i = first; i.getNum() >= 0; --i) if(canUseThisHero(player, i)) return i; } - return -1; + return HeroTypeID::RANDOM; } void CVCMIServer::optionNextBonus(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - PlayerSettings::Ebonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); + PlayerStartingBonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); - if(s.hero == PlayerSettings::NONE && - !getPlayerInfo(player.getNum()).heroesNames.size() && - ret == PlayerSettings::ARTIFACT) //no hero - can't be artifact + if(s.hero == HeroTypeID::NONE && + !getPlayerInfo(player).heroesNames.size() && + ret == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact { if(dir < 0) - ret = PlayerSettings::RANDOM; + ret = PlayerStartingBonus::RANDOM; else - ret = PlayerSettings::GOLD; + ret = PlayerStartingBonus::GOLD; } - if(ret > PlayerSettings::RESOURCE) - ret = PlayerSettings::RANDOM; - if(ret < PlayerSettings::RANDOM) - ret = PlayerSettings::RESOURCE; + if(ret > PlayerStartingBonus::RESOURCE) + ret = PlayerStartingBonus::RANDOM; + if(ret < PlayerStartingBonus::RANDOM) + ret = PlayerStartingBonus::RESOURCE; - if(s.castle == PlayerSettings::RANDOM && ret == PlayerSettings::RESOURCE) //random castle - can't be resource + if(s.castle == FactionID::RANDOM && ret == PlayerStartingBonus::RESOURCE) //random castle - can't be resource { if(dir < 0) - ret = PlayerSettings::GOLD; + ret = PlayerStartingBonus::GOLD; else - ret = PlayerSettings::RANDOM; + ret = PlayerStartingBonus::RANDOM; } } -bool CVCMIServer::canUseThisHero(PlayerColor player, int ID) +void CVCMIServer::optionSetBonus(PlayerColor player, PlayerStartingBonus id) +{ + PlayerSettings & s = si->playerInfos[player]; + + if(s.hero == HeroTypeID::NONE && + !getPlayerInfo(player).heroesNames.size() && + id == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact + return; + + if(id > PlayerStartingBonus::RESOURCE) + return; + if(id < PlayerStartingBonus::RANDOM) + return; + + if(s.castle == FactionID::RANDOM && id == PlayerStartingBonus::RESOURCE) //random castle - can't be resource + return; + + s.bonus = id;; +} + +bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID) { return VLC->heroh->size() > ID && si->playerInfos[player].castle == VLC->heroh->objects[ID]->heroClass->faction && !vstd::contains(getUsedHeroes(), ID) - && mi->mapHeader->allowedHeroes[ID]; + && mi->mapHeader->allowedHeroes.count(ID); } -std::vector CVCMIServer::getUsedHeroes() +std::vector CVCMIServer::getUsedHeroes() { - std::vector heroIds; + std::vector heroIds; for(auto & p : si->playerInfos) { - const auto & heroes = getPlayerInfo(p.first.getNum()).heroesNames; + const auto & heroes = getPlayerInfo(p.first).heroesNames; for(auto & hero : heroes) if(hero.heroId >= 0) //in VCMI map format heroId = -1 means random hero heroIds.push_back(hero.heroId); - if(p.second.hero != PlayerSettings::RANDOM) + if(p.second.hero != HeroTypeID::RANDOM) heroIds.push_back(p.second.hero); } return heroIds; @@ -999,8 +1098,6 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o ("version,v", "display version information and exit") ("run-by-client", "indicate that server launched by client on same machine") ("uuid", po::value(), "") - ("enable-shm-uuid", "use UUID for shared memory identifier") - ("enable-shm", "enable usage of shared memory") ("port", po::value(), "port at which server will listen to connections from client") ("lobby", po::value(), "address to remote lobby") ("lobby-port", po::value(), "port at which server connect to remote lobby") @@ -1013,7 +1110,7 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o { po::store(po::parse_command_line(argc, argv, opts), options); } - catch(po::error & e) + catch(boost::program_options::error & e) { std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } @@ -1028,7 +1125,7 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o #ifndef SINGLE_PROCESS_APP if(options.count("help")) { - auto time = std::time(0); + auto time = std::time(nullptr); printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); printf("This is free software; see the source for copying conditions. There is NO\n"); @@ -1093,7 +1190,7 @@ int main(int argc, const char * argv[]) try { - while(server.state != EServerState::SHUTDOWN) + while(server.getState() != EServerState::SHUTDOWN) { server.run(); } @@ -1102,7 +1199,7 @@ int main(int argc, const char * argv[]) catch(boost::system::system_error & e) //for boost errors just log, not crash - probably client shut down connection { logNetwork->error(e.what()); - server.state = EServerState::SHUTDOWN; + server.setState(EServerState::SHUTDOWN); } } catch(boost::system::system_error & e) diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 879298a8c..22db4b93c 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -23,7 +23,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CMapInfo; struct CPackForLobby; -struct SharedMemory; struct StartInfo; struct LobbyInfo; @@ -57,14 +56,15 @@ class CVCMIServer : public LobbyInfo boost::recursive_mutex mx; std::shared_ptr> applier; std::unique_ptr announceLobbyThread, remoteConnectionsThread; + std::atomic state; public: std::shared_ptr gh; - std::atomic state; ui16 port; boost::program_options::variables_map cmdLineOptions; std::set> connections; + std::set> remoteConnections; std::set> hangingConnections; //keep connections of players disconnected during the game std::atomic currentClientId; @@ -79,7 +79,7 @@ public: void startGameImmidiately(); void establishRemoteConnections(); - void connectToRemote(const std::string & addr, int port); + void connectToRemote(); void startAsyncAccept(); void connectionAccepted(const boost::system::error_code & ec); void threadHandleClient(std::shared_ptr c); @@ -102,14 +102,21 @@ public: void updateAndPropagateLobbyState(); + void setState(EServerState value); + EServerState getState() const; + // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); + void setPlayerName(PlayerColor player, std::string name); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 - int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); - bool canUseThisHero(PlayerColor player, int ID); - std::vector getUsedHeroes(); + void optionSetHero(PlayerColor player, HeroTypeID id); + HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction); + bool canUseThisHero(PlayerColor player, HeroTypeID ID); + std::vector getUsedHeroes(); void optionNextBonus(PlayerColor player, int dir); //dir == -1 or +1 + void optionSetBonus(PlayerColor player, PlayerStartingBonus id); void optionNextCastle(PlayerColor player, int dir); //dir == -1 or + + void optionSetCastle(PlayerColor player, FactionID id); // Campaigns void setCampaignMap(CampaignScenarioID mapId); diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 251f1efcb..5ee02504c 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class ClientPermissionsCheckerNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) { @@ -86,7 +86,9 @@ public: virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override; + virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; + virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override; virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; -}; \ No newline at end of file +}; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index c9ead03b5..a079309ef 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -13,7 +13,6 @@ #include "CVCMIServer.h" #include "CGameHandler.h" -#include "../lib/NetPacksLobby.h" #include "../lib/serializer/Connection.h" #include "../lib/StartInfo.h" @@ -53,11 +52,11 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientConnected(LobbyClie } } - if(srv.state == EServerState::LOBBY) - { + if(srv.getState() == EServerState::LOBBY) + { result = true; return; - } + } //disconnect immediately and ignore this client srv.connections.erase(pack.c); @@ -115,7 +114,7 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientConnected(LobbyCl // Until UUID set we only pass LobbyClientConnected to this client pack.c->uuid = pack.uuid; srv.updateAndPropagateLobbyState(); - if(srv.state == EServerState::GAMEPLAY) + if(srv.getState() == EServerState::GAMEPLAY) { //immediately start game std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); @@ -173,13 +172,13 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb if(pack.shutdownServer) { logNetwork->info("Client requested shutdown, server will close itself..."); - srv.state = EServerState::SHUTDOWN; + srv.setState(EServerState::SHUTDOWN); return; } else if(srv.connections.empty()) { logNetwork->error("Last connection lost, server will close itself..."); - srv.state = EServerState::SHUTDOWN; + srv.setState(EServerState::SHUTDOWN); } else if(pack.c == srv.hostClient) { @@ -189,6 +188,12 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb srv.addToAnnounceQueue(std::move(ph)); } srv.updateAndPropagateLobbyState(); + + if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c)) + { + srv.remoteConnections -= pack.c; + srv.connectToRemote(); + } } void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack) @@ -198,7 +203,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMess void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack) { - if(srv.state != EServerState::LOBBY) + if(srv.getState() != EServerState::LOBBY) { result = false; return; @@ -214,7 +219,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack) srv.si->mapname = pack.ourCampaign->getFilename(); srv.si->mode = StartInfo::CAMPAIGN; srv.si->campState = pack.ourCampaign; - srv.si->turnTime = 0; + srv.si->turnTimerInfo = TurnTimerInfo{}; bool isCurrentMapConquerable = pack.ourCampaign->currentScenario() && pack.ourCampaign->isAvailable(*pack.ourCampaign->currentScenario()); @@ -289,6 +294,7 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) result = false; return; } + // Server will prepare gamestate and we announce StartInfo to clients if(!srv.prepareToStartGame()) { @@ -299,6 +305,7 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) pack.initializedStartInfo = std::make_shared(*srv.gh->getStartInfo(true)); pack.initializedGameState = srv.gh->gameState(); + srv.setState(EServerState::GAMEPLAY_STARTING); result = true; } @@ -360,14 +367,23 @@ void ApplyOnServerNetPackVisitor::visitLobbyChangePlayerOption(LobbyChangePlayer { switch(pack.what) { + case LobbyChangePlayerOption::TOWN_ID: + srv.optionSetCastle(pack.color, FactionID(pack.value)); + break; case LobbyChangePlayerOption::TOWN: - srv.optionNextCastle(pack.color, pack.direction); + srv.optionNextCastle(pack.color, pack.value); + break; + case LobbyChangePlayerOption::HERO_ID: + srv.optionSetHero(pack.color, HeroTypeID(pack.value)); break; case LobbyChangePlayerOption::HERO: - srv.optionNextHero(pack.color, pack.direction); + srv.optionNextHero(pack.color, pack.value); + break; + case LobbyChangePlayerOption::BONUS_ID: + srv.optionSetBonus(pack.color, PlayerStartingBonus(pack.value)); break; case LobbyChangePlayerOption::BONUS: - srv.optionNextBonus(pack.color, pack.direction); + srv.optionNextBonus(pack.color, pack.value); break; } @@ -380,9 +396,21 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack) result = true; } +void ApplyOnServerNetPackVisitor::visitLobbySetPlayerName(LobbySetPlayerName & pack) +{ + srv.setPlayerName(pack.color, pack.name); + result = true; +} + +void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack) +{ + srv.si->simturnsInfo = pack.simturnsInfo; + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack) { - srv.si->turnTime = pack.turnTime; + srv.si->turnTimerInfo = pack.turnTimerInfo; result = true; } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 3027242c0..a3d77db31 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -1,362 +1,351 @@ -/* - * NetPacksServer.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 "ServerNetPackVisitors.h" - -#include "CGameHandler.h" -#include "HeroPoolProcessor.h" -#include "PlayerMessageProcessor.h" - -#include "../lib/IGameCallback.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/battle/BattleAction.h" -#include "../lib/battle/Unit.h" -#include "../lib/serializer/Connection.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/serializer/Cast.h" - -void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) -{ - gh.save(pack.fname); - logGlobal->info("Game has been saved as %s", pack.fname); - result = true; -} - -void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) -{ - PlayerColor currentPlayer = gs.currentPlayer; - if(pack.player != currentPlayer) - { - if(gh.getPlayerStatus(pack.player) == EPlayerStatus::INGAME) - gh.throwAndComplain(&pack, "pack.player attempted to end turn for another pack.player!"); - - logGlobal->debug("pack.player attempted to end turn after game over. Ignoring this request."); - - result = true; - return; - } - - gh.throwOnWrongPlayer(&pack, pack.player); - if(gh.queries.topQuery(pack.player)) - gh.throwAndComplain(&pack, "Cannot end turn before resolving queries!"); - - gh.states.setFlag(gs.currentPlayer, &PlayerStatus::makingTurn, false); - result = true; -} - -void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - result = gh.removeObject(gh.getObj(pack.hid)); -} - -void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, gh.getPlayerAt(pack.c)); -} - -void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - - result = gh.teleportHero(pack.hid, pack.dest, pack.source, gh.getPlayerAt(pack.c)); -} - -void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) -{ - //checks for owning in the gh func - result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, gh.getPlayerAt(pack.c)); -} - -void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) -{ - result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); -} - -void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack) -{ - result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount); -} - -void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack) -{ - result = gh.bulkMergeStacks(pack.src, pack.srcOwner); -} - -void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack) -{ - result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner); -} - -void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack) -{ - gh.throwOnWrongOwner(&pack, pack.id); - result = gh.disbandCreature(pack.id, pack.pos); -} - -void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) -{ - gh.throwOnWrongOwner(&pack, pack.tid); - result = gh.buildStructure(pack.tid, pack.bid); -} - -void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) -{ - result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level); -} - -void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) -{ - gh.throwOnWrongOwner(&pack, pack.id); - result = gh.upgradeCreature(pack.id, pack.pos, pack.cid); -} - -void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) -{ - const CGTownInstance * town = gh.getTown(pack.tid); - if(!gh.isPlayerOwns(&pack, pack.tid) && !(town->garrisonHero && gh.isPlayerOwns(&pack, town->garrisonHero->id))) - gh.throwNotAllowedAction(&pack); //neither town nor garrisoned hero (if present) is ours - result = gh.garrisonSwap(pack.tid); -} - -void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) -{ - gh.throwOnWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally - result = gh.moveArtifact(pack.src, pack.dst); -} - -void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) -{ - const CGHeroInstance * pSrcHero = gh.getHero(pack.srcHero); - gh.throwOnWrongPlayer(&pack, pSrcHero->getOwner()); - result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap); -} - -void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) -{ - gh.throwOnWrongOwner(&pack, pack.heroID); - result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo); -} - -void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) -{ - gh.throwOnWrongPlayer(&pack, pack.al.owningPlayer()); - result = gh.eraseArtifactByClient(pack.al); -} - -void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - result = gh.buyArtifact(pack.hid, pack.aid); -} - -void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) -{ - const CGObjectInstance * market = gh.getObj(pack.marketId); - if(!market) - gh.throwAndComplain(&pack, "Invalid market object"); - const CGHeroInstance * hero = gh.getHero(pack.heroId); - - //market must be owned or visited - const IMarket * m = IMarket::castFrom(market); - - if(!m) - gh.throwAndComplain(&pack, "market is not-a-market! :/"); - - PlayerColor player = market->tempOwner; - - if(player >= PlayerColor::PLAYER_LIMIT) - player = gh.getTile(market->visitablePos())->visitableObjects.back()->tempOwner; - - if(player >= PlayerColor::PLAYER_LIMIT) - gh.throwAndComplain(&pack, "No player can use this market!"); - - bool allyTownSkillTrade = (pack.mode == EMarketMode::RESOURCE_SKILL && gh.getPlayerRelations(player, hero->tempOwner) == PlayerRelations::ALLIES); - - if(hero && (!(player == hero->tempOwner || allyTownSkillTrade) - || hero->visitablePos() != market->visitablePos())) - gh.throwAndComplain(&pack, "This hero can't use this marketplace!"); - - if(!allyTownSkillTrade) - gh.throwOnWrongPlayer(&pack, player); - - result = true; - - switch(pack.mode) - { - case EMarketMode::RESOURCE_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.tradeResources(m, pack.val[i], player, pack.r1[i], pack.r2[i]); - break; - case EMarketMode::RESOURCE_PLAYER: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sendResources(pack.val[i], player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); - break; - case EMarketMode::CREATURE_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellCreatures(pack.val[i], m, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); - break; - case EMarketMode::RESOURCE_ARTIFACT: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.buyArtifact(m, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); - break; - case EMarketMode::ARTIFACT_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellArtifact(m, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); - break; - case EMarketMode::CREATURE_UNDEAD: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.transformInUndead(m, hero, SlotID(pack.r1[i])); - break; - case EMarketMode::RESOURCE_SKILL: - for(int i = 0; i < pack.r2.size(); ++i) - result &= gh.buySecSkill(m, hero, SecondarySkill(pack.r2[i])); - break; - case EMarketMode::CREATURE_EXP: - { - std::vector slotIDs(pack.r1.begin(), pack.r1.end()); - std::vector count(pack.val.begin(), pack.val.end()); - result = gh.sacrificeCreatures(m, hero, slotIDs, count); - return; - } - case EMarketMode::ARTIFACT_EXP: - { - std::vector positions(pack.r1.begin(), pack.r1.end()); - result = gh.sacrificeArtifact(m, hero, positions); - return; - } - default: - gh.throwAndComplain(&pack, "Unknown exchange pack.mode!"); - } -} - -void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - result = gh.setFormation(pack.hid, pack.formation); -} - -void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) -{ - if (!gh.hasPlayerAt(pack.player, pack.c)) - gh.throwAndComplain(&pack, "No such pack.player!"); - - result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); -} - -void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) -{ - if(gh.getPlayerRelations(gh.getOwner(pack.objid), gh.getPlayerAt(pack.c)) == PlayerRelations::ENEMIES) - gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard"); - - result = gh.buildBoat(pack.objid, gh.getPlayerAt(pack.c)); -} - -void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) -{ - auto playerToConnection = gh.connections.find(pack.player); - if(playerToConnection == gh.connections.end()) - gh.throwAndComplain(&pack, "No such pack.player!"); - if(!vstd::contains(playerToConnection->second, pack.c)) - gh.throwAndComplain(&pack, "Message came from wrong connection!"); - if(pack.qid == QueryID(-1)) - gh.throwAndComplain(&pack, "Cannot answer the query with pack.id -1!"); - - assert(vstd::contains(gh.states.players, pack.player)); - - result = gh.queryReply(pack.qid, pack.reply, pack.player); -} - -void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) -{ - boost::unique_lock lock(gh.battleActionMutex); - - const BattleInfo * b = gs.curB; - if(!b) - gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); - - if(b->tacticDistance) - { - if(pack.ba.actionType != EActionType::WALK && pack.ba.actionType != EActionType::END_TACTIC_PHASE - && pack.ba.actionType != EActionType::RETREAT && pack.ba.actionType != EActionType::SURRENDER) - gh.throwAndComplain(&pack, "Can not make actions while in tactics mode!"); - if(!vstd::contains(gh.connections[b->sides[b->tacticsSide].color], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - else - { - auto active = b->battleActiveUnit(); - if(!active) - gh.throwAndComplain(&pack, "No active unit in battle!"); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - - result = gh.makeBattleAction(pack.ba); -} - -void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) -{ - boost::unique_lock lock(gh.battleActionMutex); - - const BattleInfo * b = gs.curB; - if(!b) - gh.throwNotAllowedAction(&pack); - if(b->tacticDistance) - gh.throwNotAllowedAction(&pack); - auto active = b->battleActiveUnit(); - if(!active) - gh.throwNotAllowedAction(&pack); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwNotAllowedAction(&pack); - if(pack.ba.actionType != EActionType::HERO_SPELL) - gh.throwNotAllowedAction(&pack); - - result = gh.makeCustomAction(pack.ba); -} - -void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) -{ - gh.throwOnWrongOwner(&pack, pack.id); - result = gh.dig(gh.getHero(pack.id)); -} - -void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) -{ - gh.throwOnWrongOwner(&pack, pack.hid); - - const CSpell * s = pack.sid.toSpell(); - if(!s) - gh.throwNotAllowedAction(&pack); - const CGHeroInstance * h = gh.getHero(pack.hid); - if(!h) - gh.throwNotAllowedAction(&pack); - - AdventureSpellCastParameters p; - p.caster = h; - p.pos = pack.pos; - - result = s->adventureCast(gh.spellEnv, p); -} - -void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) -{ - if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions - gh.throwOnWrongPlayer(&pack, pack.player); - - gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); - result = true; -} +/* + * NetPacksServer.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 "ServerNetPackVisitors.h" + +#include "CGameHandler.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "processors/TurnOrderProcessor.h" +#include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" + +#include "../lib/IGameCallback.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/battle/IBattleState.h" +#include "../lib/battle/BattleAction.h" +#include "../lib/battle/Unit.h" +#include "../lib/serializer/Connection.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/spells/ISpellMechanics.h" +#include "../lib/serializer/Cast.h" + +void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) +{ + gh.save(pack.fname); + logGlobal->info("Game has been saved as %s", pack.fname); + result = true; +} + +void ApplyGhNetPackVisitor::visitGamePause(GamePause & pack) +{ + auto turnQuery = std::make_shared(&gh, pack.player); + turnQuery->queryID = QueryID::CLIENT; + gh.queries->addQuery(turnQuery); + result = true; +} + +void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.turnOrder->onPlayerEndsTurn(pack.player); +} + +void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.removeObject(gh.getObj(pack.hid), pack.player); +} + +void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, pack.player); +} + +void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + + result = gh.teleportHero(pack.hid, pack.dest, pack.source, pack.player); +} + +void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, pack.player); +} + +void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) +{ + gh.throwIfWrongOwner(&pack, pack.srcArmy); + result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); +} + +void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount); +} + +void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkMergeStacks(pack.src, pack.srcOwner); +} + +void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner); +} + +void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.disbandCreature(pack.id, pack.pos); +} + +void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) +{ + gh.throwIfWrongOwner(&pack, pack.tid); + result = gh.buildStructure(pack.tid, pack.bid); +} + +void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) +{ + gh.throwIfWrongPlayer(&pack); + // ownership checks are inside recruitCreatures + result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level, pack.player); +} + +void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.upgradeCreature(pack.id, pack.pos, pack.cid); +} + +void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) +{ + const CGTownInstance * town = gh.getTown(pack.tid); + if(!gh.isPlayerOwns(&pack, pack.tid) && !(town->garrisonHero && gh.isPlayerOwns(&pack, town->garrisonHero->id))) + gh.throwNotAllowedAction(&pack); //neither town nor garrisoned hero (if present) is ours + result = gh.garrisonSwap(pack.tid); +} + +void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) +{ + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.src.artHolder)); //second hero can be ally + result = gh.moveArtifact(pack.src, pack.dst); +} + +void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) +{ + gh.throwIfWrongOwner(&pack, pack.srcHero); + if(pack.swap) + gh.throwIfWrongOwner(&pack, pack.dstHero); + result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); +} + +void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) +{ + gh.throwIfWrongOwner(&pack, pack.heroID); + result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo); +} + +void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) +{ + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.al.artHolder)); + result = gh.eraseArtifactByClient(pack.al); +} + +void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.buyArtifact(pack.hid, pack.aid); +} + +void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) +{ + const CGObjectInstance * object = gh.getObj(pack.marketId); + const CGHeroInstance * hero = gh.getHero(pack.heroId); + const IMarket * market = IMarket::castFrom(object); + + gh.throwIfWrongPlayer(&pack); + + if(!object) + gh.throwAndComplain(&pack, "Invalid market object"); + + if(!market) + gh.throwAndComplain(&pack, "market is not-a-market! :/"); + + bool heroCanBeInvalid = false; + + if (pack.mode == EMarketMode::RESOURCE_RESOURCE || pack.mode == EMarketMode::RESOURCE_PLAYER) + { + // For resource exchange we must use our own market or visit neutral market + if (object->getOwner().isValidPlayer()) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } + + if (pack.mode == EMarketMode::CREATURE_UNDEAD) + { + // For skeleton transformer, if hero is null then object must be owned + if (!hero) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } + + if (!heroCanBeInvalid) + { + gh.throwIfWrongOwner(&pack, pack.heroId); + + if (!hero) + 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)) + gh.throwAndComplain(&pack, "Can not trade - object not visited!"); + + if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES) + gh.throwAndComplain(&pack, "Can not trade - market not owned!"); + } + + result = true; + + switch(pack.mode) + { + case EMarketMode::RESOURCE_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); + break; + case EMarketMode::RESOURCE_PLAYER: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sendResources(pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); + break; + case EMarketMode::CREATURE_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sellCreatures(pack.val[i], market, hero, pack.r1[i].as(), pack.r2[i].as()); + break; + case EMarketMode::RESOURCE_ARTIFACT: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.buyArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); + break; + case EMarketMode::ARTIFACT_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sellArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); + break; + case EMarketMode::CREATURE_UNDEAD: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.transformInUndead(market, hero, pack.r1[i].as()); + break; + case EMarketMode::RESOURCE_SKILL: + for(int i = 0; i < pack.r2.size(); ++i) + result &= gh.buySecSkill(market, hero, pack.r2[i].as()); + break; + case EMarketMode::CREATURE_EXP: + { + std::vector slotIDs; + std::vector count(pack.val.begin(), pack.val.end()); + + for(auto const & slot : pack.r1) + slotIDs.push_back(slot.as()); + + result = gh.sacrificeCreatures(market, hero, slotIDs, count); + return; + } + case EMarketMode::ARTIFACT_EXP: + { + std::vector positions; + for(auto const & slot : pack.r1) + positions.push_back(slot.as()); + + result = gh.sacrificeArtifact(market, hero, positions); + return; + } + default: + gh.throwAndComplain(&pack, "Unknown exchange pack.mode!"); + } +} + +void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.setFormation(pack.hid, pack.formation); +} + +void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) +{ + gh.throwIfWrongPlayer(&pack); + + result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); +} + +void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) +{ + gh.throwIfWrongPlayer(&pack); + + if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES) + gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard"); + + result = gh.buildBoat(pack.objid, pack.player); +} + +void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) +{ + gh.throwIfWrongPlayer(&pack); + + auto playerToConnection = gh.connections.find(pack.player); + if(playerToConnection == gh.connections.end()) + gh.throwAndComplain(&pack, "No such pack.player!"); + if(!vstd::contains(playerToConnection->second, pack.c)) + gh.throwAndComplain(&pack, "Message came from wrong connection!"); + if(pack.qid == QueryID(-1)) + gh.throwAndComplain(&pack, "Cannot answer the query with pack.id -1!"); + + result = gh.queryReply(pack.qid, pack.reply, pack.player); +} + +void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) +{ + gh.throwIfWrongPlayer(&pack); + + result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba); +} + +void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.dig(gh.getHero(pack.id)); +} + +void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + + const CSpell * s = pack.sid.toSpell(); + if(!s) + gh.throwNotAllowedAction(&pack); + const CGHeroInstance * h = gh.getHero(pack.hid); + if(!h) + gh.throwNotAllowedAction(&pack); + + AdventureSpellCastParameters p; + p.caster = h; + p.pos = pack.pos; + + result = s->adventureCast(gh.spellEnv, p); +} + +void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) +{ + if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions + gh.throwIfWrongPlayer(&pack, pack.player); + + gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); + result = true; +} diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index a9ed4595b..487af47de 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -9,18 +9,17 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class ApplyGhNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) { private: bool result; CGameHandler & gh; - CGameState & gs; public: - ApplyGhNetPackVisitor(CGameHandler & gh, CGameState & gs) - :gh(gh), gs(gs), result(false) + ApplyGhNetPackVisitor(CGameHandler & gh) + :gh(gh), result(false) { } @@ -30,6 +29,7 @@ public: } virtual void visitSaveGame(SaveGame & pack) override; + virtual void visitGamePause(GamePause & pack) override; virtual void visitEndTurn(EndTurn & pack) override; virtual void visitDismissHero(DismissHero & pack) override; virtual void visitMoveHero(MoveHero & pack) override; @@ -55,8 +55,7 @@ public: virtual void visitBuildBoat(BuildBoat & pack) override; virtual void visitQueryReply(QueryReply & pack) override; virtual void visitMakeAction(MakeAction & pack) override; - virtual void visitMakeCustomAction(MakeCustomAction & pack) override; virtual void visitDigWithHero(DigWithHero & pack) override; virtual void visitCastAdvSpell(CastAdvSpell & pack) override; virtual void visitPlayerMessage(PlayerMessage & pack) override; -}; \ No newline at end of file +}; diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 7837ecba0..b89b9ad8f 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -8,10 +8,16 @@ * */ #include "StdInc.h" -#include "../lib/gameState/CGameState.h" -#include "CGameHandler.h" #include "ServerSpellCastEnvironment.h" +#include "CGameHandler.h" +#include "queries/QueriesProcessor.h" +#include "queries/CQuery.h" + +#include "../lib/gameState/CGameState.h" +#include "../lib/networkPacks/PacksForClientBattle.h" +#include "../lib/networkPacks/SetStackEffect.h" + ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) : gh(gh) @@ -88,10 +94,10 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t return gh->moveHero(hid, dst, teleporting, false); } -void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function callback) +void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function)> callback) { - auto query = std::make_shared(&gh->queries, color, callback); + auto query = std::make_shared(gh, color, callback); request->queryID = query->queryID; - gh->queries.addQuery(query); + gh->queries->addQuery(query); gh->sendAndApply(request); } diff --git a/server/ServerSpellCastEnvironment.h b/server/ServerSpellCastEnvironment.h index 7556783e2..8894160d3 100644 --- a/server/ServerSpellCastEnvironment.h +++ b/server/ServerSpellCastEnvironment.h @@ -36,7 +36,7 @@ public: const CMap * getMap() const override; const CGameInfoCallback * getCb() const override; bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) override; - void genericQuery(Query * request, PlayerColor color, std::function callback) override; + void genericQuery(Query * request, PlayerColor color, std::function)> callback) override; private: CGameHandler * gh; -}; \ No newline at end of file +}; diff --git a/server/StdInc.cpp b/server/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/server/StdInc.cpp +++ b/server/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/server/StdInc.h b/server/StdInc.h index fbf8bbce5..d03216bdf 100644 --- a/server/StdInc.h +++ b/server/StdInc.h @@ -1,21 +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" - -#include -#include //no i/o just types -#include -#include -#include -#include - -VCMI_LIB_USING_NAMESPACE +/* + * 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/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp new file mode 100644 index 000000000..5d3365ffe --- /dev/null +++ b/server/TurnTimerHandler.cpp @@ -0,0 +1,313 @@ +/* + * TurnTimerHandler.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 "TurnTimerHandler.h" +#include "CGameHandler.h" +#include "battles/BattleProcessor.h" +#include "queries/QueriesProcessor.h" +#include "processors/TurnOrderProcessor.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/networkPacks/PacksForClient.h" +#include "../lib/networkPacks/PacksForClientBattle.h" +#include "../lib/CPlayerState.h" +#include "../lib/CStack.h" +#include "../lib/StartInfo.h" + +TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): + gameHandler(gh) +{ + +} + +void TurnTimerHandler::onGameplayStart(PlayerColor player) +{ + std::lock_guard guard(mx); + if(const auto * si = gameHandler.getStartInfo()) + { + timers[player] = si->turnTimerInfo; + timers[player].turnTimer = 0; + timers[player].isActive = true; + timers[player].isBattle = false; + lastUpdate[player] = std::numeric_limits::max(); + endTurnAllowed[player] = true; + } +} + +void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) +{ + std::lock_guard guard(mx); + assert(player.isValidPlayer()); + timers[player].isActive = enabled; + sendTimerUpdate(player); +} + +void TurnTimerHandler::setEndTurnAllowed(PlayerColor player, bool enabled) +{ + std::lock_guard guard(mx); + assert(player.isValidPlayer()); + endTurnAllowed[player] = enabled; +} + +void TurnTimerHandler::sendTimerUpdate(PlayerColor player) +{ + TurnTimeUpdate ttu; + ttu.player = player; + ttu.turnTimer = timers[player]; + gameHandler.sendAndApply(&ttu); + lastUpdate[player] = 0; +} + +void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) +{ + std::lock_guard guard(mx); + if(const auto * si = gameHandler.getStartInfo()) + { + if(si->turnTimerInfo.isEnabled()) + { + endTurnAllowed[player] = true; + auto & timer = timers[player]; + if(si->turnTimerInfo.accumulatingTurnTimer) + timer.baseTimer += timer.turnTimer; + timer.turnTimer = si->turnTimerInfo.turnTimer; + + sendTimerUpdate(player); + } + } +} + +void TurnTimerHandler::update(int waitTime) +{ + std::lock_guard guard(mx); + if(const auto * gs = gameHandler.gameState()) + { + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + if(gs->isPlayerMakingTurn(player)) + onPlayerMakingTurn(player, waitTime); + + for (auto & battle : gs->currentBattles) + onBattleLoop(battle->battleID, waitTime); + } +} + +bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor player, int waitTime) +{ + if(timer > 0) + { + timer -= waitTime; + lastUpdate[player] += waitTime; + int frequency = (timer > turnTimePropagateThreshold + && initialTimer - timer > turnTimePropagateThreshold) + ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; + + if(lastUpdate[player] >= frequency) + sendTimerUpdate(player); + + return true; + } + return false; +} + +void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs || !si->turnTimerInfo.isEnabled()) + return; + + auto & timer = timers[player]; + const auto * state = gameHandler.getPlayerState(player); + if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME) + { + if(timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if(timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + + if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries + gameHandler.turnOrder->onPlayerEndsTurn(state->color); + } +} + +bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const +{ + const auto * gs = gameHandler.gameState(); + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); + if(attacker.isValidPlayer() && defender.isValidPlayer()) + { + const auto * attackerState = gameHandler.getPlayerState(attacker); + const auto * defenderState = gameHandler.getPlayerState(defender); + if(attackerState && defenderState && attackerState->human && defenderState->human) + return true; + } + return false; +} + +void TurnTimerHandler::onBattleStart(const BattleID & battleID) +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs) + return; + + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); + + bool pvpBattle = isPvpBattle(battleID); + + for(auto i : {attacker, defender}) + { + if(i.isValidPlayer()) + { + auto & timer = timers[i]; + timer.isBattle = true; + timer.isActive = si->turnTimerInfo.isBattleEnabled(); + timer.battleTimer = si->turnTimerInfo.battleTimer; + timer.unitTimer = (pvpBattle ? si->turnTimerInfo.unitTimer : 0); + + sendTimerUpdate(i); + } + } +} + +void TurnTimerHandler::onBattleEnd(const BattleID & battleID) +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs) + { + assert(0); + return; + } + + if (!si->turnTimerInfo.isBattleEnabled()) + return; + + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); + + for(auto i : {attacker, defender}) + { + if(i.isValidPlayer()) + { + auto & timer = timers[i]; + timer.isBattle = false; + timer.isActive = true; + sendTimerUpdate(i); + } + } +} + +void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack) +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs || !gs->getBattle(battleID)) + { + assert(0); + return; + } + + if (!si->turnTimerInfo.isBattleEnabled()) + return; + + if(isPvpBattle(battleID)) + { + auto player = stack.getOwner(); + + auto & timer = timers[player]; + if(timer.accumulatingUnitTimer) + timer.battleTimer += timer.unitTimer; + timer.unitTimer = si->turnTimerInfo.unitTimer; + + sendTimerUpdate(player); + } +} + +void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs) + { + assert(0); + return; + } + + if (!si->turnTimerInfo.isBattleEnabled()) + return; + + ui8 side = 0; + const CStack * stack = nullptr; + bool isTactisPhase = gs->getBattle(battleID)->battleTacticDist() > 0; + + if(isTactisPhase) + side = gs->getBattle(battleID)->battleGetTacticsSide(); + else + { + stack = gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->getActiveStackID()); + if(!stack || !stack->getOwner().isValidPlayer()) + return; + side = stack->unitSide(); + } + + auto player = gs->getBattle(battleID)->getSidePlayer(side); + if(!player.isValidPlayer()) + return; + + const auto * state = gameHandler.getPlayerState(player); + assert(state && state->status == EPlayerStatus::INGAME); + if(!state || state->status != EPlayerStatus::INGAME || !state->human) + return; + + auto & timer = timers[player]; + if(timer.isActive && timer.isBattle) + { + if (timerCountDown(timer.unitTimer, si->turnTimerInfo.unitTimer, player, waitTime)) + return; + + if (timerCountDown(timer.battleTimer, si->turnTimerInfo.battleTimer, player, waitTime)) + return; + + if (timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if (timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + + if(isPvpBattle(battleID)) + { + BattleAction doNothing; + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; + else + { + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); + } + gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); + } + else + { + BattleAction retreat; + retreat.side = side; + retreat.actionType = EActionType::RETREAT; //harsh punishment + gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); + } + } +} diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h new file mode 100644 index 000000000..a780b466c --- /dev/null +++ b/server/TurnTimerHandler.h @@ -0,0 +1,54 @@ +/* + * TurnTimerHandler.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/TurnTimerInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class PlayerColor; +class BattleID; + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class TurnTimerHandler +{ + CGameHandler & gameHandler; + const int turnTimePropagateFrequency = 5000; + const int turnTimePropagateFrequencyCrit = 1000; + const int turnTimePropagateThreshold = 3000; + std::map timers; + std::map lastUpdate; + std::map endTurnAllowed; + std::recursive_mutex mx; + + void onPlayerMakingTurn(PlayerColor player, int waitTime); + void onBattleLoop(const BattleID & battleID, int waitTime); + + bool timerCountDown(int & timerToApply, int initialTimer, PlayerColor player, int waitTime); + bool isPvpBattle(const BattleID & battleID) const; + void sendTimerUpdate(PlayerColor player); + +public: + TurnTimerHandler(CGameHandler &); + + void onGameplayStart(PlayerColor player); + void onPlayerGetTurn(PlayerColor player); + void onBattleStart(const BattleID & battle); + void onBattleNextStack(const BattleID & battle, const CStack & stack); + void onBattleEnd(const BattleID & battleID); + void update(int waitTime); + void setTimerEnabled(PlayerColor player, bool enabled); + void setEndTurnAllowed(PlayerColor player, bool enabled); +}; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp new file mode 100644 index 000000000..56878c270 --- /dev/null +++ b/server/battles/BattleActionProcessor.cpp @@ -0,0 +1,1468 @@ +/* + * BattleActionProcessor.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 "BattleActionProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/Problem.h" + +BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +bool BattleActionProcessor::doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + + if (!canStackAct(battle, stack)) + return false; + + return true; +} + +bool BattleActionProcessor::doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + if (!battle.battleCanFlee(battle.sideToPlayer(ba.side))) + { + gameHandler->complain("Cannot retreat!"); + return false; + } + + owner->setBattleResult(battle, EBattleResult::ESCAPE, !ba.side); + return true; +} + +bool BattleActionProcessor::doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + PlayerColor player = battle.sideToPlayer(ba.side); + int cost = battle.battleGetSurrenderCost(player); + if (cost < 0) + { + gameHandler->complain("Cannot surrender!"); + return false; + } + + if (gameHandler->getResource(player, EGameResID::GOLD) < cost) + { + gameHandler->complain("Not enough gold to surrender!"); + return false; + } + + gameHandler->giveResource(player, EGameResID::GOLD, -cost); + owner->setBattleResult(battle, EBattleResult::SURRENDER, !ba.side); + return true; +} + +bool BattleActionProcessor::doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CGHeroInstance *h = battle.battleGetFightingHero(ba.side); + if (!h) + { + logGlobal->error("Wrong caster!"); + return false; + } + + const CSpell * s = ba.spell.toSpell(); + if (!s) + { + logGlobal->error("Wrong spell id (%d)!", ba.spell.getNum()); + return false; + } + + spells::BattleCast parameters(&battle, h, spells::Mode::HERO, s); + + spells::detail::ProblemImpl problem; + + auto m = s->battleMechanics(¶meters); + + if(!m->canBeCast(problem))//todo: should we check aimed cast? + { + logGlobal->warn("Spell cannot be cast!"); + std::vector texts; + problem.getAll(texts); + for(auto s : texts) + logGlobal->warn(s); + return false; + } + + parameters.cast(gameHandler->spellEnv, ba.getTarget(&battle)); + + return true; +} + +bool BattleActionProcessor::doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + + if (!canStackAct(battle, stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for move action."); + return false; + } + + int walkedTiles = moveStack(battle, ba.stackNumber, target.at(0).hexValue); //move + if (!walkedTiles) + { + gameHandler->complain("Stack failed movement!"); + return false; + } + return true; +} + +bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + + if (!canStackAct(battle, stack)) + return false; + + //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) + SetStackEffect sse; + sse.battleID = battle.getBattle()->getBattleID(); + + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE))); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(defenseBonusToAdd)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + std::vector buffer; + if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) + { + difference = 1; + buffer.push_back(alternativeWeakCreatureBonus); + } + else + { + buffer.push_back(defenseBonusToAdd); + } + + buffer.push_back(bonus2); + + sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); + gameHandler->sendAndApply(&sse); + + BattleLogMessage message; + message.battleID = battle.getBattle()->getBattleID(); + + MetaString text; + stack->addText(text, EMetaText::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.replaceNumber(difference); + + message.lines.push_back(text); + + gameHandler->sendAndApply(&message); + return true; +} + +bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + + if (!canStackAct(battle, stack)) + return false; + + if(target.size() < 2) + { + gameHandler->complain("Two destinations required for attack action."); + return false; + } + + BattleHex attackPos = target.at(0).hexValue; + BattleHex destinationTile = target.at(1).hexValue; + const CStack * destinationStack = battle.battleGetStackByPos(destinationTile, true); + + if(!destinationStack) + { + gameHandler->complain("Invalid target to attack"); + return false; + } + + BattleHex startingPos = stack->getPosition(); + int distance = moveStack(battle, ba.stackNumber, attackPos); + + logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); + + if(stack->getPosition() != attackPos && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) ) + { + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine + return true; + } + + if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + gameHandler->complain("Unit can not attack itself"); + return false; + } + + if(!CStack::isMeleeAttackPossible(stack, destinationStack)) + { + gameHandler->complain("Attack cannot be performed!"); + return false; + } + + //attack + int totalAttacks = stack->totalAttacks.getMeleeValue(); + + //TODO: move to CUnitState + const auto * attackingHero = battle.battleGetFightingHero(ba.side); + if(attackingHero) + { + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); + } + + const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + const bool retaliation = destinationStack->ableToRetaliate(); + for (int i = 0; i < totalAttacks; ++i) + { + //first strike + if(i == 0 && firstStrike && retaliation) + { + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) + { + makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + } + + //counterattack + //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) + && (i == 0 && !firstStrike) + && retaliation && destinationStack->ableToRetaliate()) + { + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + } + + //return + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) + && target.size() == 3 + && startingPos != stack->getPosition() + && startingPos == target.at(2).hexValue + && stack->alive()) + { + moveStack(battle, ba.stackNumber, startingPos); + //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) + } + return true; +} + +bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + + if (!canStackAct(battle, stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for shot action."); + return false; + } + + auto destination = target.at(0).hexValue; + + const CStack * destinationStack = battle.battleGetStackByPos(destination); + + if (!battle.battleCanShoot(stack, destination)) + { + gameHandler->complain("Cannot shoot!"); + return false; + } + + if (!destinationStack) + { + gameHandler->complain("No target to shoot!"); + return false; + } + + makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); + + //ranged counterattack + if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) + && destinationStack->ableToRetaliate() + && battle.battleCanShoot(destinationStack, stack->getPosition()) + && stack->alive()) //attacker may have died (fire shield) + { + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, true, true); + } + //allow more than one additional attack + + int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + + //TODO: move to CUnitState + const auto * attackingHero = battle.battleGetFightingHero(ba.side); + if(attackingHero) + { + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); + } + + for(int i = 1; i < totalRangedAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots.canUse() + ) + { + makeAttack(battle, stack, destinationStack, 0, destination, false, true, false); + } + } + + return true; +} + +bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + + if (!canStackAct(battle, stack)) + return false; + + std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + if(!catapultAbility || catapultAbility->subtype == BonusSubtypeID()) + { + gameHandler->complain("We do not know how to shoot :P"); + } + else + { + const CSpell * spell = catapultAbility->subtype.as().toSpell(); + spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); + parameters.setSpellLevel(shotLevel); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + SpellID spellID = ba.spell; + + if (!canStackAct(battle, stack)) + return false; + + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spellID))); + + //TODO special bonus for genies ability + if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) + spellID = battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + + if (spellID == SpellID::NONE) + gameHandler->complain("That stack can't cast spells!"); + else + { + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); + + if (!canStackAct(battle, stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for heal action."); + return false; + } + + const battle::Unit * destStack = nullptr; + std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + + if(target.at(0).unitValue) + destStack = target.at(0).unitValue; + else + destStack = battle.battleGetUnitByPos(target.at(0).hexValue); + + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == BonusSubtypeID()) + { + gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); + } + else + { + const CSpell * spell = healerAbility->subtype.as().toSpell(); + spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + auto dest = battle::Destination(destStack, target.at(0).hexValue); + parameters.setSpellLevel(0); + parameters.cast(gameHandler->spellEnv, {dest}); + } + return true; +} + +bool BattleActionProcessor::canStackAct(const CBattleInfoCallback & battle, const CStack * stack) +{ + if (!stack) + { + gameHandler->complain("No such stack!"); + return false; + } + if (!stack->alive()) + { + gameHandler->complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if (battle.battleTacticDist()) + { + if (stack && stack->unitSide() != battle.battleGetTacticsSide()) + { + gameHandler->complain("This is not a stack of side that has tactics!"); + return false; + } + } + else + { + if (stack != battle.battleActiveUnit()) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } + } + return true; +} + +bool BattleActionProcessor::dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + switch(ba.actionType) + { + case EActionType::BAD_MORALE: + case EActionType::NO_ACTION: + return doEmptyAction(battle, ba); + case EActionType::END_TACTIC_PHASE: + return doEndTacticsAction(battle, ba); + case EActionType::RETREAT: + return doRetreatAction(battle, ba); + case EActionType::SURRENDER: + return doSurrenderAction(battle, ba); + case EActionType::HERO_SPELL: + return doHeroSpellAction(battle, ba); + case EActionType::WALK: + return doWalkAction(battle, ba); + case EActionType::WAIT: + return doWaitAction(battle, ba); + case EActionType::DEFEND: + return doDefendAction(battle, ba); + case EActionType::WALK_AND_ATTACK: + return doAttackAction(battle, ba); + case EActionType::SHOOT: + return doShootAction(battle, ba); + case EActionType::CATAPULT: + return doCatapultAction(battle, ba); + case EActionType::MONSTER_SPELL: + return doUnitSpellAction(battle, ba); + case EActionType::STACK_HEAL: + return doHealAction(battle, ba); + } + gameHandler->complain("Unrecognized action type received!!"); + return false; +} + +bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction &ba) +{ + logGlobal->trace("Making action: %s", ba.toString()); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + + // for these events client does not expects StartAction/EndAction wrapper + if (!ba.isBattleEndAction()) + { + StartAction startAction(ba); + startAction.battleID = battle.getBattle()->getBattleID(); + gameHandler->sendAndApply(&startAction); + } + + bool result = dispatchBattleAction(battle, ba); + + if (!ba.isBattleEndAction()) + { + EndAction endAction; + endAction.battleID = battle.getBattle()->getBattleID(); + gameHandler->sendAndApply(&endAction); + } + + if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) + battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); + + return result; +} + +int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest) +{ + int ret = 0; + + const CStack *curStack = battle.battleGetStackByID(stack); + const CStack *stackAtEnd = battle.battleGetStackByPos(dest); + + assert(curStack); + assert(dest < GameConstants::BFIELD_SIZE); + + if (battle.battleGetTacticDist()) + { + assert(battle.isInTacticRange(dest)); + } + + auto start = curStack->getPosition(); + if (start == dest) + return 0; + + //initing necessary tables + auto accessibility = battle.getAccesibility(curStack); + std::set passed; + //Ignore obstacles on starting position + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + + //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) + if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) + { + BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); + + if(accessibility.accessible(shifted, curStack)) + dest = shifted; + } + + if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) + { + gameHandler->complain("Given destination is not accessible!"); + return 0; + } + + bool canUseGate = false; + auto dbState = battle.battleGetGateState(); + if(battle.battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + dbState != EGateState::DESTROYED && + dbState != EGateState::BLOCKED) + { + canUseGate = true; + } + + std::pair< std::vector, int > path = battle.getPath(start, dest, curStack); + + ret = path.second; + + int creSpeed = curStack->speed(0, true); + + if (battle.battleGetTacticDist() > 0 && creSpeed > 0) + creSpeed = GameConstants::BFIELD_SIZE; + + bool hasWideMoat = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == BattleHex::GATE_BRIDGE) + return true; + if (hex == BattleHex::GATE_OUTER) + return true; + if (hex == BattleHex::GATE_INNER) + return true; + + return false; + }; + + auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (isGateDrawbridgeHex(hex)) + return true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) + return true; + } + + return false; + }; + + if (curStack->hasBonusOfType(BonusType::FLYING)) + { + if (path.second <= creSpeed && path.first.size() > 0) + { + if (canUseGate && dbState != EGateState::OPENED && + occupyGateDrawbridgeHex(dest)) + { + BattleUpdateGateState db; + db.battleID = battle.getBattle()->getBattleID(); + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + + //inform clients about move + BattleStackMoved sm; + sm.battleID = battle.getBattle()->getBattleID(); + sm.stack = curStack->unitId(); + std::vector tiles; + tiles.push_back(path.first[0]); + sm.tilesToMove = tiles; + sm.distance = path.second; + sm.teleporting = false; + gameHandler->sendAndApply(&sm); + } + } + else //for non-flying creatures + { + std::vector tiles; + const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); + int v = (int)path.first.size()-1; + path.first.push_back(start); + + // check if gate need to be open or closed at some point + BattleHex openGateAtHex, gateMayCloseAtHex; + if (canUseGate) + { + for (int i = (int)path.first.size()-1; i >= 0; i--) + { + auto needOpenGates = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == BattleHex::GATE_BRIDGE) + return true; + if (hex == BattleHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == BattleHex::GATE_OUTER) + return true; + else if (hex == BattleHex::GATE_OUTER || hex == BattleHex::GATE_INNER) + return true; + + return false; + }; + + auto hex = path.first[i]; + if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) + { + if (needOpenGates(hex)) + openGateAtHex = path.first[i+1]; + + //TODO we need find batter way to handle double-wide stacks + //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && needOpenGates(otherHex)) + openGateAtHex = path.first[i+2]; + } + + //gate may be opened and then closed during stack movement, but not other way around + if (openGateAtHex.isValid()) + dbState = EGateState::OPENED; + } + + if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) + { + if (hex == BattleHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + if (hasWideMoat) + { + if (hex == BattleHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + else if (hex == BattleHex::GATE_OUTER && i-1 >= 0 && + path.first[i-1] != BattleHex::GATE_INNER && + path.first[i-1] != BattleHex::GATE_BRIDGE) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + else if (hex == BattleHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_INNER) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + } + } + + bool stackIsMoving = true; + + while(stackIsMoving) + { + if (verror("Movement terminated abnormally"); + break; + } + + bool gateStateChanging = false; + //special handling for opening gate on from starting hex + if (openGateAtHex.isValid() && openGateAtHex == start) + gateStateChanging = true; + else + { + for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) + { + BattleHex hex = path.first[v]; + tiles.push_back(hex); + + if ((openGateAtHex.isValid() && openGateAtHex == hex) || + (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) + { + gateStateChanging = true; + } + + //if we walked onto something, finalize this portion of stack movement check into obstacle + if(!battle.battleGetAllObstaclesOnPos(hex, false).empty()) + obstacleHit = true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + //two hex creature hit obstacle by backside + auto obstacle2 = battle.battleGetAllObstaclesOnPos(otherHex, false); + if(otherHex.isValid() && !obstacle2.empty()) + obstacleHit = true; + } + if(!obstacleHit) + passed.insert(hex); + } + } + + if (!tiles.empty()) + { + //commit movement + BattleStackMoved sm; + sm.battleID = battle.getBattle()->getBattleID(); + sm.stack = curStack->unitId(); + sm.distance = path.second; + sm.teleporting = false; + sm.tilesToMove = tiles; + gameHandler->sendAndApply(&sm); + tiles.clear(); + } + + //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end + if (curStack->getPosition() != dest) + { + if(stackIsMoving && start != curStack->getPosition()) + { + stackIsMoving = battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + } + if (gateStateChanging) + { + if (curStack->getPosition() == openGateAtHex) + { + openGateAtHex = BattleHex(); + //only open gate if stack is still alive + if (curStack->alive()) + { + BattleUpdateGateState db; + db.battleID = battle.getBattle()->getBattleID(); + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + } + else if (curStack->getPosition() == gateMayCloseAtHex) + { + gateMayCloseAtHex = BattleHex(); + owner->updateGateState(battle); + } + } + } + else + //movement finished normally: we reached destination + stackIsMoving = false; + } + } + //handle last hex separately for deviation + if (VLC->settings()->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())) + passed.clear(); //Just empty passed, obstacles will handled automatically + } + if(dest == start) //If dest is equal to start, then we should handle obstacles for it anyway + passed.clear(); //Just empty passed, obstacles will handled automatically + //handling obstacle on the final field (separate, because it affects both flying and walking stacks) + battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + + return ret; +} + +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) + handleAttackBeforeCasting(battle, ranged, attacker, defender); + + FireShieldInfo fireShield; + BattleAttack bat; + BattleLogMessage blm; + blm.battleID = battle.getBattle()->getBattleID(); + bat.battleID = battle.getBattle()->getBattleID(); + bat.attackerChanges.battleID = battle.getBattle()->getBattleID(); + bat.stackAttacking = attacker->unitId(); + bat.tile = targetHex; + + std::shared_ptr attackerState = attacker->acquireState(); + + if(ranged) + bat.flags |= BattleAttack::SHOT; + if(counter) + bat.flags |= BattleAttack::COUNTER; + + const int attackerLuck = attacker->luckVal(); + + if(attackerLuck > 0) + { + auto diceSize = VLC->settings()->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) + bat.flags |= BattleAttack::LUCKY; + } + + if(attackerLuck < 0) + { + auto diceSize = VLC->settings()->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) + bat.flags |= BattleAttack::UNLUCKY; + } + + if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) + { + bat.flags |= BattleAttack::DEATH_BLOW; + } + + const auto * owner = battle.battleGetFightingHero(attacker->unitSide()); + if(owner) + { + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, BonusSubtypeID(attacker->creatureId())); + if (chance > gameHandler->getRandomGenerator().nextInt(99)) + bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; + } + + int64_t drainedLife = 0; + + // only primary target + if(defender->alive()) + drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, defender, 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); + } + + std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + if(bonus && ranged) //TODO: make it work in melee? + { + //this is need for displaying hit animation + bat.flags |= BattleAttack::SPELL_LIKE; + bat.spellID = bonus->subtype.as(); + + //TODO: should spell override creature`s projectile? + + auto spell = bat.spellID.toSpell(); + + battle::Target target; + target.emplace_back(defender, targetHex); + + spells::BattleCast event(&battle, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + event.setSpellLevel(bonus->val); + + auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); + + //TODO: get exact attacked hex for defender + + 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); + } + } + + //now add effect info for all attacked stacks + for (BattleStackAttacked & bsa : bat.bsa) + { + if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield + { + //this is need for displaying affect animation + bsa.flags |= BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = bonus->subtype.as(); + } + } + } + + attackerState->afterAttack(ranged, counter); + + { + UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); + attackerState->save(info.data); + bat.attackerChanges.changedStacks.push_back(info); + } + + if (drainedLife > 0) + bat.flags |= BattleAttack::LIFE_DRAIN; + + for (BattleStackAttacked & bsa : bat.bsa) + bsa.battleID = battle.getBattle()->getBattleID(); + + gameHandler->sendAndApply(&bat); + + { + const bool multipleTargets = bat.bsa.size() > 1; + + int64_t totalDamage = 0; + int32_t totalKills = 0; + + for(const BattleStackAttacked & bsa : bat.bsa) + { + totalDamage += bsa.damageAmount; + totalKills += bsa.killedAmount; + } + + { + MetaString text; + attacker->addText(text, EMetaText::GENERAL_TXT, 376); + attacker->addNameReplacement(text); + text.replaceNumber(totalDamage); + blm.lines.push_back(text); + } + + addGenericKilledLog(blm, defender, totalKills, multipleTargets); + } + + // drain life effect (as well as log entry) must be applied after the attack + if(drainedLife > 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)); + } + + if(!fireShield.empty()) + { + //todo: this should be "virtual" spell instead, we only need fire spell school bonus here + const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); + int64_t totalDamage = 0; + + for(const auto & item : fireShield) + { + const CStack * actor = item.first; + int64_t rawDamage = item.second; + + const CGHeroInstance * actorOwner = battle.battleGetFightingHero(actor->unitOwner()); + + if(actorOwner) + { + rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); + } + else + { + rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); + } + + totalDamage+=rawDamage; + //FIXME: add custom effect on actor + } + + if (totalDamage > 0) + { + BattleStackAttacked bsa; + + bsa.battleID = battle.getBattle()->getBattleID(); + bsa.flags |= BattleStackAttacked::FIRE_SHIELD; + bsa.stackAttacked = attacker->unitId(); //invert + bsa.attackerID = defender->unitId(); + bsa.damageAmount = totalDamage; + attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured pack; + pack.battleID = battle.getBattle()->getBattleID(); + pack.stacks.push_back(bsa); + gameHandler->sendAndApply(&pack); + + // TODO: this is already implemented in Damage::describeEffect() + { + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 376); + text.replaceName(SpellID(SpellID::FIRE_SHIELD)); + text.replaceNumber(totalDamage); + blm.lines.push_back(std::move(text)); + } + addGenericKilledLog(blm, attacker, bsa.killedAmount, false); + } + } + + gameHandler->sendAndApply(&blm); + + handleAfterAttackCasting(battle, ranged, attacker, defender); +} + +void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +{ + if(attacker->hasBonusOfType(attackMode)) + { + std::set spellsToCast; + TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); + for(const auto & sf : *spells) + { + spellsToCast.insert(sf->subtype.as()); + } + for(SpellID spellID : spellsToCast) + { + bool castMe = false; + if(!defender->alive()) + { + logGlobal->debug("attackCasting: all attacked creatures have been killed"); + return; + } + int32_t spellLevel = 0; + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, BonusSubtypeID(spellID))); + for(const auto & sf : *spellsByType) + { + int meleeRanged; + if(sf->additionalInfo.size() < 2) + { + // legacy format + vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); + meleeRanged = sf->additionalInfo[0] / 1000; + } + else + { + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + } + if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) + castMe = true; + } + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, BonusSubtypeID(spellID)))); + vstd::amin(chance, 100); + + const CSpell * spell = SpellID(spellID).toSpell(); + spells::AbilityCaster caster(attacker, spellLevel); + + spells::Target target; + target.emplace_back(defender); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + + auto m = spell->battleMechanics(¶meters); + + spells::detail::ProblemImpl ignored; + + if(!m->canBeCastAt(target, ignored)) + continue; + + //check if spell should be cast (probability handling) + if(gameHandler->getRandomGenerator().nextInt(99) >= chance) + continue; + + //casting + if(castMe) + { + parameters.cast(gameHandler->spellEnv, target); + } + } + } +} + +void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) +{ + attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? +} + +void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) +{ + if(!attacker->alive() || !defender->alive()) // can be already dead + return; + + attackCasting(battle, ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); + + if(!defender->alive()) + { + //don't try death stare or acid breath on dead stack (crash!) + return; + } + + if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) + { + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0f; + vstd::amin(chanceToKill, 1); //cap at 100% + + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + + int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(staredCreatures, maxToKill); + + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); + if(staredCreatures) + { + //TODO: death stare was not originally available for multiple-hex attacks, but... + const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(staredCreatures); + parameters.cast(gameHandler->spellEnv, target); + } + } + + if(!defender->alive()) + return; + + int64_t acidDamage = 0; + TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); + for(const auto & b : *acidBreath) + { + if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99)) + acidDamage += b->val; + } + + if(acidDamage > 0) + { + const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + + parameters.setEffectValue(acidDamage * attacker->getCount()); + parameters.cast(gameHandler->spellEnv, target); + } + + + if(!defender->alive()) + return; + + if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability + { + double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; + vstd::amin(chanceToTrigger, 1); //cap at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; + + if(defender->unitType()->getIndex() == bonusAdditionalInfo || + (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) + return; + + battle::UnitInfo resurrectInfo; + resurrectInfo.id = battle.battleNextUnitId(); + resurrectInfo.summoned = false; + resurrectInfo.position = defender->getPosition(); + resurrectInfo.side = defender->unitSide(); + + if(bonusAdditionalInfo != CAddInfo::NONE) + resurrectInfo.type = CreatureID(bonusAdditionalInfo); + else + resurrectInfo.type = attacker->creatureId(); + + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusCustomSubtype::transmutationPerHealth)) + resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusCustomSubtype::transmutationPerUnit)) + resurrectInfo.count = defender->getCount(); + else + return; //wrong subtype + + BattleUnitsChanged addUnits; + addUnits.battleID = battle.getBattle()->getBattleID(); + addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); + resurrectInfo.save(addUnits.changedStacks.back().data); + + BattleUnitsChanged removeUnits; + removeUnits.battleID = battle.getBattle()->getBattleID(); + removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&removeUnits); + gameHandler->sendAndApply(&addUnits); + + // send empty event to client + // temporary(?) workaround to force animations to trigger + StacksInjured fakeEvent; + gameHandler->sendAndApply(&fakeEvent); + } + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount)) + { + double chanceToTrigger = 0; + int amountToDie = 0; + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage)) //killing by percentage + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusCustomSubtype::destructionKillPercentage)))->additionalInfo[0]; + amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); + } + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount)) //killing by count + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusCustomSubtype::destructionKillAmount)))->additionalInfo[0]; + } + + vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + BattleStackAttacked bsa; + bsa.attackerID = -1; + bsa.stackAttacked = defender->unitId(); + bsa.damageAmount = amountToDie * defender->getMaxHealth(); + bsa.flags = BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID::SLAYER; + defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured si; + si.battleID = battle.getBattle()->getBattleID(); + si.stacks.push_back(bsa); + + 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) +{ + BattleStackAttacked bsa; + if(secondary) + bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities + + bsa.attackerID = attackerState->unitId(); + bsa.stackAttacked = def->unitId(); + { + BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); + + bai.deathBlow = bat.deathBlow(); + bai.doubleDamage = bat.ballistaDoubleDmg(); + bai.luckyStrike = bat.lucky(); + bai.unluckyStrike = bat.unlucky(); + + auto range = battle.calculateDmgRange(bai); + bsa.damageAmount = battle.getBattle()->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + 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; + } + + //soul steal handling + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) + { + //we can have two bonuses - one with subtype 0 and another with subtype 1 + //try to use permanent first, use only one of two + for(const auto & subtype : { BonusCustomSubtype::soulStealBattle, BonusCustomSubtype::soulStealPermanent}) + { + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) + { + 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; + break; + } + } + } + bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated + + //fire shield handling + if(!bat.shot() && + !def->isClone() && + def->hasBonusOfType(BonusType::FIRE_SHIELD) && + !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, BonusSubtypeID(SpellSchool::FIRE)) && + !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSubtypeID(SpellSchool::FIRE)) && + attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::FIRE)) < 100 && + CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) + ) + { + //TODO: use damage with bonus but without penalties + 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) +{ + if(killed > 0) + { + BattleLogMessage blm; + blm.battleID = battle.getBattle()->getBattleID(); + addGenericKilledLog(blm, defender, killed, multiple); + gameHandler->sendAndApply(&blm); + } +} + +void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + const int32_t txtIndex = (killed > 1) ? 379 : 378; + std::string formatString = VLC->generaltexth->allTexts[txtIndex]; + + // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); + boost::algorithm::trim(formatString); + + boost::format txt(formatString); + if(killed > 1) + { + txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish + } + else //killed == 1 + { + txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes + } + MetaString line; + line.appendRawString(txt.str()); + blm.lines.push_back(std::move(line)); + } +} + +bool BattleActionProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba) +{ + return makeBattleActionImpl(battle, ba); +} + +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!")) + return false; + + if(battle.battleGetTacticDist() != 0) + { + if(!ba.isTacticsAction()) + { + gameHandler->complain("Can not make actions while in tactics mode!"); + return false; + } + + if(player != battle.sideToPlayer(ba.side)) + { + gameHandler->complain("Can not make actions in battles you are not part of!"); + return false; + } + } + else + { + auto active = battle.battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + if (ba.isUnitAction() && ba.stackNumber != active->unitId()) + { + gameHandler->complain("Can not make actions - stack is not active!"); + return false; + } + + auto unitOwner = battle.battleGetOwner(active); + + if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + return false; + } + + return makeBattleActionImpl(battle, ba); +} diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h new file mode 100644 index 000000000..34e40aa29 --- /dev/null +++ b/server/battles/BattleActionProcessor.h @@ -0,0 +1,80 @@ +/* + * BattleActionProcessor.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 BattleLogMessage; +struct BattleAttack; +class BattleAction; +class CBattleInfoCallback; +struct BattleHex; +class CStack; +class PlayerColor; +enum class BonusType; + +namespace battle +{ +class Unit; +class CUnitState; +} + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class BattleProcessor; + +/// Processes incoming battle action queries and applies requested action(s) +class BattleActionProcessor : boost::noncopyable +{ + using FireShieldInfo = std::vector>; + + BattleProcessor * owner; + CGameHandler * gameHandler; + + int moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest); //returned value - travelled distance + void makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + + void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * 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 sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple); + void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + + bool canStackAct(const CBattleInfoCallback & battle, const CStack * stack); + + bool doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba); + + bool dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction & ba); + +public: + explicit BattleActionProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool makePlayerBattleAction(const CBattleInfoCallback & battle, PlayerColor player, const BattleAction & ba); +}; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp new file mode 100644 index 000000000..0fba8f63a --- /dev/null +++ b/server/battles/BattleFlowProcessor.cpp @@ -0,0 +1,759 @@ +/* + * BattleFlowProcessor.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 "BattleFlowProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/IBattleState.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" + +BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + 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 +{ + int x = targetPosition.getX(); + int y = targetPosition.getY(); + + const bool targetIsAttacker = side == BattleSide::ATTACKER; + + if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + + //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's + if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) + { + if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } + else + { //add back-side guardians for two-hex target, side guardians for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); + + if (!targetIsTwoHex && x > 2) //back guard for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); + else if (targetIsTwoHex)//front-side guardians for two-hex target + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + if (x > 3) //back guard for two-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + } + } + + } + + else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) + { + if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + else + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); + + if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else if (targetIsTwoHex) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + if (x < GameConstants::BFIELD_WIDTH - 4) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + } + } + } + + else if (!targetIsAttacker && y % 2 == 0) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + + else if (targetIsAttacker && y % 2 == 1) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } +} + +void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle) +{ + const auto * town = battle.battleGetDefendedTown(); + + //Moat should be initialized here, because only here we can use spellcasting + if (town && town->fortLevel() >= CGTownInstance::CITADEL) + { + 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 target = spells::Target(); + cast.cast(gameHandler->spellEnv, target); + } +} + +void BattleFlowProcessor::onBattleStarted(const CBattleInfoCallback & battle) +{ + tryPlaceMoats(battle); + + gameHandler->turnTimerHandler.onBattleStart(battle.getBattle()->getBattleID()); + + if (battle.battleGetTacticDist() == 0) + onTacticsEnded(battle); +} + +void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack) +{ + if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) + return; + + std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); + auto accessibility = battle.getAccesibility(); + CreatureID creatureData = summonInfo->subtype.as(); + std::vector targetHexes; + const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard + const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); + + /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. + For one-hex targets there are four guardians - front, back and one per side (up + down). + Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front + Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ + if (!guardianIsBig) + targetHexes = stack->getSurroundingHexes(); + else + summonGuardiansHelper(battle, targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + + for(auto hex : targetHexes) + { + if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex + { + battle::UnitInfo info; + info.id = battle.battleNextUnitId(); + info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); + info.type = creatureData; + info.side = stack->unitSide(); + info.position = hex; + info.summoned = true; + + BattleUnitsChanged pack; + pack.battleID = battle.getBattle()->getBattleID(); + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + gameHandler->sendAndApply(&pack); + } + } + + // send empty event to client + // temporary(?) workaround to force animations to trigger + StacksInjured fakeEvent; + gameHandler->sendAndApply(&fakeEvent); +} + +void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle) +{ + for (int i = 0; i < 2; ++i) + { + auto h = battle.battleGetFightingHero(i); + if (!h) + continue; + + TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + + for (auto b : *bl) + { + spells::BonusCaster caster(h, b); + + const CSpell * spell = b->subtype.as().toSpell(); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + parameters.setSpellLevel(3); + parameters.setEffectDuration(b->val); + parameters.massive = true; + parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); + } + } +} + +void BattleFlowProcessor::onTacticsEnded(const CBattleInfoCallback & battle) +{ + //initial stacks appearance triggers, e.g. built-in bonus spells + auto initialStacks = battle.battleGetAllStacks(true); + + for (const CStack * stack : initialStacks) + { + trySummonGuardians(battle, stack); + stackEnchantedTrigger(battle, stack); + } + + castOpeningSpells(battle); + + // it is possible that due to opening spells one side was eliminated -> check for end of battle + if (owner->checkBattleStateChanges(battle)) + return; + + startNextRound(battle, true); + activateNextStack(battle); +} + +void BattleFlowProcessor::startNextRound(const CBattleInfoCallback & battle, bool isFirstRound) +{ + BattleNextRound bnr; + bnr.battleID = battle.getBattle()->getBattleID(); + logGlobal->debug("Next round starts"); + gameHandler->sendAndApply(&bnr); + + // operate on copy - removing obstacles will invalidate iterator on 'battle' container + auto obstacles = battle.battleGetAllObstacles(); + for (auto &obstPtr : obstacles) + { + if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) + if (sco->turnsRemaining == 0) + removeObstacle(battle, *obstPtr); + } + + for(auto stack : battle.battleGetAllStacks(true)) + { + if(stack->alive() && !isFirstRound) + stackEnchantedTrigger(battle, stack); + } +} + +const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & battle) +{ + std::vector q; + battle.battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" + + if(q.empty()) + return nullptr; + + if(q.front().empty()) + return nullptr; + + auto next = q.front().front(); + const auto stack = dynamic_cast(next); + + // regeneration takes place before everything else but only during first turn attempt in each round + // also works under blind and similar effects + if(stack && stack->alive() && !stack->waiting) + { + BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); + bte.stackID = stack->unitId(); + bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); + + const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); + if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) + bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); + + if(bte.val) // anything to heal + gameHandler->sendAndApply(&bte); + } + + if(!next || !next->willMove()) + return nullptr; + + return stack; +} + +void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle) +{ + // Find next stack that requires manual control + for (;;) + { + // battle has ended + if (owner->checkBattleStateChanges(battle)) + return; + + const CStack * next = getNextStack(battle); + + if (!next) + { + // No stacks to move - start next round + startNextRound(battle, false); + next = getNextStack(battle); + if (!next) + throw std::runtime_error("Failed to find valid stack to act!"); + } + + BattleUnitsChanged removeGhosts; + removeGhosts.battleID = battle.getBattle()->getBattleID(); + + auto pendingGhosts = battle.battleGetStacksIf([](const CStack * stack){ + return stack->ghostPending; + }); + + for(auto stack : pendingGhosts) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); + + if(!removeGhosts.changedStacks.empty()) + gameHandler->sendAndApply(&removeGhosts); + + gameHandler->turnTimerHandler.onBattleNextStack(battle.getBattle()->getBattleID(), *next); + + if (!tryMakeAutomaticAction(battle, next)) + { + setActiveStack(battle, next); + break; + } + } +} + +bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & battle, const CStack * next) +{ + // check for bad morale => freeze + int nextStackMorale = next->moraleVal(); + if(!next->hadMorale && !next->waited() && nextStackMorale < 0) + { + auto diceSize = VLC->settings()->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) + { + //unit loses its turn - empty freeze action + BattleAction ba; + ba.actionType = EActionType::BAD_MORALE; + ba.side = next->unitSide(); + ba.stackNumber = next->unitId(); + + makeAutomaticAction(battle, next, ba); + return true; + } + } + + if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk + { + logGlobal->trace("Handle Berserk effect"); + std::pair attackInfo = battle.getNearestStack(next); + if (attackInfo.first != nullptr) + { + BattleAction attack; + attack.actionType = EActionType::WALK_AND_ATTACK; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + attack.aimToHex(attackInfo.second); + attack.aimToUnit(attackInfo.first); + + makeAutomaticAction(battle, next, attack); + logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); + } + else + { + makeStackDoNothing(battle, next); + logGlobal->trace("No target found"); + } + return true; + } + + const CGHeroInstance * curOwner = battle.battleGetOwnerHero(next); + const CreatureID stackCreatureId = next->unitType()->getId(); + + if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(stackCreatureId)))) + { + BattleAction attack; + attack.actionType = EActionType::SHOOT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + //TODO: select target by priority + + 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(target == nullptr) + { + makeStackDoNothing(battle, next); + } + else + { + attack.aimToUnit(target); + makeAutomaticAction(battle, next, attack); + } + return true; + } + + if (next->unitType()->getId() == CreatureID::CATAPULT) + { + const auto & attackableBattleHexes = battle.getAttackableBattleHexes(); + + if (attackableBattleHexes.empty()) + { + makeStackDoNothing(battle, next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(CreatureID(CreatureID::CATAPULT)))) + { + BattleAction attack; + attack.actionType = EActionType::CATAPULT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + makeAutomaticAction(battle, next, attack); + return true; + } + } + + if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) + { + TStacks possibleStacks = battle.battleGetStacksIf([=](const CStack * s) + { + return s->unitOwner() == next->unitOwner() && s->canBeHealed(); + }); + + if (possibleStacks.empty()) + { + makeStackDoNothing(battle, next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(CreatureID(CreatureID::FIRST_AID_TENT)))) + { + RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); + const CStack * toBeHealed = possibleStacks.front(); + + BattleAction heal; + heal.actionType = EActionType::STACK_HEAL; + heal.aimToUnit(toBeHealed); + heal.side = next->unitSide(); + heal.stackNumber = next->unitId(); + + makeAutomaticAction(battle, next, heal); + return true; + } + } + + stackTurnTrigger(battle, next); //various effects + + if(next->fear) + { + makeStackDoNothing(battle, next); //end immediately if stack was affected by fear + return true; + } + return false; +} + +bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, const CStack * next) +{ + //check for good morale + auto nextStackMorale = next->moraleVal(); + if( !next->hadMorale + && !next->defending + && !next->waited() + && !next->fear + && next->alive() + && next->canMove() + && nextStackMorale > 0) + { + auto diceSize = VLC->settings()->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) + { + BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); + bte.stackID = next->unitId(); + bte.effect = vstd::to_underlying(BonusType::MORALE); + bte.val = 1; + bte.additionalInfo = 0; + gameHandler->sendAndApply(&bte); //play animation + return true; + } + } + return false; +} + +void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const BattleAction &ba) +{ + const auto * actedStack = battle.battleGetStackByID(ba.stackNumber, false); + const auto * activeStack = battle.battleActiveUnit(); + if (ba.actionType == EActionType::END_TACTIC_PHASE) + { + onTacticsEnded(battle); + return; + } + + + //we're after action, all results applied + + // check whether action has ended the battle + if(owner->checkBattleStateChanges(battle)) + return; + + // tactics - next stack will be selected by player + if(battle.battleGetTacticDist() != 0) + return; + + if (ba.isUnitAction()) + { + assert(activeStack != nullptr); + assert(actedStack != nullptr); + + if (rollGoodMorale(battle, actedStack)) + { + // Good morale - same stack makes 2nd turn + setActiveStack(battle, actedStack); + return; + } + } + else + { + if (activeStack && activeStack->alive()) + { + // this is action made by hero AND unit is alive (e.g. not killed by casted spell) + // keep current active stack for next action + setActiveStack(battle, activeStack); + return; + } + } + + activateNextStack(battle); +} + +void BattleFlowProcessor::makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next) +{ + BattleAction doNothing; + doNothing.actionType = EActionType::NO_ACTION; + doNothing.side = next->unitSide(); + doNothing.stackNumber = next->unitId(); + + makeAutomaticAction(battle, next, doNothing); +} + +bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle, const CStack *stack, BattleAction &ba) +{ + BattleSetActiveStack bsa; + bsa.battleID = battle.getBattle()->getBattleID(); + bsa.stack = stack->unitId(); + bsa.askPlayerInterface = false; + gameHandler->sendAndApply(&bsa); + + bool ret = owner->makeAutomaticBattleAction(battle, ba); + return ret; +} + +void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * st) +{ + auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); + for(auto b : bl) + { + const CSpell * sp = b->subtype.as().toSpell(); + if(!sp) + continue; + + const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); + const int32_t level = ((val > 3) ? (val - 3) : val); + + spells::BattleCast battleCast(&battle, st, spells::Mode::PASSIVE, sp); + //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle + battleCast.setEffectDuration(50); + battleCast.setSpellLevel(level); + spells::Target target; + + if(val > 3) + { + for(auto s : battle.battleGetAllStacks()) + if(battle.battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + target.emplace_back(s); + } + else + { + target.emplace_back(st); + } + battleCast.applyEffects(gameHandler->spellEnv, target, false, true); + } +} + +void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle) +{ + BattleObstaclesChanged obsRem; + obsRem.battleID = battle.getBattle()->getBattleID(); + obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&obsRem); +} + +void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, const CStack *st) +{ + BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); + bte.stackID = st->unitId(); + bte.effect = -1; + bte.val = 0; + bte.additionalInfo = 0; + if (st->alive()) + { + //unbind + if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) + { + bool unbind = true; + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); + auto adjacent = battle.battleAdjacentUnits(st); + + for (auto b : bl) + { + if(b->additionalInfo != CAddInfo::NONE) + { + const CStack * stack = battle.battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent + if(stack) + { + if(vstd::contains(adjacent, stack)) //binding stack is still present + unbind = false; + } + } + else + { + unbind = false; + } + } + if (unbind) + { + BattleSetStackProperty ssp; + ssp.battleID = battle.getBattle()->getBattleID(); + ssp.which = BattleSetStackProperty::UNBIND; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + + if (st->hasBonusOfType(BonusType::POISON)) + { + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) //TODO: what if not?... + { + bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); + if (bte.val < b->val) //(negative) poison effect increases - update it + { + bte.effect = vstd::to_underlying(BonusType::POISON); + gameHandler->sendAndApply(&bte); + } + } + } + if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) + { + const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st)); + const CGHeroInstance * opponentHero = battle.battleGetFightingHero(opponent); + if(opponentHero) + { + ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); + vstd::amin(manaDrained, opponentHero->mana); + if(manaDrained) + { + bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); + bte.val = manaDrained; + bte.additionalInfo = opponentHero->id.getNum(); //for sanity + gameHandler->sendAndApply(&bte); + } + } + } + if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) + { + bool fearsomeCreature = false; + for (const CStack * stack : battle.battleGetAllStacks(true)) + { + if (battle.battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) + { + fearsomeCreature = true; + break; + } + } + if (fearsomeCreature) + { + if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% + { + bte.effect = vstd::to_underlying(BonusType::FEAR); + gameHandler->sendAndApply(&bte); + } + } + } + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + int side = *battle.playerToSide(st->unitOwner()); + if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) + { + bool cast = false; + while(!bl.empty() && !cast) + { + auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); + auto spellID = bonus->subtype.as(); + const CSpell * spell = SpellID(spellID).toSpell(); + bl.remove_if([&bonus](const Bonus * b) + { + return b == bonus.get(); + }); + spells::BattleCast parameters(&battle, st, spells::Mode::ENCHANTER, spell); + parameters.setSpellLevel(bonus->val); + parameters.massive = true; + parameters.smart = true; + //todo: recheck effect level + if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) + { + cast = true; + + int cooldown = bonus->additionalInfo[0]; + BattleSetStackProperty ssp; + ssp.battleID = battle.getBattle()->getBattleID(); + ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; + ssp.absolute = false; + ssp.val = cooldown; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + } + } +} + +void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack) +{ + assert(stack); + + BattleSetActiveStack sas; + sas.battleID = battle.getBattle()->getBattleID(); + sas.stack = stack->unitId(); + gameHandler->sendAndApply(&sas); +} diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h new file mode 100644 index 000000000..929445701 --- /dev/null +++ b/server/battles/BattleFlowProcessor.h @@ -0,0 +1,60 @@ +/* + * BattleFlowProcessor.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 CStack; +struct BattleHex; +class BattleAction; +class CBattleInfoCallback; +struct CObstacleInstance; +namespace battle +{ +class Unit; +} +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class BattleProcessor; + +/// Controls flow of battles - battle startup actions and switching to next stack or next round after actions +class BattleFlowProcessor : boost::noncopyable +{ + BattleProcessor * owner; + CGameHandler * gameHandler; + + const CStack * getNextStack(const CBattleInfoCallback & battle); + + 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 trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack); + void tryPlaceMoats(const CBattleInfoCallback & battle); + void castOpeningSpells(const CBattleInfoCallback & battle); + void activateNextStack(const CBattleInfoCallback & battle); + void startNextRound(const CBattleInfoCallback & battle, bool isFirstRound); + + void stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * stack); + void removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle); + void stackTurnTrigger(const CBattleInfoCallback & battle, const CStack * stack); + void setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack); + + void makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next); + bool makeAutomaticAction(const CBattleInfoCallback & battle, const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + +public: + explicit BattleFlowProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + void onBattleStarted(const CBattleInfoCallback & battle); + void onTacticsEnded(const CBattleInfoCallback & battle); + void onActionMade(const CBattleInfoCallback & battle, const BattleAction & ba); +}; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp new file mode 100644 index 000000000..00a238dd1 --- /dev/null +++ b/server/battles/BattleProcessor.cpp @@ -0,0 +1,316 @@ +/* + * BattleProcessor.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 "BattleProcessor.h" + +#include "BattleActionProcessor.h" +#include "BattleFlowProcessor.h" +#include "BattleResultProcessor.h" + +#include "../CGameHandler.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/TerrainHandler.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/CPlayerState.h" + +BattleProcessor::BattleProcessor(CGameHandler * gameHandler) + : gameHandler(gameHandler) + , flowProcessor(std::make_unique(this)) + , actionsProcessor(std::make_unique(this)) + , resultProcessor(std::make_unique(this)) +{ + setGameHandler(gameHandler); +} + +BattleProcessor::BattleProcessor(): + BattleProcessor(nullptr) +{ +} + +BattleProcessor::~BattleProcessor() = default; + +void BattleProcessor::engageIntoBattle(PlayerColor player) +{ + //notify interfaces + PlayerBlocked pb; + pb.player = player; + pb.reason = PlayerBlocked::UPCOMING_BATTLE; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + 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) +{ + auto battle = gameHandler->gameState()->getBattle(battleID); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + + assert(lastBattleQuery); + + //existing battle query for retying auto-combat + if(lastBattleQuery) + { + const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + + for(int i : {0, 1}) + { + if(heroes[i]) + { + SetMana restoreInitialMana; + restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; + restoreInitialMana.hid = heroes[i]->id; + gameHandler->sendAndApply(&restoreInitialMana); + } + } + + lastBattleQuery->result = std::nullopt; + + assert(lastBattleQuery->belligerents[0] == battle->sides[0].armyObject); + assert(lastBattleQuery->belligerents[1] == battle->sides[1].armyObject); + } + + BattleCancelled bc; + bc.battleID = battleID; + gameHandler->sendAndApply(&bc); + + startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); +} + +void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, + 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; + + auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + + const auto * battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + //add battle bonuses based from player state only when attacks neutral creatures + const auto * attackerInfo = gameHandler->getPlayerState(army1->getOwner(), false); + if(attackerInfo && !army2->getOwner().isValidPlayer()) + { + for(auto bonus : attackerInfo->battleBonuses) + { + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero1->id; + giveBonus.bonus = bonus; + gameHandler->sendAndApply(&giveBonus); + } + } + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + + if (lastBattleQuery) + { + lastBattleQuery->battleID = battleID; + } + else + { + auto newBattleQuery = std::make_shared(gameHandler, battle); + + // store initial mana to reset if battle has been restarted + for(int i : {0, 1}) + if(heroes[i]) + newBattleQuery->initialHeroMana[i] = heroes[i]->mana; + + gameHandler->queries->addQuery(newBattleQuery); + } + + flowProcessor->onBattleStarted(*battle); +} + +void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) +{ + startBattlePrimary(army1, army2, tile, + army1->ID == Obj::HERO ? static_cast(army1) : nullptr, + army2->ID == Obj::HERO ? static_cast(army2) : nullptr, + creatureBank); +} + +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) +{ + const auto & t = *gameHandler->getTile(tile); + TerrainId terrain = t.terType->getId(); + 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")); + + //send info about battles + BattleStart bs; + bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + bs.battleID = gameHandler->gameState()->nextBattleID; + + engageIntoBattle(bs.info->sides[0].color); + engageIntoBattle(bs.info->sides[1].color); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[0].color)); + bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); + + gameHandler->sendAndApply(&bs); + + return bs.battleID; +} + +bool BattleProcessor::checkBattleStateChanges(const CBattleInfoCallback & battle) +{ + //check if drawbridge state need to be changes + if (battle.battleGetSiegeLevel() > 0) + updateGateState(battle); + + if (resultProcessor->battleIsEnding(battle)) + return true; + + //check if battle ended + if (auto result = battle.battleIsFinished()) + { + setBattleResult(battle, EBattleResult::NORMAL, *result); + return true; + } + + return false; +} + +void BattleProcessor::updateGateState(const CBattleInfoCallback & battle) +{ + // GATE_BRIDGE - leftmost tile, located over moat + // GATE_OUTER - central tile, mostly covered by gate image + // GATE_INNER - rightmost tile, inside the walls + + // GATE_OUTER or GATE_INNER: + // - if defender moves unit on these tiles, bridge will open + // - if there is a creature (dead or alive) on these tiles, bridge will always remain open + // - blocked to attacker if bridge is closed + + // GATE_BRIDGE + // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) + // - if Force Field is cast here, bridge can't open (but can close, in any town) + // - deals moat damage to attacker if bridge is closed (fortress only) + + bool hasForceFieldOnBridge = !battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty(); + bool hasStackAtGateInner = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr; + bool hasStackAtGateOuter = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr; + bool hasStackAtGateBridge = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr; + bool hasWideMoat = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + BattleUpdateGateState db; + db.state = battle.battleGetGateState(); + db.battleID = battle.getBattle()->getBattleID(); + + if (battle.battleGetWallState(EWallPart::GATE) == EWallState::DESTROYED) + { + db.state = EGateState::DESTROYED; + } + else if (db.state == EGateState::OPENED) + { + bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; + bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; + + if (gateCanClose) + db.state = EGateState::CLOSED; + else + db.state = EGateState::OPENED; + } + else // CLOSED or BLOCKED + { + bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; + + if (gateBlocked) + db.state = EGateState::BLOCKED; + else + db.state = EGateState::CLOSED; + } + + if (db.state != battle.battleGetGateState()) + gameHandler->sendAndApply(&db); +} + +bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction &ba) +{ + const auto * battle = gameHandler->gameState()->getBattle(battleID); + + if (!battle) + return false; + + bool result = actionsProcessor->makePlayerBattleAction(*battle, player, ba); + if (gameHandler->gameState()->getBattle(battleID) != nullptr && !resultProcessor->battleIsEnding(*battle)) + flowProcessor->onActionMade(*battle, ba); + return result; +} + +void BattleProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) +{ + resultProcessor->setBattleResult(battle, resultType, victoriusSide); + resultProcessor->endBattle(battle); +} + +bool BattleProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction &ba) +{ + return actionsProcessor->makeAutomaticBattleAction(battle, ba); +} + +void BattleProcessor::endBattleConfirm(const BattleID & battleID) +{ + auto battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + if (!battle) + return; + + resultProcessor->endBattleConfirm(*battle); +} + +void BattleProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult &result) +{ + resultProcessor->battleAfterLevelUp(battleID, result); +} + +void BattleProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; + + actionsProcessor->setGameHandler(newGameHandler); + flowProcessor->setGameHandler(newGameHandler); + resultProcessor->setGameHandler(newGameHandler); +} diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h new file mode 100644 index 000000000..c7423eeb2 --- /dev/null +++ b/server/battles/BattleProcessor.h @@ -0,0 +1,83 @@ +/* + * BattleProcessor.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/GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +class BattleAction; +class int3; +class CBattleInfoCallback; +struct BattleResult; +class BattleID; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class CBattleQuery; +class BattleActionProcessor; +class BattleFlowProcessor; +class BattleResultProcessor; + +/// Main class for battle handling. Contains all public interface for battles that is accessible from outside, e.g. for CGameHandler +class BattleProcessor : boost::noncopyable +{ + friend class BattleActionProcessor; + friend class BattleFlowProcessor; + friend class BattleResultProcessor; + + CGameHandler * gameHandler; + std::unique_ptr actionsProcessor; + std::unique_ptr flowProcessor; + std::unique_ptr resultProcessor; + + void updateGateState(const CBattleInfoCallback & battle); + 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); + + bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); + + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); + +public: + explicit BattleProcessor(CGameHandler * gameHandler); + BattleProcessor(); + ~BattleProcessor(); + + void setGameHandler(CGameHandler * gameHandler); + + /// 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); + /// 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); + /// 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); + + /// Processing of incoming battle action netpack + bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba); + + /// Applies results of a battle once player agrees to them + void endBattleConfirm(const BattleID & battleID); + /// Applies results of a battle after potential levelup + void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result); + + template void serialize(Handler &h, const int version) + { + + } + +}; + diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp new file mode 100644 index 000000000..52c4e7e6b --- /dev/null +++ b/server/battles/BattleResultProcessor.cpp @@ -0,0 +1,599 @@ +/* + * BattleResultProcessor.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 "BattleResultProcessor.h" + +#include "../CGameHandler.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CStack.h" +#include "../../lib/GameSettings.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/SideInBattle.h" +#include "../../lib/gameState/CGameState.h" +#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" + +BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner) +// : owner(owner) + : gameHandler(nullptr) +{ +} + +void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle): + army(battle.battleGetArmyObject(sideInBattle)) +{ + heroWithDeadCommander = ObjectInstanceID(); + + PlayerColor color = battle.sideToPlayer(sideInBattle); + + for(const CStack * stConst : battle.battleGetAllStacks(true)) + { + // Use const cast - in order to call non-const "takeResurrected" for proper calculation of casualties + // TODO: better solution + CStack * st = const_cast(stConst); + + if(st->summoned) //don't take into account temporary summoned stacks + continue; + if(st->unitOwner() != color) //remove only our stacks + continue; + + logGlobal->debug("Calculating casualties for %s", st->nodeName()); + + st->health.takeResurrected(); + + if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) + { + logGlobal->debug("Ignored arrow towers stack."); + } + else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) + { + auto warMachine = st->unitType()->warMachine; + + if(warMachine == ArtifactID::NONE) + { + logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); + } + //catapult artifact remain even if "creature" killed in siege + else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) + { + logGlobal->debug("War machine has been destroyed"); + auto hero = dynamic_ptr_cast (army); + if (hero) + removedWarMachines.push_back (ArtifactLocation(hero->id, hero->getArtPos(warMachine, true))); + else + logGlobal->error("War machine in army without hero"); + } + } + else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) + { + if(st->alive() && st->getCount() > 0) + { + logGlobal->debug("Permanently summoned %d units.", st->getCount()); + const CreatureID summonedType = st->creatureId(); + summoned[summonedType] += st->getCount(); + } + } + else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + if (nullptr == st->base) + { + logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); + } + else + { + auto c = dynamic_cast (st->base); + if(c) + { + auto h = dynamic_cast (army); + if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) + { + logGlobal->debug("Commander is dead."); + heroWithDeadCommander = army->id; //TODO: unify commander handling + } + } + else + logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); + } + } + else if(st->base && !army->slotEmpty(st->unitSlot())) + { + logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); + if(st->getCount() == 0 || !st->alive()) + { + logGlobal->debug("Stack has been destroyed."); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); + } + else if(st->getCount() < army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + else if(st->getCount() > army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + } + else + { + logGlobal->warn("Unable to process stack: %s", st->nodeName()); + } + } +} + +void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) +{ + for (TStackAndItsNewCount &ncount : newStackCounts) + { + if (ncount.second > 0) + gh->changeStackCount(ncount.first, ncount.second, true); + else + gh->eraseStack(ncount.first, true); + } + for (auto summoned_iter : summoned) + { + SlotID slot = army->getSlotFor(summoned_iter.first); + if (slot.validSlot()) + { + StackLocation location(army, slot); + gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); + } + else + { + //even if it will be possible to summon anything permanently it should be checked for free slot + //necromancy is handled separately + gh->complain("No free slot to put summoned creature"); + } + } + for (auto al : removedWarMachines) + { + gh->removeArtifact(al); + } + if (heroWithDeadCommander != ObjectInstanceID()) + { + SetCommanderProperty scp; + scp.heroid = heroWithDeadCommander; + scp.which = SetCommanderProperty::ALIVE; + scp.amount = 0; + gh->sendAndApply(&scp); + } +} + +FinishingBattleHelper::FinishingBattleHelper(const CBattleInfoCallback & info, const BattleResult & result, int remainingBattleQueriesCount) +{ + if (result.winner == BattleSide::ATTACKER) + { + winnerHero = info.getBattle()->getSideHero(BattleSide::ATTACKER); + loserHero = info.getBattle()->getSideHero(BattleSide::DEFENDER); + victor = info.getBattle()->getSidePlayer(BattleSide::ATTACKER); + loser = info.getBattle()->getSidePlayer(BattleSide::DEFENDER); + } + else + { + winnerHero = info.getBattle()->getSideHero(BattleSide::DEFENDER); + loserHero = info.getBattle()->getSideHero(BattleSide::ATTACKER); + victor = info.getBattle()->getSidePlayer(BattleSide::DEFENDER); + loser = info.getBattle()->getSidePlayer(BattleSide::ATTACKER); + } + + winnerSide = result.winner; + + this->remainingBattleQueriesCount = remainingBattleQueriesCount; +} + +//FinishingBattleHelper::FinishingBattleHelper() +//{ +// winnerHero = loserHero = nullptr; +// winnerSide = 0; +// remainingBattleQueriesCount = 0; +//} + +void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) +{ + auto const & giveExp = [](BattleResult &r) + { + if (r.winner > 1) + { + // 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[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; + } + }; + + LOG_TRACE(logGlobal); + + auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get(); + const auto * heroAttacker = battle.battleGetFightingHero(BattleSide::ATTACKER); + const auto * heroDefender = battle.battleGetFightingHero(BattleSide::DEFENDER); + + //Fill BattleResult structure with exp info + giveExp(*battleResult); + + if (battleResult->result == EBattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped + { + if(heroAttacker) + battleResult->exp[1] += 500; + if(heroDefender) + battleResult->exp[0] += 500; + } + + // Give 500 exp to winner if a town was conquered during the battle + const auto * defendedTown = battle.battleGetDefendedTown(); + if (defendedTown && battleResult->winner == BattleSide::ATTACKER) + battleResult->exp[BattleSide::ATTACKER] += 500; + + if(heroAttacker) + battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill + if(heroDefender) + battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); + + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + if (!battleQuery) + { + logGlobal->error("Cannot find battle query!"); + gameHandler->complain("Player " + boost::lexical_cast(battle.sideToPlayer(0)) + " has no battle query at the top!"); + return; + } + + battleQuery->result = std::make_optional(*battleResult); + + //Check how many battle gameHandler->queries were created (number of players blocked by battle) + const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; + + assert(finishingBattles.count(battle.getBattle()->getBattleID()) == 0); + finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique(battle, *battleResult, queriedPlayers); + + // in battles against neutrals, 1st player can ask to replay battle manually + if (!battle.sideToPlayer(1).isValidPlayer()) + { + auto battleDialogQuery = std::make_shared(gameHandler, battle.getBattle()); + battleResult->queryID = battleDialogQuery->queryID; + gameHandler->queries->addQuery(battleDialogQuery); + } + else + battleResult->queryID = QueryID::NONE; + + //set same battle result for all gameHandler->queries + for(auto q : gameHandler->queries->allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery && otherBattleQuery->battleID == battle.getBattle()->getBattleID()) + otherBattleQuery->result = battleQuery->result; + } + + gameHandler->turnTimerHandler.onBattleEnd(battle.getBattle()->getBattleID()); + gameHandler->sendAndApply(battleResult); + + if (battleResult->queryID == QueryID::NONE) + endBattleConfirm(battle); +} + +void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) +{ + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); + if(!battleQuery) + { + logGlobal->trace("No battle query, battle end was confirmed by another player"); + return; + } + + auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get(); + auto * finishingBattle = finishingBattles.at(battle.getBattle()->getBattleID()).get(); + + const EBattleResult result = battleResult->result; + + //calculate casualties before deleting 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, battle.battleGetArmyObject(0)->getOwner()); + gameHandler->sendAndApply(&ro); + } + if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed + { + RemoveObject ro(finishingBattle->winnerHero->id, battle.battleGetArmyObject(0)->getOwner()); + gameHandler->sendAndApply(&ro); + } + + if(battleResult->winner == BattleSide::DEFENDER + && finishingBattle->winnerHero + && finishingBattle->winnerHero->visitedTown + && !finishingBattle->winnerHero->inTownGarrison + && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) + { + gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place + } + //give exp + if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); + + BattleResultAccepted raccepted; + raccepted.battleID = battle.getBattle()->getBattleID(); + raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(0)); + raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(1)); + raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(0)); + raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(1)); + raccepted.heroResult[0].exp = battleResult->exp[0]; + raccepted.heroResult[1].exp = battleResult->exp[1]; + 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 +} + +void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult & result) +{ + LOG_TRACE(logGlobal); + + assert(finishingBattles.count(battleID) != 0); + if(finishingBattles.count(battleID) == 0) + return; + + auto & finishingBattle = finishingBattles[battleID]; + + finishingBattle->remainingBattleQueriesCount--; + logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); + + if (finishingBattle->remainingBattleQueriesCount > 0) + //Battle results will be handled when all battle gameHandler->queries are closed + return; + + //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible + // but the battle consequences are applied after final player is unblocked. Hard to abuse... + // Still, it looks like a hole. + + // Necromancy if applicable. + 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(); + + if (necroSlot != SlotID()) + { + finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); + gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + } + + BattleResultsApplied resultsApplied; + resultsApplied.battleID = battleID; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + 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->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + + if (result.result == EBattleResult::ESCAPE) + gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + + if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() + && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) + { + RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->winnerHero->getOwner()); + gameHandler->sendAndApply(&ro); + + if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) + gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); + } + + finishingBattles.erase(battleID); + battleResults.erase(battleID); +} + +void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) +{ + assert(battleResults.count(battle.getBattle()->getBattleID()) == 0); + + battleResults[battle.getBattle()->getBattleID()] = std::make_unique(); + + auto & battleResult = battleResults[battle.getBattle()->getBattleID()]; + battleResult->battleID = battle.getBattle()->getBattleID(); + battleResult->result = resultType; + battleResult->winner = victoriusSide; //surrendering side loses + + for(const auto & st : battle.battleGetAllStacks(true)) //setting casualties + { + si32 killed = st->getKilled(); + if(killed > 0) + battleResult->casualties[st->unitSide()][st->creatureId()] += killed; + } +} + +bool BattleResultProcessor::battleIsEnding(const CBattleInfoCallback & battle) const +{ + return battleResults.count(battle.getBattle()->getBattleID()) != 0; +} diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h new file mode 100644 index 000000000..7616c1775 --- /dev/null +++ b/server/battles/BattleResultProcessor.h @@ -0,0 +1,82 @@ +/* + * BattleProcessor.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/GameConstants.h" +#include "../../lib/networkPacks/StackLocation.h" +#include "../../lib/networkPacks/ArtifactLocation.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct SideInBattle; +struct BattleResult; +class CBattleInfoCallback; +class CGHeroInstance; +VCMI_LIB_NAMESPACE_END + +class CBattleQuery; +class BattleProcessor; +class CGameHandler; + +struct CasualtiesAfterBattle +{ + using TStackAndItsNewCount = std::pair; + using TSummoned = std::map; + const CArmedInstance * army; + std::vector newStackCounts; + std::vector removedWarMachines; + TSummoned summoned; + ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations + + CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle); + void updateArmy(CGameHandler * gh); +}; + +struct FinishingBattleHelper +{ + FinishingBattleHelper(const CBattleInfoCallback & battle, const BattleResult & result, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} + + const CGHeroInstance *winnerHero, *loserHero; + PlayerColor victor, loser; + ui8 winnerSide; + + int remainingBattleQueriesCount; + + template void serialize(Handler &h, const int version) + { + h & winnerHero; + h & loserHero; + h & victor; + h & loser; + h & winnerSide; + h & remainingBattleQueriesCount; + } +}; + +class BattleResultProcessor : boost::noncopyable +{ + // BattleProcessor * owner; + CGameHandler * gameHandler; + + std::map> battleResults; + std::map> finishingBattles; + +public: + explicit BattleResultProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + bool battleIsEnding(const CBattleInfoCallback & battle) const; + + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int 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/battles/ServerBattleCallback.cpp b/server/battles/ServerBattleCallback.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/server/battles/ServerBattleCallback.h b/server/battles/ServerBattleCallback.h new file mode 100644 index 000000000..e69de29bb diff --git a/server/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp similarity index 78% rename from server/HeroPoolProcessor.cpp rename to server/processors/HeroPoolProcessor.cpp index f32a88be0..39569a470 100644 --- a/server/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -10,17 +10,19 @@ #include "StdInc.h" #include "HeroPoolProcessor.h" -#include "CGameHandler.h" +#include "TurnOrderProcessor.h" +#include "../CGameHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameSettings.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/gameState/TavernHeroesPool.h" -#include "../lib/gameState/TavernSlot.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameSettings.h" +#include "../../lib/StartInfo.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" HeroPoolProcessor::HeroPoolProcessor() : gameHandler(nullptr) @@ -32,29 +34,6 @@ HeroPoolProcessor::HeroPoolProcessor(CGameHandler * gameHandler) { } -bool HeroPoolProcessor::playerEndedTurn(const PlayerColor & player) -{ - // our player is acting right now and have not ended turn - if (player == gameHandler->gameState()->currentPlayer) - return false; - - auto turnOrder = gameHandler->generatePlayerTurnOrder(); - - for (auto const & entry : turnOrder) - { - // our player is yet to start turn - if (entry == gameHandler->gameState()->currentPlayer) - return false; - - // our player have finished turn - if (entry == player) - return true; - } - - assert(false); - return false; -} - TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID) { const auto & heroesPool = gameHandler->gameState()->heroesPool; @@ -71,8 +50,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(HeroTypeID(heroes[0]->subID)); - auto roleRight = heroesPool->getSlotRole(HeroTypeID(heroes[1]->subID)); + auto roleLeft = heroesPool->getSlotRole(heroes[0]->getHeroType()); + auto roleRight = heroesPool->getSlotRole(heroes[1]->getHeroType()); if (roleLeft > roleRight) return TavernHeroSlot::RANDOM; @@ -90,28 +69,22 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (playerEndedTurn(color)) - sah.roleID = TavernSlotRole::SURRENDERED_TODAY; - else - sah.roleID = TavernSlotRole::SURRENDERED; + sah.roleID = TavernSlotRole::SURRENDERED; sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->subID; + sah.hid = hero->getHeroType(); gameHandler->sendAndApply(&sah); } void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (playerEndedTurn(color)) - sah.roleID = TavernSlotRole::RETREATED_TODAY; - else - sah.roleID = TavernSlotRole::RETREATED; + sah.roleID = TavernSlotRole::RETREATED; sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->subID; + sah.hid = hero->getHeroType(); sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); @@ -138,7 +111,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe if (newHero) { - sah.hid = newHero->subID; + sah.hid = newHero->getHeroType(); if (giveArmy) { @@ -154,33 +127,17 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe } else { - sah.hid = -1; + sah.hid = HeroTypeID::NONE; } gameHandler->sendAndApply(&sah); } void HeroPoolProcessor::onNewWeek(const PlayerColor & color) { - const auto & heroesPool = gameHandler->gameState()->heroesPool; - const auto & heroes = heroesPool->getHeroesFor(color); - - const auto nativeSlotRole = heroes.size() < 1 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[0]->type->getId()); - const auto randomSlotRole = heroes.size() < 2 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[1]->type->getId()); - - bool resetNativeSlot = nativeSlotRole != TavernSlotRole::RETREATED_TODAY && nativeSlotRole != TavernSlotRole::SURRENDERED_TODAY; - bool resetRandomSlot = randomSlotRole != TavernSlotRole::RETREATED_TODAY && randomSlotRole != TavernSlotRole::SURRENDERED_TODAY; - - if (resetNativeSlot) - clearHeroFromSlot(color, TavernHeroSlot::NATIVE); - - if (resetRandomSlot) - clearHeroFromSlot(color, TavernHeroSlot::RANDOM); - - if (resetNativeSlot) - selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true); - - if (resetRandomSlot) - selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); + clearHeroFromSlot(color, TavernHeroSlot::NATIVE); + clearHeroFromSlot(color, TavernHeroSlot::RANDOM); + selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true); + selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); } bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player) @@ -218,6 +175,14 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if(mapObject->ID == Obj::TAVERN) { + const CGHeroInstance * visitor = gameHandler->getVisitingHero(mapObject); + + if (!visitor || visitor->getOwner() != player) + { + gameHandler->complain("Can't buy hero in tavern not being visited!"); + return false; + } + if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!")) return false; } @@ -228,7 +193,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy for(const auto & hero : recruitableHeroes) { - if(hero->subID == heroToRecruit) + if(hero->getHeroType() == heroToRecruit) recruitedHero = hero; } @@ -241,13 +206,13 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy HeroRecruited hr; hr.tid = mapObject->id; - hr.hid = recruitedHero->subID; + hr.hid = recruitedHero->getHeroType(); hr.player = player; hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) { //Create a new boat for hero - gameHandler->createObject(targetPos , Obj::BOAT, recruitedHero->getBoatType().getNum()); + gameHandler->createObject(targetPos, player, Obj::BOAT, recruitedHero->getBoatType().getNum()); hr.boatId = gameHandler->getTopObj(targetPos)->id; } @@ -314,9 +279,9 @@ std::vector HeroPoolProcessor::findAvailableHeroesFor(const Pl const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerColor & player) { - if(player >= PlayerColor::PLAYER_LIMIT) + if(!player.isValidPlayer()) { - logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr()); + logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.toString()); return nullptr; } @@ -329,7 +294,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo if(potentialClasses.empty()) { - logGlobal->error("There are no heroes available for player %s!", player.getStr()); + logGlobal->error("There are no heroes available for player %s!", player.toString()); return nullptr; } @@ -350,7 +315,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo if (possibleClasses.empty()) { - logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr()); + logGlobal->error("Cannot pick native hero for %s. Picking any...", player.toString()); possibleClasses = potentialClasses; } @@ -386,6 +351,17 @@ CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player)); } +CRandomGenerator & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTypeID & hero) +{ + if (heroSeed.count(hero) == 0) + { + int seed = gameHandler->getRandomGenerator().nextInt(); + heroSeed.emplace(hero, std::make_unique(seed)); + } + + return *heroSeed.at(hero); +} + CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) { if (playerSeed.count(player) == 0) diff --git a/server/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h similarity index 90% rename from server/HeroPoolProcessor.h rename to server/processors/HeroPoolProcessor.h index dc69a7cf3..eed300a79 100644 --- a/server/HeroPoolProcessor.h +++ b/server/processors/HeroPoolProcessor.h @@ -29,6 +29,9 @@ class HeroPoolProcessor : boost::noncopyable /// per-player random generators std::map> playerSeed; + /// per-hero random generators used to randomize skills + std::map> heroSeed; + void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot); void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy); @@ -43,7 +46,6 @@ class HeroPoolProcessor : boost::noncopyable TavernHeroSlot selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID); - bool playerEndedTurn(const PlayerColor & player); public: CGameHandler * gameHandler; @@ -55,6 +57,8 @@ public: void onNewWeek(const PlayerColor & color); + CRandomGenerator & getHeroSkillsRandomGenerator(const HeroTypeID & hero); + /// Incoming net pack handling bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player); @@ -62,5 +66,6 @@ public: { // h & gameHandler; // FIXME: make this work instead of using deserializationFix in gameHandler h & playerSeed; + h & heroSeed; } }; diff --git a/server/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp similarity index 89% rename from server/PlayerMessageProcessor.cpp rename to server/processors/PlayerMessageProcessor.cpp index f044c17b6..030169019 100644 --- a/server/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -10,19 +10,22 @@ #include "StdInc.h" #include "PlayerMessageProcessor.h" -#include "CGameHandler.h" -#include "CVCMIServer.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameConstants.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/mapObjects/CGTownInstance.h" +#include "../../lib/serializer/Connection.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameConstants.h" +#include "../../lib/StartInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/modding/ModScope.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/StackLocation.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) @@ -69,7 +72,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st if(words[1] == "exit" || words[1] == "quit" || words[1] == "end") { broadcastSystemMessage("game was terminated"); - gameHandler->gameLobby()->state = EServerState::SHUTDOWN; + gameHandler->gameLobby()->setState(EServerState::SHUTDOWN); return true; } @@ -90,7 +93,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st { for(auto & c : gameHandler->connections) { - if(c.first.getStr(false) == playername) + if(c.first.toString() == playername) playerToKick = c.first; } } @@ -107,11 +110,18 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st } if(words.size() == 2 && words[1] == "cheaters") { - if (cheaters.empty()) - broadcastSystemMessage("No cheaters registered!"); + int playersCheated = 0; + for (const auto & player : gameHandler->gameState()->players) + { + if(player.second.cheated) + { + broadcastSystemMessage("Player " + player.first.toString() + " is cheater!"); + playersCheated++; + } + } - for (auto const & entry : cheaters) - broadcastSystemMessage("Player " + entry.getStr() + " is cheater!"); + if (!playersCheated) + broadcastSystemMessage("No cheaters registered!"); return true; } @@ -129,13 +139,13 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); ///Give all spells with bonus (to allow banned spells) - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero->id.getNum(); - giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0); + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero->id; + giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, BonusSourceID()); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { - giveBonus.bonus.subtype = level; + giveBonus.bonus.subtype = BonusCustomSubtype::spellLevel(level); gameHandler->sendAndApply(&giveBonus); } @@ -179,7 +189,7 @@ void PlayerMessageProcessor::cheatGiveArmy(PlayerColor player, const CGHeroInsta { } - std::optional creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false); + std::optional creatureId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", creatureIdentifier, false); if(creatureId.has_value()) { @@ -220,7 +230,7 @@ void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHero { for (auto const & word : words) { - auto artID = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "artifact", word, false); + auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false); if(artID && VLC->arth->objects[*artID]) gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[*artID], ArtifactPosition::FIRST_AVAILABLE); } @@ -290,11 +300,11 @@ void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInsta gameHandler->sendAndApply(&smp); - GiveBonus gb(GiveBonus::ETarget::HERO); + GiveBonus gb(GiveBonus::ETarget::OBJECT); gb.bonus.type = BonusType::FREE_SHIP_BOARDING; gb.bonus.duration = BonusDuration::ONE_DAY; gb.bonus.source = BonusSource::OTHER; - gb.id = hero->id.getNum(); + gb.id = hero->id; gameHandler->giveHeroBonus(&gb); } @@ -337,7 +347,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player) void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) { FoWChange fc; - fc.mode = reveal; + fc.mode = reveal ? ETileVisibility::REVEALED : ETileVisibility::HIDDEN; fc.player = player; const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap; const auto & mapSize = gameHandler->gameState()->getMapSize(); @@ -347,7 +357,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) for(int z = 0; z < mapSize.z; z++) for(int x = 0; x < mapSize.x; x++) for(int y = 0; y < mapSize.y; y++) - if(!(*fowMap)[z][x][y] || !fc.mode) + if(!(*fowMap)[z][x][y] || fc.mode == ETileVisibility::HIDDEN) hlp_tab[lastUnc++] = int3(x, y, z); fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); @@ -404,12 +414,15 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo if (words.front() == "ai" && i.second.human) continue; - if (words.front() != "all" && words.front() != i.first.getStr()) + if (words.front() != "all" && words.front() != i.first.toString()) continue; std::vector parameters = words; - cheaters.insert(i.first); + PlayerCheated pc; + pc.player = i.first; + gameHandler->sendAndApply(&pc); + playerTargetedCheat = true; parameters.erase(parameters.begin()); @@ -425,10 +438,13 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo executeCheatCode(cheatName, i.first, h->id, parameters); } + PlayerCheated pc; + pc.player = player; + gameHandler->sendAndApply(&pc); + if (!playerTargetedCheat) executeCheatCode(cheatName, player, currObj, words); - - cheaters.insert(player); + return true; } diff --git a/server/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h similarity index 96% rename from server/PlayerMessageProcessor.h rename to server/processors/PlayerMessageProcessor.h index bb9335676..47d2a8a4a 100644 --- a/server/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/GameConstants.h" +#include "../../lib/GameConstants.h" VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; @@ -21,8 +21,6 @@ class CGameHandler; class PlayerMessageProcessor { - std::set cheaters; - void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector & arguments ); bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj); bool handleHostCommand(PlayerColor player, const std::string & message); @@ -60,6 +58,5 @@ public: template void serialize(Handler &h, const int version) { - h & cheaters; } }; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp new file mode 100644 index 000000000..db3969124 --- /dev/null +++ b/server/processors/TurnOrderProcessor.cpp @@ -0,0 +1,334 @@ +/* + * TurnOrderProcessor.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 "TurnOrderProcessor.h" + +#include "../queries/QueriesProcessor.h" +#include "../queries/MapQueries.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" + +#include "../../lib/CPlayerState.h" +#include "../../lib/pathfinder/CPathfinder.h" +#include "../../lib/pathfinder/PathfinderOptions.h" + +TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): + gameHandler(owner) +{ + +} + +int TurnOrderProcessor::simturnsTurnsMaxLimit() const +{ + return gameHandler->getStartInfo()->simturnsInfo.optionalTurns; +} + +int TurnOrderProcessor::simturnsTurnsMinLimit() const +{ + return gameHandler->getStartInfo()->simturnsInfo.requiredTurns; +} + +void TurnOrderProcessor::updateContactStatus() +{ + blockedContacts.clear(); + + assert(actedPlayers.empty()); + assert(actingPlayers.empty()); + + for (auto left : awaitingPlayers) + { + for(auto right : awaitingPlayers) + { + if (left == right) + continue; + + if (computeCanActSimultaneously(left, right)) + blockedContacts.push_back({left, right}); + } + } +} + +bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const +{ + // TODO: refactor, cleanup and optimize + + boost::multi_array leftReachability; + boost::multi_array rightReachability; + + int3 mapSize = gameHandler->getMapSize(); + + leftReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + rightReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + const auto * leftInfo = gameHandler->getPlayerState(left, false); + const auto * rightInfo = gameHandler->getPlayerState(right, false); + + for(const auto & hero : leftInfo->heroes) + { + CPathsInfo out(mapSize, hero); + auto config = std::make_shared(out, gameHandler->gameState(), hero); + CPathfinder pathfinder(gameHandler->gameState(), config); + pathfinder.calculatePaths(); + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (out.getNode({x,y,z})->reachable()) + leftReachability[z][x][y] = true; + } + + for(const auto & hero : rightInfo->heroes) + { + CPathsInfo out(mapSize, hero); + auto config = std::make_shared(out, gameHandler->gameState(), hero); + CPathfinder pathfinder(gameHandler->gameState(), config); + pathfinder.calculatePaths(); + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (out.getNode({x,y,z})->reachable()) + rightReachability[z][x][y] = true; + } + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (leftReachability[z][x][y] && rightReachability[z][x][y]) + return true; + + return false; +} + +bool TurnOrderProcessor::isContactAllowed(PlayerColor active, PlayerColor waiting) const +{ + assert(active != waiting); + return !vstd::contains(blockedContacts, PlayerPair{active, waiting}); +} + +bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const +{ + const auto * activeInfo = gameHandler->getPlayerState(active, false); + const auto * waitingInfo = gameHandler->getPlayerState(waiting, false); + + assert(active != waiting); + assert(activeInfo); + assert(waitingInfo); + + if (gameHandler->hasBothPlayersAtSameConnection(active, waiting)) + { + if (!gameHandler->getStartInfo()->simturnsInfo.allowHumanWithAI) + return false; + + // only one AI and one human can play simultaneoulsy from single connection + if (activeInfo->human == waitingInfo->human) + return false; + } + + if (gameHandler->getDate(Date::DAY) < simturnsTurnsMinLimit()) + return true; + + if (gameHandler->getDate(Date::DAY) > simturnsTurnsMaxLimit()) + return false; + + if (playersInContact(active, waiting)) + return false; + + return true; +} + +bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) const +{ + const auto * leftInfo = gameHandler->getPlayerState(left, false); + const auto * rightInfo = gameHandler->getPlayerState(right, false); + + assert(left != right); + assert(leftInfo && rightInfo); + + if (!leftInfo) + return false; + if (!rightInfo) + return true; + + if (leftInfo->isHuman() && !rightInfo->isHuman()) + return true; + + if (!leftInfo->isHuman() && rightInfo->isHuman()) + return false; + + return false; +} + +bool TurnOrderProcessor::canStartTurn(PlayerColor which) const +{ + for (auto player : awaitingPlayers) + { + if (player != which && mustActBefore(player, which)) + return false; + } + + for (auto player : actingPlayers) + { + if (player != which && isContactAllowed(player, which)) + return false; + } + + return true; +} + +void TurnOrderProcessor::doStartNewDay() +{ + assert(awaitingPlayers.empty()); + assert(actingPlayers.empty()); + + bool activePlayer = false; + for (auto player : actedPlayers) + { + if (gameHandler->getPlayerState(player)->status == EPlayerStatus::INGAME) + activePlayer = true; + } + + if(!activePlayer) + gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED); + + std::swap(actedPlayers, awaitingPlayers); + + gameHandler->onNewTurn(); + updateContactStatus(); + tryStartTurnsForPlayers(); +} + +void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) +{ + assert(gameHandler->getPlayerState(which)); + assert(gameHandler->getPlayerState(which)->status == EPlayerStatus::INGAME); + + //Note: on game load, "actingPlayer" might already contain list of players + actingPlayers.insert(which); + awaitingPlayers.erase(which); + gameHandler->onPlayerTurnStarted(which); + + auto turnQuery = std::make_shared(gameHandler, which); + gameHandler->queries->addQuery(turnQuery); + + PlayerStartsTurn pst; + pst.player = which; + pst.queryID = turnQuery->queryID; + gameHandler->sendAndApply(&pst); + + assert(!actingPlayers.empty()); +} + +void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) +{ + assert(isPlayerMakingTurn(which)); + assert(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME); + + actingPlayers.erase(which); + actedPlayers.insert(which); + + PlayerEndsTurn pet; + pet.player = which; + gameHandler->sendAndApply(&pet); + + if (!awaitingPlayers.empty()) + tryStartTurnsForPlayers(); + + if (actingPlayers.empty()) + doStartNewDay(); + + assert(!actingPlayers.empty()); +} + +void TurnOrderProcessor::addPlayer(PlayerColor which) +{ + awaitingPlayers.insert(which); +} + +void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) +{ + awaitingPlayers.erase(which); + actingPlayers.erase(which); + actedPlayers.erase(which); + + if (!awaitingPlayers.empty()) + tryStartTurnsForPlayers(); + + if (actingPlayers.empty()) + doStartNewDay(); +} + +bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) +{ + if (!isPlayerMakingTurn(which)) + { + gameHandler->complain("Can not end turn for player that is not acting!"); + return false; + } + + if(gameHandler->getPlayerStatus(which) != EPlayerStatus::INGAME) + { + gameHandler->complain("Can not end turn for player that is not in game!"); + return false; + } + + if(gameHandler->queries->topQuery(which) != nullptr) + { + gameHandler->complain("Cannot end turn before resolving queries!"); + return false; + } + + gameHandler->onPlayerTurnEnded(which); + + // it is possible that player have lost - e.g. spent 7 days without town + // in this case - don't call doEndPlayerTurn - turn transfer was already handled by onPlayerEndsGame + if(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME) + doEndPlayerTurn(which); + + return true; +} + +void TurnOrderProcessor::onGameStarted() +{ + if (actingPlayers.empty()) + updateContactStatus(); + + // this may be game load - send notification to players that they can act + auto actingPlayersCopy = actingPlayers; + for (auto player : actingPlayersCopy) + doStartPlayerTurn(player); + + tryStartTurnsForPlayers(); +} + +void TurnOrderProcessor::tryStartTurnsForPlayers() +{ + auto awaitingPlayersCopy = awaitingPlayers; + for (auto player : awaitingPlayersCopy) + { + if (canStartTurn(player)) + doStartPlayerTurn(player); + } +} + +bool TurnOrderProcessor::isPlayerAwaitsTurn(PlayerColor which) const +{ + return vstd::contains(awaitingPlayers, which); +} + +bool TurnOrderProcessor::isPlayerMakingTurn(PlayerColor which) const +{ + return vstd::contains(actingPlayers, which); +} + +bool TurnOrderProcessor::isPlayerAwaitsNewDay(PlayerColor which) const +{ + return vstd::contains(actedPlayers, which); +} diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h new file mode 100644 index 000000000..378ed007f --- /dev/null +++ b/server/processors/TurnOrderProcessor.h @@ -0,0 +1,100 @@ +/* + * TurnOrderProcessor.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/GameConstants.h" + +class CGameHandler; + +class TurnOrderProcessor : boost::noncopyable +{ + CGameHandler * gameHandler; + + struct PlayerPair + { + PlayerColor a; + PlayerColor b; + + bool operator == (const PlayerPair & other) const + { + return (a == other.a && b == other.b) || (a == other.b && b == other.a); + } + + template + void serialize(Handler & h, const int version) + { + h & a; + h & b; + } + }; + + std::vector blockedContacts; + + std::set awaitingPlayers; + std::set actingPlayers; + std::set actedPlayers; + + /// Returns date on which simturns must end unconditionally + int simturnsTurnsMaxLimit() const; + + /// Returns date until which simturns must play unconditionally + int simturnsTurnsMinLimit() const; + + /// Returns true if players are close enough to each other for their heroes to meet on this turn + bool playersInContact(PlayerColor left, PlayerColor right) const; + + /// Returns true if waiting player can act alongside with currently acting player + bool computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const; + + /// Returns true if left player must act before right player + bool mustActBefore(PlayerColor left, PlayerColor right) const; + + /// Returns true if player is ready to start turn + bool canStartTurn(PlayerColor which) const; + + /// Starts turn for all players that can start turn + void tryStartTurnsForPlayers(); + + void updateContactStatus(); + + void doStartNewDay(); + void doStartPlayerTurn(PlayerColor which); + void doEndPlayerTurn(PlayerColor which); + + bool isPlayerAwaitsTurn(PlayerColor which) const; + bool isPlayerMakingTurn(PlayerColor which) const; + bool isPlayerAwaitsNewDay(PlayerColor which) const; + +public: + TurnOrderProcessor(CGameHandler * owner); + + bool isContactAllowed(PlayerColor left, PlayerColor right) const; + + /// Add new player to handle (e.g. on game start) + void addPlayer(PlayerColor which); + + /// NetPack call-in + bool onPlayerEndsTurn(PlayerColor which); + + /// Ends player turn and removes this player from turn order + void onPlayerEndsGame(PlayerColor which); + + /// Start game (or resume from save) and send PlayerStartsTurn pack to player(s) + void onGameStarted(); + + template + void serialize(Handler & h, const int version) + { + h & blockedContacts; + h & awaitingPlayers; + h & actingPlayers; + h & actedPlayers; + } +}; diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp new file mode 100644 index 000000000..a377e6c1b --- /dev/null +++ b/server/queries/BattleQueries.cpp @@ -0,0 +1,104 @@ +/* + * BattleQueries.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 "BattleQueries.h" +#include "MapQueries.h" +#include "QueriesProcessor.h" + +#include "../CGameHandler.h" +#include "../battles/BattleProcessor.h" + +#include "../../lib/battle/IBattleState.h" +#include "../../lib/mapObjects/CGObjectInstance.h" +#include "../../lib/networkPacks/PacksForServer.h" +#include "../../lib/serializer/Cast.h" + +void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + assert(result); + + if(result) + objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): + CQuery(owner), + battleID(bi->getBattleID()) +{ + belligerents[0] = bi->getSideArmy(0); + belligerents[1] = bi->getSideArmy(1); + + addPlayer(bi->getSidePlayer(0)); + addPlayer(bi->getSidePlayer(1)); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner): + CQuery(owner) +{ + belligerents[0] = belligerents[1] = nullptr; +} + +bool CBattleQuery::blocksPack(const CPack * pack) const +{ + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + return true; +} + +void CBattleQuery::onRemoval(PlayerColor color) +{ + assert(result); + + if(result) + gh->battles->battleAfterLevelUp(battleID, *result); +} + +void CBattleQuery::onExposure(QueryPtr topQuery) +{ + // this method may be called in two cases: + // 1) when requesting battle replay (but before replay starts -> no valid result) + // 2) when aswering on levelup queries after accepting battle result -> valid result + if(result) + owner->popQuery(*this); +} + +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi): + CDialogQuery(owner), + bi(bi) +{ + addPlayer(bi->getSidePlayer(0)); + addPlayer(bi->getSidePlayer(1)); +} + +void CBattleDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + if(*answer == 1) + { + gh->battles->restartBattlePrimary( + bi->getBattleID(), + bi->getSideArmy(0), + bi->getSideArmy(1), + bi->getLocation(), + bi->getSideHero(0), + bi->getSideHero(1), + bi->isCreatureBank(), + bi->getDefendedTown() + ); + } + else + { + gh->battles->endBattleConfirm(bi->getBattleID()); + } +} diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h new file mode 100644 index 000000000..4d2fb10fe --- /dev/null +++ b/server/queries/BattleQueries.h @@ -0,0 +1,44 @@ +/* + * BattleQueries.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" +#include "../../lib/networkPacks/PacksForClientBattle.h" + +VCMI_LIB_NAMESPACE_BEGIN +class IBattleInfo; +VCMI_LIB_NAMESPACE_END + +class CBattleQuery : public CQuery +{ +public: + std::array belligerents; + std::array initialHeroMana; + + BattleID battleID; + std::optional result; + + CBattleQuery(CGameHandler * owner); + CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; + virtual void onExposure(QueryPtr topQuery) override; +}; + +class CBattleDialogQuery : public CDialogQuery +{ +public: + CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi); + + const IBattleInfo * bi; + + virtual void onRemoval(PlayerColor color) override; +}; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp new file mode 100644 index 000000000..ad23dcc25 --- /dev/null +++ b/server/queries/CQuery.cpp @@ -0,0 +1,196 @@ +/* + * CQuery.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 "CQuery.h" + +#include "QueriesProcessor.h" + +#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(); +} + +std::ostream & operator<<(std::ostream & out, QueryPtr query) +{ + return out << "[" << query.get() << "] " << query->toString(); +} + +CQuery::CQuery(CGameHandler * gameHandler) + : owner(gameHandler->queries.get()) + , gh(gameHandler) +{ + boost::unique_lock l(QueriesProcessor::mx); + + static QueryID QID = QueryID(0); + + queryID = ++QID; + logGlobal->trace("Created a new query with id %d", queryID); +} + +CQuery::~CQuery() +{ + logGlobal->trace("Destructed the query with id %d", queryID); +} + +void CQuery::addPlayer(PlayerColor color) +{ + if(color.isValidPlayer()) + players.push_back(color); +} + +std::string CQuery::toString() const +{ + const auto size = players.size(); + const std::string plural = size > 1 ? "s" : ""; + std::string names; + + for(size_t i = 0; i < size; i++) + { + names += boost::to_upper_copy(players[i].toString()); + + if(i < size - 2) + names += ", "; + else if(size > 1 && i == size - 2) + names += " and "; + } + std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") + % typeid(*this).name() + % queryID + % plural + % names + ); + return ret; +} + +bool CQuery::endsByPlayerAnswer() const +{ + return false; +} + +void CQuery::onRemoval(PlayerColor color) +{ + +} + +bool CQuery::blocksPack(const CPack * pack) const +{ + return false; +} + +void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + +} + +void CQuery::onExposure(QueryPtr topQuery) +{ + logGlobal->trace("Exposed query with id %d", queryID); + owner->popQuery(*this); +} + +void CQuery::onAdding(PlayerColor color) +{ + +} + +void CQuery::onAdded(PlayerColor color) +{ + +} + +void CQuery::setReply(std::optional reply) +{ + +} + +bool CQuery::blockAllButReply(const CPack * pack) const +{ + //We accept only query replies from correct player + if(auto reply = dynamic_ptr_cast(pack)) + return !vstd::contains(players, reply->player); + + return true; +} + +CDialogQuery::CDialogQuery(CGameHandler * owner): + CQuery(owner) +{ + +} + +bool CDialogQuery::endsByPlayerAnswer() const +{ + return true; +} + +bool CDialogQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +void CDialogQuery::setReply(std::optional reply) +{ + if(reply.has_value()) + answer = *reply; +} + +CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback): + CQuery(gh), callback(Callback) +{ + addPlayer(color); +} + +bool CGenericQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +bool CGenericQuery::endsByPlayerAnswer() const +{ + return true; +} + +void CGenericQuery::onExposure(QueryPtr topQuery) +{ + //do nothing +} + +void CGenericQuery::setReply(std::optional reply) +{ + this->reply = reply; +} + +void CGenericQuery::onRemoval(PlayerColor color) +{ + callback(reply); +} diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h new file mode 100644 index 000000000..cf6e18254 --- /dev/null +++ b/server/queries/CQuery.h @@ -0,0 +1,91 @@ +/* + * CQuery.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/GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CPack; + +VCMI_LIB_NAMESPACE_END + +class CObjectVisitQuery; +class QueriesProcessor; +class CQuery; +class CGameHandler; + +using QueryPtr = std::shared_ptr; + +// This class represents any kind of prolonged interaction that may need to do something special after it is over. +// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. +// Examples: +// - all kinds of blocking dialog windows +// - battle +// - object visit +// - hero movement +// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. +class CQuery +{ +public: + std::vector players; //players that are affected (often "blocked") by query + QueryID queryID; + + 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. + + 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; + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; + + virtual void setReply(std::optional reply); + + virtual ~CQuery(); +protected: + QueriesProcessor * owner; + CGameHandler * gh; + void addPlayer(PlayerColor color); + bool blockAllButReply(const CPack * pack) const; +}; + +std::ostream &operator<<(std::ostream &out, const CQuery &query); +std::ostream &operator<<(std::ostream &out, QueryPtr query); + +class CDialogQuery : public CQuery +{ +public: + CDialogQuery(CGameHandler * owner); + virtual bool endsByPlayerAnswer() const override; + virtual bool blocksPack(const CPack *pack) const override; + void setReply(std::optional reply) override; +protected: + std::optional answer; +}; + +class CGenericQuery : public CQuery +{ +public: + CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback); + + bool blocksPack(const CPack * pack) const override; + bool endsByPlayerAnswer() const override; + void onExposure(QueryPtr topQuery) override; + void setReply(std::optional reply) override; + void onRemoval(PlayerColor color) override; +private: + std::function)> callback; + std::optional reply; +}; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp new file mode 100644 index 000000000..74319bc26 --- /dev/null +++ b/server/queries/MapQueries.cpp @@ -0,0 +1,309 @@ +/* + * MapQueries.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 "MapQueries.h" + +#include "QueriesProcessor.h" +#include "../CGameHandler.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/PacksForServer.h" +#include "../../lib/serializer/Cast.h" + +TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): + CQuery(owner) +{ + addPlayer(player); +} + +bool TimerPauseQuery::blocksPack(const CPack *pack) const +{ + return blockAllButReply(pack); +} + +void TimerPauseQuery::onAdding(PlayerColor color) +{ + gh->turnTimerHandler.setTimerEnabled(color, false); +} + +void TimerPauseQuery::onRemoval(PlayerColor color) +{ + gh->turnTimerHandler.setTimerEnabled(color, true); +} + +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) +{ + 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); +} + +CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): + CDialogQuery(owner) +{ + exchangingArmies[0] = up; + exchangingArmies[1] = down; + + addPlayer(up->tempOwner); + addPlayer(down->tempOwner); +} + +bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const +{ + std::set ourIds; + ourIds.insert(this->exchangingArmies[0]->id); + ourIds.insert(this->exchangingArmies[1]->id); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); + + if(auto arts = dynamic_ptr_cast(pack)) + { + if(auto id1 = arts->src.artHolder) + if(!vstd::contains(ourIds, id1)) + return true; + + if(auto id2 = arts->dst.artHolder) + if(!vstd::contains(ourIds, id2)) + return true; + return false; + } + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->id); + + if(auto arts = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); + + if(auto art = dynamic_ptr_cast(pack)) + { + if(auto id = art->al.artHolder) + return !vstd::contains(ourIds, id); + } + + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->heroID); + + if(auto upgrade = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, upgrade->id); + + if(auto formation = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, formation->hid); + + return CDialogQuery::blocksPack(pack); +} + +void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + assert(answer); + objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); +} + +CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): + CDialogQuery(owner) +{ + this->bd = bd; + addPlayer(bd.player); +} + +OpenWindowQuery::OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode): + CDialogQuery(owner), + mode(mode) +{ + addPlayer(hero->getOwner()); +} + +void OpenWindowQuery::onExposure(QueryPtr topQuery) +{ + //do nothing - wait for reply +} + +bool OpenWindowQuery::blocksPack(const CPack *pack) const +{ + if (mode == EOpenWindowMode::RECRUITMENT_FIRST || mode == EOpenWindowMode::RECRUITMENT_ALL) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + // If hero has no free slots, he might get some stacks merged automatically + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + if (mode == EOpenWindowMode::TAVERN_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + if (mode == EOpenWindowMode::UNIVERSITY_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + if (mode == EOpenWindowMode::MARKET_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + if(dynamic_ptr_cast(pack)) + return false; + + if(dynamic_ptr_cast(pack)) + return false; + + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + return CDialogQuery::blocksPack(pack); +} + +void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + // do not change to dynamic_ptr_cast - SIGSEGV! + auto obj = dynamic_cast(objectVisit.visitedObject); + if(obj) + obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); + else + logGlobal->error("Invalid instance in teleport query"); +} + +CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): + CDialogQuery(owner) +{ + this->td = td; + addPlayer(gh->getHero(td.hero)->getOwner()); +} + +CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + hlu = Hlu; + addPlayer(hero->tempOwner); +} + +void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); + gh->levelUpHero(hero, hlu.skills[*answer]); +} + +void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + clu = Clu; + addPlayer(hero->tempOwner); +} + +void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); + gh->levelUpCommander(hero->commander, clu.skills[*answer]); +} + +void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): + CQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) +{ + players.push_back(hero->tempOwner); +} + +void CHeroMovementQuery::onExposure(QueryPtr topQuery) +{ + assert(players.size() == 1); + + if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard + //TODO what if there were H4-like escape? we should also check pos + { + logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); + //finish movement + visitDestAfterVictory = false; + gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); + } + + owner->popIfTop(*this); +} + +void CHeroMovementQuery::onRemoval(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; + gh->sendAndApply(&pb); +} + +void CHeroMovementQuery::onAdding(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + gh->sendAndApply(&pb); +} diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h new file mode 100644 index 000000000..5668dad02 --- /dev/null +++ b/server/queries/MapQueries.h @@ -0,0 +1,133 @@ +/* + * MapQueries.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" +#include "../../lib/networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CGObjectInstance; +class int3; +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 +{ +public: + TimerPauseQuery(CGameHandler * owner, PlayerColor player); + + bool blocksPack(const CPack *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); + + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; + virtual 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 +{ +public: + TryMoveHero tmh; + bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated + const CGHeroInstance *hero; + + virtual void onExposure(QueryPtr topQuery) override; + + CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); + virtual void onAdding(PlayerColor color) override; + virtual void onRemoval(PlayerColor color) override; +}; + +class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs +{ +public: + std::array exchangingArmies; + + CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; +}; + +//yes/no and component selection dialogs +class CBlockingDialogQuery : public CDialogQuery +{ +public: + BlockingDialog bd; //copy of pack... debug purposes + + CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class OpenWindowQuery : public CDialogQuery +{ + EOpenWindowMode mode; +public: + OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode); + + bool blocksPack(const CPack *pack) const override; + void onExposure(QueryPtr topQuery) override; +}; + +class CTeleportDialogQuery : public CDialogQuery +{ +public: + TeleportDialog td; //copy of pack... debug purposes + + CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class CHeroLevelUpDialogQuery : public CDialogQuery +{ +public: + CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + HeroLevelUp hlu; + const CGHeroInstance * hero; +}; + +class CCommanderLevelUpDialogQuery : public CDialogQuery +{ +public: + CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + CommanderLevelUp clu; + const CGHeroInstance * hero; +}; diff --git a/server/queries/QueriesProcessor.cpp b/server/queries/QueriesProcessor.cpp new file mode 100644 index 000000000..259e33a3e --- /dev/null +++ b/server/queries/QueriesProcessor.cpp @@ -0,0 +1,129 @@ +/* + * CQuery.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 "QueriesProcessor.h" + +#include "CQuery.h" + +boost::mutex QueriesProcessor::mx; + +void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); + if(topQuery(player) != query) + { + logGlobal->trace("Cannot remove, not a top!"); + return; + } + + queries[player] -= query; + auto nextQuery = topQuery(player); + + query->onRemoval(player); + + //Exposure on query below happens only if removal didn't trigger any new query + if(nextQuery && nextQuery == topQuery(player)) + nextQuery->onExposure(query); +} + +void QueriesProcessor::popQuery(const CQuery &query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); + + assert(query.players.size()); + for(auto player : query.players) + { + auto top = topQuery(player); + if(top.get() == &query) + popQuery(top); + else + { + logGlobal->trace("Cannot remove query %s", query.toString()); + logGlobal->trace("Queries found:"); + for(auto q : queries[player]) + { + logGlobal->trace(q->toString()); + } + } + } +} + +void QueriesProcessor::popQuery(QueryPtr query) +{ + for(auto player : query->players) + popQuery(player, query); +} + +void QueriesProcessor::addQuery(QueryPtr query) +{ + for(auto player : query->players) + addQuery(player, query); + + for(auto player : query->players) + query->onAdded(player); +} + +void QueriesProcessor::addQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); + query->onAdding(player); + queries[player].push_back(query); +} + +QueryPtr QueriesProcessor::topQuery(PlayerColor player) +{ + return vstd::backOrNull(queries[player]); +} + +void QueriesProcessor::popIfTop(QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); + if(!query) + logGlobal->error("The query is nullptr! Ignoring."); + + popIfTop(*query); +} + +void QueriesProcessor::popIfTop(const CQuery & query) +{ + for(PlayerColor color : query.players) + if(topQuery(color).get() == &query) + popQuery(color, topQuery(color)); +} + +std::vector> QueriesProcessor::allQueries() const +{ + std::vector> ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +std::vector QueriesProcessor::allQueries() +{ + //TODO code duplication with const function :( + std::vector ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +QueryPtr QueriesProcessor::getQuery(QueryID queryID) +{ + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + if(query->queryID == queryID) + return query; + return nullptr; +} diff --git a/server/queries/QueriesProcessor.h b/server/queries/QueriesProcessor.h new file mode 100644 index 000000000..d0fd6df35 --- /dev/null +++ b/server/queries/QueriesProcessor.h @@ -0,0 +1,40 @@ +/* + * QueriesProcessor.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/GameConstants.h" + +class CQuery; +using QueryPtr = std::shared_ptr; + +class QueriesProcessor +{ +private: + void addQuery(PlayerColor player, QueryPtr query); + void popQuery(PlayerColor player, QueryPtr query); + + std::map> queries; //player => stack of queries + +public: + static boost::mutex mx; + + void addQuery(QueryPtr query); + void popQuery(const CQuery &query); + void popQuery(QueryPtr query); + void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) + void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) + + QueryPtr topQuery(PlayerColor player); + + std::vector> allQueries() const; + std::vector allQueries(); + QueryPtr getQuery(QueryID queryID); + //void removeQuery +}; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 847978cb3..b0e26de4f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -92,8 +92,6 @@ set(test_HEADERS spells/targetConditions/TargetConditionItemFixture.h - vcai/ResourceManagerTest.h - mock/BattleFake.h mock/mock_BonusBearer.h mock/mock_IGameCallback.h diff --git a/test/JsonComparer.cpp b/test/JsonComparer.cpp index 850e7425a..99c3c0c99 100644 --- a/test/JsonComparer.cpp +++ b/test/JsonComparer.cpp @@ -116,7 +116,7 @@ void JsonComparer::checkEqualJson(const JsonVector & actual, const JsonVector & for(size_t idx = 0; idx < sz; idx ++) { - auto guard = pushName(boost::to_string(idx)); + auto guard = pushName(std::to_string(idx)); checkEqualJson(actual.at(idx), expected.at(idx)); } diff --git a/test/StdInc.cpp b/test/StdInc.cpp index 201d9a246..02b98775f 100644 --- a/test/StdInc.cpp +++ b/test/StdInc.cpp @@ -1,12 +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" - +/* + * 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/test/StdInc.h b/test/StdInc.h index 51b284fcc..e4db74bc5 100644 --- a/test/StdInc.h +++ b/test/StdInc.h @@ -1,13 +1,13 @@ -/* - * 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 "gtest/gtest.h" -#include "gmock/gmock.h" -#include "../Global.h" +/* + * 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 "gtest/gtest.h" +#include "gmock/gmock.h" +#include "../Global.h" diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index d05ab74f2..5cef1f907 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -14,8 +14,6 @@ #include -#include "../../lib/NetPacksBase.h" - #include "mock/mock_BonusBearer.h" #include "mock/mock_battle_IBattleState.h" #include "mock/mock_battle_Unit.h" @@ -49,7 +47,7 @@ public: void makeWarMachine() { - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, 0)); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); } void redirectBonusesToFake() @@ -124,6 +122,7 @@ public: { public: + const IBattleInfo * battle; #if SCRIPTING_ENABLED scripting::Pool * pool; @@ -137,9 +136,14 @@ public: { } - void setBattle(const IBattleInfo * battleInfo) + const IBattleInfo * getBattle() const override { - CBattleInfoCallback::setBattle(battleInfo); + return battle; + } + + std::optional getPlayerID() const override + { + return std::nullopt; } #if SCRIPTING_ENABLED @@ -170,7 +174,7 @@ public: void startBattle() { - subject.setBattle(&battleMock); + subject.battle = &battleMock; } void redirectUnitsToFake() @@ -325,7 +329,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToSelf) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -356,7 +360,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -376,7 +380,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedAlly) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -391,11 +395,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -427,7 +431,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -447,7 +451,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedEnemy) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -462,11 +466,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index ed3a32c50..ac32cee5e 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -12,7 +12,6 @@ #include "mock/mock_battle_Unit.h" #include "mock/mock_BonusBearer.h" #include "../../lib/battle/CUnitState.h" -#include "../../lib/NetPacksBase.h" using namespace testing; using namespace battle; @@ -33,7 +32,7 @@ public: 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, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, BonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(UNIT_AMOUNT)); } @@ -239,7 +238,7 @@ TEST_F(HealthTest, singleUnitStack) 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, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, BonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(1)); diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index eea48a4ff..9926548ae 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -55,7 +55,7 @@ public: void makeNormalCaster() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, 0, DEFAULT_SPELL_INDEX)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, BonusSourceID(), BonusSubtypeID(SpellID(DEFAULT_SPELL_INDEX)))); } }; @@ -63,7 +63,7 @@ TEST_F(UnitStateMagicTest, initialNormal) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, BonusSourceID())); initUnit(); @@ -125,7 +125,7 @@ TEST_F(UnitStateMagicTest, effectPower) const int32_t EFFECT_POWER = 12 * 100; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, BonusSourceID())); makeNormalCaster(); EXPECT_EQ(subject.getEffectPower(&spellMock), 12 * DEFAULT_AMOUNT); @@ -148,7 +148,7 @@ TEST_F(UnitStateMagicTest, enchantPower) const int32_t ENCHANT_POWER = 42; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, BonusSourceID())); makeNormalCaster(); @@ -171,7 +171,7 @@ TEST_F(UnitStateMagicTest, effectValue) const int32_t EFFECT_VALUE = 456; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, 0, DEFAULT_SPELL_INDEX)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, BonusSourceID(), BonusSubtypeID(SpellID(DEFAULT_SPELL_INDEX)))); makeNormalCaster(); EXPECT_EQ(subject.getEffectValue(&spellMock), EFFECT_VALUE * DEFAULT_AMOUNT); @@ -201,7 +201,7 @@ TEST_F(UnitStateMagicTest, spendMana) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); initUnit(); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 79b5ce357..b4eb111c6 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -51,12 +51,12 @@ public: void setDefaultExpectations() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, BonusSourceID())); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, PrimarySkill::ATTACK)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, PrimarySkill::DEFENSE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, BonusSourceID())); EXPECT_CALL(infoMock, unitBaseAmount()).WillRepeatedly(Return(DEFAULT_AMOUNT)); EXPECT_CALL(infoMock, unitType()).WillRepeatedly(Return(pikeman)); @@ -66,8 +66,8 @@ public: void makeShooter(int32_t ammo) { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, 0)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, BonusSourceID())); } void initUnit() @@ -179,7 +179,7 @@ TEST_F(UnitStateTest, attackWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, BonusSourceID())); int expectedAttack = static_cast(DEFAULT_ATTACK + 0.5 * DEFAULT_DEFENCE); @@ -191,7 +191,7 @@ TEST_F(UnitStateTest, defenceWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, BonusSourceID())); int expectedDefence = 0; @@ -204,7 +204,7 @@ TEST_F(UnitStateTest, additionalAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonusMock.addNewBonus(bonus); } @@ -218,7 +218,7 @@ TEST_F(UnitStateTest, additionalMeleeAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; bonusMock.addNewBonus(bonus); @@ -233,7 +233,7 @@ TEST_F(UnitStateTest, additionalRangedAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; bonusMock.addNewBonus(bonus); @@ -248,10 +248,10 @@ TEST_F(UnitStateTest, getMinDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, BonusSourceID(), BonusCustomSubtype::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, 1); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMin); bonusMock.addNewBonus(bonus); } @@ -264,10 +264,10 @@ TEST_F(UnitStateTest, getMaxDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, BonusSourceID(), BonusCustomSubtype::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, 2); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMax); bonusMock.addNewBonus(bonus); } diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index 11ddce729..81befa660 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -99,7 +99,7 @@ TEST_F(CCreatureTest, JsonUpdate) EXPECT_EQ(subject->getFightValue(), 2420); EXPECT_EQ(subject->getLevel(), 6); - EXPECT_EQ(subject->getFaction(), 55); + EXPECT_EQ(subject->getFaction().getNum(), 55); EXPECT_TRUE(subject->isDoubleWide()); } @@ -107,7 +107,7 @@ TEST_F(CCreatureTest, JsonAddBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); JsonNode & toAdd = data["bonuses"]["toAdd"]; @@ -121,8 +121,8 @@ TEST_F(CCreatureTest, JsonAddBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) - && (bonus->sid == 42) - && (bonus->subtype == 43) + && (bonus->sid.as().getNum() == 42) + && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -133,10 +133,10 @@ TEST_F(CCreatureTest, JsonRemoveBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b1); - std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b2); @@ -152,8 +152,8 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) - && (bonus->sid == 42) - && (bonus->subtype == 43) + && (bonus->sid.as().getNum() == 42) + && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -165,8 +165,7 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 18) - && (bonus->sid == 42) - && (bonus->subtype == 43) + && (bonus->sid.as().getNum() == 42) && (bonus->valType == BonusValueType::BASE_NUMBER); }; diff --git a/test/events/ApplyDamageTest.cpp b/test/events/ApplyDamageTest.cpp index 7df90298b..b396ccd44 100644 --- a/test/events/ApplyDamageTest.cpp +++ b/test/events/ApplyDamageTest.cpp @@ -12,7 +12,7 @@ #include #include "../../lib/events/ApplyDamage.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../mock/mock_Environment.h" #include "../mock/mock_battle_Unit.h" diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index ae9bd2af0..1c9334d1f 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -16,14 +16,16 @@ #include "../../lib/VCMIDirs.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" #include "../../lib/battle/BattleInfo.h" #include "../../lib/CStack.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" #include "../../lib/mapping/CMap.h" @@ -126,7 +128,7 @@ public: return false; } - void genericQuery(Query * request, PlayerColor color, std::function callback) override + void genericQuery(Query * request, PlayerColor color, std::function)> callback) override { //todo: } @@ -146,7 +148,7 @@ public: si.mode = StartInfo::NEW_GAME; si.seedToBeUsed = 42; - std::unique_ptr header = mapService.loadMapHeader(ResourceID(si.mapname)); + std::unique_ptr header = mapService.loadMapHeader(ResourcePath(si.mapname)); ASSERT_NE(header.get(), nullptr); @@ -167,18 +169,19 @@ public: pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; - pset.heroName = pinfo.mainCustomHeroName; - pset.heroPortrait = pinfo.mainCustomHeroPortrait; + pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; + pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait); } pset.handicap = PlayerSettings::NO_HANDICAP; } - gameState->init(&mapService, &si, false); + Load::ProgressAccumulator progressTracker; + gameState->init(&mapService, &si, progressTracker, false); ASSERT_NE(map, nullptr); ASSERT_EQ(map->heroesOnMap.size(), 2); @@ -203,9 +206,9 @@ public: BattleStart bs; bs.info = battle; - ASSERT_EQ(gameState->curB, nullptr); + ASSERT_EQ(gameState->currentBattles.size(), 0); gameCallback->sendAndApply(&bs); - ASSERT_EQ(gameState->curB, battle); + ASSERT_EQ(gameState->currentBattles.size(), 1); } std::shared_ptr gameState; @@ -237,7 +240,7 @@ TEST_F(CGameStateTest, issue2765) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(defender, ArtifactPosition::MACH1); + pack.al = ArtifactLocation(defender->id, ArtifactPosition::MACH1); pack.art = a; gameCallback->sendAndApply(&pack); } @@ -246,11 +249,11 @@ TEST_F(CGameStateTest, issue2765) { battle::UnitInfo info; - info.id = gameState->curB->battleNextUnitId(); + info.id = gameState->currentBattles.front()->battleNextUnitId(); info.count = 1; info.type = CreatureID(69); info.side = BattleSide::ATTACKER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -262,7 +265,7 @@ TEST_F(CGameStateTest, issue2765) const CStack * att = nullptr; const CStack * def = nullptr; - for(const CStack * s : gameState->curB->stacks) + for(const CStack * s : gameState->currentBattles.front()->stacks) { if(s->unitType()->getId() == CreatureID::BALLISTA && s->unitSide() == BattleSide::DEFENDER) def = s; @@ -291,7 +294,7 @@ TEST_F(CGameStateTest, issue2765) spells::AbilityCaster caster(att, 3); //here tested ballista, but this applied to all war machines - spells::BattleCast cast(gameState->curB, &caster, spells::Mode::PASSIVE, age); + spells::BattleCast cast(gameState->currentBattles.front().get(), &caster, spells::Mode::PASSIVE, age); spells::Target target; target.emplace_back(def); @@ -331,14 +334,14 @@ TEST_F(CGameStateTest, battleResurrection) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(attacker, ArtifactPosition::SPELLBOOK); + pack.al = ArtifactLocation(attacker->id, ArtifactPosition::SPELLBOOK); pack.art = a; gameCallback->sendAndApply(&pack); } startTestBattle(attacker, defender); - uint32_t unitId = gameState->curB->battleNextUnitId(); + uint32_t unitId = gameState->currentBattles.front()->battleNextUnitId(); { battle::UnitInfo info; @@ -346,7 +349,7 @@ TEST_F(CGameStateTest, battleResurrection) info.count = 10; info.type = CreatureID(13); info.side = BattleSide::ATTACKER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -357,11 +360,11 @@ TEST_F(CGameStateTest, battleResurrection) { battle::UnitInfo info; - info.id = gameState->curB->battleNextUnitId(); + info.id = gameState->currentBattles.front()->battleNextUnitId(); info.count = 10; info.type = CreatureID(13); info.side = BattleSide::DEFENDER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -370,7 +373,7 @@ TEST_F(CGameStateTest, battleResurrection) gameCallback->sendAndApply(&pack); } - CStack * unit = gameState->curB->getStack(unitId); + CStack * unit = gameState->currentBattles.front()->getStack(unitId); ASSERT_NE(unit, nullptr); @@ -389,7 +392,7 @@ TEST_F(CGameStateTest, battleResurrection) const CSpell * spell = SpellID(SpellID::RESURRECTION).toSpell(); ASSERT_NE(spell, nullptr); - spells::BattleCast cast(gameState->curB, attacker, spells::Mode::HERO, spell); + spells::BattleCast cast(gameState->currentBattles.front().get(), attacker, spells::Mode::HERO, spell); spells::Target target; target.emplace_back(unit); diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index 0b29d06ef..aefffdbc2 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" -#include "../lib/filesystem/ResourceID.h" +#include "../lib/filesystem/ResourcePath.h" #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "../lib/TerrainHandler.h" @@ -111,7 +111,7 @@ TEST(MapManager, DrawTerrain_View) { try { - const ResourceID testMap("test/TerrainViewTest", EResType::MAP); + const ResourcePath testMap("test/TerrainViewTest", EResType::MAP); // Load maps and json config CMapService mapService; const auto originalMap = mapService.loadMap(testMap); @@ -120,7 +120,7 @@ TEST(MapManager, DrawTerrain_View) // Validate edit manager auto editManager = map->getEditManager(); CRandomGenerator gen; - const JsonNode viewNode(ResourceID("test/terrainViewMappings", EResType::TEXT)); + const JsonNode viewNode(JsonPath::builtin("test/terrainViewMappings")); const auto & mappingsNode = viewNode["mappings"].Vector(); for (const auto & node : mappingsNode) { diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index e1d88f7a4..20078ea11 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -31,7 +31,7 @@ static void saveTestMap(CMemoryBuffer & serializeBuffer, const std::string & fil { auto path = VCMIDirs::get().userDataPath() / filename; boost::filesystem::remove(path); - boost::filesystem::ofstream tmp(path, boost::filesystem::ofstream::binary); + std::ofstream tmp(path.c_str(), std::ofstream::binary); tmp.write((const char *)serializeBuffer.getBuffer().data(), serializeBuffer.getSize()); tmp.flush(); @@ -46,7 +46,7 @@ TEST(MapFormat, Random) CRmgTemplate tmpl; std::shared_ptr zoneOptions = std::make_shared(); - const_cast(tmpl.getCpuPlayers()).addRange(1, 4); + const_cast(tmpl.getHumanPlayers()).addRange(1, 4); const_cast(tmpl.getZones())[0] = zoneOptions; zoneOptions->setOwner(1); @@ -55,7 +55,7 @@ TEST(MapFormat, Random) opt.setHeight(CMapHeader::MAP_SIZE_MIDDLE); opt.setWidth(CMapHeader::MAP_SIZE_MIDDLE); opt.setHasTwoLevels(true); - opt.setPlayerCount(4); + opt.setHumanOrCpuPlayerCount(4); opt.setPlayerTypeForStandardPlayer(PlayerColor(0), EPlayerType::HUMAN); opt.setPlayerTypeForStandardPlayer(PlayerColor(1), EPlayerType::AI); @@ -65,7 +65,7 @@ TEST(MapFormat, Random) CMapGenerator gen(opt, TEST_RANDOM_SEED); std::unique_ptr initialMap = gen.generate(); - initialMap->name = "Test"; + initialMap->name.appendRawString("Test"); SCOPED_TRACE("MapFormat_Random generated"); CMemoryBuffer serializeBuffer; @@ -90,7 +90,7 @@ TEST(MapFormat, Random) static JsonNode getFromArchive(CZipLoader & archive, const std::string & archiveFilename) { - ResourceID resource(archiveFilename, EResType::TEXT); + JsonPath resource = JsonPath::builtin(archiveFilename); if(!archive.existsResource(resource)) throw std::runtime_error(archiveFilename + " not found"); @@ -153,14 +153,14 @@ TEST(MapFormat, Objects) { static const std::string MAP_DATA_PATH = "test/ObjectPropertyTest/"; - const JsonNode initialHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now + const JsonNode initialHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now - const JsonNode initialObjects(ResourceID(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedObjects(ResourceID(MAP_DATA_PATH + "objects.ex.json")); + const JsonNode initialObjects(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedObjects(JsonPath::builtin(MAP_DATA_PATH + "objects.ex.json")); - const JsonNode expectedSurface(ResourceID(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourceID(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(JsonPath::builtin(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(JsonPath::builtin(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(initialHeader, initialObjects, expectedSurface, expectedUnderground); @@ -192,11 +192,11 @@ TEST(MapFormat, Terrain) { static const std::string MAP_DATA_PATH = "test/TerrainTest/"; - const JsonNode expectedHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedObjects(ResourceID(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedObjects(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedSurface(ResourceID(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourceID(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(JsonPath::builtin(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(JsonPath::builtin(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(expectedHeader, expectedObjects, expectedSurface, expectedUnderground); diff --git a/test/map/MapComparer.cpp b/test/map/MapComparer.cpp index 317e89fe5..62ffcabfb 100644 --- a/test/map/MapComparer.cpp +++ b/test/map/MapComparer.cpp @@ -56,7 +56,7 @@ void checkEqual(const std::set & actual, const std::set & expe for(auto elem : expected) { if(!vstd::contains(actual, elem)) - FAIL() << "Required element not found "+boost::to_string(elem); + FAIL() << "Required element not found "+std::to_string(elem); } } @@ -76,7 +76,7 @@ void checkEqual(const PlayerInfo & actual, const PlayerInfo & expected) VCMI_CHECK_FIELD_EQUAL(isFactionRandom); VCMI_CHECK_FIELD_EQUAL(mainCustomHeroPortrait); - VCMI_CHECK_FIELD_EQUAL(mainCustomHeroName); + VCMI_CHECK_FIELD_EQUAL(mainCustomHeroNameTextId); VCMI_CHECK_FIELD_EQUAL(mainCustomHeroId); @@ -202,8 +202,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::to_string(boost::format("%s(%d)|%s(%d) %d") % actual->typeName % actual->ID % actual->subTypeName % actual->subID % actual->tempOwner); - std::string expectedFullID = boost::to_string(boost::format("%s(%d)|%s(%d) %d") % expected->typeName % expected->ID % expected->subTypeName % expected->subID % expected->tempOwner); + 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); EXPECT_EQ(actualFullID, expectedFullID); diff --git a/test/mock/BattleFake.cpp b/test/mock/BattleFake.cpp index a2f5a9337..b00631d61 100644 --- a/test/mock/BattleFake.cpp +++ b/test/mock/BattleFake.cpp @@ -87,7 +87,6 @@ BattleFake::BattleFake() = default; void BattleFake::setUp() { - CBattleInfoCallback::setBattle(this); } void BattleFake::setupEmptyBattlefield() diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index 9c9287dac..8ce30c38f 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -20,7 +20,6 @@ #endif #include "../../lib/JsonNode.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/battle/CBattleInfoCallback.h" namespace test @@ -88,6 +87,17 @@ public: pack->applyBattle(this); } + const IBattleInfo * getBattle() const override + { + return nullptr; + } + + std::optional getPlayerID() const override + { + return std::nullopt; + } + + protected: private: diff --git a/test/mock/mock_Creature.h b/test/mock/mock_Creature.h index 2ce93f407..7df167aec 100644 --- a/test/mock/mock_Creature.h +++ b/test/mock/mock_Creature.h @@ -57,7 +57,7 @@ public: MOCK_CONST_METHOD1(getCost, int32_t(int32_t)); MOCK_CONST_METHOD0(isDoubleWide, bool()); - MOCK_CONST_METHOD1(getRecruitCost, int32_t(Identifier)); + MOCK_CONST_METHOD1(getRecruitCost, int32_t(GameResID)); MOCK_CONST_METHOD0(getFullRecruitCost, ResourceSet()); MOCK_CONST_METHOD0(hasUpgrades, bool()); }; diff --git a/test/mock/mock_Environment.h b/test/mock/mock_Environment.h index ae03ef348..5b5829984 100644 --- a/test/mock/mock_Environment.h +++ b/test/mock/mock_Environment.h @@ -16,7 +16,7 @@ class EnvironmentMock : public Environment { public: MOCK_CONST_METHOD0(services, const Services *()); - MOCK_CONST_METHOD0(battle, const BattleCb *()); + MOCK_CONST_METHOD1(battle, const BattleCb *(const BattleID & battleID)); MOCK_CONST_METHOD0(game, const GameCb *()); MOCK_CONST_METHOD0(logger, ::vstd::CLoggerBase *()); MOCK_CONST_METHOD0(eventBus, ::events::EventBus *()); diff --git a/test/mock/mock_IBattleInfoCallback.h b/test/mock/mock_IBattleInfoCallback.h index 402439524..f28dd37f6 100644 --- a/test/mock/mock_IBattleInfoCallback.h +++ b/test/mock/mock_IBattleInfoCallback.h @@ -29,13 +29,15 @@ public: MOCK_CONST_METHOD0(battleNextUnitId, uint32_t()); - MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD1(battleGetUnitByID, const battle::Unit *(uint32_t)); MOCK_CONST_METHOD2(battleGetUnitByPos, const battle::Unit *(BattleHex, bool)); MOCK_CONST_METHOD0(battleActiveUnit, const battle::Unit *()); MOCK_CONST_METHOD0(getBonusBearer, IBonusBearer*()); + MOCK_CONST_METHOD0(getBattle, IBattleInfo*()); + MOCK_CONST_METHOD0(getPlayerID, std::optional()); MOCK_CONST_METHOD2(battleGetAllObstaclesOnPos, std::vector>(BattleHex, bool)); MOCK_CONST_METHOD2(getAllAffectedObstaclesByStack, std::vector>(const battle::Unit *, const std::set &)); diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index f6ed9e856..48a2fb639 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -35,20 +35,21 @@ public: //TODO: fail all stub calls - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) override {} + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {} void showInfoDialog(InfoWindow * iw) override {} void showInfoDialog(const std::string & msg, PlayerColor player) override {} void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {} - bool removeObject(const CGObjectInstance * obj) override {return false;} - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) 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 setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs=false) 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 showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window void showTeleportDialog(TeleportDialog *iw) override {} - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {} + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void giveResource(PlayerColor player, GameResID which, int val) override {} void giveResources(PlayerColor player, TResources resources) override {} @@ -66,8 +67,7 @@ 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 giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override {} + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;} void removeArtifact(const ArtifactLocation &al) override {} bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) override {return false;} @@ -83,10 +83,10 @@ public: void setMovePoints(SetMovePoints * smp) override {} void setManaPoints(ObjectInstanceID hid, int val) override {} void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {} - void changeObjPos(ObjectInstanceID objid, int3 newPos) override {} + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override {} + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) override {} void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {} ///useful callback methods diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index fb754691d..94939fc24 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -16,12 +16,16 @@ class IGameInfoCallbackMock : public IGameInfoCallback { public: //various - MOCK_CONST_METHOD1(getDate, int(Date::EDateType)); - MOCK_CONST_METHOD2(isAllowed, bool(int32_t, int32_t)); + MOCK_CONST_METHOD1(getDate, int(Date)); + + MOCK_CONST_METHOD1(isAllowed, bool(SpellID)); + MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID)); + MOCK_CONST_METHOD1(isAllowed, bool(SecondarySkill)); //player MOCK_CONST_METHOD1(getPlayer, const Player *(PlayerColor)); MOCK_CONST_METHOD0(getLocalPlayer, PlayerColor()); + MOCK_CONST_METHOD0(getPlayerID, std::optional()); //hero MOCK_CONST_METHOD1(getHero, const CGHeroInstance *(ObjectInstanceID)); diff --git a/test/mock/mock_MapService.cpp b/test/mock/mock_MapService.cpp index 1b85b9edf..d9883c9d6 100644 --- a/test/mock/mock_MapService.cpp +++ b/test/mock/mock_MapService.cpp @@ -23,15 +23,15 @@ MapServiceMock::MapServiceMock(const std::string & path, MapListener * mapListen CZipSaver saver(io, "_"); - const JsonNode header(ResourceID(path+CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode objects(ResourceID(path+CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode surface(ResourceID(path+"surface_terrain.json")); + const JsonNode header(JsonPath::builtin(path+CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode objects(JsonPath::builtin(path+CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode surface(JsonPath::builtin(path+"surface_terrain.json")); addToArchive(saver, header, CMapFormatJson::HEADER_FILE_NAME); addToArchive(saver, objects, CMapFormatJson::OBJECTS_FILE_NAME); addToArchive(saver, surface, "surface_terrain.json"); - ResourceID undergroundPath(path+"underground_terrain.json"); + auto undergroundPath = JsonPath::builtin(path+"underground_terrain.json"); if(CResourceHandler::get()->existsResource(undergroundPath)) { @@ -53,12 +53,12 @@ std::unique_ptr MapServiceMock::loadMap() const return res; } -std::unique_ptr MapServiceMock::loadMap(const ResourceID & name) const +std::unique_ptr MapServiceMock::loadMap(const ResourcePath & name) const { return loadMap(); } -std::unique_ptr MapServiceMock::loadMapHeader(const ResourceID & name) const +std::unique_ptr MapServiceMock::loadMapHeader(const ResourcePath & name) const { initialBuffer.seek(0); CMapLoaderJson initialLoader(&initialBuffer); diff --git a/test/mock/mock_MapService.h b/test/mock/mock_MapService.h index 656b3c771..cf2da28a9 100644 --- a/test/mock/mock_MapService.h +++ b/test/mock/mock_MapService.h @@ -29,8 +29,8 @@ class MapServiceMock : public IMapService public: MapServiceMock(const std::string & path, MapListener * mapListener_); - std::unique_ptr loadMap(const ResourceID & name) const override; - std::unique_ptr loadMapHeader(const ResourceID & name) const override; + std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMapHeader(const ResourcePath & name) const override; std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index dbd30c0ce..6ef0998d8 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -11,13 +11,14 @@ #pragma once #include "../../lib/battle/IBattleState.h" +#include "../../lib/int3.h" class BattleStateMock : public IBattleState { public: MOCK_CONST_METHOD0(getActiveStackID, int32_t()); - MOCK_CONST_METHOD1(getStacksIf, TStacks(TStackFilter)); - MOCK_CONST_METHOD1(getUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(getStacksIf, TStacks(const TStackFilter&)); + MOCK_CONST_METHOD1(getUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD0(getBattlefieldType, BattleField()); MOCK_CONST_METHOD0(getTerrainType, TerrainId()); MOCK_CONST_METHOD0(getAllObstacles, IBattleInfo::ObstacleCList()); @@ -34,8 +35,12 @@ public: 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_METHOD1(nextRound, void(int32_t)); + MOCK_METHOD0(nextRound, void()); MOCK_METHOD1(nextTurn, void(uint32_t)); MOCK_METHOD2(addUnit, void(uint32_t, const JsonNode &)); MOCK_METHOD3(setUnitState, void(uint32_t, const JsonNode &, int64_t)); diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index 5b32c7e48..c9ea0625a 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -19,7 +19,7 @@ public: MOCK_CONST_METHOD0(getTreeVersion, int64_t()); MOCK_CONST_METHOD0(getCasterUnitId, int32_t()); - MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, int32_t *)); + MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, SpellSchool *)); MOCK_CONST_METHOD1(getEffectLevel, int32_t(const spells::Spell *)); MOCK_CONST_METHOD3(getSpellBonus, int64_t(const spells::Spell *, int64_t, const battle::Unit *)); MOCK_CONST_METHOD2(getSpecificSpellBonus, int64_t(const spells::Spell *, int64_t)); diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index e3efe9950..985e093f3 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -18,7 +18,7 @@ namespace spells class MechanicsMock : public Mechanics { public: - MOCK_CONST_METHOD2(adaptProblem, bool(ESpellCastProblem::ESpellCastProblem, Problem &)); + MOCK_CONST_METHOD2(adaptProblem, bool(ESpellCastProblem, Problem &)); MOCK_CONST_METHOD1(adaptGenericProblem, bool(Problem &)); MOCK_CONST_METHOD1(rangeInHexes, std::vector(BattleHex)); @@ -64,8 +64,6 @@ public: MOCK_CONST_METHOD1(applySpecificSpellBonus,int64_t(int64_t)); MOCK_CONST_METHOD2(calculateRawEffectValue, int64_t(int32_t, int32_t)); - MOCK_CONST_METHOD0(getElementalImmunity, std::vector()); - MOCK_CONST_METHOD1(ownerMatches, bool(const battle::Unit *)); MOCK_CONST_METHOD2(ownerMatches, bool(const battle::Unit *, const boost::logic::tribool)); diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index f507e3836..3e8aa442e 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -45,7 +45,7 @@ public: MOCK_CONST_METHOD0(isOffensive, bool()); MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); - MOCK_CONST_METHOD1(hasSchool, bool(ESpellSchool)); + MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); MOCK_CONST_METHOD0(getCastSound, const std::string &()); MOCK_CONST_METHOD1(registerIcons, void(const IconRegistar &)); diff --git a/test/netpacks/EntitiesChangedTest.cpp b/test/netpacks/EntitiesChangedTest.cpp index adb6b0647..e4423176a 100644 --- a/test/netpacks/EntitiesChangedTest.cpp +++ b/test/netpacks/EntitiesChangedTest.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "NetPackFixture.h" +#include "../../lib/networkPacks/PacksForClient.h" namespace test { diff --git a/test/netpacks/NetPackFixture.h b/test/netpacks/NetPackFixture.h index 2e995cba1..176d37e21 100644 --- a/test/netpacks/NetPackFixture.h +++ b/test/netpacks/NetPackFixture.h @@ -10,7 +10,6 @@ #pragma once -#include "../../lib/NetPacks.h" #include "../../lib/gameState/CGameState.h" namespace test diff --git a/test/scripting/LuaSpellEffectAPITest.cpp b/test/scripting/LuaSpellEffectAPITest.cpp index 0a477f41b..8040b99df 100644 --- a/test/scripting/LuaSpellEffectAPITest.cpp +++ b/test/scripting/LuaSpellEffectAPITest.cpp @@ -11,7 +11,7 @@ #include "ScriptFixture.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../mock/mock_ServerCallback.h" diff --git a/test/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index 224d746c0..e350c6c61 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -8,7 +8,7 @@ * */ #include "StdInc.h" -#include "lib/CModHandler.h" +#include "lib/modding/ModScope.h" #include "ScriptFixture.h" @@ -30,7 +30,7 @@ void ScriptFixture::loadScriptFromFile(const std::string & path) void ScriptFixture::loadScript(const JsonNode & scriptConfig) { - subject = VLC->scriptHandler->loadFromJson(&loggerMock, CModHandler::scopeBuiltin(), scriptConfig, "test"); + subject = VLC->scriptHandler->loadFromJson(&loggerMock, ModScope::scopeBuiltin(), scriptConfig, "test"); GTEST_ASSERT_NE(subject, nullptr); @@ -64,7 +64,6 @@ void ScriptFixture::setUp() { pool = std::make_shared(); - EXPECT_CALL(environmentMock, battle()).WillRepeatedly(Return(&binfoMock)); EXPECT_CALL(environmentMock, game()).WillRepeatedly(Return(&infoMock)); EXPECT_CALL(environmentMock, logger()).WillRepeatedly(Return(&loggerMock)); EXPECT_CALL(environmentMock, eventBus()).WillRepeatedly(Return(&eventBus)); diff --git a/test/scripting/ScriptFixture.h b/test/scripting/ScriptFixture.h index 4d7b780bc..ea9f0cbe3 100644 --- a/test/scripting/ScriptFixture.h +++ b/test/scripting/ScriptFixture.h @@ -16,7 +16,6 @@ #include "../../lib/JsonNode.h" #include "../../lib/ScriptHandler.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/bonuses/Bonus.h" diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index e9f9a47e7..02c3a9e44 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -13,7 +13,6 @@ #include "mock/mock_BonusBearer.h" #include "mock/mock_spells_Spell.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/spells/AbilityCaster.h" namespace test @@ -56,7 +55,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, SpellSchool(ESpellSchool::ANY))); + 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, getTreeVersion()).Times(AtLeast(0)); @@ -70,7 +69,7 @@ TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, SpellSchool(ESpellSchool::AIR))); + 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, getTreeVersion()).Times(AtLeast(0)); diff --git a/test/spells/TargetConditionTest.cpp b/test/spells/TargetConditionTest.cpp index 88abf7fcd..2f6f604fb 100644 --- a/test/spells/TargetConditionTest.cpp +++ b/test/spells/TargetConditionTest.cpp @@ -11,7 +11,6 @@ #include -#include "../../lib/NetPacksBase.h" #include "../../lib/spells/TargetCondition.h" #include "../../lib/serializer/JsonDeserializer.h" diff --git a/test/spells/effects/CloneTest.cpp b/test/spells/effects/CloneTest.cpp index dd6a16181..a9599ea44 100644 --- a/test/spells/effects/CloneTest.cpp +++ b/test/spells/effects/CloneTest.cpp @@ -138,7 +138,7 @@ public: EXPECT_EQ(marker.duration, BonusDuration::N_TURNS); EXPECT_EQ(marker.turnsRemain, effectDuration); EXPECT_EQ(marker.source, BonusSource::SPELL_EFFECT); - EXPECT_EQ(marker.sid, SpellID::CLONE); + EXPECT_EQ(marker.sid, BonusSourceID(SpellID(SpellID::CLONE))); } void setDefaultExpectations() diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index a0fdb17fb..11a998a6c 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -95,7 +95,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); @@ -157,7 +157,7 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, getCount()).WillOnce(Return(unitAmount)); @@ -202,7 +202,7 @@ TEST_F(DamageApplyTest, DoesDamageByCount) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); diff --git a/test/spells/effects/DispelTest.cpp b/test/spells/effects/DispelTest.cpp index 13ad0070f..10ac75f0f 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -74,7 +74,7 @@ TEST_F(DispelTest, ApplicableToAliveUnitWithTimedEffect) auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(SpellID(negativeID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).WillOnce(Return(true)); @@ -101,7 +101,7 @@ TEST_F(DispelTest, IgnoresOwnEffects) } auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, neutralID.toEnum())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(SpellID(neutralID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).Times(AtMost(1)).WillRepeatedly(Return(true)); @@ -182,23 +182,23 @@ TEST_F(DispelApplyTest, RemovesEffects) EXPECT_CALL(unit1, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(unitIds[1])); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum()); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum()); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, negativeID.toEnum()); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, positiveID.toEnum()); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, BonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, positiveID.toEnum()); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, BonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); } diff --git a/test/spells/effects/EffectFixture.cpp b/test/spells/effects/EffectFixture.cpp index 0395700aa..88d1ed2f4 100644 --- a/test/spells/effects/EffectFixture.cpp +++ b/test/spells/effects/EffectFixture.cpp @@ -13,7 +13,8 @@ #include -#include "../../../lib/NetPacks.h" +#include "../../../lib/networkPacks/PacksForClientBattle.h" +#include "../../../lib/networkPacks/SetStackEffect.h" #include "../../../lib/serializer/JsonDeserializer.h" diff --git a/test/spells/effects/EffectFixture.h b/test/spells/effects/EffectFixture.h index bc07ae1fa..dde8e9a2d 100644 --- a/test/spells/effects/EffectFixture.h +++ b/test/spells/effects/EffectFixture.h @@ -34,7 +34,6 @@ #include "../../../lib/JsonNode.h" -#include "../../../lib/NetPacksBase.h" #include "../../../lib/battle/CBattleInfoCallback.h" namespace battle diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index 1b26abb1b..24da47673 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -91,7 +91,7 @@ TEST_F(HealTest, ApplicableIfActuallyResurrects) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(1000)); EXPECT_CALL(mechanicsMock, isSmart()).WillOnce(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -117,7 +117,7 @@ TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -143,7 +143,7 @@ TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -271,7 +271,7 @@ TEST_F(HealTest, NotApplicableIfEffectValueTooLow) EXPECT_CALL(unit, getTotalHealth()).WillOnce(Return(200)); EXPECT_CALL(unit, getAvailableHealth()).WillOnce(Return(100)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(199)); @@ -348,7 +348,7 @@ TEST_P(HealApplyTest, Heals) EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitType()).WillRepeatedly(Return(pikeman)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index f8384540d..c0a3b5117 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -179,13 +179,13 @@ TEST_F(SacrificeApplyTest, ResurrectsTarget) EXPECT_CALL(mechanicsMock, applySpellBonus(_, Eq(&targetUnit))).WillOnce(ReturnArg<0>()); EXPECT_CALL(mechanicsMock, calculateRawEffectValue(_,_)).WillOnce(Return(effectValue)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); auto & victim = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(victim, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(victimId)); EXPECT_CALL(victim, getCount()).Times(AtLeast(1)).WillRepeatedly(Return(victimCount)); - victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, 0)); + victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, BonusSourceID())); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId), _, Eq(expectedHealValue))).Times(1); diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index 214b72f35..33e4cde61 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -244,12 +244,12 @@ TEST_P(SummonApplyTest, UpdatesOldUnit) setDefaultExpectaions(); acquired = std::make_shared(); - acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, 0)); + acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, BonusSourceID())); acquired->redirectBonusesToFake(); acquired->expectAnyBonusSystemCall(); auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, BonusSourceID())); { EXPECT_CALL(unit, acquire()).WillOnce(Return(acquired)); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index 7eb41bf72..f399eb81e 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -12,7 +12,7 @@ #include "EffectFixture.h" #include -#include "lib/CModHandler.h" +#include "lib/modding/ModScope.h" namespace test { @@ -71,16 +71,16 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, PrimarySkill::KNOWLEDGE); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, PrimarySkill::KNOWLEDGE); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); options["cumulative"].Bool() = cumulative; options["bonus"]["test1"] = testBonus1.toJsonNode(); options["bonus"]["test2"] = testBonus2.toJsonNode(); - options.setMeta(CModHandler::scopeBuiltin()); + options.setMeta(ModScope::scopeBuiltin()); setupEffect(options); const uint32_t unitId = 42; @@ -103,7 +103,7 @@ TEST_P(TimedApplyTest, ChangesBonuses) for(auto & bonus : expectedBonus) { bonus.source = BonusSource::SPELL_EFFECT; - bonus.sid = spellIndex; + bonus.sid = BonusSourceID(SpellID(spellIndex)); } if(cumulative) diff --git a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp index 2ec3ffff1..ae2641c6e 100644 --- a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp @@ -54,7 +54,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -67,7 +67,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -79,7 +79,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -90,7 +90,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) TEST_F(AbsoluteLevelConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID()); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index 9a776f4de..1e062eca6 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 402e38e1d..4e18d1c62 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -42,14 +42,14 @@ TEST_F(BonusConditionTest, ImmuneByDefault) TEST_F(BonusConditionTest, ReceptiveIfMatchesType) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, BonusSourceID())); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::FIRE_IMMUNITY, BonusSource::OTHER, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 0b7fd5262..b0085dee7 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -20,18 +20,20 @@ class ElementalConditionTest : public TargetConditionItemTest, public WithParamI { public: bool isPositive; + void setDefaultExpectations() { EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); - std::vector immunityList = + EXPECT_CALL(mechanicsMock, getSpell()).Times(AtLeast(1)).WillRepeatedly(Return(&spellMock)); + EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { - BonusType::AIR_IMMUNITY, - BonusType::FIRE_IMMUNITY, - }; + bool stop = false; + cb(SpellSchool::AIR, stop); + cb(SpellSchool::FIRE, stop); + }); - EXPECT_CALL(mechanicsMock, getElementalImmunity()).Times(AtLeast(1)).WillRepeatedly(Return(immunityList)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); } @@ -54,15 +56,23 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } +TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) +{ + setDefaultExpectations(); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::WATER))); + + EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); +} + TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -70,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 0)); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index 8652d9db1..d6aa51b98 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -57,7 +57,7 @@ TEST_P(ImmunityNegationConditionTest, WithHeroNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, BonusSourceID(), BonusCustomSubtype::immunityEnemyHero)); EXPECT_EQ(isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -66,7 +66,7 @@ TEST_P(ImmunityNegationConditionTest, WithBattleWideNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, BonusSourceID(), BonusCustomSubtype::immunityBattleWide)); //This should return if ownerMatches, because anyone should cast onto owner's stacks, but not on enemyStacks EXPECT_EQ(ownerMatches && isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalLevelConditionTest.cpp b/test/spells/targetConditions/NormalLevelConditionTest.cpp index e6546d7af..e6895e232 100644 --- a/test/spells/targetConditions/NormalLevelConditionTest.cpp +++ b/test/spells/targetConditions/NormalLevelConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(NormalLevelConditionTest, DefaultForNormal) TEST_P(NormalLevelConditionTest, ReceptiveNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(4)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -66,7 +66,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveNormal) TEST_P(NormalLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(0)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -75,7 +75,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveAbility) TEST_P(NormalLevelConditionTest, ImmuneNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(2)); EXPECT_EQ(!isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 93f3b83ec..3abf12ae9 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) TEST_P(NormalSpellConditionTest, ChecksNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); if(immuneSpell == castSpell) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp index ecd8525de..5aef4f202 100644 --- a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp +++ b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp @@ -31,7 +31,7 @@ public: EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); if(hasBonus) - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, BonusSourceID())); } protected: diff --git a/test/spells/targetConditions/SpellEffectConditionTest.cpp b/test/spells/targetConditions/SpellEffectConditionTest.cpp index fdbbae891..af887798a 100644 --- a/test/spells/targetConditions/SpellEffectConditionTest.cpp +++ b/test/spells/targetConditions/SpellEffectConditionTest.cpp @@ -45,7 +45,7 @@ TEST_F(SpellEffectConditionTest, ImmuneByDefault) TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, SpellID::AGE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, BonusSourceID(SpellID(SpellID::AGE)))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -53,14 +53,14 @@ TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) TEST_F(SpellEffectConditionTest, ImmuneIfHasEffectFromOtherSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, SpellID::AIR_SHIELD)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, BonusSourceID(SpellID(SpellID::AIR_SHIELD)))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(SpellEffectConditionTest, ImmuneIfHasNoSpellEffects) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, BonusSourceID())); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/TargetConditionItemFixture.h b/test/spells/targetConditions/TargetConditionItemFixture.h index b67714eda..67df578a4 100644 --- a/test/spells/targetConditions/TargetConditionItemFixture.h +++ b/test/spells/targetConditions/TargetConditionItemFixture.h @@ -12,11 +12,11 @@ #include -#include "../../../lib/NetPacksBase.h" #include "../../../lib/spells/TargetCondition.h" #include "mock/mock_spells_Mechanics.h" +#include "mock/mock_spells_Spell.h" #include "mock/mock_BonusBearer.h" #include "mock/mock_battle_Unit.h" @@ -30,6 +30,8 @@ public: ::testing::StrictMock mechanicsMock; ::testing::StrictMock unitMock; + ::testing::StrictMock spellMock; + BonusBearerMock unitBonuses; protected: void SetUp() override diff --git a/test/testdata/erm/list-manipulation.json b/test/testdata/erm/list-manipulation.json index fc19ae42c..670224ead 100644 --- a/test/testdata/erm/list-manipulation.json +++ b/test/testdata/erm/list-manipulation.json @@ -5,7 +5,7 @@ "MDATA":{}, "Q":{}, "v":{ - "1":5 + "1":5, "2":5, "3":6 }, diff --git a/test/testdata/erm/std.json b/test/testdata/erm/std.json index efd95fad6..a504e39a2 100644 --- a/test/testdata/erm/std.json +++ b/test/testdata/erm/std.json @@ -5,7 +5,7 @@ "MDATA":{}, "Q":{}, "v":{ - "1":1 + "1":1, "2":40320 }, "z":{ diff --git a/test/vcai/ResourceManagerTest.h b/test/vcai/ResourceManagerTest.h deleted file mode 100644 index 4b3992cca..000000000 --- a/test/vcai/ResourceManagerTest.h +++ /dev/null @@ -1,13 +0,0 @@ -/* -* ResourceManagerTest.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 - -extern boost::thread_specific_ptr cb; \ No newline at end of file diff --git a/test/vcai/ResurceManagerTest.cpp b/test/vcai/ResurceManagerTest.cpp index df6fd90be..c7a4cb840 100644 --- a/test/vcai/ResurceManagerTest.cpp +++ b/test/vcai/ResurceManagerTest.cpp @@ -12,7 +12,6 @@ #include "gtest/gtest.h" #include "../AI/VCAI/VCAI.h" -#include "ResourceManagerTest.h" #include "../AI/VCAI/Goals/Goals.h" #include "mock_VCAI_CGoal.h" #include "mock_VCAI.h" diff --git a/xcode/launch-c.in b/xcode/launch-c.in new file mode 100644 index 000000000..59ced1e88 --- /dev/null +++ b/xcode/launch-c.in @@ -0,0 +1,3 @@ +#!/bin/sh +export CCACHE_CPP2=true +ccache clang "$@" diff --git a/xcode/launch-cxx.in b/xcode/launch-cxx.in new file mode 100644 index 000000000..2d52f768c --- /dev/null +++ b/xcode/launch-cxx.in @@ -0,0 +1,3 @@ +#!/bin/sh +export CCACHE_CPP2=true +ccache clang++ "$@"