diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index ac20a6aea..b728a0127 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -312,7 +312,7 @@ jobs: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs - name: Upload build - if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }} + if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }} continue-on-error: true run: | if cd '${{github.workspace}}/android/vcmi-app/build/outputs/apk/daily' ; then diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 5c447bf03..7e615131d 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -147,7 +147,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex, bestAttack.attack.chargeDistance, - bestAttack.attack.attacker->speed(0, true), + bestAttack.attack.attacker->getMovementRange(0), bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, score @@ -225,7 +225,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) } } - return BattleAction::makeDefend(stack); + return stack->waited() ? BattleAction::makeDefend(stack) : BattleAction::makeWait(stack); } uint64_t timeElapsed(std::chrono::time_point start) @@ -553,7 +553,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) 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(); + return !original || u->getMovementRange() != original->getMovementRange(); }); DamageCache safeCopy = damageCache; @@ -609,7 +609,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) if(ourUnit * goodEffect == 1) { - if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot())) + if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) continue; ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index dc37412da..9846a6efe 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -258,7 +258,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( updateReachabilityMap(hb); - if(result.bestAttack.attack.shooting && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) + if(result.bestAttack.attack.shooting + && !activeStack->waited() + && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) { if(!canBeHitThisTurn(result.bestAttack)) return result; // lets wait @@ -295,7 +297,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( if(targets.unreachableEnemies.empty()) return result; - auto speed = activeStack->speed(); + auto speed = activeStack->getMovementRange(); if(speed == 0) return result; @@ -322,7 +324,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( auto turnsToRich = (distance - 1) / speed + 1; auto hexes = closestStack->getSurroundingHexes(); - auto enemySpeed = closestStack->speed(); + auto enemySpeed = closestStack->getMovementRange(); auto speedRatio = speed / static_cast(enemySpeed); auto multiplier = speedRatio > 1 ? 1 : speedRatio; @@ -481,11 +483,6 @@ float BattleExchangeEvaluator::evaluateExchange( 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 @@ -687,11 +684,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( 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: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); #endif @@ -699,69 +691,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( return v.getScore(); } -void BattleExchangeVariant::adjustPositions( - std::vector attackers, - const AttackPossibility & ap, - std::map & reachabilityMap) -{ - auto hexes = ap.attack.defender->getSurroundingHexes(); - - boost::sort(attackers, [&](const battle::Unit * u1, const battle::Unit * u2) -> bool - { - if(attackerValue[u1->unitId()].isRetalitated && !attackerValue[u2->unitId()].isRetalitated) - return true; - - if(attackerValue[u2->unitId()].isRetalitated && !attackerValue[u1->unitId()].isRetalitated) - return false; - - return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value; - }); - - vstd::erase_if_present(hexes, ap.from); - vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); - - float notRealizedDamage = 0; - - for(auto unit : attackers) - { - if(unit->unitId() == ap.attack.attacker->unitId()) - continue; - - if(!vstd::contains_if(hexes, [&](BattleHex h) -> bool - { - return vstd::contains(reachabilityMap[h], unit); - })) - { - notRealizedDamage += attackerValue[unit->unitId()].value; - continue; - } - - auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float - { - auto score = vstd::contains(reachabilityMap[h], unit) - ? reachabilityMap[h].size() - : 0; - - if(unit->doubleWide()) - { - auto backHex = unit->occupiedHex(h); - - if(vstd::contains(hexes, backHex)) - score += reachabilityMap[backHex].size(); - } - - return score; - }); - - hexes.erase(desiredPosition); - } - - if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value) - { - dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); - } -} - bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) { for(auto pos : ap.attack.attacker->getSurroundingHexes()) @@ -824,7 +753,7 @@ std::vector BattleExchangeEvaluator::getOneTurnReachableUn continue; } - auto unitSpeed = unit->speed(turn); + auto unitSpeed = unit->getMovementRange(turn); auto radius = unitSpeed * (turn + 1); ReachabilityInfo unitReachability = vstd::getOrCompute( @@ -887,10 +816,10 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb continue; auto blockedUnitDamage = unit->getMinDamage(hb.battleCanShoot(unit)) * unit->getCount(); - auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage); + float ratio = blockedUnitDamage / (float)(blockedUnitDamage + activeUnitDamage + 0.01); auto unitReachability = turnBattle.getReachability(unit); - auto unitSpeed = unit->speed(turn); // Cached value, to avoid performance hit + auto unitSpeed = unit->getMovementRange(turn); // Cached value, to avoid performance hit for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) { diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index cca98ae12..97af9a3f7 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -106,11 +106,6 @@ public: const BattleScore & getScore() const { return dpsScore; } - void adjustPositions( - std::vector attackers, - const AttackPossibility & ap, - std::map & reachabilityMap); - private: BattleScore dpsScore; std::map attackerValue; diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index e0eb6333b..527471b08 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -117,7 +117,7 @@ std::vector::iterator ArmyManager::getWeakestCreature(std::vectorgetLevel() != right.creature->getLevel()) return left.creature->getLevel() < right.creature->getLevel(); - return left.creature->speed() > right.creature->speed(); + return left.creature->getMovementRange() > right.creature->getMovementRange(); }); return weakest; diff --git a/AI/VCAI/ArmyManager.cpp b/AI/VCAI/ArmyManager.cpp index d639c125e..72ab24d6b 100644 --- a/AI/VCAI/ArmyManager.cpp +++ b/AI/VCAI/ArmyManager.cpp @@ -63,7 +63,7 @@ std::vector::iterator ArmyManager::getWeakestCreature(std::vectorgetLevel() != right.creature->getLevel()) return left.creature->getLevel() < right.creature->getLevel(); - return left.creature->speed() > right.creature->speed(); + return left.creature->getMovementRange() > right.creature->getMovementRange(); }); return weakest; diff --git a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp index 68e516e1a..dc067db74 100644 --- a/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/VCAI/Pathfinding/AIPathfinderConfig.cpp @@ -41,6 +41,7 @@ namespace AIPathfinding std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero()) { + options.ignoreGuards = false; options.useEmbarkAndDisembark = true; options.useTeleportTwoWay = true; options.useTeleportOneWay = true; diff --git a/CCallback.cpp b/CCallback.cpp index 7b87bc913..85cf0471b 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -405,7 +405,10 @@ std::optional CBattleCallback::makeSurrenderRetreatDecision(const std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) { - return activeBattles.at(battleID); + if (activeBattles.count(battleID)) + return activeBattles.at(battleID); + + throw std::runtime_error("Failed to find battle " + std::to_string(battleID.getNum()) + " of player " + player->toString() + ". Number of ongoing battles: " + std::to_string(activeBattles.size())); } std::optional CBattleCallback::getPlayerID() const @@ -415,10 +418,18 @@ std::optional CBattleCallback::getPlayerID() const void CBattleCallback::onBattleStarted(const IBattleInfo * info) { + if (activeBattles.count(info->getBattleID()) > 0) + throw std::runtime_error("Player " + player->toString() + " is already engaged in battle " + std::to_string(info->getBattleID().getNum())); + + logGlobal->debug("Battle %d started for player %s", info->getBattleID(), player->toString()); activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); } void CBattleCallback::onBattleEnded(const BattleID & battleID) { + if (activeBattles.count(battleID) == 0) + throw std::runtime_error("Player " + player->toString() + " is not engaged in battle " + std::to_string(battleID.getNum())); + + logGlobal->debug("Battle %d ended for player %s", battleID, player->toString()); activeBattles.erase(battleID); } diff --git a/ChangeLog.md b/ChangeLog.md index f55e37766..20db36fee 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,91 @@ +# 1.4.2 -> 1.4.3 + +### General +* Fixed the synchronisation of the audio and video of the opening movies. +* Fixed a bug that caused spells from mods to not show up in the Mage's Guild. +* Changed the default SDL driver on Windows from opengl to autodetection +* When a hero visits a town with a garrisoned hero, they will now automatically exchange spells if one of them has the Scholar skill. +* Movement and mana points are now replenished for new heroes in taverns. + +### Multiplayer +* Simturn contact detection will now correctly check for hero moving range +* Simturn contact detection will now ignore wandering monsters +* Right-clicking the Simturns AI option now displays a tooltip +* Interaction attempts with other players during simturns will now have more concise error messages +* Turn timers are now limited to 24 hours in order to prevent bugs caused by an integer overflow. +* Fixed delays when editing turn timer duration +* Ending a turn during simturns will now block the interface correctly. + +### Campaigns +* Player will no longer start the United Front of Song for the Father campaign with two Nimbuses. +* Fixed missing campaign description after loading saved game +* Campaign completion checkmarks will now be displayed after the entire campaign has been completed, rather than just after the first scenario. +* Fixed positioning of prologue and epilogue text during campaign scenario intros + +### Interface +* Added an option to hide adventure map window when town or battle window are open +* Fixed switching between pages on small version of spellbook +* Saves with long filenames are now truncated in the UI to prevent overflow. +* Added option to sort saved games by change date +* Game now shows correct resource when selecting start bonus +* It is now possible to inspect commander skills during battles. +* Fixed incorrect cursor being displayed when hovering over navigable water tiles +* Fixed incorrect cursor display when hovering over water objects accessible from shore + +### Stability +* Fixed a crash when using the 'vcmiobelisk' cheat more than once. +* Fixed crash when reaching level 201. The maximum level is now limited to 197. +* Fixed crash when accessing a spell with an invalid SPELLCASTER bonus +* Fixed crash when trying to play music for an inaccessible tile +* Fixed memory corruption on loading of old mods with illegal 'index' field +* Fixed possible crash on server shutdown on Android +* Fixed possible crash when the affinity of the hero class is set to an invalid value +* Fixed crash on invalid creature in hero army due to outdated or broken mods +* Failure to initialise video subsystem now displays error message instead of silent crash + +### Random Maps Generator +* Fixed possible creation of a duplicate hero in a random map when the player has chosen the starting hero. +* Fixed banning of quest artifacts on random maps +* Fixed banning of heroes in prison on random maps + +### Battles +* Battle turn queue now displays current turn +* Added option to show unit statistics sidebar in battle +* Right-clicking on a unit in the battle turn queue now displays the unit details popup. +* Fixed error messages for SUMMON_GUARDIANS and TRANSMUTATION bonuses +* Fixed Dendroid Bind ability +* Black Dragons no longer hate Giants, only Titans +* Spellcasting units such as Archangels can no longer cast spells on themselves. +* Coronius specialty will now correctly select affected units + +### Launcher +* Welcome screen will automatically detect existing Heroes 3 installation on Windows +* It is now possible to install mods by dragging and dropping onto the launcher. +* It is now possible to install maps and campaigns by dragging and dropping onto the launcher. +* Czech launcher translation added +* Added option to select preferred SDL driver in launcher + +### Map Editor +* Fixed saving of allowed abilities, spells, artifacts or heroes + +### AI +* AI will no longer attempt to move immobilized units, such as those under the effect of Dendroid Bind. +* Fixed shooters not shooting when they have a range penalty +* Fixed Fire Elemental spell casting +* Fixed rare bug where unit would sometimes do nothing in battle + +### Modding +* Added better reporting of "invalid identifiers" errors with suggestions on how to fix them +* Added FEROCITY bonus (HotA Aysiud) +* Added ENEMY_ATTACK_REDUCTION bonus (HotA Nix) +* Added REVENGE bonus (HotA Haspid) +* Extended DEATH_STARE bonus to support Pirates ability (HotA) +* DEATH_STARE now supports spell ID in addInfo field to override used spell +* SPELL_BEFORE_ATTACK bonus now supports spell priorities +* FIRST_STRIKE bonus supports subtypes damageTypeMelee, damageTypeRanged and damageTypeAll +* BLOCKS_RETALIATION now also blocks FIRST_STRIKE bonus +* Added 'canCastOnSelf' field for spells to allow creatures to cast spells on themselves. + # 1.4.1 -> 1.4.2 ### General diff --git a/Global.h b/Global.h index e3987bbbe..1e8d6347f 100644 --- a/Global.h +++ b/Global.h @@ -100,6 +100,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #define _USE_MATH_DEFINES +#ifndef NDEBUG +// Enable additional debug checks from glibc / libstdc++ when building with enabled assertions +// Since these defines must be declared BEFORE including glibc header we can not check for __GLIBCXX__ macro to detect that glibc is in use +# define _GLIBCXX_ASSERTIONS +#endif + #include #include #include diff --git a/Mods/vcmi/Sprites/lobby/selectionTabSortDate.json b/Mods/vcmi/Sprites/lobby/selectionTabSortDate.json new file mode 100644 index 000000000..9022e4244 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/selectionTabSortDate.json @@ -0,0 +1,7 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "selectionTabSortDate.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/selectionTabSortDate.png b/Mods/vcmi/Sprites/lobby/selectionTabSortDate.png new file mode 100644 index 000000000..4e58b2838 Binary files /dev/null and b/Mods/vcmi/Sprites/lobby/selectionTabSortDate.png differ diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index fc00410a3..690e34700 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -70,6 +70,7 @@ "vcmi.lobby.mapPreview" : "Map preview", "vcmi.lobby.noPreview" : "no preview", "vcmi.lobby.noUnderground" : "no underground", + "vcmi.lobby.sortDate" : "Sorts maps by change date", "vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.", "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", @@ -144,6 +145,8 @@ "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.adventureOptions.hideBackground.hover" : "Hide Background", + "vcmi.adventureOptions.hideBackground.help" : "{Hide Background}\n\nHide the adventuremap in the background and show a texture instead.", "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", @@ -183,6 +186,10 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.battleWindow.killed" : "Killed", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s were killed by accurate shots!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s was killed with an accurate shot!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s were killed by accurate shots!", "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", @@ -327,7 +334,7 @@ "vcmi.stackExperience.rank.8" : "Elite", "vcmi.stackExperience.rank.9" : "Master", "vcmi.stackExperience.rank.10" : "Ace", - + "core.bonus.ADDITIONAL_ATTACK.name": "Double Strike", "core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice", "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", @@ -366,6 +373,8 @@ "core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn", "core.bonus.ENCHANTED.name": "Enchanted", "core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignore Attack (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "When being attacked, ${val}% of the attacker's attack is ignored", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored", "core.bonus.FIRE_IMMUNITY.name": "Fire immunity", @@ -378,6 +387,8 @@ "core.bonus.FEAR.description": "Causes Fear on an enemy stack", "core.bonus.FEARLESS.name": "Fearless", "core.bonus.FEARLESS.description": "Immune to Fear ability", + "core.bonus.FEROCITY.name": "Ferocity", + "core.bonus.FEROCITY.description": "Attacks ${val} additional times if killed anybody", "core.bonus.FLYING.name": "Fly", "core.bonus.FLYING.description": "Flies when moving (ignores obstacles)", "core.bonus.FREE_SHOOTING.name": "Shoot Close", @@ -432,6 +443,8 @@ "core.bonus.REBIRTH.description": "${val}% of stack will rise after death", "core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return", "core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack", + "core.bonus.REVENGE.name": "Revenge", + "core.bonus.REVENGE.description": "Deals extra damage based on attacker's lost health in battle", "core.bonus.SHOOTER.name": "Ranged", "core.bonus.SHOOTER.description": "Creature can shoot", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 90b0fafb6..a8946cebc 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -70,7 +70,9 @@ "vcmi.lobby.mapPreview" : "Kartenvorschau", "vcmi.lobby.noPreview" : "Keine Vorschau", "vcmi.lobby.noUnderground" : "Kein Untergrund", + "vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum", + "vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.", "vcmi.server.errors.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}", @@ -101,7 +103,7 @@ "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.hover" : "Skalierung auswählen", "vcmi.systemOptions.scalingMenu.help" : "Ändern der Skalierung des Interfaces im Spiel.", "vcmi.systemOptions.longTouchButton.hover" : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" "vcmi.systemOptions.longTouchButton.help" : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", @@ -135,12 +137,16 @@ "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.smoothDragging.hover" : "Nahtloses Ziehen der Karte", "vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.", + "vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen", + "vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Fading-Effekte überspringen}\n\nWenn diese Funktion aktiviert ist, werden das Ausblenden von Objekten und ähnliche Effekte übersprungen (Ressourcensammlung, Anlegen von Schiffen usw.). Macht die Benutzeroberfläche in einigen Fällen auf Kosten der Ästhetik reaktiver. Besonders nützlich in PvP-Spielen. Für maximale Bewegungsgeschwindigkeit ist das Überspringen unabhängig von dieser Einstellung aktiv.", "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.adventureOptions.hideBackground.hover" : "Hintergrund ausblenden", + "vcmi.adventureOptions.hideBackground.help" : "{Hintergrund ausblenden}\n\nDie Abenteuerkarte im Hintergrund ausblenden und stattdessen eine Textur anzeigen.", "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", @@ -180,6 +186,10 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", + "vcmi.battleWindow.killed" : "Getötet", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s wurden durch gezielte Schüsse getötet!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s wurde mit einem gezielten Schuss getötet!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s wurden durch gezielte Schüsse getötet!", "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", @@ -242,18 +252,29 @@ "vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen", "vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen", + "vcmi.optionsTab.selectPreset" : "Voreinstellung", "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.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Zug beendet. Jeder laufende Kampf endet mit einem Verlust.", "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.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn der {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", "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.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.", + "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", @@ -278,14 +299,6 @@ "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 @@ -321,7 +334,7 @@ "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", @@ -360,6 +373,8 @@ "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_ATTACK_REDUCTION.name": "Angriff ignorieren (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Bei Angriff, wird ${val}% des Angreifers ignoriert.", "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", @@ -372,6 +387,8 @@ "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.FEROCITY.name": "Wildheit", + "core.bonus.FEROCITY.description": "Greift ${val} zusätzliche Male an, wenn jemand getötet wird", "core.bonus.FLYING.name": "Fliegen", "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", "core.bonus.FREE_SHOOTING.name": "Nah schießen", @@ -426,6 +443,8 @@ "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.REVENGE.name": "Rache", + "core.bonus.REVENGE.description": "Verursacht zusätzlichen Schaden basierend auf der verlorenen Gesundheit des Angreifers im Kampf", "core.bonus.SHOOTER.name": "Fernkämpfer", "core.bonus.SHOOTER.description": "Kreatur kann schießen", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 533f91c29..9445da006 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -54,6 +54,8 @@ "vcmi.radialWheel.moveDown" : "Przenieś w dół", "vcmi.radialWheel.moveBottom" : "Przenieś na spód", + "vcmi.spellBook.search" : "szukaj...", + "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", "vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się", @@ -68,7 +70,9 @@ "vcmi.lobby.mapPreview" : "Podgląd mapy", "vcmi.lobby.noPreview" : "brak podglądu", "vcmi.lobby.noUnderground" : "brak podziemi", + "vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji", + "vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.", "vcmi.server.errors.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}", @@ -139,6 +143,8 @@ "vcmi.adventureOptions.mapScrollSpeed1.help": "Ustaw szybkość przesuwania mapy na bardzo wolną.", "vcmi.adventureOptions.mapScrollSpeed5.help": "Ustaw szybkość przesuwania mapy na bardzo szybką.", "vcmi.adventureOptions.mapScrollSpeed6.help": "Ustaw szybkość przesuwania mapy na błyskawiczną.", + "vcmi.adventureOptions.hideBackground.hover" : "Ukryj tło", + "vcmi.adventureOptions.hideBackground.help" : "{Ukryj tło}\n\nUkryj mapę przygody w tle i pokaż zastępczo teksturę.", "vcmi.battleOptions.queueSizeLabel.hover": "Pokaż kolejkę ruchu jednostek", "vcmi.battleOptions.queueSizeNoneButton.hover": "BRAK", @@ -178,6 +184,10 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d", "vcmi.battleWindow.damageEstimation.kills" : "zginie: %d", "vcmi.battleWindow.damageEstimation.kills.1" : "zginie: %d", + "vcmi.battleWindow.killed" : "Zabici", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s zostało zabitych poprzez celne strzały!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s został zabity poprzez celny strzał!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s zostali zabici poprzez celne strzały!", "vcmi.battleResultsWindow.applyResultsLabel" : "Zatwierdź wynik bitwy", @@ -349,6 +359,8 @@ "core.bonus.ENCHANTER.description": "Może rzucać masowy czar ${subtype.spell} każdej tury", "core.bonus.ENCHANTED.name": "Zaczarowany", "core.bonus.ENCHANTED.description": "Pod wpływem trwałego ${subtype.spell}", + "core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignoruje Atak (${val}%)", + "core.bonus.ENEMY_ATTACK_REDUCTION.description": "Przy zostaniu zaatakowanym ignoruje ${val}% ataku wroga", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoruje Obronę (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoruje część obrony podczas ataku", "core.bonus.FIRE_IMMUNITY.name": "Odporność na ogień", @@ -361,6 +373,8 @@ "core.bonus.FEAR.description": "Wzbudza strach na wrogim stworzeniu", "core.bonus.FEARLESS.name": "Nieustraszony", "core.bonus.FEARLESS.description": "Odporny na strach", + "core.bonus.FEROCITY.name": "Dzikość", + "core.bonus.FEROCITY.description": "Dodatkowe ${val} ataków jeżeli zabito kogokolwiek", "core.bonus.FLYING.name": "Lot", "core.bonus.FLYING.description": "Może latać (ignoruje przeszkody)", "core.bonus.FREE_SHOOTING.name": "Bliski Strzał", @@ -415,6 +429,8 @@ "core.bonus.REBIRTH.description": "${val}% stworzeń powstanie po śmierci", "core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót", "core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz", + "core.bonus.REVENGE.name": "Odwet", + "core.bonus.REVENGE.description": "Zadaje dodatkowe obrażenia zależne od strat własnych oddziału", "core.bonus.SHOOTER.name": "Dystansowy", "core.bonus.SHOOTER.description": "Stworzenie może strzelać", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index dd689a9c1..50f171df1 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -70,6 +70,7 @@ "vcmi.lobby.mapPreview" : "Огляд мапи", "vcmi.lobby.noPreview" : "огляд недоступний", "vcmi.lobby.noUnderground" : "немає підземелля", + "vcmi.lobby.sortDate" : "Сортувати мапи за датою зміни", "vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.", "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", @@ -144,6 +145,8 @@ "vcmi.adventureOptions.mapScrollSpeed1.help": "Встановити швидкість розгортання мапи - дуже повільно", "vcmi.adventureOptions.mapScrollSpeed5.help": "Встановити швидкість розгортання мапи - дуже швидко", "vcmi.adventureOptions.mapScrollSpeed6.help": "Встановити швидкість розгортання мапи - миттєво", + "vcmi.adventureOptions.hideBackground.hover" : "Приховувати тло", + "vcmi.adventureOptions.hideBackground.help" : "{Приховувати тло}\n\nПриховати мапу пригод на задньому тлі і показати замість неї текстуру.", "vcmi.battleOptions.queueSizeLabel.hover": "Вигляд черги ходу істот", "vcmi.battleOptions.queueSizeNoneButton.hover": "ВИМК", @@ -183,6 +186,10 @@ "vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження", "vcmi.battleWindow.damageEstimation.kills" : "%d загинуть", "vcmi.battleWindow.damageEstimation.kills.1" : "%d загине", + "vcmi.battleWindow.killed" : "Загинуло", + "vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s було вбито влучними пострілами!", + "vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s було вбито влучним пострілом!", + "vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s було вбито влучними пострілами!", "vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою", diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 0ade17ddb..fea14c8d2 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 1421 - versionName "1.4.2" + versionCode 1430 + versionName "1.4.3" setProperty("archivesBaseName", "vcmi") } diff --git a/client/CMT.cpp b/client/CMT.cpp index 21f84425a..a3ff7e266 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -205,7 +205,7 @@ int main(int argc, char * argv[]) logGlobal->info("The log file will be saved to %s", logPath); // Init filesystem and settings - preinitDLL(::console); + preinitDLL(::console, false); Settings session = settings.write["session"]; auto setSettingBool = [](std::string key, std::string arg) { diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 722048dd5..8884294e2 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1676,7 +1676,8 @@ void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const C { EVENT_HANDLER_CALLED_BY_CLIENT; auto onWindowClosed = [this, queryID](){ - cb->selectionMade(0, queryID); + if (queryID != QueryID::NONE) + cb->selectionMade(0, queryID); }; GH.windows().createAndPushWindow(object, onWindowClosed); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 2def4c5fa..688030c75 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -729,7 +729,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto finisher = [=]() { - if(ourCampaign->campaignSet != "") + if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished()) { Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()]; entry->Bool() = true; diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index f76d1912b..9f5129898 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -615,6 +615,14 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath return dat; } +Point CVideoPlayer::size() +{ + if(frame) + return Point(frame->width, frame->height); + else + return Point(0, 0); +} + // Plays a video. Only works for overlays. bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) { @@ -626,6 +634,8 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) pos.y = y; frameTime = 0.0; + auto lastTimePoint = boost::chrono::steady_clock::now(); + while(nextFrame()) { if(stopOnKey) @@ -646,10 +656,17 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) #else auto packet_duration = frame->duration; #endif - double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); - uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); + // Framerate delay + double targetFrameTimeSeconds = packet_duration * av_q2d(format->streams[stream]->time_base); + auto targetFrameTime = boost::chrono::milliseconds(static_cast(1000 * (targetFrameTimeSeconds))); - boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); + auto timePointAfterPresent = boost::chrono::steady_clock::now(); + auto timeSpentBusy = boost::chrono::duration_cast(timePointAfterPresent - lastTimePoint); + + if (targetFrameTime > timeSpentBusy) + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + + lastTimePoint = boost::chrono::steady_clock::now(); } return true; diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 211b9db3f..38499bede 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -38,6 +38,7 @@ public: return false; } virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; + virtual Point size() { return Point(0, 0); }; }; class CEmptyVideoPlayer final : public IMainVideoPlayer @@ -109,6 +110,8 @@ public: std::pair, si64> getAudio(const VideoPath & videoToOpen) override; + Point size() override; + //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; diff --git a/client/Client.cpp b/client/Client.cpp index f41ba25d4..3ddfc03e8 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -560,18 +560,16 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player) void CClient::battleStarted(const BattleInfo * info) { + std::shared_ptr att, def; + auto & leftSide = info->sides[0]; + auto & rightSide = info->sides[1]; + for(auto & battleCb : battleCallbacks) { - if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || !battleCb.first.isValidPlayer()) - { + if(!battleCb.first.isValidPlayer() || battleCb.first == leftSide.color || battleCb.first == rightSide.color) 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) { diff --git a/client/Client.h b/client/Client.h index 32cbe1db2..c6890cd31 100644 --- a/client/Client.h +++ b/client/Client.h @@ -164,6 +164,7 @@ public: 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 giveExperience(const CGHeroInstance * hero, TExpType val) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; @@ -201,6 +202,7 @@ public: 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 setMovePoints(ObjectInstanceID hid, int val, bool absolute) 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 {}; diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 3ac27b79e..935d6b999 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -31,6 +31,7 @@ #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../CMT.h" #include "../PlayerLocalState.h" #include "../CPlayerInterface.h" @@ -168,6 +169,15 @@ void AdventureMapInterface::show(Canvas & to) void AdventureMapInterface::dim(Canvas & to) { + if(settings["adventure"]["hideBackground"].Bool()) + for (auto window : GH.windows().findWindows()) + { + if(!std::dynamic_pointer_cast(window) && std::dynamic_pointer_cast(window) && std::dynamic_pointer_cast(window)->pos.w >= 800 && std::dynamic_pointer_cast(window)->pos.w >= 600) + { + to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"))); + return; + } + } for (auto window : GH.windows().findWindows()) { if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) @@ -467,6 +477,18 @@ void AdventureMapInterface::hotkeyEndingTurn() LOCPLINT->cb->endTurn(); mapAudio->onPlayerTurnEnded(); + + // Normally, game will receive PlayerStartsTurn call almost instantly with new player ID that will switch UI to waiting mode + // However, when simturns are active it is possible for such call not to come because another player is still acting + // So find first player other than ours that is acting at the moment and update UI as if he had started turn + for (auto player = PlayerColor(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (player != LOCPLINT->playerID && LOCPLINT->cb->isPlayerMakingTurn(player)) + { + onEnemyTurnStarted(player, LOCPLINT->cb->getStartInfo()->playerInfos.at(player).isControlledByHuman()); + break; + } + } } const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) @@ -679,7 +701,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorMove[turns]); else - CCS->curh->set(cursorSailVisit[turns]); + CCS->curh->set(cursorSail[turns]); break; case EPathNodeAction::VISIT: @@ -694,6 +716,15 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) } else if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorVisit[turns]); + else if (pathNode->layer == EPathfindingLayer::SAIL && + objAtTile && + objAtTile->isCoastVisitable() && + pathNode->theNodeBefore && + pathNode->theNodeBefore->layer == EPathfindingLayer::LAND ) + { + // exception - when visiting shipwreck located on coast from land - show 'horse' cursor, not 'ship' cursor + CCS->curh->set(cursorVisit[turns]); + } else CCS->curh->set(cursorSailVisit[turns]); break; diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index f5235b976..8efabe249 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -243,6 +243,9 @@ void CInGameConsole::startEnteringText() if (!isActive()) return; + if(enteredText != "") + return; + assert(currentStatusBar.expired());//effectively, nullptr check currentStatusBar = GH.statusbar(); diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 64218498a..81a7cf002 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -173,8 +173,10 @@ void MapAudioPlayer::updateMusic() { if(audioPlaying && playerMakingTurn && currentSelection) { - const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; - CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); + const auto * tile = LOCPLINT->cb->getTile(currentSelection->visitablePos()); + + if (tile) + CCS->musich->playMusicFromSet("terrain", tile->terType->getJsonKey(), true, false); } if(audioPlaying && enemyMakingTurn) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index b058bd263..a6e4f5784 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -568,7 +568,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (targetStack && targetStackOwned && targetStack->speed() > 0); + return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0); case PossiblePlayerBattleAction::CREATURE_INFO: return (targetStack && targetStackOwned && targetStack->alive()); diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index f56298930..2bff6e8ce 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -363,7 +363,7 @@ bool MovementAnimation::init() Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); - progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); + progressPerSecond = AnimationControls::getMovementRange(stack->unitType()); begX = begPosition.x; begY = begPosition.y; diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index b0ee095b5..d55c22360 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -11,6 +11,7 @@ #include "BattleFieldController.h" #include "BattleInterface.h" +#include "BattleWindow.h" #include "BattleActionsController.h" #include "BattleInterfaceClasses.h" #include "BattleEffectsController.h" @@ -360,10 +361,7 @@ std::set BattleFieldController::getMovementRangeForHoveredStack() 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); + auto hoveredStack = getHoveredStack(); if(hoveredStack) { std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); @@ -591,10 +589,9 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); BattleHex hoveredHex = getHoveredHex(); - if(hoveredHex == BattleHex::INVALID) - return; - const CStack * hoveredStack = getHoveredStack(); + if(!hoveredStack && hoveredHex == BattleHex::INVALID) + return; // skip range limit calculations if unit hovered is not a shooter if(hoveredStack && hoveredStack->isShooter()) @@ -608,7 +605,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas) calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); } - auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; + auto const & hoveredMouseHexes = hoveredHex != BattleHex::INVALID && owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) { @@ -676,6 +673,14 @@ const CStack* BattleFieldController::getHoveredStack() auto hoveredHex = getHoveredHex(); const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(owner.windowObject->getQueueHoveredUnitId().has_value()) + { + auto stacks = owner.getBattle()->battleGetAllStacks(); + for(const CStack * stack : stacks) + if(stack->unitId() == *owner.windowObject->getQueueHoveredUnitId()) + hoveredStack = stack; + } + return hoveredStack; } diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 99920ab2a..c2255b6f5 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -640,7 +640,7 @@ void BattleInterface::tacticPhaseEnd() static bool immobile(const CStack *s) { - return !s->speed(0, true); //should bound stacks be immobile? + return s->getMovementRange() == 0; //should bound stacks be immobile? } void BattleInterface::tacticNextStack(const CStack * current) diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 0986834ce..b8667fdc3 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -36,6 +36,7 @@ #include "../widgets/TextControls.h" #include "../widgets/MiscWidgets.h" #include "../windows/CMessage.h" +#include "../windows/CCreatureWindow.h" #include "../windows/CSpellWindow.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" @@ -447,6 +448,119 @@ void HeroInfoBasicPanel::show(Canvas & to) CIntObject::show(to); } + +StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground) + : CIntObject(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if (position != nullptr) + moveTo(*position); + + if(initializeBackground) + { + background = std::make_shared(ImagePath::builtin("CCRPOP")); + background->pos.y += 37; + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background->colorize(stack->getOwner()); + background2 = std::make_shared(ImagePath::builtin("CHRPOP")); + background2->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background2->colorize(stack->getOwner()); + } + + initializeData(stack); +} + +void StackInfoBasicPanel::initializeData(const CStack * stack) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), stack->creatureId() + 2, 0, 10, 6)); + labels.push_back(std::make_shared(10 + 58, 6 + 64, FONT_MEDIUM, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(stack->getCount(), 4))); + + auto attack = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getAttack(stack->isShooter())) + "(" + std::to_string(stack->getAttack(stack->isShooter())) + ")"; + auto defense = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getDefense(stack->isShooter())) + "(" + std::to_string(stack->getDefense(stack->isShooter())) + ")"; + auto damage = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getMinDamage(stack->isShooter())) + "-" + std::to_string(stack->getMaxDamage(stack->isShooter())); + auto health = CGI->creatures()->getByIndex(stack->creatureIndex())->getMaxHealth(); + auto morale = stack->moraleVal(); + auto luck = stack->luckVal(); + + auto killed = stack->getKilled(); + auto healthRemaining = TextOperations::formatMetric(std::max(stack->getAvailableHealth() - (stack->getCount() - 1) * health, (si64)0), 4); + + //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[386] + ":")); + labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[389] + ":")); + + labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, attack)); + labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, defense)); + labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, damage)); + labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(health))); + + //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)); + + //extra information + labels.push_back(std::make_shared(9, 168, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, VLC->generaltexth->translate("vcmi.battleWindow.killed") + ":")); + labels.push_back(std::make_shared(9, 180, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[389] + ":")); + + labels.push_back(std::make_shared(69, 180, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(killed))); + labels.push_back(std::make_shared(69, 192, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, healthRemaining)); + + //spells + static const Point firstPos(15, 206); // position of 1st spell box + static const Point offset(0, 38); // offset of each spell box from previous + + for(int i = 0; i < 3; i++) + icons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), 78, 0, firstPos.x + offset.x * i, firstPos.y + offset.y * i)); + + int printed=0; //how many effect pics have been printed + std::vector spells = stack->activeSpells(); + for(SpellID effect : spells) + { + //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) + { + //FIXME: support permanent duration + int duration = stack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; + + icons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + if(settings["general"]["enableUiEnhancements"].Bool()) + labels.push_back(std::make_shared(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration))); + if(++printed >= 3 || (printed == 2 && spells.size() > 3)) // interface limit reached + break; + } + } + + if(spells.size() == 0) + labelsMultiline.push_back(std::make_shared(Rect(firstPos.x, firstPos.y, 48, 36), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[674])); + if(spells.size() > 3) + labelsMultiline.push_back(std::make_shared(Rect(firstPos.x + offset.x * 2, firstPos.y + offset.y * 2 - 4, 48, 36), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "...")); +} + +void StackInfoBasicPanel::update(const CStack * updatedInfo) +{ + icons.clear(); + labels.clear(); + labelsMultiline.clear(); + + initializeData(updatedInfo); +} + +void StackInfoBasicPanel::show(Canvas & to) +{ + showAll(to); + CIntObject::show(to); +} + HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) { @@ -870,6 +984,10 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) icon->setFrame(owner->getSiegeShooterIconID(), 1); + roundRect->setEnabled(currentTurn.has_value()); + if(!owner->embedded) + round->setEnabled(currentTurn.has_value()); + amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); if(currentTurn && !owner->embedded) { @@ -878,9 +996,6 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std:: roundRect->pos.w = len + 6; round->setText(tmp); } - roundRect->setEnabled(currentTurn.has_value()); - if(!owner->embedded) - round->setEnabled(currentTurn.has_value()); if(stateIcon) { @@ -939,3 +1054,11 @@ void StackQueue::StackBox::show(Canvas & to) if(isBoundUnitHighlighted()) to.drawBorder(background->pos, Colors::CYAN, 2); } + +void StackQueue::StackBox::showPopupWindow(const Point & cursorPosition) +{ + auto stacks = owner->owner.getBattle()->battleGetAllStacks(); + for(const CStack * stack : stacks) + if(boundUnitID.has_value() && stack->unitId() == *boundUnitID) + GH.windows().createAndPushWindow(stack, true); +} diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 5670edb0b..e1e9c4263 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -37,6 +37,7 @@ class CFilledTexture; class CButton; class CToggleButton; class CLabel; +class CMultiLineLabel; class CTextBox; class CAnimImage; class TransparentFilledRectangle; @@ -145,6 +146,23 @@ public: void update(const InfoAboutHero & updatedInfo); }; +class StackInfoBasicPanel : public CIntObject +{ +private: + std::shared_ptr background; + std::shared_ptr background2; + std::vector> labels; + std::vector> labelsMultiline; + std::vector> icons; +public: + StackInfoBasicPanel(const CStack * stack, Point * position, bool initializeBackground = true); + + void show(Canvas & to) override; + + void initializeData(const CStack * stack); + void update(const CStack * updatedInfo); +}; + class HeroInfoWindow : public CWindowObject { private: @@ -212,6 +230,7 @@ class StackQueue : public CIntObject void show(Canvas & to) override; void showAll(Canvas & to) override; + void showPopupWindow(const Point & cursorPosition) override; bool isBoundUnitHighlighted() const; public: diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index afdd0b124..70532320f 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -26,6 +26,7 @@ #include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" #include "../render/Colors.h" #include "../render/Canvas.h" #include "../render/IRenderHandler.h" @@ -326,7 +327,7 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); } - Point textPosition = amountBG->dimensions()/2 + boxPosition; + Point textPosition = amountBG->dimensions()/2 + boxPosition + Point(0, 1); canvas.draw(amountBG, boxPosition); canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); @@ -812,6 +813,9 @@ void BattleStacksController::updateHoveredStacks() { auto newStacks = selectHoveredStacks(); + if(newStacks.size() == 0) + owner.windowObject->updateStackInfoWindow(nullptr); + for(const auto * stack : mouseHoveredStacks) { if (vstd::contains(newStacks, stack)) @@ -828,11 +832,15 @@ void BattleStacksController::updateHoveredStacks() if (vstd::contains(mouseHoveredStacks, stack)) continue; + owner.windowObject->updateStackInfoWindow(newStacks.size() == 1 && vstd::find_pos(newStacks, stack) == 0 ? stack : nullptr); stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); } + if(mouseHoveredStacks != newStacks) + GH.windows().totalRedraw(); //fix for frozen stack info window and blue border in action bar + mouseHoveredStacks = newStacks; } diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index ccf66c5a9..010602306 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -94,7 +94,6 @@ class BattleStacksController void tickFrameBattleAnimations(uint32_t msPassed); void updateBattleAnimations(uint32_t msPassed); - void updateHoveredStacks(); std::vector selectHoveredStacks(); @@ -127,6 +126,8 @@ public: void showAliveStack(Canvas & canvas, const CStack * stack); void showStack(Canvas & canvas, const CStack * stack); + void updateHoveredStacks(); + void collectRenderableObjects(BattleRenderer & renderer); /// Adds new color filter effect targeting stack diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 892d76395..b25418744 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -287,6 +287,35 @@ void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero panelToUpdate->update(hero); } +void BattleWindow::updateStackInfoWindow(const CStack * stack) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); + + if(stack && stack->unitSide() == BattleSide::DEFENDER) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y) + : Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 135); + defenderStackWindow = std::make_shared(stack, &position); + defenderStackWindow->setEnabled(showInfoWindows); + } + else + defenderStackWindow = nullptr; + + if(stack && stack->unitSide() == BattleSide::ATTACKER) + { + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y) + : Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 135); + attackerStackWindow = std::make_shared(stack, &position); + attackerStackWindow->setEnabled(showInfoWindows); + } + else + attackerStackWindow = nullptr; +} + void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) { if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 0bc57e1f7..c8317e884 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -26,6 +26,7 @@ class BattleRenderer; class StackQueue; class TurnTimerWidget; class HeroInfoBasicPanel; +class StackInfoBasicPanel; /// GUI object that handles functionality of panel at the bottom of combat screen class BattleWindow : public InterfaceObjectConfigurable @@ -36,6 +37,8 @@ class BattleWindow : public InterfaceObjectConfigurable std::shared_ptr console; std::shared_ptr attackerHeroWindow; std::shared_ptr defenderHeroWindow; + std::shared_ptr attackerStackWindow; + std::shared_ptr defenderStackWindow; std::shared_ptr attackerTimerWidget; std::shared_ptr defenderTimerWidget; @@ -100,6 +103,9 @@ public: /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side + void updateStackInfoWindow(const CStack * stack); + /// Get mouse-hovered battle queue unit ID if any found std::optional getQueueHoveredUnitId(); diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index d3c6f7632..2addb4c3d 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -148,7 +148,7 @@ float AnimationControls::getSpellEffectSpeed() return static_cast(getAnimationSpeedFactor() * 10); } -float AnimationControls::getMovementDistance(const CCreature * creature) +float AnimationControls::getMovementRange(const CCreature * creature) { // H3 speed: 2/4/6 tiles per second return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 9029f7437..686e941b5 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -50,7 +50,7 @@ namespace AnimationControls float getSpellEffectSpeed(); /// returns speed of movement animation across the screen, in tiles per second - float getMovementDistance(const CCreature * creature); + float getMovementRange(const CCreature * creature); /// returns speed of movement animation across the screen, in pixels per seconds float getFlightDistance(const CCreature * creature); diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 9a0be245e..2f602e023 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -69,7 +69,7 @@ int ISelectionScreenInfo::getCurrentDifficulty() PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color) { - return getMapInfo()->mapHeader->players[color.getNum()]; + return getMapInfo()->mapHeader->players.at(color.getNum()); } CSelectionBase::CSelectionBase(ESelectionScreen type) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 246104a76..5987d72a6 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -400,12 +400,11 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) - : CWindowObject(BORDERED) +OptionsTab::SelectionWindow::SelectionWindow(const PlayerColor & color, SelType _type) + : CWindowObject(BORDERED), color(color) { addUsedEvents(LCLICK | SHOW_POPUP); - color = _color; type = _type; initialFaction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; @@ -483,7 +482,8 @@ void OptionsTab::SelectionWindow::reopen() { std::shared_ptr window = std::shared_ptr(new SelectionWindow(color, type)); close(); - GH.windows().pushWindow(window); + if(CSH->isMyColor(color) || CSH->isHost()) + GH.windows().pushWindow(window); } void OptionsTab::SelectionWindow::recreate() @@ -537,11 +537,11 @@ void OptionsTab::SelectionWindow::recreate() 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)); + components.push_back(std::make_shared(x-1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x+1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y-1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y+1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text, 56)); + components.push_back(std::make_shared(x, y, FONT_TINY, ETextAlignment::CENTER, color, text, 56)); } void OptionsTab::SelectionWindow::genContentGrid(int lines) @@ -632,7 +632,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() void OptionsTab::SelectionWindow::genContentBonus() { - PlayerSettings set = PlayerSettings(); + PlayerSettings set = SEL->getStartInfo()->playerInfos.find(color)->second; int i = 0; for(auto elem : allowedBonus) @@ -774,7 +774,7 @@ OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSett OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; image = std::make_shared(getImageName(), getImageIndex()); - subtitle = std::make_shared(23, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName()); + subtitle = std::make_shared(24, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName(), 71); pos = image->pos; @@ -889,7 +889,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con 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); + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name, 95); else { labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 691a162bb..d25737fe8 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -148,7 +148,7 @@ private: public: void reopen(); - SelectionWindow(PlayerColor _color, SelType _type); + SelectionWindow(const PlayerColor & color, SelType _type); }; /// Image with current town/hero/bonus diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 3dc78819b..8b12101f8 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -22,6 +22,14 @@ #include "../../lib/MetaString.h" #include "../../lib/CGeneralTextHandler.h" +static std::string timeToString(int time) +{ + std::stringstream ss; + ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; + return ss.str(); +}; + + std::vector OptionsTabBase::getTimerPresets() const { std::vector result; @@ -141,43 +149,51 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) else if(l.empty()) return sec; - return std::stoi(l) * 60 + std::stoi(r); + return std::min(24*60, std::stoi(l)) * 60 + std::stoi(r); }; - addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ + addCallback("parseAndSetTimer_base", [this, parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.baseTimer = time; CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(time), false); } }); - addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ + addCallback("parseAndSetTimer_turn", [this, parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.turnTimer = time; CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(time), false); } }); - addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ + addCallback("parseAndSetTimer_battle", [this, parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.battleTimer = time; CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(time), false); } }); - addCallback("parseAndSetTimer_unit", [parseTimerString](const std::string & str){ + addCallback("parseAndSetTimer_unit", [this, parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.unitTimer = time; CSH->setTurnTimerInfo(tinfo); + if(auto ww = widget("chessFieldUnit")) + ww->setText(timeToString(time), false); } }); @@ -359,14 +375,6 @@ void OptionsTabBase::recreate() } } - //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")) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 8098715f2..ec0d388a7 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -110,6 +110,8 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); case _fileName: //by filename return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); + case _changeDate: //by changedate + return aaa->lastWrite < bbb->lastWrite; default: return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); } @@ -149,9 +151,11 @@ SelectionTab::SelectionTab(ESelectionScreen Type) : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false) { OBJ_CONSTRUCTION; - + generalSortingBy = getSortBySelectionScreen(tabType); + bool enableUiEnhancements = settings["general"]["enableUiEnhancements"].Bool(); + if(tabType != ESelectionScreen::campaignList) { sortingBy = _format; @@ -208,6 +212,12 @@ SelectionTab::SelectionTab(ESelectionScreen Type) break; } + if(enableUiEnhancements) + { + buttonsSortBy.push_back(std::make_shared(Point(371, 85), AnimationPath::builtin("lobby/selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate))); + buttonsSortBy.back()->setAnimateLonelyFrame(true); + } + iconsMapFormats = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCSELC.DEF")); iconsVictoryCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRVICT.DEF")); iconsLossCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRLOSS.DEF")); @@ -215,7 +225,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); labelTabTitle = std::make_shared(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, tabTitle); - slider = std::make_shared(Point(372, 86), tabType != ESelectionScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE); + slider = std::make_shared(Point(372, 86 + (enableUiEnhancements ? 30 : 0)), (tabType != ESelectionScreen::saveGame ? 480 : 430) - (enableUiEnhancements ? 30 : 0), std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE); slider->setPanningStep(24); // create scroll bounds that encompass all area in this UI element to the left of slider (including area of slider itself) @@ -823,7 +833,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico { 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 = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "", 185); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelAmountOfPlayers->setAutoRedraw(false); @@ -866,11 +876,13 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconLossCondition->disable(); labelNumberOfCampaignMaps->disable(); labelName->enable(); + labelName->setMaxWidth(316); labelName->setText(info->folderName); labelName->setColor(color); return; } + labelName->enable(); if(info->campaign) { labelAmountOfPlayers->disable(); @@ -885,6 +897,7 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool ostr << info->campaign->scenariosCount(); labelNumberOfCampaignMaps->setText(ostr.str()); labelNumberOfCampaignMaps->setColor(color); + labelName->setMaxWidth(316); } else { @@ -905,8 +918,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool iconVictoryCondition->setFrame(info->mapHeader->victoryIconIndex, 0); iconLossCondition->enable(); iconLossCondition->setFrame(info->mapHeader->defeatIconIndex, 0); + labelName->setMaxWidth(185); } - labelName->enable(); labelName->setText(info->getNameForList()); labelName->setColor(color); } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index b457bcef8..529c2487f 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -23,7 +23,7 @@ class IImage; enum ESortBy { - _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName + _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName, _changeDate }; //_numOfMaps is for campaigns class ElementInfo : public CMapInfo diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 6446d6d1b..d81bd07e7 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -45,10 +45,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f void CPrologEpilogVideo::show(Canvas & to) { to.drawColor(pos, Colors::BLACK); - //BUG: some videos are 800x600 in size while some are 800x400 - //VCMI should center them in the middle of the screen. Possible but needs modification - //of video player API which I'd like to avoid until we'll get rid of Windows-specific player - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); + //some videos are 800x600 in size while some are 800x400 + CCS->videoh->update(pos.x, pos.y + (CCS->videoh->size().y == 400 ? 100 : 0), to.getInternalSurface(), true, false); //move text every 5 calls/frames; seems to be good enough ++positionCounter; diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index 261c7b556..8d8939d76 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -182,6 +182,20 @@ void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); } +void Canvas::fillTexture(const std::shared_ptr& image) +{ + assert(image); + if (!image) + return; + + Rect imageArea = Rect(Point(0, 0), image->dimensions()); + for (int y=0; y < surface->h; y+= imageArea.h) + { + for (int x=0; x < surface->w; x+= imageArea.w) + image->draw(surface, renderArea.x + x, renderArea.y + y); + } +} + SDL_Surface * Canvas::getInternalSurface() { return surface; diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 8a4abcf67..647c1ddde 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -99,6 +99,9 @@ public: /// fills selected area with blended color void drawColorBlended(const Rect & target, const ColorRGBA & color); + /// fills canvas with texture + void fillTexture(const std::shared_ptr& image); + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 1371c51c7..265c3b021 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -284,7 +284,12 @@ void ScreenHandler::initializeWindow() mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), rendererFlags); if(mainRenderer == nullptr) - throw std::runtime_error("Unable to create renderer\n"); + { + const char * error = SDL_GetError(); + std::string messagePattern = "Failed to create SDL renderer. Reason: %s"; + std::string message = boost::str(boost::format(messagePattern) % error); + handleFatalError(message, true); + } SDL_RendererInfo info; SDL_GetRendererInfo(mainRenderer, &info); diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index c985e35e6..f60696d09 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -611,6 +611,8 @@ void MoraleLuckBox::set(const AFactionMember * node) 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 + if(settings["general"]["enableUiEnhancements"].Bool()) + label = std::make_shared(small ? 30 : 42, small ? 20 : 38, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(modifierList->totalValue())); } MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 59a1004dd..35c34712c 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -238,6 +238,7 @@ public: class MoraleLuckBox : public LRClickableAreaWTextComp { std::shared_ptr image; + std::shared_ptr label; public: bool morale; //true if morale, false if luck bool small; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 57d1db55f..344d20757 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -47,8 +47,8 @@ void CLabel::showAll(Canvas & to) } -CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) - : CTextContainer(Align, Font, Color), text(Text) +CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text, int maxWidth) + : CTextContainer(Align, Font, Color), text(Text), maxWidth(maxWidth) { setRedrawParent(true); autoRedraw = true; @@ -56,6 +56,8 @@ CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA pos.y += y; pos.w = pos.h = 0; + trimText(); + if(alignment == ETextAlignment::TOPLEFT) // causes issues for MIDDLE { pos.w = (int)graphics->fonts[font]->getStringWidth(visibleText().c_str()); @@ -81,6 +83,9 @@ void CLabel::setAutoRedraw(bool value) void CLabel::setText(const std::string & Txt) { text = Txt; + + trimText(); + if(autoRedraw) { if(background || !parent) @@ -90,6 +95,18 @@ void CLabel::setText(const std::string & Txt) } } +void CLabel::setMaxWidth(int width) +{ + maxWidth = width; +} + +void CLabel::trimText() +{ + if(maxWidth > 0) + while ((int)graphics->fonts[font]->getStringWidth(visibleText().c_str()) > maxWidth) + TextOperations::trimRightUnicode(text); +} + void CLabel::setColor(const ColorRGBA & Color) { color = Color; @@ -444,7 +461,7 @@ void CGStatusBar::clear() } CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) - : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "") + : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "", background_->pos.w) , enteringText(false) { addUsedEvents(LCLICK); @@ -542,6 +559,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList(bgName, bgOffset.x, bgOffset.y); @@ -575,6 +594,7 @@ CTextInput::CTextInput(const Rect & Pos, std::shared_ptr srf) background = std::make_shared(srf, Pos); pos.w = background->pos.w; pos.h = background->pos.h; + maxWidth = Pos.w; background->pos = pos; addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT); @@ -683,7 +703,7 @@ void CTextInput::textInputed(const std::string & enteredText) return; std::string oldText = text; - text += enteredText; + setText(getText() + enteredText); filters(text, oldText); if(text != oldText) diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 35e21245c..7c6b5266b 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -43,9 +43,11 @@ class CLabel : public CTextContainer protected: Point getBorderSize() override; virtual std::string visibleText(); + virtual void trimText(); std::shared_ptr background; std::string text; + int maxWidth; bool autoRedraw; //whether control will redraw itself on setTxt public: @@ -53,11 +55,12 @@ public: std::string getText(); virtual void setAutoRedraw(bool option); virtual void setText(const std::string & Txt); + virtual void setMaxWidth(int width); 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 ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); + const ColorRGBA & Color = Colors::WHITE, const std::string & Text = "", int maxWidth = 0); void showAll(Canvas & to) override; //shows statusbar (with current text) }; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index d501464c2..cf95c139a 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1430,7 +1430,7 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * 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()); + name = std::make_shared(78, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated(), 150); //todo: add support for all possible states if(state >= EBuildingState::BUILDING_ERROR) @@ -1769,7 +1769,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * if(getMyBuilding() != nullptr) { buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); - buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); + buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated(), 152); if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) { @@ -1783,7 +1783,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * { 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()); + new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated(), 152); Rect sizes(287, 4, 96, 18); values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index f6e073f2c..41b0c5137 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -229,6 +229,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int 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)); + labels.push_back(std::make_shared(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration))); clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); if(++printed >= 8) // interface limit reached break; @@ -542,7 +543,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s 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()); + addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), battleStack->getMovementRange()); if(battleStack->isShooter()) addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); @@ -562,7 +563,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s 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()); + addStatLabel(EStat::SPEED, parent->info->creature->getMovementRange(), parent->info->stackNode->getMovementRange()); if(shooter) addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); @@ -665,6 +666,7 @@ CStackWindow::CStackWindow(const CStack * stack, bool popup) { info->stack = stack; info->stackNode = stack->base; + info->commander = dynamic_cast(stack->base); info->creature = stack->unitType(); info->creatureCount = stack->getCount(); info->popupWindow = popup; diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index b20a1ef39..de5997764 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -72,6 +72,7 @@ class CStackWindow : public CWindowObject { std::vector> spellIcons; std::vector> clickableAreas; + std::vector> labels; public: ActiveSpellsSection(CStackWindow * owner, int yOffset); }; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index f5e396ac3..7b2ffdbd2 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -322,10 +322,10 @@ void CSpellWindow::processSpells() sitesPerTabAdv[v] = 1; else { - if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; + if((sitesPerTabAdv[v] - (spellsPerPage - 2)) % spellsPerPage == 0) + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - (spellsPerPage - 2)) / spellsPerPage + 1; else - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - (spellsPerPage - 2)) / spellsPerPage + 2; } } @@ -340,10 +340,10 @@ void CSpellWindow::processSpells() sitesPerTabBattle[v] = 1; else { - if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; + if((sitesPerTabBattle[v] - (spellsPerPage - 2)) % spellsPerPage == 0) + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - (spellsPerPage - 2)) / spellsPerPage + 1; else - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - (spellsPerPage - 2)) / spellsPerPage + 2; } } } diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 7fdb9b911..f514f33f1 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -134,6 +134,10 @@ AdventureOptionsTab::AdventureOptionsTab() { return setBoolSetting("gameTweaks", "skipAdventureMapAnimations", value); }); + addCallback("hideBackgroundChanged", [](bool value) + { + return setBoolSetting("adventure", "hideBackground", value); + }); build(config); std::shared_ptr playerHeroSpeedToggle = widget("heroMovementSpeedPicker"); @@ -179,4 +183,7 @@ AdventureOptionsTab::AdventureOptionsTab() std::shared_ptr skipAdventureMapAnimationsCheckbox = widget("skipAdventureMapAnimationsCheckbox"); skipAdventureMapAnimationsCheckbox->setSelected(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool()); + + std::shared_ptr hideBackgroundCheckbox = widget("hideBackgroundCheckbox"); + hideBackgroundCheckbox->setSelected(settings["adventure"]["hideBackground"].Bool()); } diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index bcf8c1979..2709974d3 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -156,6 +156,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.cpp ${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.cpp ${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.cpp + ${MAIN_LIB_DIR}/rmg/modificators/PrisonHeroPlacer.cpp ${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.cpp ${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.cpp ${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.cpp @@ -526,6 +527,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/rmg/modificators/ObjectDistributor.h ${MAIN_LIB_DIR}/rmg/modificators/RoadPlacer.h ${MAIN_LIB_DIR}/rmg/modificators/TreasurePlacer.h + ${MAIN_LIB_DIR}/rmg/modificators/PrisonHeroPlacer.h ${MAIN_LIB_DIR}/rmg/modificators/QuestArtifactPlacer.h ${MAIN_LIB_DIR}/rmg/modificators/ConnectionsPlacer.h ${MAIN_LIB_DIR}/rmg/modificators/WaterAdopter.h diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index d54fa1188..203958907 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,6 +1,6 @@ set(VCMI_VERSION_MAJOR 1) set(VCMI_VERSION_MINOR 4) -set(VCMI_VERSION_PATCH 2) +set(VCMI_VERSION_PATCH 3) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} -DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR} diff --git a/config/artifacts.json b/config/artifacts.json index 4645b53bb..ae5501f30 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -1730,7 +1730,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel1", + "subtype" : "creatureLevel2", "val" : 5, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1743,7 +1743,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel2", + "subtype" : "creatureLevel3", "val" : 4, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1756,7 +1756,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel3", + "subtype" : "creatureLevel4", "val" : 3, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1769,7 +1769,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel4", + "subtype" : "creatureLevel5", "val" : 2, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1782,7 +1782,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : "creatureLevel5", + "subtype" : "creatureLevel6", "val" : 1, "propagator": "VISITED_TOWN_AND_VISITOR" } diff --git a/config/bonuses.json b/config/bonuses.json index 877161cf8..350ec5d5a 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -145,6 +145,14 @@ } }, + "ENEMY_ATTACK_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RDEF" + } + }, + "ENEMY_DEFENCE_REDUCTION": { "graphics": @@ -185,6 +193,14 @@ } }, + "FEROCITY": + { + "graphics": + { + "icon": "" + } + }, + "FLYING": { "graphics": @@ -428,6 +444,14 @@ } }, + "REVENGE": + { + "graphics": + { + "icon": "" + } + }, + "SHOOTER": { "graphics": diff --git a/config/commanders.json b/config/commanders.json index d1516cf15..d1267f47c 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -5,7 +5,7 @@ [ ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+2 minimum damage ["CREATURE_DAMAGE", 4, "creatureDamageMax", 0 ], //+4 maximum damage - ["STACK_HEALTH", 20, null, 0 ] //+5 hp + ["STACK_HEALTH", 20, null, 0 ] //+20 hp ], //Value of bonuses given by each skill level "skillLevels": diff --git a/config/creatures/dungeon.json b/config/creatures/dungeon.json index ef0d4e368..e3e03e9b1 100644 --- a/config/creatures/dungeon.json +++ b/config/creatures/dungeon.json @@ -411,12 +411,6 @@ "type" : "LEVEL_SPELL_IMMUNITY", "val" : 5 }, - "hateGiants" : - { - "type" : "HATE", - "subtype" : "creature.giant", - "val" : 50 - }, "hateTitans" : { "type" : "HATE", diff --git a/config/gameConfig.json b/config/gameConfig.json index 5b6e17b5c..56260f4e2 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -371,6 +371,8 @@ "pathfinder" : { + // if enabled, pathfinder will build path through locations guarded by wandering monsters + "ignoreGuards" : false, // if enabled, pathfinder will take use of any available boats "useBoat" : true, // if enabled, pathfinder will take use of any bidirectional monoliths diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 87f61a70d..48c3e1211 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -199,6 +199,7 @@ }, "driver" : { "type" : "string", + "defaultWindows" : "", "default" : "opengl", "description" : "preferred graphics backend driver name for SDL2" }, @@ -240,7 +241,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -293,6 +294,10 @@ "type" : "number", "default" : 128 }, + "hideBackground" : { + "type" : "boolean", + "default" : false + } } }, "battle" : { diff --git a/config/widgets/settings/adventureOptionsTab.json b/config/widgets/settings/adventureOptionsTab.json index 02a748960..faa0d01ab 100644 --- a/config/widgets/settings/adventureOptionsTab.json +++ b/config/widgets/settings/adventureOptionsTab.json @@ -297,6 +297,9 @@ }, { "text": "vcmi.adventureOptions.showGrid.hover" + }, + { + "text": "vcmi.adventureOptions.hideBackground.hover" } ] }, @@ -324,6 +327,11 @@ "name": "showGridCheckbox", "help": "vcmi.adventureOptions.showGrid", "callback": "showGridChanged" + }, + { + "name": "hideBackgroundCheckbox", + "help": "vcmi.adventureOptions.hideBackground", + "callback": "hideBackgroundChanged" } ] }, diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index 0c0e6c220..1449d5728 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -317,6 +317,7 @@ { "name": "buttonSimturnsAI", "position": {"x": 70, "y": 535}, + "help" : "vcmi.optionsTab.simturnsAI", "type": "toggleButton", "image": "lobby/checkbox", "callback" : "setSimturnAI" diff --git a/debian/changelog b/debian/changelog index 2100ab4ad..ffac0ac51 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.4.3) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 19 Jan 2024 12:00:00 +0200 + vcmi (1.4.2) jammy; urgency=medium * New upstream release diff --git a/docs/Readme.md b/docs/Readme.md index ce334ddb5..37b2a519e 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -2,6 +2,7 @@ [![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/1.4.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.2) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.3/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.3) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) # VCMI Project diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 6836066cc..0df82d18e 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -244,7 +244,7 @@ Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray de "subtype" : "spell.disruptingRay", "type" : "SPECIAL_ADD_VALUE_ENCHANT" } -`````` +``` - subtype: affected spell identifier - additionalInfo: value to add @@ -587,6 +587,11 @@ Affected unit will attack units on all hexes that surround attacked hex Affected unit will retaliate before enemy attacks, if able +- subtype: + - damageTypeMelee: only melee attacks affected + - damageTypeRanged: only ranged attacks affected. Note that unit also requires ability to retaliate in ranged, such as RANGED_RETALIATION bonus + - damageTypeAll: any attacks are affected + ### SHOOTS_ALL_ADJACENT Affected unit will attack units on all hexes that surround attacked hex in ranged attacks @@ -608,6 +613,25 @@ Affected unit can use ranged attacks only within specified range - val: max shooting range in hexes - addInfo: optional, range at which ranged penalty will trigger (default is 10) +### FEROCITY + +Affected unit will attack additional times if killed creatures in target unit during attacking (including ADDITIONAL_ATTACK bonus attacks) + +- val: amount of additional attacks (negative number will reduce number of unperformed attacks if any left) +- addInfo: optional, amount of creatures needed to kill (default is 1) + +### ENEMY_ATTACK_REDUCTION + +Affected unit will ignore specified percentage of attacked unit attack (Nix) + +- val: amount of attack points to ignore, percentage + +### REVENGE + +Affected unit will deal more damage based on percentage of self health lost compared to amount on start of battle +(formula: `square_root((total_unit_count + 1) * 1_creature_max_health / (current_whole_unit_health + 1_creature_max_health) - 1)`. +Result is then multiplied separately by min and max base damage of unit and result is additive bonus to total damage at end of calculation) + ## Special abilities ### CATAPULT @@ -708,14 +732,20 @@ Affected unit will deal additional damage after attack ### DEATH_STARE -Affected unit will kill additional units after attack +Affected unit will kill additional units after attack. Used for Death stare (Mighty Gorgon) ability and for Accurate Shot (Pirates, HotA) - subtype: - - deathStareGorgon: random amount - - deathStareCommander: fixed amount + - deathStareGorgon: only melee attack, random amount of killed units + - deathStareNoRangePenalty: only ranged attacks without obstacle (walls) or range penalty + - deathStareRangePenalty: only ranged attacks with range penalty + - deathStareObstaclePenalty: only ranged attacks with obstacle (walls) penalty + - deathStareRangeObstaclePenalty: only ranged attacks with both range and obstacle penalty + - deathStareCommander: fixed amount, both melee and ranged attacks - val: - - for 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 + - for all other subtypes: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once, rounded up +- addInfo: + - SpellID to be used as hit effect. If not set - 'deathStare' spell will be used. If set to "accurateShot" battle log messages will use alternative description ### SPECIAL_CRYSTAL_GENERATION @@ -759,17 +789,21 @@ Determines how many times per combat affected creature can cast its targeted spe - subtype - spell id, eg. spell.iceBolt - value - chance (percent) -- additional info - \[X, Y\] - - X - spell level +- additional info - \[X, Y, Z\] + - X - spell mastery level (1 - Basic, 3 - Expert) - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_AFTER_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. ### SPELL_BEFORE_ATTACK - subtype - spell id - value - chance % -- additional info - \[X, Y\] - - X - spell level +- additional info - \[X, Y, Z\] + - X - spell mastery level (1 - Basic, 3 - Expert) - Y = 0 - all attacks, 1 - shot only, 2 - melee only + - Z (optional) - layer for multiple SPELL_BEFORE_ATTACK bonuses and multi-turn casting. Empty or value less than 0 = not participating in layering. + When enabled - spells from specific layer will not be cast until target has all spells from previous layer on him. Spell from last layer is on repeat if none of spells on lower layers expired. ### SPECIFIC_SPELL_POWER @@ -778,7 +812,7 @@ Determines how many times per combat affected creature can cast its targeted spe ### CREATURE_SPELL_POWER -- 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. +- value: Spell Power of offensive spell cast unit, multiplied by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack. ### CREATURE_ENCHANT_POWER diff --git a/docs/modders/Entities_Format/Creature_Format.md b/docs/modders/Entities_Format/Creature_Format.md index 96d9ce1c1..5bff93d9c 100644 --- a/docs/modders/Entities_Format/Creature_Format.md +++ b/docs/modders/Entities_Format/Creature_Format.md @@ -59,8 +59,8 @@ In order to make functional creature you also need: // Basic growth of this creature in town or in external dwellings "growth" : 0, - // Bonus growth of this creature from built horde - "hordeGrowth" : 0, + // Bonus growth of this creature from built horde, if any + "horde" : 0, // Creature stats in battle "attack" : 0, diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index 350851acf..0fa81c50e 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -61,6 +61,10 @@ "positive": true, }, + // If true, then creature capable of casting this spell can cast this spell on itself + // If false, then creature can only cast this spell on other units + "canCastOnSelf" : false, + // If true, spell won't be available on a map without water "onlyOnWaterMap" : true, diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index 556caae7e..80bbf0838 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -144,7 +144,7 @@ It's the new feature meant for testing game performance on various platforms. 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 +### Original Heroes III Support `This is white` @@ -154,7 +154,7 @@ Additional color are supported for text fields (e.g. map description). Uses HTML This is yellow -##### New +### New `{#ff0000|This is red}` @@ -164,6 +164,41 @@ Additional color are supported for text fields (e.g. map description). Uses HTML This is green +# Multiplayer + +Opening new Turn Option menu in scenario selection dialog allows detailed configuration of turn timers and simultaneous turns + +## Turn Timers + +TODO + +## Simultaneous turns + +Simultaneous turns allow multiple players to act at the same time, speeding up early game phase in multiplayer games. During this phase if different players (allies or not) attempt to interact with each other, such as capture objects owned by other players (mines, dwellings, towns) or attack their heroes, game will block such actions. Interaction with same map objects at the same time, such as attacking same wandering monster is also blocked. + +Following options can be used to configure simultaneous turns: +- Minimal duration (at least for): this is duration during which simultaneous turns will run unconditionally. Until specified number of days have passed, simultaneous turns will never break and game will not attempt to detect contacts. +- Maximal duration (at most for): this is duration after which simultaneous turns will end unconditionally, even if players still have not contacted each other. However if contact detection discovers contact between two players, simultaneous turns between them might end before specified duration. +- Simultaneous turns for AI: If this option is on, AI can act at the same time as human players. Note that AI shares settings for simultaneous turns with human players - if no simultaneous turns have been set up this option has no effect. + +### Contact detection + +While simultaneous turns are active, VCMI tracks contacts for each pair of player separately. + +Players are considered to be "in contact" if movement range of their heroes at the start of turn overlaps, or, in other words - if their heroes can meet on this turn if both walk towards each other. When calculating movement range, game uses rules similar to standard movement range calculation in vcmi, meaning that game will track movement through monoliths and subterranean gates, but will not account for any removable obstacles, such as pickable treasures that block path between heroes. Any existing wandering monsters that block path between heroes are ignored for range calculation. At the moment, game will not account for any ways to extend movement range - Dimension Door or Town Portal spells, visiting map objects such as Stables, releasing heroes from prisons, etc. + +Once detected, contact can never be "lost". If game detected contact between two players, this contact will remain active till the end of the game, even if their heroes move far enough from each other. + +Game performs contact detection once per turn, at the very start of each in-game day. Once contact detection has been performed, players that are not in contact with each other can start making turn. For example, in game with 4 players: red, blue, brown and green. If game detected contact between red and blue following will happen: +- red, brown and green will all instantly start turn +- once red ends his turn, blue will be able to start his own turn (even if brown or green are still making turn) + +Once maximal duration of simultaneous turns (as specified during scenario setup) has been reached, or if all players are in contact with each other, game will return to standard turn order: red, blue, brown, green... + +### Differences compared to HD Mod version + +- In VCMI, players can see actions of other players immediately (provided that they have revealed fog of war) instead of waiting for next turn +- In VCMI, attempt to attack hero of another player during simultaneous turns will be blocked instead of reloading save from start of turn like in HD Mod # Manuals and guides diff --git a/include/vcmi/Creature.h b/include/vcmi/Creature.h index 645d144ab..8bff40f86 100644 --- a/include/vcmi/Creature.h +++ b/include/vcmi/Creature.h @@ -23,7 +23,7 @@ class DLL_LINKAGE ACreature: public AFactionMember { public: bool isLiving() const; //non-undead, non-non living or alive - ui32 speed(int turn = 0, bool useBind = false) const; //get speed (in moving tiles) of creature with all modificators + ui32 getMovementRange(int turn = 0) const; //get speed (in moving tiles) of creature with all modificators virtual ui32 getMaxHealth() const; //get max HP of stack with all modifiers }; diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 60e7b8023..1822639e1 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -44,6 +44,7 @@ public: virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind) virtual bool hasSchool(SpellSchool school) const = 0; + virtual bool canCastOnSelf() const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 5ab52821f..613ebecfe 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -59,6 +59,7 @@ set(launcher_FORMS set(launcher_TS translation/chinese.ts + translation/czech.ts translation/english.ts translation/french.ts translation/german.ts diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 2ddeeb0c6..54a0337b8 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -76,6 +76,7 @@ + diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index 7d09dd688..5a30b5c62 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -134,7 +134,16 @@ void FirstLaunchView::activateTabHeroesData() ui->pushButtonDataHelp->hide(); ui->labelDataHelp->hide(); } - heroesDataUpdate(); + if(heroesDataUpdate()) + return; + + QString installPath = getHeroesInstallDir(); + if(!installPath.isEmpty()) + { + auto reply = QMessageBox::question(this, tr("Heroes III installation found!"), tr("Copy data to VCMI folder?"), QMessageBox::Yes | QMessageBox::No); + if(reply == QMessageBox::Yes) + copyHeroesData(installPath); + } } void FirstLaunchView::activateTabModPreset() @@ -164,12 +173,14 @@ void FirstLaunchView::languageSelected(const QString & selectedLanguage) mainWindow->updateTranslation(); } -void FirstLaunchView::heroesDataUpdate() +bool FirstLaunchView::heroesDataUpdate() { - if(heroesDataDetect()) + bool detected = heroesDataDetect(); + if(detected) heroesDataDetected(); else heroesDataMissing(); + return detected; } void FirstLaunchView::heroesDataMissing() @@ -254,9 +265,26 @@ void FirstLaunchView::forceHeroesLanguage(const QString & language) node->String() = language.toStdString(); } -void FirstLaunchView::copyHeroesData() +QString FirstLaunchView::getHeroesInstallDir() { - QDir sourceRoot = QFileDialog::getExistingDirectory(this, "", "", QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); +#ifdef VCMI_WINDOWS + QString gogPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\GOG.com\\Games\\1207658787", QSettings::NativeFormat).value("path").toString(); + if(!gogPath.isEmpty()) + return gogPath; + + QString cdPath = QSettings("HKEY_LOCAL_MACHINE\\SOFTWARE\\New World Computing\\Heroes of Might and Magic® III\\1.0", QSettings::NativeFormat).value("AppPath").toString(); + if(!cdPath.isEmpty()) + return cdPath; +#endif + return QString{}; +} + +void FirstLaunchView::copyHeroesData(const QString & path) +{ + QDir sourceRoot = QDir(path); + + if(path.isEmpty()) + sourceRoot.setPath(QFileDialog::getExistingDirectory(this, {}, {}, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks)); if(!sourceRoot.exists()) return; diff --git a/launcher/firstLaunch/firstlaunch_moc.h b/launcher/firstLaunch/firstlaunch_moc.h index dd773c365..d17438000 100644 --- a/launcher/firstLaunch/firstlaunch_moc.h +++ b/launcher/firstLaunch/firstlaunch_moc.h @@ -42,7 +42,7 @@ class FirstLaunchView : public QWidget void languageSelected(const QString & languageCode); // Tab Heroes III Data - void heroesDataUpdate(); + bool heroesDataUpdate(); bool heroesDataDetect(); void heroesDataMissing(); @@ -51,7 +51,8 @@ class FirstLaunchView : public QWidget void heroesLanguageUpdate(); void forceHeroesLanguage(const QString & language); - void copyHeroesData(); + QString getHeroesInstallDir(); + void copyHeroesData(const QString & path = {}); // Tab Mod Preset void modPresetUpdate(); diff --git a/launcher/languages.cpp b/launcher/languages.cpp index 670faa345..8dcd86d39 100644 --- a/launcher/languages.cpp +++ b/launcher/languages.cpp @@ -86,7 +86,7 @@ QString Languages::generateLanguageName(const Languages::Options & language) void Languages::fillLanguages(QComboBox * widget, bool includeAll) { - widget->blockSignals(true); // we do not want calls caused by initialization + QSignalBlocker guard(widget); // we do not want calls caused by initialization widget->clear(); std::string activeLanguage = includeAll ? @@ -115,13 +115,11 @@ void Languages::fillLanguages(QComboBox * widget, bool includeAll) if(activeLanguage == language.identifier) widget->setCurrentIndex(widget->count() - 1); } - - widget->blockSignals(false); } void Languages::fillLanguages(QListWidget * widget, bool includeAll) { - widget->blockSignals(true); // we do not want calls caused by initialization + QSignalBlocker guard(widget); // we do not want calls caused by initialization widget->clear(); std::string activeLanguage = includeAll ? @@ -154,5 +152,4 @@ void Languages::fillLanguages(QListWidget * widget, bool includeAll) if(activeLanguage == language.identifier) widget->setCurrentRow(widget->count() - 1); } - widget->blockSignals(false); } diff --git a/launcher/launcherdirs.cpp b/launcher/launcherdirs.cpp index 97d456bb5..81e474f94 100644 --- a/launcher/launcherdirs.cpp +++ b/launcher/launcherdirs.cpp @@ -34,3 +34,8 @@ QString CLauncherDirs::modsPath() { return pathToQString(VCMIDirs::get().userDataPath() / "Mods"); } + +QString CLauncherDirs::mapsPath() +{ + return pathToQString(VCMIDirs::get().userDataPath() / "Maps"); +} diff --git a/launcher/launcherdirs.h b/launcher/launcherdirs.h index 9117bd9fb..e549fb218 100644 --- a/launcher/launcherdirs.h +++ b/launcher/launcherdirs.h @@ -19,4 +19,5 @@ public: QString downloadsPath(); QString modsPath(); + QString mapsPath(); }; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 97913cc40..f27573aef 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -48,6 +48,43 @@ void CModListView::changeEvent(QEvent *event) QWidget::changeEvent(event); } +void CModListView::dragEnterEvent(QDragEnterEvent* event) +{ + if(event->mimeData()->hasUrls()) + for(const auto & url : event->mimeData()->urls()) + for(const auto & ending : QStringList({".zip", ".h3m", ".h3c", ".vmap", ".vcmp"})) + if(url.fileName().endsWith(ending, Qt::CaseInsensitive)) + { + event->acceptProposedAction(); + return; + } +} + +void CModListView::dropEvent(QDropEvent* event) +{ + const QMimeData* mimeData = event->mimeData(); + + if(mimeData->hasUrls()) + { + const QList urlList = mimeData->urls(); + + for (const auto & url : urlList) + { + QString urlStr = url.toString(); + QString fileName = url.fileName(); + if(urlStr.endsWith(".zip", Qt::CaseInsensitive)) + downloadFile(fileName.toLower() + // mod name currently comes from zip file -> remove suffixes from github zip download + .replace(QRegularExpression("-[0-9a-f]{40}"), "") + .replace(QRegularExpression("-vcmi-.+\\.zip"), ".zip") + .replace("-main.zip", ".zip") + , urlStr, "mods", 0); + else + downloadFile(fileName, urlStr, "mods", 0); + } + } +} + void CModListView::setupFilterModel() { filterModel = new CModFilterModel(modModel, this); @@ -100,6 +137,8 @@ CModListView::CModListView(QWidget * parent) { ui->setupUi(this); + setAcceptDrops(true); + setupModModel(); setupFilterModel(); setupModsView(); @@ -677,15 +716,18 @@ void CModListView::hideProgressBar() void CModListView::installFiles(QStringList files) { QStringList mods; + QStringList maps; QStringList images; QVector repositories; // TODO: some better way to separate zip's with mods and downloaded repository files for(QString filename : files) { - if(filename.endsWith(".zip")) + if(filename.endsWith(".zip", Qt::CaseInsensitive)) mods.push_back(filename); - if(filename.endsWith(".json")) + else if(filename.endsWith(".h3m", Qt::CaseInsensitive) || filename.endsWith(".h3c", Qt::CaseInsensitive) || filename.endsWith(".vmap", Qt::CaseInsensitive) || filename.endsWith(".vcmp", Qt::CaseInsensitive)) + maps.push_back(filename); + else if(filename.endsWith(".json", Qt::CaseInsensitive)) { //download and merge additional files auto repoData = JsonUtils::JsonFromFile(filename).toMap(); @@ -709,7 +751,7 @@ void CModListView::installFiles(QStringList files) } repositories.push_back(repoData); } - if(filename.endsWith(".png")) + else if(filename.endsWith(".png", Qt::CaseInsensitive)) images.push_back(filename); } @@ -718,6 +760,9 @@ void CModListView::installFiles(QStringList files) if(!mods.empty()) installMods(mods); + if(!maps.empty()) + installMaps(maps); + if(!images.empty()) loadScreenshots(); } @@ -794,6 +839,16 @@ void CModListView::installMods(QStringList archives) QFile::remove(archive); } +void CModListView::installMaps(QStringList maps) +{ + QString destDir = CLauncherDirs::get().mapsPath() + "/"; + + for(QString map : maps) + { + QFile(map).rename(destDir + map.section('/', -1, -1)); + } +} + void CModListView::on_refreshButton_clicked() { loadRepositories(); @@ -963,4 +1018,3 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) return; } } - diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index bf6d07f55..2edbab3a8 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -54,12 +54,15 @@ class CModListView : public QWidget void downloadFile(QString file, QString url, QString description, qint64 size = 0); void installMods(QStringList archives); + void installMaps(QStringList maps); void installFiles(QStringList mods); QString genChangelogText(CModEntry & mod); QString genModInfoText(CModEntry & mod); void changeEvent(QEvent *event) override; + void dragEnterEvent(QDragEnterEvent* event) override; + void dropEvent(QDropEvent *event) override; signals: void modsChanged(); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 068518553..959a429c5 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -161,9 +161,6 @@ bool CModManager::canInstallMod(QString modname) if(mod.isInstalled()) return addError(modname, "Mod is already installed"); - - if(!mod.isAvailable()) - return addError(modname, "Mod is not available"); return true; } diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index e0c6bb635..368ad48f0 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -117,6 +117,7 @@ void CSettingsView::loadSettings() ui->lineEditAutoSavePrefix->setEnabled(settings["general"]["useSavePrefix"].Bool()); Languages::fillLanguages(ui->comboBoxLanguage, false); + fillValidRenderers(); std::string cursorType = settings["video"]["cursor"].String(); size_t cursorTypeIndex = boost::range::find(cursorTypesList, cursorType) - cursorTypesList; @@ -163,6 +164,26 @@ void CSettingsView::fillValidScalingRange() #ifndef VCMI_MOBILE +static QStringList getAvailableRenderingDrivers() +{ + SDL_Init(SDL_INIT_VIDEO); + QStringList result; + + result += QString(); // empty value for autoselection + + int driversCount = SDL_GetNumRenderDrivers(); + + for(int it = 0; it < driversCount; it++) + { + SDL_RendererInfo info; + if (SDL_GetRenderDriverInfo(it, &info) == 0) + result += QString::fromLatin1(info.name); + } + + SDL_Quit(); + return result; +} + static QVector findAvailableResolutions(int displayIndex) { // Ugly workaround since we don't actually need SDL in Launcher @@ -197,13 +218,13 @@ static QVector findAvailableResolutions(int displayIndex) void CSettingsView::fillValidResolutionsForScreen(int screenIndex) { - ui->comboBoxResolution->blockSignals(true); // avoid saving wrong resolution after adding first item from the list + QSignalBlocker guard(ui->comboBoxResolution); // avoid saving wrong resolution after adding first item from the list + ui->comboBoxResolution->clear(); bool fullscreen = settings["video"]["fullscreen"].Bool(); bool realFullscreen = settings["video"]["realFullscreen"].Bool(); - if (!fullscreen || realFullscreen) { QVector resolutions = findAvailableResolutions(screenIndex); @@ -225,8 +246,21 @@ void CSettingsView::fillValidResolutionsForScreen(int screenIndex) // if selected resolution no longer exists, force update value to the largest (last) resolution if(resIndex == -1) ui->comboBoxResolution->setCurrentIndex(ui->comboBoxResolution->count() - 1); +} - ui->comboBoxResolution->blockSignals(false); +void CSettingsView::fillValidRenderers() +{ + QSignalBlocker guard(ui->comboBoxRendererType); // avoid saving wrong renderer after adding first item from the list + + ui->comboBoxRendererType->clear(); + + auto driversList = getAvailableRenderingDrivers(); + ui->comboBoxRendererType->addItems(driversList); + + std::string rendererName = settings["video"]["driver"].String(); + + int index = ui->comboBoxRendererType->findText(QString::fromStdString(rendererName)); + ui->comboBoxRendererType->setCurrentIndex(index); } #else void CSettingsView::fillValidResolutionsForScreen(int screenIndex) @@ -235,6 +269,13 @@ void CSettingsView::fillValidResolutionsForScreen(int screenIndex) ui->comboBoxResolution->hide(); ui->labelResolution->hide(); } + +void CSettingsView::fillValidRenderers() +{ + // untested on mobile platforms + ui->comboBoxRendererType->hide(); + ui->labelRendererType->hide(); +} #endif CSettingsView::CSettingsView(QWidget * parent) @@ -542,3 +583,10 @@ void CSettingsView::on_spinBoxReservedArea_valueChanged(int arg1) node->Float() = float(arg1) / 100; // percentage -> ratio } + +void CSettingsView::on_comboBoxRendererType_currentTextChanged(const QString &arg1) +{ + Settings node = settings.write["video"]["driver"]; + node->String() = arg1.toStdString(); +} + diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 0a144f7f5..084c0bc50 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -76,9 +76,12 @@ private slots: void on_spinBoxReservedArea_valueChanged(int arg1); + void on_comboBoxRendererType_currentTextChanged(const QString &arg1); + private: Ui::CSettingsView * ui; + void fillValidRenderers(); void fillValidResolutionsForScreen(int screenIndex); void fillValidScalingRange(); QSize getPreferredRenderingResolution(); diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index deae0833c..240216df1 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -6,8 +6,8 @@ 0 0 - 832 - 350 + 985 + 683 @@ -26,65 +26,6 @@ 0 - - - - - 1 - 0 - - - - - 200 - 0 - - - - - true - - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - false - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SelectRows - - - 4 - - - - General - - - - - Video - - - - - Artificial Intelligence - - - - - Mod Repositories - - - - @@ -106,28 +47,105 @@ 0 - -356 - 610 - 873 + 0 + 969 + 818 - - + + + + + - Heroes III Translation + - - + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + 75 true - General + Video + + + + + + + + 75 + true + + + + Artificial Intelligence + + + + + + + Cursor + + + + + + + + + + Heroes III Data Language + + + + + + + + + + Display index + + + + + + + + + + + + + true @@ -138,72 +156,62 @@ - - + + - Interface Scaling + Heroes III Translation - - + + - Autosave prefix + Enemy AI in battles - + + + + Additional repository + + + + + + + + + + + + + + Show intro + + + + + + + + 75 + true + + + + Mod Repositories + + + + Adventure Map Allies - - - - - true - - - - Video - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - - true - - - - Artificial Intelligence - - - - - - - empty = map name prefix - - - - + true @@ -216,27 +224,40 @@ - - + + - - - - - - - - Autosave limit (0 = off) + Interface Scaling - + + + 1 + + + + Off + + + + + On + + + - - - - Additional repository + + + + 50 + + + 400 + + + 10 @@ -259,56 +280,21 @@ - - - - Show intro + + + + 1024 + + + 65535 + + + 3030 - - - - 1 - - - - Off - - - - - On - - - - - - - - Enemy AI in battles - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - + + false @@ -327,10 +313,99 @@ - - + + - + + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + + + + true + + + + + + + Reserved screen area + + + + + + + Resolution + + + + + + + VCMI Language + + + + + + + Framerate Limit + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + Neutral AI in battles + + + + + + + Friendly AI in battles + + + + + + + @@ -365,123 +440,10 @@ 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 + Autosave @@ -492,84 +454,13 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Resolution + Autosave prefix - - - - 1024 - - - 65535 - - - 3030 - - - - - - - Autosave - - - - - - - Cursor - - - - - - - - Hardware - - - - - Software - - - - - - - - - - - Network port - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - @@ -587,59 +478,58 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + empty = map name prefix + + + + + + + + 75 + true + + - Refresh now + General - + - BattleAI + VCAI - BattleAI + VCAI - StupidAI + Nullkiller - - - - Check on startup - + + + + + Hardware + + + + + Software + + - - - - Neutral AI in battles - - - - - - - Reserved screen area - - - - - - - Adventure Map Enemies - - - - + 1 @@ -656,6 +546,16 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use + + + + Adventure Map Enemies + + + + + + @@ -663,10 +563,65 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + - + Check on startup + + + + + + + Autosave limit (0 = off) + + + + + + + Network port + + + + + + + Refresh now + + + + + + + Default repository + + + + + + + + + + Renderer diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 3b65d01a2..77582801a 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -253,7 +253,7 @@ - + Description 详细介绍 @@ -303,123 +303,123 @@ 终止 - + 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: @@ -428,35 +428,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 截图 %1 - + Mod is incompatible MOD不兼容 @@ -464,123 +464,126 @@ Install successfully downloaded? 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 - - - + + Renderer + + + + + + On 开启 - + Cursor 鼠标指针 - + Heroes III Data Language 英雄无敌3数据语言 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -591,105 +594,103 @@ 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 安装 diff --git a/launcher/translation/czech.ts b/launcher/translation/czech.ts index 595fd591c..abc893247 100644 --- a/launcher/translation/czech.ts +++ b/launcher/translation/czech.ts @@ -252,7 +252,7 @@ - + Description Popis @@ -302,123 +302,123 @@ Zrušit - + Mod name Název modifikace - + Installed version Nainstalovaná verze - + Latest version Nejnovější verze - + Size Velikost - + Download size Velikost ke stažení - + Authors Autoři - + License Licence - + Contact Kontakt - + Compatibility Kompabilita - - + + Required VCMI version Vyžadovaná verze VCMI - + Supported VCMI version Podporovaná verze VCMI - + Supported VCMI versions Podporované verze VCMI - + Languages Jazyky - + Required mods Vyžadované modifikace VCMI - + Conflicting mods Modifikace v kolizi - + This mod can not be installed or enabled because the following dependencies are not present Tato modifikace nemůže být nainstalována nebo povolena, protože následující závislosti nejsou přítomny - + This mod can not be enabled because the following mods are incompatible with it Tato modifikace nemůže být povolena, protože následující modifikace s ní nejsou kompatibilní - + This mod cannot be disabled because it is required by the following mods Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi - + This mod cannot be uninstalled or updated because it is required by the following mods Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Toto je podmodifikace, která nemůže být nainstalována nebo odinstalována bez její rodičovské modifikace - + Notes Poznámky - + Downloading %s%. %p% (%v MB out of %m MB) finished Stahování %s%. %p% (%v MB z %m MB) dokončeno - + Download failed Stahování selhalo - + Unable to download all files. Encountered errors: @@ -431,7 +431,7 @@ Vyskytly se chyby: - + Install successfully downloaded? @@ -440,29 +440,29 @@ Install successfully downloaded? Nainstalovat úspěšně stažené? - + Installing mod %1 Instalování modifikace %1 - + Operation failed Operace selhala - + Encountered errors: Vyskytly se chyby: - + Screenshot %1 Snímek obrazovky %1 - + Mod is incompatible Modifikace není kompatibilní @@ -470,123 +470,126 @@ Nainstalovat úspěšně stažené? CSettingsView - - - + + + Off Vypnuto - - + Artificial Intelligence Umělá inteligence - - + Mod Repositories Repozitáře modifikací - + Interface Scaling Škálování rozhraní - + Neutral AI in battles Neutrální AI v bitvách - + Enemy AI in battles Nepřátelská AI v bitvách - + Additional repository Další repozitáře - + Adventure Map Allies Spojenci na mapě světa - + Adventure Map Enemies Nepřátelé na mapě světa - + Windowed V okně - + Borderless fullscreen Celá obrazovka bez okrajů - + Exclusive fullscreen Exkluzivní celá obrazovka - + Autosave limit (0 = off) Limit aut. uložení (0=vypnuto) - + Friendly AI in battles Přátelské AI v bitvách - + Framerate Limit Omezení snímků za sekundu - + Autosave prefix Předpona aut. uložení - + empty = map name prefix prázná = předpona - název mapy - + Refresh now Obnovit nyní - + Default repository Výchozí repozitář - - - + + Renderer + + + + + + On Zapnuto - + Cursor Kurzor - + Heroes III Data Language Jazyk dat Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -603,106 +606,104 @@ Celá obrazovka bez okrajů- hra poběží v okně, které zakryje vaši celou Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybrané rozlišení. - + Reserved screen area Vyhrazená část obrazovky - + Hardware Hardware - + Software Software - + Heroes III Translation Překlad Heroes III - + Check on startup Zkontrolovat při zapnutí - + Fullscreen Celá obrazovka - - + General Všeobecné - + VCMI Language Jazyk VCMI - + Resolution Rozlišení - + Autosave Automatické uložení - + VSync VSync - + Display index - + Network port Síťový port - - + Video Zobrazení - + Show intro Zobrazit intro - + Active Aktivní - + Disabled - + Enable - + Povolit - + Not Installed - + Install - + Instalovat diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 691c1b400..2e6764c8f 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -252,7 +252,7 @@ - + Description @@ -302,123 +302,123 @@ - + 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: @@ -427,35 +427,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 - + Mod is incompatible @@ -463,123 +463,126 @@ Install successfully downloaded? 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 - - - + + Renderer + + + + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -590,104 +593,102 @@ 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 diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 1f1949470..ff8e76145 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -252,7 +252,7 @@ - + Description Description @@ -302,128 +302,128 @@ 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: @@ -432,35 +432,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -468,48 +468,46 @@ Install successfully downloaded? 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 @@ -526,179 +524,182 @@ 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 - + + Renderer + + + + 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 diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index b9f875973..84073ee83 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -252,7 +252,7 @@ - + Description Beschreibung @@ -302,123 +302,123 @@ 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: @@ -431,7 +431,7 @@ Es sind Fehler aufgetreten: - + Install successfully downloaded? @@ -440,29 +440,29 @@ 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 @@ -470,123 +470,126 @@ Installation erfolgreich heruntergeladen? 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 - - - + + Renderer + + + + + + On An - + Cursor Zeiger - + Heroes III Data Language Sprache der Heroes III Daten - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -603,104 +606,102 @@ 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 diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 554fab79f..d4a7e0747 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -252,7 +252,7 @@ - + Description Opis @@ -302,123 +302,123 @@ 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: @@ -431,7 +431,7 @@ Napotkane błędy: - + Install successfully downloaded? @@ -440,29 +440,29 @@ 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 @@ -470,123 +470,126 @@ Zainstalować pomyślnie pobrane? 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 - - - + + Renderer + + + + + + On Włączony - + Cursor Kursor - + Heroes III Data Language Język plików Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -603,104 +606,102 @@ 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 diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index c421df14b..40a305787 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -252,7 +252,7 @@ - + Description Описание @@ -302,123 +302,123 @@ Отмена - + 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: @@ -427,35 +427,35 @@ Encountered errors: - + Install successfully downloaded? - + Installing mod %1 - + Operation failed - + Encountered errors: - + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим @@ -463,154 +463,156 @@ Install successfully downloaded? 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 - + + Renderer + + + + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -621,73 +623,72 @@ 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 Установить diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 2dba9fdbe..10c94498e 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -252,7 +252,7 @@ - + Description Descripción @@ -302,123 +302,123 @@ 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: @@ -427,35 +427,35 @@ 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 @@ -463,175 +463,176 @@ Install successfully downloaded? 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 - - - + + Renderer + + + + + + 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 @@ -642,52 +643,52 @@ 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 diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 971474a49..392c21cfd 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -252,7 +252,7 @@ - + Description Опис @@ -302,123 +302,123 @@ Відмінити - + 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: @@ -431,7 +431,7 @@ Encountered errors: - + Install successfully downloaded? @@ -440,29 +440,29 @@ Install successfully downloaded? Встановити успішно завантажені? - + Installing mod %1 Встановлення модифікації %1 - + Operation failed Операція завершилася невдало - + Encountered errors: Виникли помилки: - + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -470,123 +470,126 @@ Install successfully downloaded? 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 Стандартний репозиторій - - - + + Renderer + Рендерер + + + + + 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 @@ -603,104 +606,102 @@ 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 Встановити diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index a5a1c99c1..495ef09b2 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -252,7 +252,7 @@ - + Description Mô tả @@ -302,123 +302,123 @@ 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: @@ -427,35 +427,35 @@ 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 @@ -463,123 +463,126 @@ Install successfully downloaded? 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 - - - + + Renderer + + + + + + On Bật - + Cursor Con trỏ - + Heroes III Data Language Ngôn ngữ dữ liệu Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -596,104 +599,102 @@ Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng độ phân giải được chọn. - + Reserved screen area Diện tích màn hình dành riêng - + Hardware Phần cứng - + Software Phần mềm - + Heroes III Translation Bản dịch Heroes III - + Check on startup Kiểm tra khi khởi động - + Fullscreen Toàn màn hình - - + General Chung - + VCMI Language Ngôn ngữ VCMI - + Resolution Độ phân giải - + Autosave Tự động lưu - + VSync - + Display index Mục hiện thị - + Network port Cổng mạng - - + Video Phim ảnh - + Show intro Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt diff --git a/launcher/vcmilauncher.desktop b/launcher/vcmilauncher.desktop index b916a002d..cd465c47b 100644 --- a/launcher/vcmilauncher.desktop +++ b/launcher/vcmilauncher.desktop @@ -1,10 +1,15 @@ [Desktop Entry] Type=Application Name=VCMI -GenericName=Strategy Game Engine -Comment=Launcher for open engine of Heroes of Might and Magic 3 +GenericName=Strategy Game +GenericName[cs]=Strategická hra +GenericName[de]=Strategiespiel +Comment=Open-source recreation of Heroes of Might & Magic III +Comment[cs]=Spouštěč enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[de]=Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmiclient Exec=vcmilauncher Categories=Game;StrategyGame; -Version=1.0 -Keywords=heroes;homm3; +Version=1.5 +Keywords=heroes of might and magic;heroes;homm;homm3;strategy; +SingleMainWindow=yes diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index d9f662a96..5ff9a1fe0 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -168,15 +168,14 @@ ui32 ACreature::getMaxHealth() const return std::max(1, value); //never 0 } -ui32 ACreature::speed(int turn, bool useBind) const +ui32 ACreature::getMovementRange(int turn) const { //war machines cannot move if(getBonusBearer()->hasBonus(Selector::type()(BonusType::SIEGE_WEAPON).And(Selector::turns(turn)))) { return 0; } - //bind effect check - doesn't influence stack initiative - if(useBind && getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn)))) + if(getBonusBearer()->hasBonus(Selector::type()(BonusType::BIND_EFFECT).And(Selector::turns(turn)))) { return 0; } diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index 9b64a5336..6ca3262d3 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -21,6 +21,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto * info = new BattleFieldInfo(BattleField(index), identifier); + info->modScope = scope; info->graphics = ImagePath::fromJson(json["graphics"]); info->icon = json["icon"].String(); info->name = json["name"].String(); @@ -66,7 +67,7 @@ int32_t BattleFieldInfo::getIconIndex() const std::string BattleFieldInfo::getJsonKey() const { - return identifier; + return modScope + ':' + identifier; } std::string BattleFieldInfo::getNameTextID() const diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index ec5535bc0..126b4e3f9 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -27,6 +27,7 @@ public: bool isSpecial; ImagePath graphics; std::string name; + std::string modScope; std::string identifier; std::string icon; si32 iconIndex; diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index b5cce121f..7244c62d2 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -76,10 +76,10 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonu 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) + if (text.find("${subtype.creature}") != std::string::npos && bonus->subtype.as() != CreatureID::NONE) boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as().toCreature()->getNamePluralTranslated()); - if (text.find("${subtype.spell}") != std::string::npos) + if (text.find("${subtype.spell}") != std::string::npos && bonus->subtype.as() != SpellID::NONE) boost::algorithm::replace_all(text, "${subtype.spell}", bonus->subtype.as().toSpell()->getNameTranslated()); return text; @@ -174,6 +174,9 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu if (bonus->subtype == BonusCustomSubtype::damageTypeRanged) fileName = "DamageReductionRanged.bmp"; + if (bonus->subtype == BonusCustomSubtype::damageTypeAll) + fileName = "DamageReductionAll.bmp"; + break; } diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 3b72d3719..adf0c0c67 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -397,7 +397,10 @@ void CCreature::serializeJson(JsonSerializeFormat & handler) if(!handler.saving) { if(ammMin > ammMax) + { logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); + std::swap(ammMin, ammMax); + } } } @@ -612,10 +615,20 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json 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); + int minDamage = node["damage"]["min"].Integer(); + int maxDamage = node["damage"]["max"].Integer(); - assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); + if (minDamage <= maxDamage) + { + cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + } + else + { + logMod->error("Mod %s: creature %s has minimal damage (%d) greater than maximal damage (%d)!", scope, identifier, minDamage, maxDamage); + cre->addBonus(maxDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(minDamage, BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); + } if(!node["shots"].isNull()) cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 4c6ba5c49..aa963c965 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -264,11 +264,14 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) { + assert(!vstd::contains(subContainers, &container)); subContainers.push_back(&container); } void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { + assert(vstd::contains(subContainers, &container)); + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); } @@ -414,6 +417,28 @@ void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const } } +TextContainerRegistrable::TextContainerRegistrable() +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::~TextContainerRegistrable() +{ + VLC->generaltexth->removeSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(const TextContainerRegistrable & other) + : TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + +TextContainerRegistrable::TextContainerRegistrable(TextContainerRegistrable && other) noexcept + :TextLocalizationContainer(other) +{ + VLC->generaltexth->addSubContainer(*this); +} + void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) { CLegacyConfigParser parser(TextPath::builtin(sourceName)); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 980a3c03e..7b57d9fdb 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -218,6 +218,18 @@ public: } }; +class DLL_LINKAGE TextContainerRegistrable : public TextLocalizationContainer +{ +public: + TextContainerRegistrable(); + ~TextContainerRegistrable(); + + TextContainerRegistrable(const TextContainerRegistrable & other); + TextContainerRegistrable(TextContainerRegistrable && other) noexcept; + + TextContainerRegistrable& operator=(TextContainerRegistrable b) = delete; +}; + /// Handles all text-related data in game class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer { diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 3ae6931a1..46481be32 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -257,7 +257,15 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); - heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + if (vstd::contains(affinityStr, node["affinity"].String())) + { + heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + } + else + { + logGlobal->error("Mod '%s', hero class '%s': invalid affinity '%s'! Expected 'might' or 'magic'!", scope, identifier, node["affinity"].String()); + heroClass->affinity = CHeroClass::MIGHT; + } fillPrimarySkillData(node, heroClass, PrimarySkill::ATTACK); fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); @@ -465,7 +473,11 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const 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); + if (hero->initialArmy[i].minAmount > hero->initialArmy[i].maxAmount) + { + logMod->error("Hero %s has minimal army size (%d) greater than maximal size (%d)!", hero->getJsonKey(), hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + std::swap(hero->initialArmy[i].minAmount, hero->initialArmy[i].maxAmount); + } VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) { @@ -654,14 +666,21 @@ void CHeroHandler::loadExperience() expPerLevel.push_back(24320); expPerLevel.push_back(28784); expPerLevel.push_back(34140); - while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) + + for (;;) { auto i = expPerLevel.size() - 1; - auto diff = expPerLevel[i] - expPerLevel[i-1]; - diff += diff / 5; - expPerLevel.push_back (expPerLevel[i] + diff); + auto currExp = expPerLevel[i]; + auto prevExp = expPerLevel[i-1]; + auto prevDiff = currExp - prevExp; + auto nextDiff = prevDiff + prevDiff / 5; + auto maxExp = std::numeric_limits::max(); + + if (currExp > maxExp - nextDiff) + break; // overflow point reached + + expPerLevel.push_back (currExp + nextDiff); } - expPerLevel.pop_back();//last value is broken } /// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) @@ -741,12 +760,12 @@ void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNod registerObject(scope, "hero", name, object->getIndex()); } -ui32 CHeroHandler::level (ui64 experience) const +ui32 CHeroHandler::level (TExpType experience) const { return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); } -ui64 CHeroHandler::reqExp (ui32 level) const +TExpType CHeroHandler::reqExp (ui32 level) const { if(!level) return 0; @@ -762,6 +781,11 @@ ui64 CHeroHandler::reqExp (ui32 level) const } } +ui32 CHeroHandler::maxSupportedLevel() const +{ + return expPerLevel.size(); +} + std::set CHeroHandler::getDefaultAllowed() const { std::set result; diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 3103594bc..88ce86f5b 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -176,8 +176,8 @@ protected: 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; + /// consists of 196 values. Any higher levels require experience larger that TExpType can hold + std::vector expPerLevel; /// helpers for loading to avoid huge load functions void loadHeroArmy(CHero * hero, const JsonNode & node) const; @@ -191,8 +191,9 @@ class DLL_LINKAGE CHeroHandler : public CHandlerBase loadLegacyData() override; diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 657356411..037207c76 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -37,21 +37,25 @@ void CRandomGenerator::resetSeed() TRandI CRandomGenerator::getIntRange(int lower, int upper) { + assert(lower <= upper); return std::bind(TIntDist(lower, upper), std::ref(rand)); } vstd::TRandI64 CRandomGenerator::getInt64Range(int64_t lower, int64_t upper) { + assert(lower <= upper); return std::bind(TInt64Dist(lower, upper), std::ref(rand)); } int CRandomGenerator::nextInt(int upper) { + assert(0 <= upper); return getIntRange(0, upper)(); } int CRandomGenerator::nextInt(int lower, int upper) { + assert(lower <= upper); return getIntRange(lower, upper)(); } @@ -62,16 +66,19 @@ int CRandomGenerator::nextInt() vstd::TRand CRandomGenerator::getDoubleRange(double lower, double upper) { - return std::bind(TRealDist(lower, upper), std::ref(rand)); + assert(lower <= upper); + return std::bind(TRealDist(lower, upper), std::ref(rand)); } double CRandomGenerator::nextDouble(double upper) { + assert(0 <= upper); return getDoubleRange(0, upper)(); } double CRandomGenerator::nextDouble(double lower, double upper) { + assert(lower <= upper); return getDoubleRange(lower, upper)(); } diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 6df29e210..e3ef38929 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -95,6 +95,7 @@ void GameSettings::load(const JsonNode & input) {EGameSettings::TEXTS_ROAD, "textData", "road" }, {EGameSettings::TEXTS_SPELL, "textData", "spell" }, {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, + {EGameSettings::PATHFINDER_IGNORE_GUARDS, "pathfinder", "ignoreGuards" }, {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, diff --git a/lib/GameSettings.h b/lib/GameSettings.h index 750c9abab..7241e9b92 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -60,6 +60,7 @@ enum class EGameSettings MAP_FORMAT_JSON_VCMI, MAP_FORMAT_IN_THE_WAKE_OF_GODS, PATHFINDER_USE_BOAT, + PATHFINDER_IGNORE_GUARDS, PATHFINDER_USE_MONOLITH_TWO_WAY, PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 076cfd804..01502021b 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -84,6 +84,7 @@ public: 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 giveExperience(const CGHeroInstance * hero, TExpType val) =0; virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; virtual void showBlockingDialog(BlockingDialog *iw) =0; @@ -121,6 +122,7 @@ public: virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; virtual void giveHeroBonus(GiveBonus * bonus)=0; virtual void setMovePoints(SetMovePoints * smp)=0; + virtual void setMovePoints(ObjectInstanceID hid, int val, bool absolute)=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; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 593cbdf19..6e8cc0f17 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -537,6 +537,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES: case BonusType::CREATURE_DAMAGE: case BonusType::FLYING: + case BonusType::FIRST_STRIKE: case BonusType::GENERAL_DAMAGE_REDUCTION: case BonusType::PERCENTAGE_DAMAGE_BOOST: case BonusType::SOUL_STEAL: @@ -1288,7 +1289,12 @@ static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) #elif defined(VCMI_ANDROID) if (!fieldProps["defaultAndroid"].isNull()) return fieldProps["defaultAndroid"]; -#elif !defined(VCMI_MOBILE) +#elif defined(VCMI_WINDOWS) + if (!fieldProps["defaultWindows"].isNull()) + return fieldProps["defaultWindows"]; +#endif + +#if !defined(VCMI_MOBILE) if (!fieldProps["defaultDesktop"].isNull()) return fieldProps["defaultDesktop"]; #endif diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index f297e5dff..94d3f57fd 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -47,14 +47,14 @@ VCMI_LIB_NAMESPACE_BEGIN LibClasses * VLC = nullptr; -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, 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); + VLC->loadModFilesystem(); } @@ -182,12 +182,12 @@ void LibClasses::loadFilesystem(bool extractArchives) logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); } -void LibClasses::loadModFilesystem(bool onlyEssential) +void LibClasses::loadModFilesystem() { CStopWatch loadTime; modh = new CModHandler(); identifiersHandler = new CIdentifierStorage(); - modh->loadMods(onlyEssential); + modh->loadMods(); logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); modh->loadModFilesystems(); diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index dc9912e3b..6622d9822 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -115,7 +115,7 @@ public: // basic initialization. should be called before init(). Can also extract original H3 archives void loadFilesystem(bool extractArchives); - void loadModFilesystem(bool onlyEssential); + void loadModFilesystem(); #if SCRIPTING_ENABLED void scriptsLoaded(); @@ -124,7 +124,7 @@ public: extern DLL_LINKAGE LibClasses * VLC; -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool extractArchives); DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 340dca2b1..7db5cc7aa 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -267,7 +267,7 @@ std::vector CBattleInfoCallback::getClientActionsFor 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 + if(stack->canMove() && stack->getMovementRange(0)) //probably no reason to try move war machines or bound stacks allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); const auto * siegedTown = battleGetDefendedTown(); @@ -570,7 +570,7 @@ std::vector CBattleInfoCallback::battleGetAvailableHexes(const Reacha if(!unit->getPosition().isValid()) //turrets return ret; - auto unitSpeed = unit->speed(0, true); + auto unitSpeed = unit->getMovementRange(0); const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); @@ -741,15 +741,15 @@ DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * { RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int movementDistance = reachability[attackerPosition]; - return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); + int getMovementRange = reachability[attackerPosition]; + return battleEstimateDamage(attacker, defender, getMovementRange, retaliationDmg); } -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, DamageEstimation * retaliationDmg) const { RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); + const BattleAttackInfo bai(attacker, defender, getMovementRange, shooting); return battleEstimateDamage(bai, retaliationDmg); } diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index ed5c8a0d8..1ace49408 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -98,7 +98,7 @@ public: /// 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; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int getMovementRange, 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; diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index d735222f4..58279e118 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" + #include "DamageCalculator.h" #include "CBattleInfoCallback.h" #include "Unit.h" @@ -124,7 +125,19 @@ int DamageCalculator::getActorAttackBase() const int DamageCalculator::getActorAttackEffective() const { - return getActorAttackBase() + getActorAttackSlayer(); + return getActorAttackBase() + getActorAttackSlayer() + getActorAttackIgnored(); +} + +int DamageCalculator::getActorAttackIgnored() const +{ + int multAttackReductionPercent = battleBonusValue(info.defender, Selector::type()(BonusType::ENEMY_ATTACK_REDUCTION)); + + if(multAttackReductionPercent > 0) + { + int reduction = (getActorAttackBase() * multAttackReductionPercent + 49) / 100; //using ints so 1.5 for 5 attack is rounded down as in HotA / h3assist etc. (keep in mind h3assist 1.2 shows wrong value for 15 attack points and unupg. nix) + return -std::min(reduction, getActorAttackBase()); + } + return 0; } int DamageCalculator::getActorAttackSlayer() const @@ -266,6 +279,20 @@ double DamageCalculator::getAttackHateFactor() const return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0; } +double DamageCalculator::getAttackRevengeFactor() const +{ + if(info.attacker->hasBonusOfType(BonusType::REVENGE)) //HotA Haspid ability + { + int totalStackCount = info.attacker->unitBaseAmount(); + int currentStackHealth = info.attacker->getAvailableHealth(); + int creatureHealth = info.attacker->getMaxHealth(); + + return sqrt(static_cast((totalStackCount + 1) * creatureHealth) / (currentStackHealth + creatureHealth) - 1); + } + + return 0.0; +} + double DamageCalculator::getDefenseSkillFactor() const { int defenseAdvantage = getTargetDefenseEffective() - getActorAttackEffective(); @@ -433,7 +460,8 @@ std::vector DamageCalculator::getAttackFactors() const getAttackJoustingFactor(), getAttackDeathBlowFactor(), getAttackDoubleDamageFactor(), - getAttackHateFactor() + getAttackHateFactor(), + getAttackRevengeFactor() }; } @@ -503,12 +531,10 @@ DamageEstimation DamageCalculator::calculateDmgRange() const for (auto & factor : defenseFactors) { assert(factor >= 0.0); - defenseFactorTotal *= ( 1 - std::min(1.0, factor)); + defenseFactorTotal *= (1 - std::min(1.0, factor)); } - double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - - info.defender->getTotalHealth(); + double resultingFactor = attackFactorTotal * defenseFactorTotal; DamageRange damageDealt { std::max( 1.0, std::floor(damageBase.min * resultingFactor)), diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index 9548f950f..d7f91e0b5 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -38,6 +38,7 @@ class DLL_LINKAGE DamageCalculator int getActorAttackBase() const; int getActorAttackEffective() const; int getActorAttackSlayer() const; + int getActorAttackIgnored() const; int getTargetDefenseBase() const; int getTargetDefenseEffective() const; int getTargetDefenseIgnored() const; @@ -50,6 +51,7 @@ class DLL_LINKAGE DamageCalculator double getAttackDeathBlowFactor() const; double getAttackDoubleDamageFactor() const; double getAttackHateFactor() const; + double getAttackRevengeFactor() const; double getDefenseSkillFactor() const; double getDefenseArmorerFactor() const; diff --git a/lib/bonuses/BonusCustomTypes.cpp b/lib/bonuses/BonusCustomTypes.cpp index 4e793ac21..6f6ac83b0 100644 --- a/lib/bonuses/BonusCustomTypes.cpp +++ b/lib/bonuses/BonusCustomTypes.cpp @@ -23,6 +23,10 @@ const BonusCustomSubtype BonusCustomSubtype::heroMovementLand(1); const BonusCustomSubtype BonusCustomSubtype::heroMovementSea(0); const BonusCustomSubtype BonusCustomSubtype::deathStareGorgon(0); const BonusCustomSubtype BonusCustomSubtype::deathStareCommander(1); +const BonusCustomSubtype BonusCustomSubtype::deathStareNoRangePenalty(2); +const BonusCustomSubtype BonusCustomSubtype::deathStareRangePenalty(3); +const BonusCustomSubtype BonusCustomSubtype::deathStareObstaclePenalty(4); +const BonusCustomSubtype BonusCustomSubtype::deathStareRangeObstaclePenalty(5); const BonusCustomSubtype BonusCustomSubtype::rebirthRegular(0); const BonusCustomSubtype BonusCustomSubtype::rebirthSpecial(1); const BonusCustomSubtype BonusCustomSubtype::visionsMonsters(0); diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h index a6a2c96e2..824561929 100644 --- a/lib/bonuses/BonusCustomTypes.h +++ b/lib/bonuses/BonusCustomTypes.h @@ -45,6 +45,10 @@ public: static const BonusCustomSubtype deathStareGorgon; // 0 static const BonusCustomSubtype deathStareCommander; + static const BonusCustomSubtype deathStareNoRangePenalty; + static const BonusCustomSubtype deathStareRangePenalty; + static const BonusCustomSubtype deathStareObstaclePenalty; + static const BonusCustomSubtype deathStareRangeObstaclePenalty; static const BonusCustomSubtype rebirthRegular; // 0 static const BonusCustomSubtype rebirthSpecial; // 1 diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 0451cbba8..f6cb258e0 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -48,7 +48,7 @@ class JsonNode; BONUS_NAME(FLYING) \ BONUS_NAME(SHOOTER) \ BONUS_NAME(CHARGE_IMMUNITY) \ - BONUS_NAME(ADDITIONAL_ATTACK) \ + BONUS_NAME(ADDITIONAL_ATTACK) /*val: number of additional attacks to perform*/ \ BONUS_NAME(UNLIMITED_RETALIATIONS) \ BONUS_NAME(NO_MELEE_PENALTY) \ BONUS_NAME(JOUSTING) /*for champions*/ \ @@ -57,8 +57,8 @@ class JsonNode; BONUS_NAME(MAGIC_RESISTANCE) /*in % (value)*/ \ BONUS_NAME(CHANGES_SPELL_COST_FOR_ALLY) /*in mana points (value) , eg. mage*/ \ BONUS_NAME(CHANGES_SPELL_COST_FOR_ENEMY) /*in mana points (value) , eg. pegasus */ \ - BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \ - BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only] */ \ + BONUS_NAME(SPELL_AFTER_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_AFTER_ATTACK bonuses (default none [-1]) */ \ + BONUS_NAME(SPELL_BEFORE_ATTACK) /* subtype - spell id, value - chance %, addInfo[0] - level, addInfo[1] -> [0 - all attacks, 1 - shot only, 2 - melee only], addInfo[2] -> spell layer for multiple SPELL_BEFORE_ATTACK bonuses (default none [-1]) */ \ BONUS_NAME(SPELL_RESISTANCE_AURA) /*eg. unicorns, value - resistance bonus in % for adjacent creatures*/ \ BONUS_NAME(LEVEL_SPELL_IMMUNITY) /*creature is immune to all spell with level below or equal to value of this bonus */ \ BONUS_NAME(BLOCK_MAGIC_ABOVE) /*blocks casting spells of the level > value */ \ @@ -173,6 +173,9 @@ class JsonNode; BONUS_NAME(UNLIMITED_MOVEMENT) /*cheat bonus*/ \ BONUS_NAME(MAX_MORALE) /*cheat bonus*/ \ BONUS_NAME(MAX_LUCK) /*cheat bonus*/ \ + BONUS_NAME(FEROCITY) /*extra attacks, only if at least some creatures killed while attacking target unit, val = amount of additional attacks, additional info = amount of creatures killed to trigger (default 1)*/ \ + BONUS_NAME(ENEMY_ATTACK_REDUCTION) /*in % (value) eg. Nix (HotA)*/ \ + BONUS_NAME(REVENGE) /*additional damage based on how many units in stack died - formula: sqrt((number of creatures at battle start + 1) * creature health) / (total health now + 1 creature health) - 1) * 100% */ \ /* end of list */ diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index ad59559df..ae4d424e5 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -124,7 +124,7 @@ static std::string convertMapName(std::string input) return input; } -std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) +std::string CampaignHandler::readLocalizedString(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) { TextIdentifier stringID( "campaign", convertMapName(filename), identifier); @@ -133,7 +133,7 @@ std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::st if (input.empty()) return ""; - VLC->generaltexth->registerString(modName, stringID, input); + target.getTexts().registerString(modName, stringID, input); return stringID.get(); } @@ -383,8 +383,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader ret.version = static_cast(reader.readUInt32()); ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] ret.loadLegacyData(campId); - ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); - ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); + ret.name.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "name")); + ret.description.appendTextID(readLocalizedString(ret, reader, filename, modName, encoding, "description")); if (ret.version > CampaignVersion::RoE) ret.difficultyChoosenByPlayer = reader.readInt8(); else @@ -396,7 +396,7 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader ret.encoding = encoding; } -CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) +CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, CampaignHeader & header) { auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog { @@ -410,7 +410,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader 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)); + ret.prologText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, identifier)); } return ret; }; @@ -428,7 +428,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader } ret.regionColor = reader.readUInt8(); ret.difficulty = reader.readUInt8(); - ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); + ret.regionText.appendTextID(readLocalizedString(header, reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index d6e2d0579..e1c54c6e8 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -16,7 +16,7 @@ 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 std::string readLocalizedString(CampaignHeader & target, 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); @@ -27,7 +27,7 @@ class DLL_LINKAGE CampaignHandler //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 CampaignScenario readScenarioFromMemory(CBinaryReader & reader, 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 diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index d23ffd314..98623a509 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -169,6 +169,11 @@ const CampaignRegions & CampaignHeader::getRegions() const return campaignRegions; } +TextContainerRegistrable & CampaignHeader::getTexts() +{ + return textContainer; +} + bool CampaignState::isConquered(CampaignScenarioID whichScenario) const { return vstd::contains(mapsConquered, whichScenario); diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index cbe133467..37e7d91f3 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -9,9 +9,10 @@ */ #pragma once -#include "../lib/GameConstants.h" -#include "../lib/MetaString.h" -#include "../lib/filesystem/ResourcePath.h" +#include "../GameConstants.h" +#include "../MetaString.h" +#include "../filesystem/ResourcePath.h" +#include "../CGeneralTextHandler.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" @@ -87,6 +88,8 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable void loadLegacyData(ui8 campId); + TextContainerRegistrable textContainer; + public: bool playerSelectedDifficulty() const; bool formatVCMI() const; @@ -99,6 +102,7 @@ public: AudioPath getMusic() const; const CampaignRegions & getRegions() const; + TextContainerRegistrable & getTexts(); template void serialize(Handler &h, const int formatVersion) { @@ -112,6 +116,8 @@ public: h & modName; h & music; h & encoding; + if (formatVersion >= 832) + h & textContainer; } }; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 60cf811c2..597343cc2 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -292,7 +292,7 @@ const Skill * SecondarySkill::toEntity(const Services * services) const const CCreature * CreatureIDBase::toCreature() const { - return VLC->creh->objects.at(num); + return dynamic_cast(toEntity(VLC)); } const Creature * CreatureIDBase::toEntity(const Services * services) const @@ -324,12 +324,7 @@ std::string CreatureID::entityType() 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]; + return dynamic_cast(toEntity(VLC)); } const spells::Spell * SpellIDBase::toEntity(const Services * services) const diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index b78828b57..e12a3e373 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -210,17 +210,21 @@ void CGameStateCampaign::placeCampaignHeroes() // with the same hero type id std::vector removedHeroes; - std::set heroesToRemove = campaignState->getReservedHeroes(); + std::set reservedHeroes = campaignState->getReservedHeroes(); + std::set heroesToRemove; + + for (auto const & heroID : reservedHeroes ) + { + // Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear + if (!campaignState->getHeroByType(heroID).isNull()) + heroesToRemove.insert(heroID); + } for(auto & campaignHeroReplacement : campaignHeroReplacements) 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) { diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 40161ff2e..bafaf28b0 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -40,7 +40,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const return TavernSlotRole::NONE; } -void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role) +void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints) { vstd::erase_if(currentTavern, [&](const TavernSlot & entry){ return entry.player == player && entry.slot == slot; @@ -54,6 +54,12 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, if (h && army) h->setToArmy(army); + if (h && replenishPoints) + { + h->setMovementPoints(h->movementPointsLimit(true)); + h->mana = h->manaLimit(); + } + TavernSlot newSlot; newSlot.hero = h; newSlot.player = player; diff --git a/lib/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h index fb5dc136f..4e109f8c0 100644 --- a/lib/gameState/TavernHeroesPool.h +++ b/lib/gameState/TavernHeroesPool.h @@ -74,7 +74,7 @@ public: 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); + void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role, bool replenishPoints); template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index f7fbcd46c..2c758b0f6 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -177,8 +177,10 @@ void CObjectClassesHandler::loadSubObject(const std::string & scope, const std:: auto object = loadSubObjectFromJson(scope, identifier, entry, obj, index); assert(object); - assert(obj->objects[index] == nullptr); // ensure that this id was not loaded before - obj->objects[index] = object; + if (obj->objects.at(index) != nullptr) + throw std::runtime_error("Attempt to load already loaded object:" + identifier); + + obj->objects.at(index) = object; registerObject(scope, obj->getJsonKey(), object->getSubTypeName(), object->subtype); for(const auto & compatID : entry["compatibilityIdentifiers"].Vector()) @@ -259,10 +261,16 @@ std::unique_ptr CObjectClassesHandler::loadFromJson(const std::stri { const std::string & subMeta = subData.second["index"].meta; - if ( subMeta != "core") - logMod->warn("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first ); - size_t subIndex = subData.second["index"].Integer(); - loadSubObject(subData.second.meta, subData.first, subData.second, obj.get(), subIndex); + if ( subMeta == "core") + { + size_t subIndex = subData.second["index"].Integer(); + loadSubObject(subData.second.meta, subData.first, subData.second, obj.get(), subIndex); + } + else + { + logMod->error("Object %s:%s.%s - attempt to load object with preset index! This option is reserved for built-in mod", subMeta, name, subData.first ); + loadSubObject(subData.second.meta, subData.first, subData.second, obj.get()); + } } else loadSubObject(subData.second.meta, subData.first, subData.second, obj.get()); @@ -283,28 +291,28 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) { - assert(objects[index] == nullptr); // ensure that this id was not loaded before + assert(objects.at(index) == nullptr); // ensure that this id was not loaded before - objects[index] = loadFromJson(scope, data, name, index); - VLC->identifiersHandler->registerObject(scope, "object", name, objects[index]->id); + objects.at(index) = loadFromJson(scope, data, name, index); + VLC->identifiersHandler->registerObject(scope, "object", name, objects.at(index)->id); } void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL - assert(objects[ID.getNum()]); + assert(objects.at(ID.getNum())); - if ( subID.getNum() >= objects[ID.getNum()]->objects.size()) - objects[ID.getNum()]->objects.resize(subID.getNum()+1); + if ( subID.getNum() >= objects.at(ID.getNum())->objects.size()) + objects.at(ID.getNum())->objects.resize(subID.getNum()+1); JsonUtils::inherit(config, objects.at(ID.getNum())->base); - loadSubObject(config.meta, identifier, config, objects[ID.getNum()].get(), subID.getNum()); + loadSubObject(config.meta, identifier, config, objects.at(ID.getNum()).get(), subID.getNum()); } void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(objects[ID.getNum()]); - objects[ID.getNum()]->objects[subID.getNum()] = nullptr; + assert(objects.at(ID.getNum())); + objects.at(ID.getNum())->objects.at(subID.getNum()) = nullptr; } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const @@ -337,11 +345,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scop std::optional id = VLC->identifiers()->getIdentifier(scope, "object", type); if(id) { - const auto & object = objects[id.value()]; + const auto & object = objects.at(id.value()); std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); if (subID) - return object->objects[subID.value()]; + return object->objects.at(subID.value()); } std::string errorString = "Failed to find object of type " + type + "::" + subtype; @@ -472,8 +480,8 @@ std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubI if (handler && handler->hasNameTextID()) return handler->getNameTranslated(); - if (objects[type.getNum()]) - return objects[type.getNum()]->getNameTranslated(); + if (objects.at(type.getNum())) + return objects.at(type.getNum())->getNameTranslated(); return objects.front()->getNameTranslated(); } @@ -487,7 +495,7 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObject if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - if(objects[type.getNum()]) + if(objects.at(type.getNum())) return getHandlerFor(type, subtype)->getSounds(); else return objects.front()->objects.front()->getSounds(); diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 88f3578c5..2276d8a0d 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -320,7 +320,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); const CCreature * cre =creatures[i].second[0].toCreature(); - TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH); + TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(cre->getLevel())); if (creaturesAccumulate && ID != Obj::REFUGEE_CAMP) //camp should not try to accumulate different kinds of creatures sac.creatures[i].first += amount; else diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ac5fc907a..3577b5e33 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -436,14 +436,14 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) int count = rand.nextInt(stack.minAmount, stack.maxAmount); - const CCreature * creature = stack.creature.toCreature(); - - if(creature == nullptr) + if(stack.creature == CreatureID::NONE) { - logGlobal->error("Hero %s has invalid creature with id %d in initial army", getNameTranslated(), stack.creature.toEnum()); + logGlobal->error("Hero %s has invalid creature in initial army", getNameTranslated()); continue; } + const CCreature * creature = stack.creature.toCreature(); + if(creature->warMachine != ArtifactID::NONE) //war machine { warMachinesGiven++; @@ -1439,7 +1439,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 bool CGHeroInstance::gainsLevel() const { - return exp >= static_cast(VLC->heroh->reqExp(level+1)); + return level < VLC->heroh->maxSupportedLevel() && exp >= static_cast(VLC->heroh->reqExp(level+1)); } void CGHeroInstance::levelUp(const std::vector & skills) diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index f00aa72f2..feca2de77 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -151,11 +151,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const gb.id = heroID; cb->giveHeroBonus(&gb); - SetMovePoints mp; - mp.val = 600; - mp.absolute = false; - mp.hid = heroID; - cb->setMovePoints(&mp); + cb->setMovePoints(heroID, 600, false); iw.text.appendRawString(VLC->generaltexth->allTexts[580]); cb->showInfoDialog(&iw); @@ -249,8 +245,12 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const iw.player = cb->getOwner(heroID); iw.text.appendRawString(getVisitingBonusGreeting()); cb->showInfoDialog(&iw); - cb->changePrimSkill (cb->getHero(heroID), what, val); - town->addHeroToStructureVisitors(h, indexOnTV); + if (what == PrimarySkill::EXPERIENCE) + cb->giveExperience(cb->getHero(heroID), val); + else + cb->changePrimSkill(cb->getHero(heroID), what, val); + + town->addHeroToStructureVisitors(h, indexOnTV); } } } diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 0f7c49bcf..70d83674a 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -164,7 +164,8 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level))); + // Note: bonus uses 1-based levels (Pikeman is level 1), town list uses 0-based (Pikeman in 0-th creatures entry) + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level+1))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 853466a14..36cda23e1 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1138,7 +1138,7 @@ void CGSirens::onHeroVisit( const CGHeroInstance * h ) const xp = h->calculateXp(static_cast(xp)); iw.text.appendLocalString(EMetaText::ADVOB_TXT,132); iw.text.replaceNumber(static_cast(xp)); - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, xp, false); + cb->giveExperience(h, xp); } else { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index c825899e2..58721b5d0 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -649,9 +649,18 @@ void CMap::banWaterHeroes() void CMap::banHero(const HeroTypeID & id) { + if (!vstd::contains(allowedHeroes, id)) + logGlobal->warn("Attempt to ban hero %s, who is already not allowed", id.encode(id)); allowedHeroes.erase(id); } +void CMap::unbanHero(const HeroTypeID & id) +{ + if (vstd::contains(allowedHeroes, id)) + logGlobal->warn("Attempt to unban hero %s, who is already allowed", id.encode(id)); + allowedHeroes.insert(id); +} + void CMap::initTerrain() { terrain.resize(boost::extents[levels()][width][height]); diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 7922df144..051d991aa 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -112,6 +112,7 @@ public: void banWaterArtifacts(); void banWaterHeroes(); void banHero(const HeroTypeID& id); + void unbanHero(const HeroTypeID & id); void banWaterSpells(); void banWaterSkills(); void banWaterContent(); diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index cacfc4c24..1f5f7ee33 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -122,13 +122,9 @@ 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); -} +CMapHeader::~CMapHeader() = default; ui8 CMapHeader::levels() const { @@ -137,9 +133,6 @@ ui8 CMapHeader::levels() const 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; @@ -193,7 +186,7 @@ void CMapHeader::registerMapStrings() JsonUtils::mergeCopy(data, translations[language]); for(auto & s : data.Struct()) - registerString("map", TextIdentifier(s.first), s.second.String(), language); + texts.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) @@ -203,7 +196,7 @@ std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeade std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) { - mapHeader.registerString(modContext, UID, localized, language); + mapHeader.texts.registerString(modContext, UID, localized, language); mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; return UID.get(); } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 4ba9d9a0a..324b7aa48 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -192,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: public TextLocalizationContainer +class DLL_LINKAGE CMapHeader { void setupEvents(); public: @@ -240,13 +240,14 @@ public: /// translations for map to be transferred over network JsonNode translations; + TextContainerRegistrable texts; void registerMapStrings(); template void serialize(Handler & h, const int Version) { - h & static_cast(*this); + h & texts; h & version; h & mods; h & name; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 0c6e03cff..39a0ee831 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -64,8 +64,8 @@ void CMapInfo::saveInit(const ResourcePath & file) 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 = TextOperations::getFormattedDateTimeLocal(time); + lastWrite = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); + date = TextOperations::getFormattedDateTimeLocal(lastWrite); // We absolutely not need this data for lobby and server will read it from save // FIXME: actually we don't want them in CMapHeader! diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 512cff481..b086a38af 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -28,8 +28,9 @@ public: 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 originalFileURI; // no need to serialize + std::string fullFileURI; // no need to serialize + std::time_t lastWrite; // no need to serialize std::string date; int amountOfPlayersOnMap; int amountOfHumanControllablePlayers; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 605cf52aa..78e6b2b68 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2223,17 +2223,10 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt } { - std::set spellsMask; + std::set spellsMask = VLC->spellh->getDefaultAllowed(); // by default - include spells from mods 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) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 88e3f1851..1fe9d4b17 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -237,19 +237,12 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co } } -void CModHandler::loadMods(bool onlyEssential) +void CModHandler::loadMods() { JsonNode modConfig; - if(onlyEssential) - { - loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods - } - else - { - modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); - loadMods("", "", modConfig["activeMods"], true); - } + 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"))); } @@ -346,20 +339,25 @@ void CModHandler::loadModFilesystems() TModID CModHandler::findResourceOrigin(const ResourcePath & name) const { - for(const auto & modID : boost::adaptors::reverse(activeMods)) + try { - if(CResourceHandler::get(modID)->existsResource(name)) - return modID; + 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 } - - 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 ""; + catch( const std::out_of_range & e) + { + // no-op + } + throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found."); } std::string CModHandler::getModLanguage(const TModID& modId) const diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 4028ce6c2..30eea7a38 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -58,7 +58,7 @@ public: /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); - void loadMods(bool onlyEssential = false); + void loadMods(); void loadModFilesystems(); /// returns ID of mod that provides selected file resource diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 977ad172b..59e1ee718 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -111,15 +111,17 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) // - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases) // - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data // so emit warning and skip such case - logMod->warn("Mod %s attempts to edit object %s from mod %s but no such object exist!", data.meta, name, modName); + logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.meta, name, objectName, 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); + bool hasIndex = vstd::contains(data.Struct(), "index") && !data["index"].isNull(); + if (hasIndex && modName != "core") + logMod->error("Mod %s is attempting to load original data! This option is reserved for built-in mod.", modName); + + if (hasIndex && modName == "core") + { // try to add H3 object data size_t index = static_cast(data["index"].Float()); @@ -155,6 +157,33 @@ void ContentTypeHandler::loadCustom() void ContentTypeHandler::afterLoadFinalization() { + for (auto const & data : modData) + { + if (data.second.modData.isNull()) + { + for (auto node : data.second.patches.Struct()) + logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.meta, node.first, data.first); + } + + for(auto & otherMod : modData) + { + if (otherMod.first == data.first) + continue; + + if (otherMod.second.modData.isNull()) + continue; + + for(auto & otherObject : otherMod.second.modData.Struct()) + { + if (data.second.modData.Struct().count(otherObject.first)) + { + logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); + logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + } + } + } + } + handler->afterLoadFinalization(); } diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 2a360db36..d8b16d7d0 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -53,6 +53,10 @@ CIdentifierStorage::CIdentifierStorage() registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareNoRangePenalty", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangePenalty", 3); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareObstaclePenalty", 4); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareRangeObstaclePenalty", 5); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthSpecial", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsMonsters", 0); @@ -132,6 +136,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameW result.name = typeAndName.second; result.callback = callback; result.optional = optional; + result.dynamicType = true; return result; } @@ -160,6 +165,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameA result.name = typeAndName.second; result.callback = callback; result.optional = optional; + result.dynamicType = false; return result; } @@ -197,58 +203,121 @@ std::optional CIdentifierStorage::getIdentifier(const std::string & scope, { 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(); + auto options = ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent); + return getIdentifierImpl(options, silent); } 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)); + auto options = 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(); + return getIdentifierImpl(options, silent); } 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(); + auto options = ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent); + return getIdentifierImpl(options, silent); } 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)); + auto options = ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent); + return getIdentifierImpl(options, silent); +} + +std::optional CIdentifierStorage::getIdentifierImpl(const ObjectCallback & options, bool silent) const +{ + auto idList = getPossibleIdentifiers(options); if (idList.size() == 1) return idList.front().id; if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); - + showIdentifierResolutionErrorDetails(options); return std::optional(); } +void CIdentifierStorage::showIdentifierResolutionErrorDetails(const ObjectCallback & options) const +{ + auto idList = getPossibleIdentifiers(options); + + logMod->error("Failed to resolve identifier '%s' of type '%s' from mod '%s'", options.name, options.type, options.localScope); + + if (options.dynamicType && options.type.empty()) + { + bool suggestionFound = false; + + for (auto const & entry : registeredObjects) + { + if (!boost::algorithm::ends_with(entry.first, options.name)) + continue; + + suggestionFound = true; + logMod->error("Perhaps you wanted to use identifier '%s' from mod '%s' instead?", entry.first, entry.second.scope); + } + + if (suggestionFound) + return; + } + + if (idList.empty()) + { + // check whether identifier is unavailable due to a missing dependency on a mod + ObjectCallback testOptions = options; + testOptions.localScope = ModScope::scopeGame(); + testOptions.remoteScope = {}; + + auto testList = getPossibleIdentifiers(testOptions); + if (testList.empty()) + { + logMod->error("Identifier '%s' of type '%s' does not exists in any loaded mod!", options.name, options.type); + } + else + { + // such identifiers exists, but were not picked for some reason + if (options.remoteScope.empty()) + { + // attempt to access identifier from mods that is not dependency + for (auto const & testOption : testList) + { + logMod->error("Identifier '%s' exists in mod %s", options.name, testOption.scope); + logMod->error("Please add mod '%s' as dependency of mod '%s' to access this identifier", testOption.scope, options.localScope); + } + } + else + { + // attempt to access identifier in form 'modName:object', but identifier is only present in different mod + for (auto const & testOption : testList) + { + logMod->error("Identifier '%s' exists in mod '%s' but identifier was explicitly requested from mod '%s'!", options.name, testOption.scope, options.remoteScope); + if (options.dynamicType) + logMod->error("Please use form '%s.%s' or '%s:%s.%s' to access this identifier", options.type, options.name, testOption.scope, options.type, options.name); + else + logMod->error("Please use form '%s' or '%s:%s' to access this identifier", options.name, testOption.scope, options.name); + } + } + } + } + else + { + logMod->error("Multiple possible candidates:"); + for (auto const & testOption : idList) + { + logMod->error("Identifier %s exists in mod %s", options.name, testOption.scope); + if (options.dynamicType) + logMod->error("Please use '%s:%s.%s' to access this identifier", testOption.scope, options.type, options.name); + else + logMod->error("Please use '%s:%s' to access this identifier", testOption.scope, options.name); + } + } +} + void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) { assert(state != ELoadingState::FINISHED); @@ -362,17 +431,7 @@ bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const } // 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); - } + showIdentifierResolutionErrorDetails(request); return false; } @@ -381,26 +440,16 @@ 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; + resolveIdentifier(request); } - debugDumpIdentifiers(); - - if (errorsFound) - logMod->error("All known identifiers were dumped into log file"); - - assert(errorsFound == false); state = ELoadingState::FINISHED; - } void CIdentifierStorage::debugDumpIdentifiers() diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 31857c447..6a657d715 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -32,6 +32,7 @@ class DLL_LINKAGE CIdentifierStorage std::string name; /// string ID std::function callback; bool optional; + bool dynamicType; /// 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); @@ -69,6 +70,8 @@ class DLL_LINKAGE CIdentifierStorage bool resolveIdentifier(const ObjectCallback & callback) const; std::vector getPossibleIdentifiers(const ObjectCallback & callback) const; + void showIdentifierResolutionErrorDetails(const ObjectCallback & callback) const; + std::optional getIdentifierImpl(const ObjectCallback & callback, bool silent) const; public: CIdentifierStorage(); virtual ~CIdentifierStorage() = default; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index a0f3656e8..7fcbf8aab 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -959,7 +959,7 @@ void FoWChange::applyGs(CGameState *gs) void SetAvailableHero::applyGs(CGameState *gs) { - gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); + gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID, replenishPoints); } void GiveBonus::applyGs(CGameState *gs) diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 9462179f6..d8531cd61 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -352,6 +352,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient PlayerColor player; HeroTypeID hid; //HeroTypeID::NONE if no hero CSimpleArmy army; + bool replenishPoints; void visitTyped(ICPackVisitor & visitor) override; @@ -362,6 +363,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient h & player; h & hid; h & army; + h & replenishPoints; } }; diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 4c83acafc..5b0ca709a 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -21,6 +21,7 @@ VCMI_LIB_NAMESPACE_BEGIN PathfinderOptions::PathfinderOptions() : useFlying(true) , useWaterWalking(true) + , ignoreGuards(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_IGNORE_GUARDS)) , useEmbarkAndDisembark(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_BOAT)) , useTeleportTwoWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY)) , useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE)) diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 96d75cb2a..973135e30 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -25,6 +25,7 @@ struct DLL_LINKAGE PathfinderOptions bool useFlying; bool useWaterWalking; bool useEmbarkAndDisembark; + bool ignoreGuards; bool useTeleportTwoWay; // Two-way monoliths and Subterranean Gate bool useTeleportOneWay; // One-way monoliths with one known exit only bool useTeleportOneWayRandom; // One-way monoliths with more than one known exit diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index d041aff85..49de31ff7 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -271,7 +271,12 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking case EPathNodeAction::BATTLE: /// Movement after BATTLE action only possible from guarded tile to guardian tile if(destination.guarded) - return BlockingReason::DESTINATION_GUARDED; + { + if (pathfinderHelper->options.ignoreGuards) + return BlockingReason::DESTINATION_GUARDED; + else + return BlockingReason::NONE; + } break; } @@ -299,6 +304,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(source.guarded) { if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) + && !pathfinderConfig->options.ignoreGuards && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { return BlockingReason::SOURCE_GUARDED; diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 534641f37..d7358efb9 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -117,7 +117,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); - si64 expToGive = 0; + TExpType expToGive = 0; if (info.reward.heroLevel > 0) expToGive += VLC->heroh->reqExp(hero->level+info.reward.heroLevel) - VLC->heroh->reqExp(hero->level); @@ -126,7 +126,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R expToGive += hero->calculateXp(info.reward.heroExperience); if(expToGive) - cb->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expToGive); + cb->giveExperience(hero, expToGive); } void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Rewardable::VisitInfo & info, const CArmedInstance * army, const CGHeroInstance * hero) const diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 4f89f7398..fe233bf99 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -388,6 +388,13 @@ void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, Faction it->second.setStartingTown(town); } +void CMapGenOptions::setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero) +{ + auto it = players.find(color); + assert(it != players.end()); + it->second.setStartingHero(hero); +} + void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType) { // FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color? @@ -746,7 +753,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand return *RandomGeneratorUtil::nextItem(templates, rand); } -CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) +CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), startingHero(HeroTypeID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) { } @@ -778,6 +785,17 @@ void CMapGenOptions::CPlayerSettings::setStartingTown(FactionID value) startingTown = value; } +HeroTypeID CMapGenOptions::CPlayerSettings::getStartingHero() const +{ + return startingHero; +} + +void CMapGenOptions::CPlayerSettings::setStartingHero(HeroTypeID value) +{ + assert(value == HeroTypeID::RANDOM || value.toEntity(VLC) != nullptr); + startingHero = value; +} + EPlayerType CMapGenOptions::CPlayerSettings::getPlayerType() const { return playerType; diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 8e25f2619..6e121186c 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -45,6 +45,11 @@ public: FactionID getStartingTown() const; void setStartingTown(FactionID value); + /// The starting hero of the player ranging from 0 to hero max count or RANDOM_HERO. + /// The default value is RANDOM_HERO + HeroTypeID getStartingHero() const; + void setStartingHero(HeroTypeID value); + /// The default value is EPlayerType::AI. EPlayerType getPlayerType() const; void setPlayerType(EPlayerType value); @@ -56,6 +61,7 @@ public: private: PlayerColor color; FactionID startingTown; + HeroTypeID startingHero; EPlayerType playerType; TeamID team; @@ -68,6 +74,10 @@ public: h & playerType; if(version >= 806) h & team; + if (version >= 832) + h & startingHero; + else + startingHero = HeroTypeID::RANDOM; } }; @@ -120,6 +130,7 @@ public: const std::map & getPlayersSettings() const; const std::map & getSavedPlayersMap() const; void setStartingTownForPlayer(const PlayerColor & color, FactionID town); + void setStartingHeroForPlayer(const PlayerColor & color, HeroTypeID hero); /// 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); diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 188ebb5df..070e507c2 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -35,7 +35,7 @@ VCMI_LIB_NAMESPACE_BEGIN CMapGenerator::CMapGenerator(CMapGenOptions& mapGenOptions, int RandomSeed) : mapGenOptions(mapGenOptions), randomSeed(RandomSeed), - allowedPrisons(0), monolithIndex(0) + monolithIndex(0) { loadConfig(); rand.setSeed(this->randomSeed); @@ -96,12 +96,6 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const return mapGenOptions; } -void CMapGenerator::initPrisonsRemaining() -{ - 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() { //TODO: Move to QuestArtifactPlacer? @@ -122,7 +116,6 @@ std::unique_ptr CMapGenerator::generate() addHeaderInfo(); map->initTiles(*this, rand); Load::Progress::step(); - initPrisonsRemaining(); initQuestArtsRemaining(); genZones(); Load::Progress::step(); @@ -468,11 +461,6 @@ int CMapGenerator::getNextMonlithIndex() } } -int CMapGenerator::getPrisonsRemaning() const -{ - return allowedPrisons; -} - std::shared_ptr CMapGenerator::getZonePlacer() const { return placer; @@ -488,31 +476,42 @@ 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 (HeroTypeID hero : map->getMap(this).allowedHeroes) { auto * h = dynamic_cast(VLC->heroTypes()->getById(hero)); - if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) - { + if(h->onlyOnWaterMap && !isWaterMap) continue; - } - else + + if(h->onlyOnMapWithoutWater && isWaterMap) + continue; + + bool heroUsedAsStarting = false; + for (auto const & player : map->getMapGenOptions().getPlayersSettings()) { - ret.push_back(hero); + if (player.second.getStartingHero() == hero) + { + heroUsedAsStarting = true; + break; + } } + + if (heroUsedAsStarting) + continue; + + ret.push_back(hero); } return ret; } void CMapGenerator::banQuestArt(const ArtifactID & id) { - //TODO: Protect with mutex map->getMap(this).allowedArtifact.erase(id); } -void CMapGenerator::banHero(const HeroTypeID & id) +void CMapGenerator::unbanQuestArt(const ArtifactID & id) { - //TODO: Protect with mutex - map->getMap(this).banHero(id); + map->getMap(this).allowedArtifact.insert(id); } Zone * CMapGenerator::getZoneWater() const diff --git a/lib/rmg/CMapGenerator.h b/lib/rmg/CMapGenerator.h index dd18108c5..372704d93 100644 --- a/lib/rmg/CMapGenerator.h +++ b/lib/rmg/CMapGenerator.h @@ -65,8 +65,7 @@ public: const std::vector & getAllPossibleQuestArtifacts() const; const std::vector getAllPossibleHeroes() const; void banQuestArt(const ArtifactID & id); - void banHero(const HeroTypeID& id); - + void unbanQuestArt(const ArtifactID & id); Zone * getZoneWater() const; void addWaterTreasuresInfo(); @@ -82,7 +81,6 @@ private: std::vector connectionsLeft; - int allowedPrisons; int monolithIndex; std::vector questArtifacts; diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index c7eb58581..749e0efdb 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -19,6 +19,7 @@ #include "modificators/ObjectManager.h" #include "modificators/RoadPlacer.h" #include "modificators/TreasurePlacer.h" +#include "modificators/PrisonHeroPlacer.h" #include "modificators/QuestArtifactPlacer.h" #include "modificators/ConnectionsPlacer.h" #include "modificators/TownPlacer.h" @@ -127,6 +128,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand) void RmgMap::addModificators() { bool hasObjectDistributor = false; + bool hasHeroPlacer = false; bool hasRockFiller = false; for(auto & z : getZones()) @@ -139,6 +141,11 @@ void RmgMap::addModificators() zone->addModificator(); hasObjectDistributor = true; } + if (!hasHeroPlacer) + { + zone->addModificator(); + hasHeroPlacer = true; + } zone->addModificator(); zone->addModificator(); zone->addModificator(); diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 5e9eacca8..89910c33f 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -15,6 +15,7 @@ #include "../RmgMap.h" #include "../CMapGenerator.h" #include "TreasurePlacer.h" +#include "PrisonHeroPlacer.h" #include "QuestArtifactPlacer.h" #include "TownPlacer.h" #include "TerrainPainter.h" @@ -75,7 +76,6 @@ void ObjectDistributor::distributeLimitedObjects() auto rmgInfo = handler->getRMGInfo(); - // FIXME: Random order of distribution RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand()); for (auto& zone : matchingZones) { @@ -146,7 +146,18 @@ void ObjectDistributor::distributePrisons() RandomGeneratorUtil::randomShuffle(zones, zone.getRand()); - size_t allowedPrisons = generator.getPrisonsRemaning(); + // TODO: Some shorthand for unique Modificator + PrisonHeroPlacer * prisonHeroPlacer = nullptr; + for(auto & z : map.getZones()) + { + prisonHeroPlacer = z.second->getModificator(); + if (prisonHeroPlacer) + { + break; + } + } + + size_t allowedPrisons = prisonHeroPlacer->getPrisonsRemaning(); for (int i = zones.size() - 1; i >= 0; i--) { auto zone = zones[i].second; diff --git a/lib/rmg/modificators/PrisonHeroPlacer.cpp b/lib/rmg/modificators/PrisonHeroPlacer.cpp new file mode 100644 index 000000000..d4787784d --- /dev/null +++ b/lib/rmg/modificators/PrisonHeroPlacer.cpp @@ -0,0 +1,73 @@ +/* +* PrisonHeroPlacer.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#include "StdInc.h" +#include "PrisonHeroPlacer.h" +#include "../CMapGenerator.h" +#include "../RmgMap.h" +#include "TreasurePlacer.h" +#include "../CZonePlacer.h" +#include "../../VCMI_Lib.h" +#include "../../mapObjectConstructors/AObjectTypeHandler.h" +#include "../../mapObjectConstructors/CObjectClassesHandler.h" +#include "../../mapObjects/MapObjects.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void PrisonHeroPlacer::process() +{ + getAllowedHeroes(); +} + +void PrisonHeroPlacer::init() +{ + // Reserve at least 16 heroes for each player + reservedHeroes = 16 * generator.getMapGenOptions().getHumanOrCpuPlayerCount(); +} + +void PrisonHeroPlacer::getAllowedHeroes() +{ + // TODO: Give each zone unique HeroPlacer with private hero list? + + // Call that only once + if (allowedHeroes.empty()) + { + allowedHeroes = generator.getAllPossibleHeroes(); + } +} + +int PrisonHeroPlacer::getPrisonsRemaning() const +{ + return std::max(allowedHeroes.size() - reservedHeroes, 0); +} + +HeroTypeID PrisonHeroPlacer::drawRandomHero() +{ + RecursiveLock lock(externalAccessMutex); + if (getPrisonsRemaning() > 0) + { + RandomGeneratorUtil::randomShuffle(allowedHeroes, zone.getRand()); + HeroTypeID ret = allowedHeroes.back(); + allowedHeroes.pop_back(); + return ret; + } + else + { + throw rmgException("No unused heroes left for prisons!"); + } +} + +void PrisonHeroPlacer::restoreDrawnHero(const HeroTypeID & hid) +{ + RecursiveLock lock(externalAccessMutex); + allowedHeroes.push_back(hid); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/PrisonHeroPlacer.h b/lib/rmg/modificators/PrisonHeroPlacer.h new file mode 100644 index 000000000..62c32d381 --- /dev/null +++ b/lib/rmg/modificators/PrisonHeroPlacer.h @@ -0,0 +1,41 @@ +/* +* PrisonHeroPlacer, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#pragma once +#include "../Zone.h" +#include "../Functions.h" +#include "../../mapObjects/ObjectTemplate.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CRandomGenerator; + +class PrisonHeroPlacer : public Modificator +{ +public: + MODIFICATOR(PrisonHeroPlacer); + + void process() override; + void init() override; + + int getPrisonsRemaning() const; + [[nodiscard]] HeroTypeID drawRandomHero(); + void restoreDrawnHero(const HeroTypeID & hid); + +private: + void getAllowedHeroes(); + size_t reservedHeroes; + +protected: + + std::vector allowedHeroes; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/QuestArtifactPlacer.cpp b/lib/rmg/modificators/QuestArtifactPlacer.cpp index fd9fe44d8..17a181716 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.cpp +++ b/lib/rmg/modificators/QuestArtifactPlacer.cpp @@ -40,11 +40,18 @@ void QuestArtifactPlacer::addQuestArtZone(std::shared_ptr otherZone) void QuestArtifactPlacer::addQuestArtifact(const ArtifactID& id) { + logGlobal->info("Need to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); RecursiveLock lock(externalAccessMutex); - logGlobal->info("Need to place quest artifact artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); questArtifactsToPlace.emplace_back(id); } +void QuestArtifactPlacer::removeQuestArtifact(const ArtifactID& id) +{ + logGlobal->info("Will not try to place quest artifact %s", VLC->artifacts()->getById(id)->getNameTranslated()); + RecursiveLock lock(externalAccessMutex); + vstd::erase_if_present(questArtifactsToPlace, id); +} + void QuestArtifactPlacer::rememberPotentialArtifactToReplace(CGObjectInstance* obj) { RecursiveLock lock(externalAccessMutex); @@ -131,9 +138,10 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact() RecursiveLock lock(externalAccessMutex); if (!questArtifacts.empty()) { + RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand()); ArtifactID ret = questArtifacts.back(); questArtifacts.pop_back(); - RandomGeneratorUtil::randomShuffle(questArtifacts, zone.getRand()); + generator.banQuestArt(ret); return ret; } else @@ -142,10 +150,11 @@ ArtifactID QuestArtifactPlacer::drawRandomArtifact() } } -void QuestArtifactPlacer::addRandomArtifact(ArtifactID artid) +void QuestArtifactPlacer::addRandomArtifact(const ArtifactID & artid) { RecursiveLock lock(externalAccessMutex); questArtifacts.push_back(artid); + generator.unbanQuestArt(artid); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/QuestArtifactPlacer.h b/lib/rmg/modificators/QuestArtifactPlacer.h index fb46c8de6..b5b9f5987 100644 --- a/lib/rmg/modificators/QuestArtifactPlacer.h +++ b/lib/rmg/modificators/QuestArtifactPlacer.h @@ -29,14 +29,15 @@ public: void findZonesForQuestArts(); void addQuestArtifact(const ArtifactID& id); + void removeQuestArtifact(const ArtifactID& id); void rememberPotentialArtifactToReplace(CGObjectInstance* obj); std::vector getPossibleArtifactsToReplace() const; void placeQuestArtifacts(CRandomGenerator & rand); void dropReplacedArtifact(CGObjectInstance* obj); size_t getMaxQuestArtifactCount() const; - ArtifactID drawRandomArtifact(); - void addRandomArtifact(ArtifactID artid); + [[nodiscard]] ArtifactID drawRandomArtifact(); + void addRandomArtifact(const ArtifactID & artid); protected: diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index f3b4a4c17..20c12e2eb 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -18,6 +18,7 @@ #include "../RmgMap.h" #include "../TileInfo.h" #include "../CZonePlacer.h" +#include "PrisonHeroPlacer.h" #include "QuestArtifactPlacer.h" #include "../../ArtifactUtils.h" #include "../../mapObjectConstructors/AObjectTypeHandler.h" @@ -32,6 +33,12 @@ VCMI_LIB_NAMESPACE_BEGIN +ObjectInfo::ObjectInfo(): + destroyObject([](){}) +{ + +} + void TreasurePlacer::process() { addAllPossibleObjects(); @@ -45,6 +52,7 @@ void TreasurePlacer::init() maxPrisons = 0; //Should be in the constructor, but we use macro for that DEPENDENCY(ObjectManager); DEPENDENCY(ConnectionsPlacer); + DEPENDENCY_ALL(PrisonHeroPlacer); POSTFUNCTION(RoadPlacer); } @@ -90,6 +98,16 @@ void TreasurePlacer::addAllPossibleObjects() auto prisonTemplates = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType()); if (!prisonTemplates.empty()) { + PrisonHeroPlacer * prisonHeroPlacer = nullptr; + for(auto & z : map.getZones()) + { + prisonHeroPlacer = z.second->getModificator(); + if (prisonHeroPlacer) + { + break; + } + } + //prisons //levels 1, 5, 10, 20, 30 static int prisonsLevels = std::min(generator.getConfig().prisonExperience.size(), generator.getConfig().prisonValues.size()); @@ -97,16 +115,22 @@ void TreasurePlacer::addAllPossibleObjects() size_t prisonsLeft = getMaxPrisons(); for (int i = prisonsLevels - 1; i >= 0; i--) { + ObjectInfo oi; // Create new instance which will hold destructor operation + oi.value = generator.getConfig().prisonValues[i]; if (oi.value > zone.getMaxTreasureValue()) { continue; } - oi.generateObject = [i, this]() -> CGObjectInstance* + oi.generateObject = [i, this, prisonHeroPlacer, &oi]() -> CGObjectInstance* { - auto possibleHeroes = generator.getAllPossibleHeroes(); - HeroTypeID hid = *RandomGeneratorUtil::nextItem(possibleHeroes, zone.getRand()); + HeroTypeID hid = prisonHeroPlacer->drawRandomHero(); + oi.destroyObject = [hid, prisonHeroPlacer]() + { + // Hero can be used again + prisonHeroPlacer->restoreDrawnHero(hid); + }; auto factory = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0); auto* obj = dynamic_cast(factory->create()); @@ -114,7 +138,6 @@ void TreasurePlacer::addAllPossibleObjects() obj->setHeroType(hid); //will be initialized later obj->exp = generator.getConfig().prisonExperience[i]; obj->setOwner(PlayerColor::NEUTRAL); - generator.banHero(hid); return obj; }; @@ -441,6 +464,19 @@ void TreasurePlacer::addAllPossibleObjects() RandomGeneratorUtil::randomShuffle(creatures, zone.getRand()); + auto setRandomArtifact = [qap, &oi](CGSeerHut * obj) + { + ArtifactID artid = qap->drawRandomArtifact(); + oi.destroyObject = [artid, qap]() + { + // Artifact can be used again + qap->addRandomArtifact(artid); + qap->removeQuestArtifact(artid); + }; + obj->quest->mission.artifacts.push_back(artid); + qap->addQuestArtifact(artid); + }; + for(int i = 0; i < static_cast(creatures.size()); i++) { auto * creature = creatures[i]; @@ -451,7 +487,8 @@ void TreasurePlacer::addAllPossibleObjects() int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); - oi.generateObject = [creature, creaturesAmount, randomAppearance, this, qap]() -> CGObjectInstance * + // FIXME: Remove duplicated code for gold, exp and creaure reward + oi.generateObject = [creature, creaturesAmount, randomAppearance, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); @@ -461,11 +498,7 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); + setRandomArtifact(obj); return obj; }; @@ -499,7 +532,7 @@ void TreasurePlacer::addAllPossibleObjects() oi.probability = 10; oi.maxPerZone = 1; - oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * + oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); @@ -508,20 +541,16 @@ void TreasurePlacer::addAllPossibleObjects() 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->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); - + + setRandomArtifact(obj); + return obj; }; if(!oi.templates.empty()) possibleSeerHuts.push_back(oi); - oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * + oi.generateObject = [i, randomAppearance, this, setRandomArtifact]() -> CGObjectInstance * { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); @@ -531,11 +560,7 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->mission.artifacts.push_back(artid); - - generator.banQuestArt(artid); - zone.getModificator()->addQuestArtifact(artid); + setRandomArtifact(obj); return obj; }; @@ -641,8 +666,14 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector } auto * object = oi->generateObject(); + if(oi->templates.empty()) + { + logGlobal->warn("Deleting randomized object with no templates: %s", object->getObjectName()); + oi->destroyObject(); + delete object; continue; + } auto templates = object->getObjectHandler()->getMostSpecificTemplates(zone.getTerrainType()); @@ -721,7 +752,7 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector instanceAccessibleArea.add(instance.getVisitablePosition()); } - //first object is good + //Do not clean up after first object if(rmgObject.instances().size() == 1) break; @@ -800,10 +831,10 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) { for (auto* oi : treasurePile) { + oi->destroyObject(); oi->maxPerZone++; } }; - //place biggest treasures first at large distance, place smaller ones inbetween auto treasureInfo = zone.getTreasureInfo(); boost::sort(treasureInfo, valueComparator); diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index 88d10f5a7..ec87bfe8d 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -22,12 +22,15 @@ class CRandomGenerator; struct ObjectInfo { + ObjectInfo(); + std::vector> templates; ui32 value = 0; ui16 probability = 0; ui32 maxPerZone = 1; //ui32 maxPerMap; //unused std::function generateObject; + std::function destroyObject; void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); }; diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 76a372690..58914184c 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 831; +const ui32 SERIALIZATION_VERSION = 832; const ui32 MINIMAL_SERIALIZATION_VERSION = 831; const std::string SAVEGAME_MAGIC = "VCMISVG"; diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index fc333fc93..42ba4458c 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -213,7 +213,24 @@ bool BattleSpellMechanics::canBeCastAt(const Target & target, Problem & problem) Target spellTarget = transformSpellTarget(target); - return effects->applicable(problem, this, target, spellTarget); + const battle::Unit * mainTarget = nullptr; + + if (!getSpell()->canCastOnSelf()) + { + if(spellTarget.front().unitValue) + { + mainTarget = target.front().unitValue; + } + else if(spellTarget.front().hexValue.isValid()) + { + mainTarget = battle()->battleGetUnitByPos(target.front().hexValue, true); + } + + if (mainTarget && mainTarget == caster) + return false; // can't cast on self + } + + return effects->applicable(problem, this, target, spellTarget); } std::vector BattleSpellMechanics::getAffectedStacks(const Target & target) const diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index aeceb6f7f..9d1e4d7fc 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -76,6 +76,7 @@ CSpell::CSpell(): power(0), combat(false), creatureAbility(false), + castOnSelf(false), positiveness(ESpellPositiveness::NEUTRAL), defaultProbability(0), rising(false), @@ -285,6 +286,11 @@ bool CSpell::hasBattleEffects() const return levels[0].battleEffects.getType() == JsonNode::JsonType::DATA_STRUCT && !levels[0].battleEffects.Struct().empty(); } +bool CSpell::canCastOnSelf() const +{ + return castOnSelf; +} + const std::string & CSpell::getIconImmune() const { return iconImmune; @@ -702,6 +708,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & spell->school[info.id] = schoolNames[info.jsonName].Bool(); } + spell->castOnSelf = json["canCastOnSelf"].Bool(); spell->level = static_cast(json["level"].Integer()); spell->power = static_cast(json["power"].Integer()); diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index efe807831..666e58b51 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -203,6 +203,7 @@ public: int64_t calculateDamage(const spells::Caster * caster) const override; bool hasSchool(SpellSchool school) const override; + bool canCastOnSelf() const override; /** * Calls cb for each school this spell belongs to @@ -329,6 +330,7 @@ private: 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 + bool castOnSelf; // if set, creature caster can cast this spell on itself 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 diff --git a/lib/spells/ObstacleCasterProxy.cpp b/lib/spells/ObstacleCasterProxy.cpp index 874871ec5..8909fd17e 100644 --- a/lib/spells/ObstacleCasterProxy.cpp +++ b/lib/spells/ObstacleCasterProxy.cpp @@ -71,7 +71,8 @@ SilentCaster::SilentCaster(PlayerColor owner_, const Caster * hero_): void SilentCaster::getCasterName(MetaString & text) const { - logGlobal->error("Unexpected call to SilentCaster::getCasterName"); + // NOTE: can be triggered (for example) if creature steps into Tower mines/moat while hero has Recanter's Cloak + logGlobal->debug("Unexpected call to SilentCaster::getCasterName"); } void SilentCaster::getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 6a28a5349..425607008 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -20,10 +20,12 @@ #include "../../battle/CBattleInfoCallback.h" #include "../../networkPacks/PacksForClientBattle.h" #include "../../CGeneralTextHandler.h" +#include "../../Languages.h" #include "../../serializer/JsonSerializeFormat.h" #include + VCMI_LIB_NAMESPACE_BEGIN namespace spells @@ -152,6 +154,16 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, m->caster->getCasterName(line); log.push_back(line); } + else if(m->getSpell()->getJsonKey().find("accurateShot") != std::string::npos && !multiple) + { + MetaString line; + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string textID = "vcmi.battleWindow.accurateShot.resultDescription"; + line.appendTextID(Languages::getPluralFormTextID( preferredLanguage, kills, textID)); + line.replaceNumber(kills); + firstTarget->addNameReplacement(line, kills != 1); + log.push_back(line); + } else if(m->getSpellIndex() == SpellID::THUNDERBOLT && !multiple) { { diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index e006d37a4..3bedc80ac 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -169,7 +169,6 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg case 1: //Coronius style specialty bonus. //Please note that actual Coronius isnt here, because Slayer is a spell that doesnt affect monster stats and is used only in calculateDmgRange - power = std::max(5 - tier, 0); break; } if(m->isNegativeSpell()) diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 3dd588fed..3e7cf3715 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -105,6 +105,7 @@ set(editor_FORMS ) set(editor_TS + translation/czech.ts translation/english.ts translation/french.ts translation/german.ts diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index aaf5b19f5..b9e4cb09f 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -177,7 +177,7 @@ MainWindow::MainWindow(QWidget* parent) : logGlobal->info("The log file will be saved to %s", logPath); //init - preinitDLL(::console, false, extractionOptions.extractArchives); + preinitDLL(::console, extractionOptions.extractArchives); // Initialize logging based on settings logConfig->configure(); diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 00dcaad1c..8af35854b 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -131,7 +131,12 @@ void MapController::repairMap(CMap * map) const //fix hero instance if(auto * nih = dynamic_cast(obj.get())) { + // All heroes present on map or in prisons need to be allowed to rehire them after they are defeated + + // FIXME: How about custom scenarios where defeated hero cannot be hired again? + map->allowedHeroes.insert(nih->getHeroType()); + auto type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); //TODO: find a way to get proper type name @@ -198,8 +203,6 @@ void MapController::repairMap(CMap * map) const auto a = ArtifactUtils::createScroll(*RandomGeneratorUtil::nextItem(out, CRandomGenerator::getDefault())); art->storedArtifact = a; } - else - map->allowedArtifact.insert(art->getArtifact()); } } } diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 369e1007f..978916e77 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -81,7 +81,7 @@ void MapSettings::on_pushButton_clicked() auto updateMapArray = [](const QListWidget * widget, auto & arr) { arr.clear(); - for(int i = 0; i < arr.size(); ++i) + for(int i = 0; i < widget->count(); ++i) { auto * item = widget->item(i); if (item->checkState() == Qt::Checked) diff --git a/mapeditor/vcmieditor.desktop b/mapeditor/vcmieditor.desktop index 9996b973f..115cf3145 100644 --- a/mapeditor/vcmieditor.desktop +++ b/mapeditor/vcmieditor.desktop @@ -1,10 +1,16 @@ [Desktop Entry] Type=Application Name=VCMI Map Editor -GenericName=Strategy Game Engine -Comment=Map editor for open engine of Heroes of Might and Magic 3 +Name[cs]=Editor map VCMI +Name[de]=VCMI Karteneditor +GenericName=Strategy Game Map Editor +GenericName[cs]=Editor map strategické hry +GenericName[de]=Karteneditor für Strategiespiel +Comment=Map editor for the open-source recreation of Heroes of Might & Magic III +Comment[cs]=Editor map enginu s otevřeným kódem pro Heroes of Might and Magic III +Comment[de]=Karteneditor für den Open-Source-Nachbau von Heroes of Might and Magic III Icon=vcmieditor Exec=vcmieditor Categories=Game;StrategyGame; -Version=1.0 -Keywords=heroes;homm3; +Version=1.1 +Keywords=heroes of might and magic;heroes;homm;homm3;strategy; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3a943d2b8..ad9baed9e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -365,52 +365,60 @@ void CGameHandler::expGiven(const CGHeroInstance *hero) // levelUpHero(hero); } -void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) +void CGameHandler::giveExperience(const CGHeroInstance * hero, TExpType amountToGain) { - if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached - { - if (gs->map->levelLimit != 0) - { - TExpType expLimit = VLC->heroh->reqExp(gs->map->levelLimit); - TExpType resultingExp = abs ? val : hero->exp + val; - if (resultingExp > expLimit) - { - // set given experience to max possible, but don't decrease if hero already over top - abs = true; - val = std::max(expLimit, hero->exp); + TExpType maxExp = VLC->heroh->reqExp(VLC->heroh->maxSupportedLevel()); + TExpType currExp = hero->exp; - InfoWindow iw; - iw.player = hero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP - iw.text.replaceRawString(hero->getNameTranslated()); - sendAndApply(&iw); - } - } + if (gs->map->levelLimit != 0) + maxExp = VLC->heroh->reqExp(gs->map->levelLimit); + + TExpType canGainExp = 0; + if (maxExp > currExp) + canGainExp = maxExp - currExp; + + if (amountToGain > canGainExp) + { + // set given experience to max possible, but don't decrease if hero already over top + amountToGain = canGainExp; + + InfoWindow iw; + iw.player = hero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 1); //can gain no more XP + iw.text.replaceRawString(hero->getNameTranslated()); + sendAndApply(&iw); } + SetPrimSkill sps; + sps.id = hero->id; + sps.which = PrimarySkill::EXPERIENCE; + sps.abs = false; + sps.val = amountToGain; + sendAndApply(&sps); + + //hero may level up + if (hero->commander && hero->commander->alive) + { + //FIXME: trim experience according to map limit? + SetCommanderProperty scp; + scp.heroid = hero->id; + scp.which = SetCommanderProperty::EXPERIENCE; + scp.amount = amountToGain; + sendAndApply (&scp); + CBonusSystemNode::treeHasChanged(); + } + + expGiven(hero); +} + +void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) +{ SetPrimSkill sps; sps.id = hero->id; sps.which = which; sps.abs = abs; sps.val = val; sendAndApply(&sps); - - //only for exp - hero may level up - if (which == PrimarySkill::EXPERIENCE) - { - if (hero->commander && hero->commander->alive) - { - //FIXME: trim experience according to map limit? - SetCommanderProperty scp; - scp.heroid = hero->id; - scp.which = SetCommanderProperty::EXPERIENCE; - scp.amount = val; - sendAndApply (&scp); - CBonusSystemNode::treeHasChanged(); - } - - expGiven(hero); - } } void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs) @@ -658,7 +666,7 @@ void CGameHandler::onNewTurn() { if (obj && obj->ID == Obj::PRISON) //give imprisoned hero 0 exp to level him up. easiest to do at this point { - changePrimSkill (getHero(obj->id), PrimarySkill::EXPERIENCE, 0); + giveExperience(getHero(obj->id), 0); } } } @@ -1128,16 +1136,16 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo }; if (guardian && getVisitingHero(guardian) != nullptr) - return complainRet("Cannot move hero, destination monster is busy!"); + return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this wandering monster!"); if (objectToVisit && getVisitingHero(objectToVisit) != nullptr && getVisitingHero(objectToVisit) != h) - return complainRet("Cannot move hero, destination object is busy!"); + return complainRet("You cannot move your hero there. Simultaneous turns are active and another player is interacting with this map object!"); 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!"); + return complainRet("You cannot move your hero there. This object belongs to another player and simultaneous turns are still active!"); //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) @@ -1459,6 +1467,9 @@ void CGameHandler::heroVisitCastle(const CGTownInstance * obj, const CGHeroInsta sendAndApply(&vc); visitCastleObjects(obj, hero); giveSpells (obj, hero); + + if (obj->visitingHero && obj->garrisonHero) + useScholarSkill(obj->visitingHero->id, obj->garrisonHero->id); checkVictoryLossConditionsForPlayer(hero->tempOwner); //transported artifact? } @@ -1502,6 +1513,15 @@ void CGameHandler::setMovePoints(SetMovePoints * smp) sendAndApply(smp); } +void CGameHandler::setMovePoints(ObjectInstanceID hid, int val, bool absolute) +{ + SetMovePoints smp; + smp.hid = hid; + smp.val = val; + smp.absolute = absolute; + sendAndApply(&smp); +} + void CGameHandler::setManaPoints(ObjectInstanceID hid, int val) { SetMana sm; @@ -3284,7 +3304,11 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) for (auto & i : ev.buildings) { - if (!town->hasBuilt(i)) + // Only perform action if: + // 1. Building exists in town (don't attempt to build Lvl 5 guild in Fortress + // 2. Building was not built yet + // othervice, silently ignore / skip it + if (town->town->buildings.count(i) && !town->hasBuilt(i)) { buildStructure(town->id, i, true); iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); @@ -3708,7 +3732,7 @@ bool CGameHandler::sacrificeCreatures(const IMarket * market, const CGHeroInstan int expSum = 0; auto finish = [this, &hero, &expSum]() { - changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum)); + giveExperience(hero, hero->calculateXp(expSum)); }; for(int i = 0; i < slot.size(); ++i) @@ -3749,7 +3773,7 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h int expSum = 0; auto finish = [this, &hero, &expSum]() { - changePrimSkill(hero, PrimarySkill::EXPERIENCE, hero->calculateXp(expSum)); + giveExperience(hero, hero->calculateXp(expSum)); }; for(int i = 0; i < slot.size(); ++i) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 73603fba9..152452d6a 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -102,6 +102,7 @@ public: bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; + void giveExperience(const CGHeroInstance * hero, TExpType val) override; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; @@ -141,6 +142,7 @@ public: 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 setMovePoints(ObjectInstanceID hid, int val, bool absolute) 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; @@ -247,7 +249,7 @@ public: void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); /// Unconditionally throws with "Action not allowed" message - void throwNotAllowedAction(CPackForServer * pack); + [[noreturn]] 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 @@ -255,7 +257,7 @@ public: /// 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); + [[noreturn]] void throwAndComplain(CPackForServer * pack, std::string txt); bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index f7d894f33..6d29bd416 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -141,7 +141,11 @@ CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) if(cmdLineOptions.count("run-by-client")) { logNetwork->error("Port must be specified when run-by-client is used!!"); - exit(0); +#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE) + ::exit(0); +#else + std::quick_exit(0); +#endif } acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); port = acceptor->local_endpoint().port(); @@ -758,6 +762,7 @@ void CVCMIServer::updateAndPropagateLobbyState() { const auto & pset = psetPair.second; si->mapGenOptions->setStartingTownForPlayer(pset.color, pset.castle); + si->mapGenOptions->setStartingHeroForPlayer(pset.color, pset.hero); if(pset.isControlledByHuman()) { si->mapGenOptions->setPlayerTypeForStandardPlayer(pset.color, EPlayerType::HUMAN); @@ -1172,7 +1177,7 @@ int main(int argc, const char * argv[]) boost::program_options::variables_map opts; handleCommandOptions(argc, argv, opts); - preinitDLL(console); + preinitDLL(console, false); logConfig.configure(); loadDLLClasses(); diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index bb816a854..9aa3e5dc7 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -94,8 +94,14 @@ void TurnTimerHandler::update(int waitTime) if(gs->isPlayerMakingTurn(player)) onPlayerMakingTurn(player, waitTime); + // create copy for iterations - battle might end during onBattleLoop call + std::vector ongoingBattles; + for (auto & battle : gs->currentBattles) - onBattleLoop(battle->battleID, waitTime); + ongoingBattles.push_back(battle->battleID); + + for (auto & battleID : ongoingBattles) + onBattleLoop(battleID, waitTime); } } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index a55e42db3..af8e88994 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -268,12 +268,17 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeMelee)); + const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector); + const bool retaliation = destinationStack->ableToRetaliate(); + bool ferocityApplied = false; + int32_t defenderInitialQuantity = destinationStack->getCount(); + for (int i = 0; i < totalAttacks; ++i) { //first strike - if(i == 0 && firstStrike && retaliation) + if(i == 0 && firstStrike && retaliation && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION)) { makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); } @@ -282,6 +287,18 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c 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 + + if(!ferocityApplied && stack->hasBonusOfType(BonusType::FEROCITY)) + { + auto ferocityBonus = stack->getBonus(Selector::type()(BonusType::FEROCITY)); + int32_t requiredCreaturesToKill = ferocityBonus->additionalInfo != CAddInfo::NONE ? ferocityBonus->additionalInfo[0] : 1; + if(defenderInitialQuantity - destinationStack->getCount() >= requiredCreaturesToKill) + { + ferocityApplied = true; + int additionalAttacksCount = stack->valOfBonuses(BonusType::FEROCITY); + totalAttacks += additionalAttacksCount; + } + } } //counterattack @@ -338,7 +355,11 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co return false; } - makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); + static const auto firstStrikeSelector = Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeAll).Or(Selector::typeSubtype(BonusType::FIRST_STRIKE, BonusCustomSubtype::damageTypeRanged)); + const bool firstStrike = destinationStack->hasBonus(firstStrikeSelector); + + if (!firstStrike) + makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); //ranged counterattack if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) @@ -360,7 +381,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } - for(int i = 1; i < totalRangedAttacks; ++i) + for(int i = firstStrike ? 0:1; i < totalRangedAttacks; ++i) { if( stack->alive() @@ -644,7 +665,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta ret = path.second; - int creSpeed = curStack->speed(0, true); + int creSpeed = curStack->getMovementRange(0); if (battle.battleGetTacticDist() > 0 && creSpeed > 0) creSpeed = GameConstants::BFIELD_SIZE; @@ -1104,19 +1125,13 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const handleAfterAttackCasting(battle, ranged, attacker, defender); } -void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender) { if(attacker->hasBonusOfType(attackMode)) { - std::set spellsToCast; TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - if (sf->subtype.as() != SpellID()) - spellsToCast.insert(sf->subtype.as()); - else - logMod->error("Invalid spell to cast during attack!"); - } + std::set spellsToCast = getSpellsForAttackCasting(spells, defender); + for(SpellID spellID : spellsToCast) { bool castMe = false; @@ -1130,18 +1145,10 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo 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)) + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + + if (meleeRanged == CAddInfo::NONE || meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) castMe = true; } int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, BonusSubtypeID(spellID)))); @@ -1175,11 +1182,130 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo } } +std::set BattleActionProcessor::getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender) +{ + std::set spellsToCast; + constexpr int unlayeredItemsInternalLayer = -1; + + std::map>> spellsWithBackupLayers; + + for(int i = 0; i < spells->size(); i++) + { + std::shared_ptr bonus = spells->operator[](i); + int layer = bonus->additionalInfo[2]; + vstd::amax(layer, -1); + spellsWithBackupLayers[layer].push_back(bonus); + } + + auto addSpellsFromLayer = [&](int layer) -> void + { + assert(spellsWithBackupLayers.find(layer) != spellsWithBackupLayers.end()); + + for(const auto & spell : spellsWithBackupLayers[layer]) + { + if (spell->subtype.as() != SpellID()) + spellsToCast.insert(spell->subtype.as()); + else + logGlobal->error("Invalid spell to cast during attack!"); + } + }; + + if(spellsWithBackupLayers.find(unlayeredItemsInternalLayer) != spellsWithBackupLayers.end()) + { + addSpellsFromLayer(unlayeredItemsInternalLayer); + spellsWithBackupLayers.erase(unlayeredItemsInternalLayer); + } + + for(auto item : spellsWithBackupLayers) + { + bool areCurrentLayerSpellsApplied = std::all_of(item.second.begin(), item.second.end(), + [&](const std::shared_ptr spell) + { + std::vector activeSpells = defender->activeSpells(); + return vstd::find(activeSpells, spell->subtype.as()) != activeSpells.end(); + }); + + if(!areCurrentLayerSpellsApplied || item.first == spellsWithBackupLayers.rbegin()->first) + { + addSpellsFromLayer(item.first); + break; + } + } + + return spellsToCast; +} + 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::handleDeathStare(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) +{ + // 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); + + /* mechanics of Accurate Shot as in HotA: + * each creature in an attacking stack has a X% chance of killing a creature in the attacked squad, + * but the total number of killed creatures cannot be more than (number of creatures in an attacking squad) * X/100 (rounded up). + * X = 3 multiplier for shooting without penalty and X = 2 if shooting with penalty. Ability doesn't work if shooting at creatures behind walls. + */ + + auto subtype = BonusCustomSubtype::deathStareGorgon; + + if (ranged) + { + bool rangePenalty = battle.battleHasDistancePenalty(attacker, attacker->getPosition(), defender->getPosition()); + bool obstaclePenalty = battle.battleHasWallPenalty(attacker, attacker->getPosition(), defender->getPosition()); + + if(rangePenalty) + { + if(obstaclePenalty) + subtype = BonusCustomSubtype::deathStareRangeObstaclePenalty; + else + subtype = BonusCustomSubtype::deathStareRangePenalty; + } + else + { + if(obstaclePenalty) + subtype = BonusCustomSubtype::deathStareObstaclePenalty; + else + subtype = BonusCustomSubtype::deathStareNoRangePenalty; + } + } + + int singleCreatureKillChancePercent = attacker->valOfBonuses(BonusType::DEATH_STARE, subtype); + double chanceToKill = singleCreatureKillChancePercent / 100.0; + vstd::amin(chanceToKill, 1); //cap at 100% + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + int killedCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + int maxToKill = (attacker->getCount() * singleCreatureKillChancePercent + 99) / 100; + vstd::amin(killedCreatures, maxToKill); + + killedCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); + + if(killedCreatures) + { + //TODO: death stare or accurate shot was not originally available for multiple-hex attacks, but... + + SpellID spellID = SpellID(SpellID::DEATH_STARE); //also used as fallback spell for ACCURATE_SHOT + auto bonus = attacker->getBonus(Selector::typeSubtype(BonusType::DEATH_STARE, subtype)); + if(bonus && bonus->additionalInfo[0] != SpellID::NONE) + spellID = SpellID(bonus->additionalInfo[0]); + + const CSpell * spell = spellID.toSpell(); + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(killedCreatures); + parameters.cast(gameHandler->spellEnv, target); + } +} + void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) { if(!attacker->alive() || !defender->alive()) // can be already dead @@ -1194,37 +1320,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & } 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); - } - } + handleDeathStare(battle, ranged, attacker, defender); if(!defender->alive()) return; @@ -1301,6 +1397,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // send empty event to client // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; + fakeEvent.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&fakeEvent); } diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 34e40aa29..6c28b5950 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -8,6 +8,7 @@ * */ #pragma once +#include "bonuses/BonusList.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,8 +44,13 @@ class BattleActionProcessor : boost::noncopyable 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 handleDeathStare(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); + void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const CStack * defender); + + std::set getSpellsForAttackCasting(TConstBonusListPtr spells, const CStack *defender); // damage, drain life & fire shield; returns amount of drained life int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 0fba8f63a..975f281df 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -181,6 +181,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, // send empty event to client // temporary(?) workaround to force animations to trigger StacksInjured fakeEvent; + fakeEvent.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&fakeEvent); } @@ -676,8 +677,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c } if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) { - const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st)); - const CGHeroInstance * opponentHero = battle.battleGetFightingHero(opponent); + const CGHeroInstance * opponentHero = battle.battleGetFightingHero(battle.otherSide(st->unitSide())); if(opponentHero) { ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); @@ -712,6 +712,11 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c } } BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + bl.remove_if([](const Bonus * b) + { + return b->subtype.as() == SpellID::NONE; + }); + int side = *battle.playerToSide(st->unitOwner()); if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 52c4e7e6b..7652d275c 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -494,14 +494,14 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) } //give exp if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); + gameHandler->giveExperience(finishingBattle->winnerHero, 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].army = const_cast(battle.battleGetArmyObject(BattleSide::ATTACKER)); + raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(BattleSide::DEFENDER)); + raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(BattleSide::ATTACKER)); + raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(BattleSide::DEFENDER)); raccepted.heroResult[0].exp = battleResult->exp[0]; raccepted.heroResult[1].exp = battleResult->exp[1]; raccepted.winnerSide = finishingBattle->winnerSide; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 39569a470..623340e6c 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -74,6 +74,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; sah.hid = hero->getHeroType(); + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } @@ -87,6 +88,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.hid = hero->getHeroType(); sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } @@ -98,6 +100,7 @@ void HeroPoolProcessor::clearHeroFromSlot(const PlayerColor & color, TavernHeroS sah.roleID = TavernSlotRole::NONE; sah.slotID = slot; sah.hid = HeroTypeID::NONE; + sah.replenishPoints = false; gameHandler->sendAndApply(&sah); } @@ -106,6 +109,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe SetAvailableHero sah; sah.player = color; sah.slotID = slot; + sah.replenishPoints = true; CGHeroInstance *newHero = pickHeroFor(needNativeHero, color); @@ -129,6 +133,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe { sah.hid = HeroTypeID::NONE; } + gameHandler->sendAndApply(&sah); } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index afb331148..ecd7c5813 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -261,7 +261,7 @@ void PlayerMessageProcessor::cheatLevelup(PlayerColor player, const CGHeroInstan levelsToGain = 1; } - gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level)); + gameHandler->giveExperience(hero, VLC->heroh->reqExp(hero->level + levelsToGain) - VLC->heroh->reqExp(hero->level)); } void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -280,7 +280,7 @@ void PlayerMessageProcessor::cheatExperience(PlayerColor player, const CGHeroIns expAmountProcessed = 10000; } - gameHandler->changePrimSkill(hero, PrimarySkill::EXPERIENCE, expAmountProcessed); + gameHandler->giveExperience(hero, expAmountProcessed); } void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words) @@ -384,7 +384,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player) for(auto & obj : gameHandler->gameState()->map->objects) { - if(obj && obj->ID == Obj::OBELISK) + if(obj && obj->ID == Obj::OBELISK && !obj->wasVisited(player)) { gameHandler->setObjPropertyID(obj->id, ObjProperty::OBELISK_VISITED, t->id); for(const auto & color : t->players) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index f0b0fda8d..0d762794e 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -106,6 +106,8 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); + config->options.ignoreGuards = true; + config->options.turnLimit = 1; CPathfinder pathfinder(gameHandler->gameState(), config); pathfinder.calculatePaths(); @@ -120,6 +122,8 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c { CPathsInfo out(mapSize, hero); auto config = std::make_shared(out, gameHandler->gameState(), hero); + config->options.ignoreGuards = true; + config->options.turnLimit = 1; CPathfinder pathfinder(gameHandler->gameState(), config); pathfinder.calculatePaths(); diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 48a2fb639..077001c85 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -44,6 +44,7 @@ public: 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 giveExperience(const CGHeroInstance * hero, TExpType val) override {} void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} void showBlockingDialog(BlockingDialog *iw) override {} @@ -81,6 +82,7 @@ public: bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;} void giveHeroBonus(GiveBonus * bonus) override {} void setMovePoints(SetMovePoints * smp) override {} + void setMovePoints(ObjectInstanceID hid, int val, bool absolute) 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 {} diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index 3e8aa442e..ccd33d129 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -45,6 +45,7 @@ public: MOCK_CONST_METHOD0(isOffensive, bool()); MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); + MOCK_CONST_METHOD0(canCastOnSelf, bool()); MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); MOCK_CONST_METHOD0(getCastSound, const std::string &());