diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 7dd30449b..cfda077f3 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -13,9 +13,9 @@ // Eventually only IBattleInfoCallback and battle::Unit should be used, // CUnitState should be private and CStack should be removed completely -uint64_t averageDmg(const TDmgRange & range) +uint64_t averageDmg(const DamageRange & range) { - return (range.first + range.second) / 2; + return (range.min + range.max) / 2; } AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) @@ -52,7 +52,7 @@ int64_t AttackPossibility::calculateDamageReduce( // FIXME: provide distance info for Jousting bonus auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0); auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0); - auto enemyDamage = averageDmg(enemyDamageBeforeAttack); + auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage); auto damagePerEnemy = enemyDamage / (double)defender->getCount(); return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth())); @@ -85,7 +85,7 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo); auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo); - int64_t gain = averageDmg(rangeDmg) - averageDmg(meleeDmg) + 1; + int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; res += gain; } @@ -156,16 +156,16 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf { int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; - TDmgRange retaliation(0, 0); + DamageEstimation retaliation; auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation); - vstd::amin(attackDmg.first, defenderState->getAvailableHealth()); - vstd::amin(attackDmg.second, defenderState->getAvailableHealth()); + vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth()); + vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth()); - vstd::amin(retaliation.first, ap.attackerState->getAvailableHealth()); - vstd::amin(retaliation.second, ap.attackerState->getAvailableHealth()); + vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth()); + vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth()); - damageDealt = averageDmg(attackDmg); + damageDealt = averageDmg(attackDmg.damage); defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state); ap.attackerState->afterAttack(attackInfo.shooting, false); @@ -175,7 +175,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) { - damageReceived = averageDmg(retaliation); + damageReceived = averageDmg(retaliation.damage); attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state); defenderState->afterAttack(attackInfo.shooting, true); } diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index c426e2186..cbc4a2d7f 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -68,7 +68,7 @@ int64_t BattleExchangeVariant::trackAttack( static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - TDmgRange retaliation; + DamageEstimation retaliation; // FIXME: provide distance info for Jousting bonus BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); @@ -78,7 +78,7 @@ int64_t BattleExchangeVariant::trackAttack( } auto attack = cb.battleEstimateDamage(bai, &retaliation); - int64_t attackDamage = (attack.first + attack.second) / 2; + int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2; int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb); int64_t attackerDamageReduce = 0; @@ -108,9 +108,9 @@ int64_t BattleExchangeVariant::trackAttack( if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) { - if(retaliation.second != 0) + if(retaliation.damage.max != 0) { - auto retaliationDamage = (retaliation.first + retaliation.second) / 2; + auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2; attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb); if(!evaluateOnly) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 27d223359..78fcdbb1b 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -428,9 +428,9 @@ uint32_t HypotheticBattle::nextUnitId() const return nextId++; } -int64_t HypotheticBattle::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const +int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const { - return (damage.first + damage.second) / 2; + return (damage.min + damage.max) / 2; } int64_t HypotheticBattle::getTreeVersion() const diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 48070ca40..916c7b8c0 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -138,7 +138,7 @@ public: uint32_t nextUnitId() const override; - int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int64_t getTreeVersion() const; diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 446fbb29c..ff0887c94 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -56,9 +56,10 @@ public: void calcDmg(const CStack * ourStack) { // FIXME: provide distance info for Jousting bonus - TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); - adi = static_cast((dmg.first + dmg.second) / 2); - adr = static_cast((retal.first + retal.second) / 2); + DamageEstimation retal; + DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); + adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); + adr = static_cast((retal.damage.min + retal.damage.max) / 2); } bool operator==(const EnemyInfo& ei) const diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json new file mode 100644 index 000000000..3135bd2c8 --- /dev/null +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -0,0 +1,289 @@ +{ + "vcmi.adventureMap.monsterThreat.title" : "\n\n 威胁等级: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "极低", + "vcmi.adventureMap.monsterThreat.levels.1" : "很低", + "vcmi.adventureMap.monsterThreat.levels.2" : "低", + "vcmi.adventureMap.monsterThreat.levels.3" : "较低", + "vcmi.adventureMap.monsterThreat.levels.4" : "中等", + "vcmi.adventureMap.monsterThreat.levels.5" : "较高", + "vcmi.adventureMap.monsterThreat.levels.6" : "高", + "vcmi.adventureMap.monsterThreat.levels.7" : "很高", + "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", + "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", + "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", + "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的", + + "vcmi.adventureMap.confirmRestartGame" : "你想要重新开始游戏吗?", + "vcmi.adventureMap.noTownWithMarket" : "没有足够的市场。", + "vcmi.adventureMap.noTownWithTavern" : "没有酒馆可供查看。", + "vcmi.adventureMap.spellUnknownProblem" : "无此魔法的信息。", + "vcmi.adventureMap.playerAttacked" : "玩家遭受攻击: %s", + "vcmi.adventureMap.moveCostDetails" : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING", + + "vcmi.server.errors.existingProcess" : "另一个VCMI进程在运行,请结束当前进程。", + "vcmi.server.errors.modsIncompatibility" : "需要加载mod:", + "vcmi.server.confirmReconnect" : "连接到上次吗?", + + "vcmi.settingsMainWindow.generalTab.hover" : "常规", + "vcmi.settingsMainWindow.generalTab.help" : "切换到“系统选项”选项卡 - 这些设置与常规游戏客户端行为相关", + "vcmi.settingsMainWindow.battleTab.hover" : "战斗", + "vcmi.settingsMainWindow.battleTab.help" : "切换到“战斗选项”选项卡 - 这些设置允许配置战斗界面和相关内容", + "vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图", + "vcmi.settingsMainWindow.adventureTab.help" : "切换到“冒险地图”选项卡 - 冒险地图允许你移动英雄", + "vcmi.settingsMainWindow.otherTab.hover" : "其他设置", + "vcmi.settingsMainWindow.otherTab.help" : "切换到“其他设置”选项卡 - 由于各种原因,这些选项不适合其他类别", + + "vcmi.systemOptions.fullscreenButton.hover" : "全屏", + "vcmi.systemOptions.fullscreenButton.help" : "{全屏n}\n\n 当你选择全屏时,VCMI将会全屏运行,否则只会运行在指定框内", + "vcmi.systemOptions.resolutionButton.hover" : "分辨率", + "vcmi.systemOptions.resolutionButton.help" : "{选择分辨率}\n\n 改变游戏的分辨率,达到更加清晰的效果。需要重新启动才能完成更改。", + "vcmi.systemOptions.resolutionMenu.hover" : "选择分辨率", + "vcmi.systemOptions.resolutionMenu.help" : "选择游戏的分辨率。", + "vcmi.systemOptions.fullscreenFailed" : "{全屏}\n\n 选择切换到全屏失败!当前分辨率不支持全屏!", + "vcmi.systemOptions.framerateButton.hover" : "显示传输帧数", + "vcmi.systemOptions.framerateButton.help" : "{显示传输帧数}\n\n 打开/关闭在游戏窗口角落的传输帧数计数器。", + + "vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示", + "vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n 以数字 A-B 格式显示不准确的敌方生物数量。", + "vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力", + "vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n 不需要按ALT就可以显示移动力。", + "vcmi.adventureOptions.showGrid.hover" : "显示六角网格", + "vcmi.adventureOptions.showGrid.help" : "{显示六角网格}\n\n 在战场上显示六角网格。", + "vcmi.adventureOptions.mapScrollSpeed4.hover": "4", + "vcmi.adventureOptions.mapScrollSpeed4.help": "设置动画速度为超快", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "5", + "vcmi.adventureOptions.mapScrollSpeed5.help": "设置动画速度为极速", + + "vcmi.battleOptions.showQueue.hover": "显示移动次序", + "vcmi.battleOptions.showQueue.help": "{显示移动次序}\n\n 显示当前生物的移动次序。", + "vcmi.battleOptions.queueSizeLabel.hover": "次序条尺寸 (设置后下一场战斗生效)", + "vcmi.battleOptions.queueSizeAutoButton.hover": "自动设置尺寸", + "vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏分辨率设置尺寸 (像素小于700为小尺寸,根据实际调整)", + "vcmi.battleOptions.queueSizeSmallButton.hover": "小尺寸", + "vcmi.battleOptions.queueSizeSmallButton.help": "设置次序条为小尺寸", + "vcmi.battleOptions.queueSizeBigButton.hover": "大尺寸", + "vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(不能在像素小于700时生效)", + "vcmi.battleOptions.animationsSpeed4.hover": "4", + "vcmi.battleOptions.animationsSpeed4.help": "设置动画速度为超快", + "vcmi.battleOptions.animationsSpeed5.hover": "5", + "vcmi.battleOptions.animationsSpeed5.help": "设置动画速度为极速", + "vcmi.battleOptions.animationsSpeed6.hover": "6", + "vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为最快", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过开场音乐", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过开场音乐}\n\n 战斗开始时跳过开场音乐,直接按Esc也可以跳过。", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示所有可以招募的城镇生物", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示所有可以招募的城镇生物}\n\n 显示当前所有可供招募的城镇生物 (左下角)。", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "缩小城镇生物信息", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{缩小城镇生物信息}\n\n 将城镇生物信息最小化.", + + "vcmi.townHall.missingBase" : "你必须先建造%s ", + "vcmi.townHall.noCreaturesToRecruit" : "没有可供雇佣的生物。", + "vcmi.townHall.greetingManaVortex" : "当你接近%s时,你的身体充满了新的能量。这使你的魔法值加倍。", + "vcmi.townHall.greetingKnowledge" : "你学习了%s上的图形,并深入了解各种魔法的运作,这使你的知识点数+1。", + "vcmi.townHall.greetingSpellPower" : "%s教你新的方法来集中你的魔法力量,这使你的力量点数+1。", + "vcmi.townHall.greetingExperience" : "访问%s给你提供了更好的学习方法。这使你的经验值+1000。", + "vcmi.townHall.greetingAttack" : "在%s参观后给你提供了更好的战斗技巧,这使你的攻击点数+1。", + "vcmi.townHall.greetingDefence" : "在%s中度过一段时间后,经验丰富的勇士会教你额外的防御技能,这使你的防御点数+1。", + "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", + "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", + "vcmi.townHall.greetingCustomBonus" : "参观%s后,你的技巧有了提升。这使你受益匪浅。并且使你+%d %s%s", + "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", + "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", + + "vcmi.logicalExpressions.anyOf" : "以下任何前提:", + "vcmi.logicalExpressions.allOf" : "以下所有前提:", + "vcmi.logicalExpressions.noneOf" : "无前提:", + + "vcmi.heroWindow.openCommander.hover" : "开启指挥官界面", + "vcmi.heroWindow.openCommander.help" : "开启英雄的指挥官界面", + + "vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?", + + "vcmi.creatureWindow.showBonuses.hover" : "属性界面", + "vcmi.creatureWindow.showBonuses.help" : "显示指挥官的所有增强属性", + "vcmi.creatureWindow.showSkills.hover" : "技能页面", + "vcmi.creatureWindow.showSkills.help" : "显示指挥官的所有技能", + "vcmi.creatureWindow.returnArtifact.hover" : "交换宝物", + "vcmi.creatureWindow.returnArtifact.help" : "将宝物还到英雄的背包里", + + "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", + "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", + + "vcmi.randomMapTab.widgets.defaultTemplate" : "默认", + "vcmi.randomMapTab.widgets.templateLabel" : "格式", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设置...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i", + "vcmi.stackExperience.rank.1" : "新兵 1级", + "vcmi.stackExperience.rank.2" : "列兵 2级", + "vcmi.stackExperience.rank.3" : "下士 3级", + "vcmi.stackExperience.rank.4" : "中士 4级", + "vcmi.stackExperience.rank.5" : "上士 5级", + "vcmi.stackExperience.rank.6" : "少尉 6级", + "vcmi.stackExperience.rank.7" : "中尉 7级", + "vcmi.stackExperience.rank.8" : "上尉 8级", + "vcmi.stackExperience.rank.9" : "少校 9级", + "vcmi.stackExperience.rank.10" : "中校 10级", + "vcmi.stackExperience.rank.11" : "上校 11级", + + "core.bonus.ADDITIONAL_ATTACK.name": "双击", + "core.bonus.ADDITIONAL_ATTACK.description": "可以攻击两次", + "core.bonus.ADDITIONAL_RETALIATION.name": "额外反击", + "core.bonus.ADDITIONAL_RETALIATION.description": "可以额外反击 ${val} 次", + "core.bonus.AIR_IMMUNITY.name": "气系免疫", + "core.bonus.AIR_IMMUNITY.description": "免疫所有气系魔法", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "环击", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "攻击所有相邻部队", + "core.bonus.BLOCKS_RETALIATION.name": "无反击", + "core.bonus.BLOCKS_RETALIATION.description": "敌人无法反击", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "远程无反击", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "敌人无法对射击进行反击", + "core.bonus.CATAPULT.name": "攻城", + "core.bonus.CATAPULT.description": "可以攻击城墙", + "core.bonus.CATAPULT_EXTRA_SHOTS.name": "额外攻击城墙", + "core.bonus.CATAPULT_EXTRA_SHOTS.description": "可以额外攻击城墙 ${val} 次", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "施法消耗 - (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "减少英雄的施法消耗", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "对方施法消耗 + (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "增加对方施法消耗", + "core.bonus.CHARGE_IMMUNITY.name": "I免疫冲锋", + "core.bonus.CHARGE_IMMUNITY.description": "对冲锋特技的额外伤害免疫", + "core.bonus.DARKNESS.name": "黑暗天幕", + "core.bonus.DARKNESS.description": "增加 ${val} 半径黑幕", + "core.bonus.DEATH_STARE.name": "死亡凝视 (${val}%)", + "core.bonus.DEATH_STARE.description": "${val}% 几率直接杀死生物", + "core.bonus.DEFENSIVE_STANCE.name": "防御奖励", + "core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val} 防御力", + "core.bonus.DESTRUCTION.name": "毁灭", + "core.bonus.DESTRUCTION.description": "有${val}% 杀死额外数量的部队", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "致命一击", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% 几率造成双倍伤害", + "core.bonus.DRAGON_NATURE.name": "龙", + "core.bonus.DRAGON_NATURE.description": "生物属于龙类", + "core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "魔法伤害免疫", + "core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "对魔法伤害免疫", + "core.bonus.EARTH_IMMUNITY.name": "土系免疫", + "core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法", + "core.bonus.ENCHANTER.name": "施法者", + "core.bonus.ENCHANTER.description": "每回合群体施放 ${subtype.spell} ", + "core.bonus.ENCHANTED.name": "魔法护身", + "core.bonus.ENCHANTED.description": "自身被 ${subtype.spell} 魔法影响", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "攻击时忽略对方部分防御力", + "core.bonus.FIRE_IMMUNITY.name": "火系免疫", + "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法", + "core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)", + "core.bonus.FIRE_SHIELD.description": "拥有烈火神盾护身", + "core.bonus.FIRST_STRIKE.name": "抢先攻击", + "core.bonus.FIRST_STRIKE.description": "在被反击前做出攻击", + "core.bonus.FEAR.name": "恐惧", + "core.bonus.FEAR.description": "引起恐惧", + "core.bonus.FEARLESS.name": "无惧", + "core.bonus.FEARLESS.description": "免疫恐惧", + "core.bonus.FLYING.name": "飞行兵种", + "core.bonus.FLYING.description": "生物可以飞行", + "core.bonus.FREE_SHOOTING.name": "近身射击", + "core.bonus.FREE_SHOOTING.description": "靠近敌方也能射击", + "core.bonus.FULL_HP_REGENERATION.name": "重生", + "core.bonus.FULL_HP_REGENERATION.description": "可以自动恢复所有生命值", + "core.bonus.GARGOYLE.name": "石像鬼属性", + "core.bonus.GARGOYLE.description": "不能被复活或治疗", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "受攻击时减少受到的伤害", + "core.bonus.HATE.name": "${subtype.creature}的死敌", + "core.bonus.HATE.description": "对该部队造成 ${val}% 的额外伤害", + "core.bonus.HEALER.name": "治疗", + "core.bonus.HEALER.description": "可以治疗友军单位", + "core.bonus.HP_REGENERATION.name": "重生", + "core.bonus.HP_REGENERATION.description": "每回合恢复 ${val} 点生命值", + "core.bonus.JOUSTING.name": "冲锋", + "core.bonus.JOUSTING.description": "每格行动增加+5%伤害", + "core.bonus.KING1.name": "一般顶级怪物", + "core.bonus.KING1.description": "被初级屠戮成性影响", + "core.bonus.KING2.name": "智慧顶级怪物", + "core.bonus.KING2.description": "被中级屠戮成性影响", + "core.bonus.KING3.name": "精神顶级怪物", + "core.bonus.KING3.description":"被高级屠戮成性影响", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫 1-${val} 级魔法", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫等级为 1-${val} 级的所有魔法", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "半程射击", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "超过 ${val} 格不能射击", + "core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)", + "core.bonus.LIFE_DRAIN.description": "吸取 ${val}% 伤害回复自身", + "core.bonus.MANA_CHANNELING.name": "偷取魔法 ${val}%", + "core.bonus.MANA_CHANNELING.description": "偷取部分敌人施法消耗", + "core.bonus.MANA_DRAIN.name": "吸取魔力", + "core.bonus.MANA_DRAIN.description": "每回合吸取 ${val} 魔法值", + "core.bonus.MAGIC_MIRROR.name": "带有魔法神镜 (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "${val}% 几率反射魔法", + "core.bonus.MAGIC_RESISTANCE.name": "(${MR}%) 魔法抵抗", + "core.bonus.MAGIC_RESISTANCE.description": "${MR}% 几率抵抗敌人的魔法", + "core.bonus.MIND_IMMUNITY.name": "免疫心智", + "core.bonus.MIND_IMMUNITY.description": "不受心智魔法的影响", + "core.bonus.NO_DISTANCE_PENALTY.name": "无障碍射击", + "core.bonus.NO_DISTANCE_PENALTY.description": "射击不受距离影响", + "core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚", + "core.bonus.NO_MELEE_PENALTY.description": "近战伤害不减", + "core.bonus.NO_MORALE.name": "无士气", + "core.bonus.NO_MORALE.description": "生物不受士气影响", + "core.bonus.NO_WALL_PENALTY.name": "无城墙影响", + "core.bonus.NO_WALL_PENALTY.description": "射击不受城墙的影响", + "core.bonus.NON_LIVING.name": "无生命", + "core.bonus.NON_LIVING.description": "不受只对生命实体生物有效的魔法", + "core.bonus.RANDOM_SPELLCASTER.name": "随机施法", + "core.bonus.RANDOM_SPELLCASTER.description": "随机施放增益魔法", + "core.bonus.RANGED_RETALIATION.name": "远程反击", + "core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击", + "core.bonus.RECEPTIVE.name": "接受有益魔法", + "core.bonus.RECEPTIVE.description": "不会免疫有益的魔法", + "core.bonus.REBIRTH.name": "复生 (${val}%)", + "core.bonus.REBIRTH.description": "{val}% 数量死亡后会复活", + "core.bonus.RETURN_AFTER_STRIKE.name": "攻击并返回", + "core.bonus.RETURN_AFTER_STRIKE.description": "攻击后回到初始位置", + "core.bonus.SELF_LUCK.name": "永久幸运", + "core.bonus.SELF_LUCK.description": "永久拥有幸运值", + "core.bonus.SELF_MORALE.name": "士气高涨", + "core.bonus.SELF_MORALE.description": "永久拥有高昂的士气", + "core.bonus.SHOOTER.name": "射手", + "core.bonus.SHOOTER.description": "生物可以设计", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "远程攻击可伤害范围内的多个目标", + "core.bonus.SOUL_STEAL.name": "杀死敌人复生", + "core.bonus.SOUL_STEAL.description": "当杀死敌人时获得 ${val} 数量", + "core.bonus.SPELLCASTER.name": "施法者", + "core.bonus.SPELLCASTER.description": "生物可以施放 ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "攻击后施法", + "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% 攻击后施放 ${subtype.spell}", + "core.bonus.SPELL_BEFORE_ATTACK.name": "攻击前施法", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% 攻击前施放 ${subtype.spell}", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "魔法伤害抵抗", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "受魔法攻击时伤害减少 ${val}%", + "core.bonus.SPELL_IMMUNITY.name": "特定魔法免疫", + "core.bonus.SPELL_IMMUNITY.description": "免疫 ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "魔法攻击", + "core.bonus.SPELL_LIKE_ATTACK.description": "攻击时使用 ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "抗魔光环", + "core.bonus.SPELL_RESISTANCE_AURA.description": "邻近部队获得 ${val}% 魔法抵抗", + "core.bonus.SUMMON_GUARDIANS.name": "召唤守卫", + "core.bonus.SUMMON_GUARDIANS.description": "战斗前召唤 ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "可协助攻击", + "core.bonus.SYNERGY_TARGET.description": "生物受到协助攻击的影响", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "龙息", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击2个部队", + "core.bonus.THREE_HEADED_ATTACK.name": "半环击", + "core.bonus.THREE_HEADED_ATTACK.description": "攻击正前方多个敌人", + "core.bonus.TRANSMUTATION.name": "变换", + "core.bonus.TRANSMUTATION.description": "${val}% 机会将敌人变成其他生物", + "core.bonus.UNDEAD.name": "不死生物", + "core.bonus.UNDEAD.description": "生物有丧尸属性", + "core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击", + "core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人", + "core.bonus.WATER_IMMUNITY.name": "水系免疫", + "core.bonus.WATER_IMMUNITY.description": "免疫水系魔法", + "core.bonus.WIDE_BREATH.name": "弧形焰息", + "core.bonus.WIDE_BREATH.description": "吐息攻击前方扇形6个部队" +} diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 0903a347d..68b0c81ee 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -84,10 +84,21 @@ "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to skip battle intro", + "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Shoot %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Shoot %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d shots left", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d shot left", + "vcmi.battleWindow.damageEstimation.damage" : "%d damage", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", + "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\n Shows creatures available to purchase instead of their growth in town summary (bottom-left corner).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (bottom-left corner).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\n Shows creatures' weekly growth instead of available amount in town summary (bottom-left corner).", "vcmi.otherOptions.compactTownCreatureInfo.hover": "Compact Creature Info", "vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\n Smaller town creatures information in town summary.", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index cdaebfa6c..68affb700 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -1,5 +1,5 @@ { - "vcmi.adventureMap.monsterThreat.title" : "\n\n Zagrożenie: ", + "vcmi.adventureMap.monsterThreat.title" : "\n\n Poziom zagrożenia: ", "vcmi.adventureMap.monsterThreat.levels.0" : "Zerowy", "vcmi.adventureMap.monsterThreat.levels.1" : "Bardzo słaby", "vcmi.adventureMap.monsterThreat.levels.2" : "Słaby", @@ -25,12 +25,82 @@ "vcmi.server.errors.modsIncompatibility" : "Mody wymagane do wczytania gry:", "vcmi.server.confirmReconnect" : "Połączyć z ostatnią sesją?", + "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", + "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", + "vcmi.settingsMainWindow.battleTab.hover" : "Bitwa", + "vcmi.settingsMainWindow.battleTab.help" : "Przełącza do zakładki opcji bitewnych, która pozwala konfigurować zachowanie gry w bitwach", + "vcmi.settingsMainWindow.adventureTab.hover" : "Mapa przygody", + "vcmi.settingsMainWindow.adventureTab.help" : "Przełącza do zakładki opcji mapy przygody - mapa przygody to część gry, w której poruszasz bohaterami", + + "vcmi.systemOptions.videoGroup" : "Ustawienia grafiki", + "vcmi.systemOptions.audioGroup" : "Ustawienia audio", + "vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now + "vcmi.systemOptions.townsGroup" : "Ekran miasta", + "vcmi.systemOptions.fullscreenButton.hover" : "Pełny ekran", - "vcmi.systemOptions.fullscreenButton.help" : "{Fullscreen}\n\n Po wybraniu VCMI uruchomi się w trybie pełnoekranowym, w przeciwnym wypadku uruchomi się w oknie", + "vcmi.systemOptions.fullscreenButton.help" : "{Pełny ekran}\n\n Po wybraniu VCMI uruchomi się w trybie pełnoekranowym, w przeciwnym wypadku uruchomi się w oknie", "vcmi.systemOptions.resolutionButton.hover" : "Rozdzielczość: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Select resolution}\n\n Zmień rozdzielczość ekranu w grze. Restart gry jest wymagany, by zmiany zostały uwzględnione.", + "vcmi.systemOptions.resolutionButton.help" : "{Wybierz rozdzielczość}\n\n Zmień rozdzielczość ekranu w grze. Restart gry jest wymagany, by zmiany zostały uwzględnione.", "vcmi.systemOptions.resolutionMenu.hover" : "Wybierz rozdzielczość", "vcmi.systemOptions.resolutionMenu.help" : "Zmień rozdzielczość ekranu w grze.", + "vcmi.systemOptions.fullscreenFailed" : "{Pełny ekran}\n\n Nieudane przełączenie w tryb pełnoekranowy! Obecna rozdzielczość nie jest wspierana przez wyświetlacz!", + "vcmi.systemOptions.framerateButton.hover" : "Pokaż FPS", + "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", + + "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", + "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", + "vcmi.adventureOptions.numericQuantities.hover" : "Liczbowe ilości stworzeń", + "vcmi.adventureOptions.numericQuantities.help" : "{Liczbowe ilości stworzeń}\n\n Pokazuje przybliżone ilości wrogów w liczbowym formacie A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Zawsze pokazuj koszt ruchu", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Zawsze pokazuj koszt ruchu}\n\n Zastępuje domyślne informacje paska statusu danymi o ruchu bez potrzeby przytrzymywania klawisza ALT.", + "vcmi.adventureOptions.showGrid.hover" : "Pokaż siatkę", + "vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.", + "vcmi.adventureOptions.mapSwipe.hover" : "Przeciąganie mapy", + "vcmi.adventureOptions.mapSwipe.help" : "{Przeciąganie mapy}\n\n Pozwala przesuwać mapę przygody palcem dla systemów z ekranami dotykowymi. Obecnie pozwala też przesuwać mapę lewym przyciskiem myszy.", + "vcmi.adventureOptions.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "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.battleOptions.queueSizeLabel.hover": "Pokaż kolejkę ruchu jednostek", + "vcmi.battleOptions.queueSizeNoneButton.hover": "BRAK", + "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover": "MAŁA", + "vcmi.battleOptions.queueSizeBigButton.hover": "DUŻA", + "vcmi.battleOptions.queueSizeNoneButton.help": "Kompletnie wyłącza widoczność kolejki ruchu jednostek", + "vcmi.battleOptions.queueSizeAutoButton.help": "Ustawia rozmiar kolejki zależnie od rozdzielczości gry (mała dla rozdzielczości z wysokością poniżej 700 pikseli, duża dla pozostałych)", + "vcmi.battleOptions.queueSizeSmallButton.help": "Ustawia rozmiar kolejki na mały", + "vcmi.battleOptions.queueSizeBigButton.help": "Ustawia rozmiar kolejki na duży (nie wspierane dla rozdzielczości z wysokością mniejszą niż 700 pikseli)", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Ustawia szybkość animacji na bardzo wolną", + "vcmi.battleOptions.animationsSpeed5.help": "Ustawia szybkość animacji na bardzo szybką", + "vcmi.battleOptions.animationsSpeed6.help": "Ustawia szybkość animacji na błyskawiczną", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń muzykę startową", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń muzykę startową}\n\n Pomija krótką muzykę, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji. Może również być pominięta poprzez naciśnięcie ESC.", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by pominąć muzykę startową", + + "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "pozostałe strzały: %d", + "vcmi.battleWindow.damageEstimation.shots.1" : "pozostał %d strzał", + "vcmi.battleWindow.damageEstimation.damage" : "obrażenia: %d", + "vcmi.battleWindow.damageEstimation.damage.1" : "obrażenia: %d", + "vcmi.battleWindow.damageEstimation.kills" : "%d zginie", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d zginie", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Pokaż tygodniowy przyrost stworzeń}\n\n Shows creatures' weekly growth instead of avaialable amount in town summary (lewy dolny r óg).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompaktowa informacja o stworzeniu", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktowa informacja o stworzeniu}\n\n Zmniejszona informacja o stworzeniu w podsumowaniu miasta.", "vcmi.townHall.missingBase" : "Podstawowy budynek %s musi zostać najpierw wybudowany", "vcmi.townHall.noCreaturesToRecruit" : "Brak stworzeń do rekrutacji!", @@ -69,6 +139,7 @@ "vcmi.randomMapTab.widgets.templateLabel" : "Szablon", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Ustaw...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Typy dróg", "core.bonus.ADDITIONAL_ATTACK.name": "Podwójne Uderzenie", "core.bonus.ADDITIONAL_ATTACK.description": "Atakuje podwójnie", @@ -140,12 +211,8 @@ "core.bonus.HP_REGENERATION.description": "Leczy ${SHval} punktów zdrowia każdej rundy", "core.bonus.JOUSTING.name": "Szarża Czempiona", "core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa", - "core.bonus.KING1.name": "Król 1", - "core.bonus.KING1.description": "Wrażliwy na podstawowy czar POGROMCA", - "core.bonus.KING2.name": "Król 2", - "core.bonus.KING2.description": "Wrażliwy na zaawansowany czar POGROMCA", - "core.bonus.KING3.name": "Król 3", - "core.bonus.KING3.description":"Wrażliwy na ekspercki czar POGROMCA", + "core.bonus.KING.name": "Król", + "core.bonus.KING.description": "Wrażliwy na czar POGROMCA stopnia zaawansowania ${val} lub wyższego", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność na czary 1-${val}", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poziomu", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 13d028381..758bb53b8 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -84,6 +84,17 @@ "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій", + + "vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Стріляти в %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d пострілів залишилось", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d постріл залишився", + "vcmi.battleWindow.damageEstimation.damage" : "%d одиниць пошкоджень", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d одиниця пошкодження", + "vcmi.battleWindow.damageEstimation.kills" : "%d загинуть", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d загине", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).", diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index 614e38573..be2734f86 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -1,6 +1,16 @@ { "name" : "VCMI essential files", "description" : "Essential files required for VCMI to run correctly", + + "chinese" : { + "name" : "VCMI essential files", + "description" : "Essential files required for VCMI to run correctly", + + "skipValidation" : true, + "translations" : [ + "config/vcmi/chinese.json" + ] + }, "german" : { "name" : "VCMI - grundlegende Dateien", diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 8d7ea219a..6927611f0 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -583,6 +583,7 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) castleInt->garr->setArmy(town->visitingHero, 1); castleInt->garr->recreateSlots(); castleInt->heroes->update(); + castleInt->redraw(); } for (auto isa : GH.listInt) { @@ -591,9 +592,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) { ki->townChanged(town); ki->updateGarrisons(); + ki->redraw(); } } - GH.totalRedraw(); } void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) { @@ -1510,6 +1511,7 @@ void CPlayerInterface::objectRemoved(const CGObjectInstance * obj) const CGHeroInstance * h = static_cast(obj); heroKilled(h); } + GH.fakeMouseMove(); } void CPlayerInterface::objectRemovedAfter() @@ -1559,7 +1561,6 @@ void CPlayerInterface::update() } assert(adventureInt); - assert(adventureInt->selection); // Handles mouse and key input GH.updateTime(); @@ -2032,7 +2033,9 @@ bool CPlayerInterface::capturedAllEvents() return true; } - if (ignoreEvents) + bool needToLockAdventureMap = adventureInt->active && CGI->mh->hasOngoingAnimations(); + + if (ignoreEvents || needToLockAdventureMap) { boost::unique_lock un(eventsM); while(!SDLEventsQueue.empty()) diff --git a/client/ClientCommandManager.h b/client/ClientCommandManager.h index 61b2175fb..14ce180af 100644 --- a/client/ClientCommandManager.h +++ b/client/ClientCommandManager.h @@ -17,7 +17,7 @@ class CIntObject; class ClientCommandManager //take mantis #2292 issue about account if thinking about handling cheats from command-line { - bool currentCallFromIngameConsole; + bool currentCallFromIngameConsole = false; void giveTurn(const PlayerColor &color); void printInfoAboutInterfaceObject(const CIntObject *obj, int level); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 9806932bb..a05aa2d3c 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -514,6 +514,11 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) { callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 1); } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town); + CGI->mh->onObjectInstantAdd(town); + } void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) { @@ -522,6 +527,10 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) { callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 2); } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town); + CGI->mh->onObjectInstantAdd(town); } void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) @@ -607,7 +616,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) if (pack.what == ObjProperty::OWNER) { - // invalidate section of map view with our objec and force an update with new flag color + // invalidate section of map view with our object and force an update with new flag color CGI->mh->onObjectInstantRemove(gs.getObjInstance(pack.id)); CGI->mh->onObjectInstantAdd(gs.getObjInstance(pack.id)); } @@ -739,11 +748,14 @@ void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pa void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) { callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, &pack); + + // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit + // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot()); } void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot()); } void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) diff --git a/client/adventureMap/CAdvMapInt.cpp b/client/adventureMap/CAdvMapInt.cpp index b33697bd3..841d973d3 100644 --- a/client/adventureMap/CAdvMapInt.cpp +++ b/client/adventureMap/CAdvMapInt.cpp @@ -986,7 +986,6 @@ void CAdvMapInt::initializeNewTurn() { heroList->update(); townList->update(); - mapAudio->onPlayerTurnStarted(); const CGHeroInstance * heroToSelect = nullptr; @@ -1017,6 +1016,7 @@ void CAdvMapInt::initializeNewTurn() updateNextHero(nullptr); showAll(screen); + mapAudio->onPlayerTurnStarted(); if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) { @@ -1404,6 +1404,7 @@ void CAdvMapInt::aiTurnStarted() mapAudio->onEnemyTurnStarted(); adventureInt->minimap->setAIRadar(true); adventureInt->infoBar->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer()); + adventureInt->minimap->showAll(screen);//force refresh on inactive object adventureInt->infoBar->showAll(screen);//force refresh on inactive object } diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index e62ec3e39..ed4588ea4 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -24,71 +24,85 @@ #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CArmedInstance.h" -#include - CInGameConsole::CInGameConsole() - : CIntObject(KEYBOARD | TEXTINPUT), - prevEntDisp(-1), - defaultTimeout(10000), - maxDisplayedTexts(10) + : CIntObject(KEYBOARD | TIME | TEXTINPUT) + , prevEntDisp(-1) { + type |= REDRAW_PARENT; +} + +void CInGameConsole::showAll(SDL_Surface * to) +{ + show(to); } void CInGameConsole::show(SDL_Surface * to) { int number = 0; - std::vector >::iterator> toDel; - boost::unique_lock lock(texts_mx); - for(auto it = texts.begin(); it != texts.end(); ++it, ++number) + for(auto & text : texts) { Point leftBottomCorner(0, pos.h); + Point textPosition(leftBottomCorner.x + 50, leftBottomCorner.y - texts.size() * 20 - 80 + number * 20); - graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, it->first, Colors::GREEN, - Point(leftBottomCorner.x + 50, leftBottomCorner.y - (int)texts.size() * 20 - 80 + number*20)); + graphics->fonts[FONT_MEDIUM]->renderTextLeft(to, text.text, Colors::GREEN, textPosition ); - if((int)(SDL_GetTicks() - it->second) > defaultTimeout) - { - toDel.push_back(it); - } - } - - for(auto & elem : toDel) - { - texts.erase(elem); + number++; } } -void CInGameConsole::print(const std::string &txt) +void CInGameConsole::tick(uint32_t msPassed) { - boost::unique_lock lock(texts_mx); - int lineLen = conf.go()->ac.outputLineLength; - - if(txt.size() < lineLen) + size_t sizeBefore = texts.size(); { - texts.push_back(std::make_pair(txt, SDL_GetTicks())); - if(texts.size() > maxDisplayedTexts) - { - texts.pop_front(); - } - } - else - { - assert(lineLen); - for(int g=0; g lock(texts_mx); - texts.push_back(std::make_pair(part, SDL_GetTicks())); - if(texts.size() > maxDisplayedTexts) + for(auto & text : texts) + text.timeOnScreen += msPassed; + + vstd::erase_if( + texts, + [&](const auto & value) { - texts.pop_front(); + return value.timeOnScreen > defaultTimeout; + } + ); + } + + if(sizeBefore != texts.size()) + GH.totalRedraw(); // FIXME: ingame console has no parent widget set +} + +void CInGameConsole::print(const std::string & txt) +{ + // boost::unique_lock scope + { + boost::unique_lock lock(texts_mx); + int lineLen = conf.go()->ac.outputLineLength; + + if(txt.size() < lineLen) + { + texts.push_back({txt, 0}); + } + else + { + assert(lineLen); + for(int g = 0; g < txt.size() / lineLen + 1; ++g) + { + std::string part = txt.substr(g * lineLen, lineLen); + if(part.empty()) + break; + + texts.push_back({part, 0}); } } + + while(texts.size() > maxDisplayedTexts) + texts.erase(texts.begin()); } + + GH.totalRedraw(); // FIXME: ingame console has no parent widget set } void CInGameConsole::keyPressed (const SDL_Keycode & key) @@ -136,7 +150,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key) } case SDLK_UP: //up arrow { - if(previouslyEntered.size() == 0) + if(previouslyEntered.empty()) break; if(prevEntDisp == -1) @@ -178,7 +192,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key) void CInGameConsole::textInputed(const std::string & inputtedText) { - if(!captureAllKeys || enteredText.size() == 0) + if(!captureAllKeys || enteredText.empty()) return; enteredText.resize(enteredText.size()-1); diff --git a/client/adventureMap/CInGameConsole.h b/client/adventureMap/CInGameConsole.h index 93d5a204d..0d5266967 100644 --- a/client/adventureMap/CInGameConsole.h +++ b/client/adventureMap/CInGameConsole.h @@ -14,20 +14,39 @@ class CInGameConsole : public CIntObject { private: - std::list< std::pair< std::string, uint32_t > > texts; //list - boost::mutex texts_mx; // protects texts - std::vector< std::string > previouslyEntered; //previously entered texts, for up/down arrows to work - int prevEntDisp; //displayed entry from previouslyEntered - if none it's -1 - int defaultTimeout; //timeout for new texts (in ms) - int maxDisplayedTexts; //hiw many texts can be displayed simultaneously + struct TextState + { + std::string text; + uint32_t timeOnScreen; + }; + + /// Currently visible texts in the overlay + std::vector texts; + + /// protects texts + boost::mutex texts_mx; + + /// previously entered texts, for up/down arrows to work + std::vector previouslyEntered; + + /// displayed entry from previouslyEntered - if none it's -1 + int prevEntDisp; + + /// timeout for new texts (in ms) + static constexpr int defaultTimeout = 10000; + + /// how many texts can be displayed simultaneously + static constexpr int maxDisplayedTexts = 10; std::weak_ptr currentStatusBar; std::string enteredText; public: - void print(const std::string &txt); + void print(const std::string & txt); + void tick(uint32_t msPassed) override; void show(SDL_Surface * to) override; + void showAll(SDL_Surface * to) override; void keyPressed(const SDL_Keycode & key) override; void textInputed(const std::string & enteredText) override; void textEdited(const std::string & enteredText) override; diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index a3999c5e5..d1c1b1b6e 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -254,11 +254,21 @@ void CInfoBar::showSelection() showGameStatus();//FIXME: may be incorrect but shouldn't happen in general } -void CInfoBar::tick() +void CInfoBar::tick(uint32_t msPassed) { - removeUsedEvents(TIME); - if(GH.topInt() == adventureInt) - popComponents(true); + assert(timerCounter > 0); + + if (msPassed >= timerCounter) + { + timerCounter = 0; + removeUsedEvents(TIME); + if(GH.topInt() == adventureInt) + popComponents(true); + } + else + { + timerCounter -= msPassed; + } } void CInfoBar::clickLeft(tribool down, bool previousState) @@ -290,6 +300,7 @@ void CInfoBar::hover(bool on) CInfoBar::CInfoBar(const Rect & position) : CIntObject(LCLICK | RCLICK | HOVER, position.topLeft()), + timerCounter(0), state(EMPTY) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -302,6 +313,14 @@ CInfoBar::CInfoBar(const Point & position): CInfoBar(Rect(position.x, position.y { } + +void CInfoBar::setTimer(uint32_t msToTrigger) +{ + if (!(active & TIME)) + addUsedEvents(TIME); + timerCounter = msToTrigger; +} + void CInfoBar::showDate() { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); @@ -312,7 +331,6 @@ void CInfoBar::showDate() redraw(); } - void CInfoBar::pushComponents(const std::vector & components, std::string message, int timer) { auto actualPush = [&](const std::vector & components, std::string message, int timer, size_t max){ diff --git a/client/adventureMap/CInfoBar.h b/client/adventureMap/CInfoBar.h index df7d1b2ef..3a507e879 100644 --- a/client/adventureMap/CInfoBar.h +++ b/client/adventureMap/CInfoBar.h @@ -138,6 +138,7 @@ private: std::shared_ptr visibleInfo; EState state; + uint32_t timerCounter; bool shouldPopAll = false; std::queue> componentsQueue; @@ -151,13 +152,14 @@ private: //removes all information about current state, deactivates timer (if any) void reset(); - void tick() override; + void tick(uint32_t msPassed) override; void clickLeft(tribool down, bool previousState) override; void clickRight(tribool down, bool previousState) override; void hover(bool on) override; void playNewDaySound(); + void setTimer(uint32_t msToTrigger); public: CInfoBar(const Rect & pos); CInfoBar(const Point & pos); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index e60b08c71..d09f53dee 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -77,7 +77,7 @@ void CList::CListItem::onSelect(bool on) if(on) selection = genSelection(); select(on); - GH.totalRedraw(); + redraw(); } CList::CList(int Size, Point position, std::string btnUp, std::string btnDown, size_t listAmount, int helpUp, int helpDown, CListBox::CreateFunc create) diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 22d7ac72c..b53e58f4c 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -222,10 +222,7 @@ void CMinimap::setAIRadar(bool on) aiShield->disable(); update(); } - - // this may happen during AI turn when this interface is inactive - // force redraw in order to properly update interface - GH.totalRedraw(); + redraw(); } void CMinimap::updateTile(const int3 &pos) diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index bb054deec..e289c80f8 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -131,7 +131,11 @@ std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) { const auto & object = CGI->mh->getMap()->objects[objectID.getNum()]; - if(object->getAmbientSound()) + assert(object); + if (!object) + logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z); + + if(object && object->getAmbientSound()) result.push_back(object->getAmbientSound().get()); } @@ -194,8 +198,10 @@ MapAudioPlayer::MapAudioPlayer() objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); for(const auto & obj : CGI->mh->getMap()->objects) + { if (obj) addObject(obj); + } } MapAudioPlayer::~MapAudioPlayer() diff --git a/client/adventureMap/MapAudioPlayer.h b/client/adventureMap/MapAudioPlayer.h index b4f1cbf9f..31a00e6d3 100644 --- a/client/adventureMap/MapAudioPlayer.h +++ b/client/adventureMap/MapAudioPlayer.h @@ -47,6 +47,10 @@ protected: void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + public: MapAudioPlayer(); ~MapAudioPlayer() override; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 4995a78c9..6d86edb31 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -25,25 +25,95 @@ #include "../windows/CCreatureWindow.h" #include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/Problem.h" -#include "../../lib/CGeneralTextHandler.h" -static std::string formatDmgRange(std::pair dmgRange) +struct TextReplacement { - if (dmgRange.first != dmgRange.second) - return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str(); - else - return (boost::format("%d") % dmgRange.first).str(); + std::string placeholder; + std::string replacement; +}; + +using TextReplacementList = std::vector; + +static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) +{ + for(const auto & entry : format) + boost::replace_all(input, entry.placeholder, entry.replacement); + + return input; +} + +static std::string translatePlural(int amount, const std::string& baseTextID) +{ + if(amount == 1) + return CGI->generaltexth->translate(baseTextID + ".1"); + return CGI->generaltexth->translate(baseTextID); +} + +static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) +{ + std::string baseString = translatePlural(amount, baseTextID); + TextReplacementList replacements { + { "%d", amountString } + }; + + return replacePlaceholders(baseString, replacements); +} + +static std::string formatPlural(int amount, const std::string & baseTextID) +{ + return formatPluralImpl(amount, std::to_string(amount), baseTextID); +} + +static std::string formatPlural(DamageRange range, const std::string & baseTextID) +{ + if (range.min == range.max) + return formatPlural(range.min, baseTextID); + + std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); + + return formatPluralImpl(range.max, rangeString, baseTextID); +} + +static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) +{ + TextReplacementList replacements = { + { "%CREATURE", creatureName }, + { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, + { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, + { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, + }; + + return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); +} + +static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.melee" : + "vcmi.battleWindow.damageEstimation.meleeKills"; + + return formatAttack(estimation, creatureName, baseTextID, 0); +} + +static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.ranged" : + "vcmi.battleWindow.damageEstimation.rangedKills"; + + return formatAttack(estimation, creatureName, baseTextID, shotsLeft); } BattleActionsController::BattleActionsController(BattleInterface & owner): owner(owner), - heroSpellToCast(nullptr), - creatureSpellToCast(nullptr) + heroSpellToCast(nullptr) {} void BattleActionsController::endCastingSpell() @@ -63,8 +133,8 @@ bool BattleActionsController::isActiveStackSpellcaster() const if (!casterStack) return false; - const auto randomSpellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER)); - return (randomSpellcaster && casterStack->canCast()); + bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER); + return (spellcaster && casterStack->canCast()); } void BattleActionsController::enterCreatureCastingMode() @@ -83,10 +153,13 @@ void BattleActionsController::enterCreatureCastingMode() if (!isActiveStackSpellcaster()) return; - if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION)) + for (auto const & action : possibleActions) { + if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) + continue; + const spells::Caster * caster = owner.stacksController->getActiveStack(); - const CSpell * spell = getStackSpellToCast(); + const CSpell * spell = action.spell().toSpell(); spells::Target target; target.emplace_back(); @@ -105,31 +178,26 @@ void BattleActionsController::enterCreatureCastingMode() CCS->curh->set(Cursor::Combat::POINTER); } + return; } - else + + possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); + + auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) { - possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); + return !x.spellcast(); + }; - auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) - { - return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) && - (x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) && - (x != PossiblePlayerBattleAction::OBSTACLE); - }; - - vstd::erase_if(possibleActions, actionFilterPredicate); - GH.fakeMouseMove(); - } + vstd::erase_if(possibleActions, actionFilterPredicate); + GH.fakeMouseMove(); } std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const { BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass - if (getStackSpellToCast()) - data.creatureSpellToCast = getStackSpellToCast()->getId(); - else - data.creatureSpellToCast = SpellID::NONE; + for (auto const & spell : creatureSpells) + data.creatureSpellsToCast.push_back(spell->id); data.tacticsMode = owner.tacticsMode; auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data); @@ -146,7 +214,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it { - switch(item) + switch(item.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::ANY_LOCATION: @@ -207,7 +275,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) assert(castingHero); // code below assumes non-null hero PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); - if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location + if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location { heroSpellToCast->aimToHex(BattleHex::INVALID); owner.curInt->cb->battleMakeAction(heroSpellToCast.get()); @@ -228,19 +296,30 @@ const CSpell * BattleActionsController::getHeroSpellToCast( ) const return nullptr; } -const CSpell * BattleActionsController::getStackSpellToCast( ) const +const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) { - if (isActiveStackSpellcaster()) - return creatureSpellToCast; + if (heroSpellToCast) + return nullptr; - return nullptr; + if (!owner.stacksController->getActiveStack()) + return nullptr; + + if (!hoveredHex.isValid()) + return nullptr; + + auto action = selectAction(hoveredHex); + + if (action.spell() == SpellID::NONE) + return nullptr; + + return action.spell().toSpell(); } -const CSpell * BattleActionsController::getCurrentSpell( ) const +const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) { if (getHeroSpellToCast()) return getHeroSpellToCast(); - return getStackSpellToCast(); + return getStackSpellToCast(hoveredHex); } const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) @@ -253,7 +332,7 @@ const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) { - switch (action) + switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: CCS->curh->set(Cursor::Combat::POINTER); @@ -316,7 +395,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) { - switch (action) + switch (action.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: @@ -339,7 +418,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle { const CStack * targetStack = getStackForHex(targetHex); - switch (action) //display console message, realize selected action + switch (action.get()) //display console message, realize selected action { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s @@ -356,26 +435,29 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - TDmgRange damage = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - return (boost::format(CGI->generaltexth->allTexts[36]) % targetStack->getName() % estDmgText).str(); //Attack %s (%s damage) + DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatMeleeAttack(estimation, targetStack->getName()); } case PossiblePlayerBattleAction::SHOOT: { - auto const * shooter = owner.stacksController->getActiveStack(); + const auto * shooter = owner.stacksController->getActiveStack(); - TDmgRange damage = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); - std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg - //printing - Shoot %s (%d shots left, %s damage) - return (boost::format(CGI->generaltexth->allTexts[296]) % targetStack->getName() % shooter->shots.available() % estDmgText).str(); + DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); } case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % getCurrentSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s + return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s case PossiblePlayerBattleAction::ANY_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % getCurrentSpell()->getNameTranslated()); //Cast %s + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % @@ -390,7 +472,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % getCurrentSpell()->getNameTranslated()); //Cast %s + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s case PossiblePlayerBattleAction::HEAL: return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s @@ -410,7 +492,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) { - switch (action) + switch (action.get()) { case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: @@ -423,7 +505,7 @@ std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlaye return CGI->generaltexth->allTexts[543]; //choose army to sacrifice break; case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % getCurrentSpell()->getNameTranslated()); //No room to place %s here + return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here break; default: return ""; @@ -435,7 +517,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B const CStack * targetStack = getStackForHex(targetHex); bool targetStackOwned = targetStack && targetStack->owner == owner.curInt->playerID; - switch (action) + switch (action.get()) { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: case PossiblePlayerBattleAction::CREATURE_INFO: @@ -472,11 +554,14 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::SHOOT: return owner.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); + case PossiblePlayerBattleAction::NO_LOCATION: + return false; + case PossiblePlayerBattleAction::ANY_LOCATION: - return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex); + return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex); case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return targetStack && isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex); + return targetStack && isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex); case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures @@ -497,8 +582,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::OBSTACLE: case PossiblePlayerBattleAction::FREE_LOCATION: - return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex); - return isCastingPossibleHere(owner.stacksController->getActiveStack(), targetStack, targetHex); + return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex); + return isCastingPossibleHere(action.spell().toSpell(), owner.stacksController->getActiveStack(), targetStack, targetHex); case PossiblePlayerBattleAction::CATAPULT: return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); @@ -515,7 +600,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B { const CStack * targetStack = getStackForHex(targetHex); - switch (action) //display console message, realize selected action + switch (action.get()) //display console message, realize selected action { case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: { @@ -546,7 +631,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { - bool returnAfterAttack = action == PossiblePlayerBattleAction::ATTACK_AND_RETURN; + bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) { @@ -599,16 +684,16 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::SACRIFICE: case PossiblePlayerBattleAction::FREE_LOCATION: { - if (action == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) + if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) { - if (getCurrentSpell()->id == SpellID::SACRIFICE) + if (action.spell() == SpellID::SACRIFICE) { heroSpellToCast->aimToHex(targetHex); possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE); owner.stacksController->setSelectedStack(targetStack); return; } - if (getCurrentSpell()->id == SpellID::TELEPORT) + if (action.spell() == SpellID::TELEPORT) { heroSpellToCast->aimToUnit(targetStack); possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT); @@ -619,9 +704,9 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B if (!spellcastingModeActive()) { - if (getStackSpellToCast()) + if (action.spell().toSpell()) { - owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, getStackSpellToCast()->getId()); + owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); } else //unknown random spell { @@ -750,12 +835,25 @@ void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) { - const auto spellcaster = casterStack->getBonusLocalFirst(Selector::type()(Bonus::SPELLCASTER)); + creatureSpells.clear(); + + bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER); if(casterStack->canCast() && spellcaster) { // faerie dragon can cast only one, randomly selected spell until their next move //TODO: faerie dragon type spell should be selected by server - creatureSpellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + const auto * spellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + + if (spellToCast) + creatureSpells.push_back(spellToCast); + } + + TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(Bonus::SPELLCASTER)); + + for (auto const & bonus : *bl) + { + if (bonus->additionalInfo[0] <= 0) + creatureSpells.push_back(SpellID(bonus->subtype).toSpell()); } } @@ -776,11 +874,9 @@ spells::Mode BattleActionsController::getCurrentCastMode() const } -bool BattleActionsController::isCastingPossibleHere(const CStack *casterStack, const CStack *targetStack, BattleHex targetHex) +bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *casterStack, const CStack *targetStack, BattleHex targetHex) { - auto currentSpell = getCurrentSpell(); assert(currentSpell); - if (!currentSpell) return false; @@ -823,7 +919,7 @@ void BattleActionsController::activateStack() std::list actionsToSelect; if(!possibleActions.empty()) { - switch(possibleActions.front()) + switch(possibleActions.front().get()) { case PossiblePlayerBattleAction::SHOOT: actionsToSelect.push_back(possibleActions.front()); @@ -873,12 +969,7 @@ bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) auto action = selectAction(hoveredHex); - return - action == PossiblePlayerBattleAction::ANY_LOCATION || - action == PossiblePlayerBattleAction::NO_LOCATION || - action == PossiblePlayerBattleAction::FREE_LOCATION || - action == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE || - action == PossiblePlayerBattleAction::OBSTACLE; + return action.spellcast(); } const std::vector & BattleActionsController::getPossibleActions() const diff --git a/client/battle/BattleActionsController.h b/client/battle/BattleActionsController.h index 9ce9be196..855a211be 100644 --- a/client/battle/BattleActionsController.h +++ b/client/battle/BattleActionsController.h @@ -45,9 +45,9 @@ class BattleActionsController std::string currentConsoleMsg; /// if true, active stack could possibly cast some target spell - const CSpell * creatureSpellToCast; + std::vector creatureSpells; - bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber); + bool isCastingPossibleHere (const CSpell * spell, const CStack *sactive, const CStack *shere, BattleHex myNumber); bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback std::vector getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context); @@ -74,7 +74,7 @@ class BattleActionsController const CSpell * getHeroSpellToCast() const; /// if current stack is spellcaster, returns spell being cast, or null othervice - const CSpell * getStackSpellToCast( ) const; + const CSpell * getStackSpellToCast(BattleHex hoveredHex); /// returns true if current stack is a spellcaster bool isActiveStackSpellcaster() const; @@ -116,7 +116,7 @@ public: void onHexRightClicked(BattleHex clickedHex); const spells::Caster * getCurrentSpellcaster() const; - const CSpell * getCurrentSpell() const; + const CSpell * getCurrentSpell(BattleHex hoveredHex); spells::Mode getCurrentCastMode() const; /// methods to work with array of possible actions, needed to control special creatures abilities diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 108850527..369fc78ac 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -263,7 +263,7 @@ std::set BattleFieldController::getHighlightedHexesSpellRange() const CSpell *spell = nullptr; spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); caster = owner.actionsController->getCurrentSpellcaster(); if(caster && spell) //when casting spell diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index ac63b6ee8..f61d9214c 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -54,6 +54,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * , attackerInt(att) , defenderInt(defen) , curInt(att) + , battleOpeningDelayActive(true) { if(spectatorInt) { @@ -112,7 +113,7 @@ void BattleInterface::playIntroSoundAndUnlockInterface() } }; - battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); + int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); if (battleIntroSoundChannel != -1) { CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); @@ -120,8 +121,15 @@ void BattleInterface::playIntroSoundAndUnlockInterface() if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool()) openingEnd(); } - else + else // failed to play sound + { onIntroSoundPlayed(); + } +} + +bool BattleInterface::openingPlaying() +{ + return battleOpeningDelayActive; } void BattleInterface::onIntroSoundPlayed() @@ -132,6 +140,19 @@ void BattleInterface::onIntroSoundPlayed() CCS->musich->playMusicFromSet("battle", true, true); } +void BattleInterface::openingEnd() +{ + assert(openingPlaying()); + if (!openingPlaying()) + return; + + onAnimationsFinished(); + if(tacticsMode) + tacticNextStack(nullptr); + activateStack(); + battleOpeningDelayActive = false; +} + BattleInterface::~BattleInterface() { CPlayerInterface::battleInt = nullptr; @@ -310,6 +331,12 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) { windowObject->blockUI(true); + // Disable current active stack duing the cast + // Store the current activeStack to stackToActivate + stacksController->deactivateStack(); + + CCS->curh->set(Cursor::Combat::BLOCKED); + const SpellID spellID = sc->spellID; const CSpell * spell = spellID.toSpell(); auto targetedTile = sc->tile; @@ -524,24 +551,6 @@ void BattleInterface::activateStack() GH.fakeMouseMove(); } -bool BattleInterface::openingPlaying() -{ - return battleIntroSoundChannel != -1; -} - -void BattleInterface::openingEnd() -{ - assert(openingPlaying()); - if (!openingPlaying()) - return; - - onAnimationsFinished(); - if(tacticsMode) - tacticNextStack(nullptr); - activateStack(); - battleIntroSoundChannel = -1; -} - bool BattleInterface::makingTurn() const { return stacksController->getActiveStack() != nullptr; @@ -551,6 +560,9 @@ void BattleInterface::endAction(const BattleAction* action) { const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); + // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast + activateStack(); + stacksController->endAction(action); windowObject->updateQueue(); @@ -693,6 +705,8 @@ void BattleInterface::requestAutofightingAIToTakeAction() auto ba = std::make_unique(curInt->autofightingAI->activeStack(activeStack)); givenCommand.setn(ba.release()); } + + stacksController->setActiveStack(nullptr); } }); diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index e2d4db1ff..0ff9a9868 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -111,8 +111,8 @@ class BattleInterface /// defender interface, not null if attacker is human in our vcmiclient std::shared_ptr defenderInt; - /// ID of channel on which battle opening sound is playing, or -1 if none - int battleIntroSoundChannel; + /// if set to true, battle is still starting and waiting for intro sound to end / key press from player + bool battleOpeningDelayActive; void playIntroSoundAndUnlockInterface(); void onIntroSoundPlayed(); diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 561c1874c..7ad36f2ac 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -88,16 +88,13 @@ BattleStacksController::BattleStacksController(BattleInterface & owner): static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f ); static const auto shifterNeutral = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f ); - amountNormal->adjustPalette(shifterNormal, 0); - amountPositive->adjustPalette(shifterPositive, 0); - amountNegative->adjustPalette(shifterNegative, 0); - amountEffNeutral->adjustPalette(shifterNeutral, 0); + // do not change border color + static const int32_t ignoredMask = 1 << 26; - //Restore border color {255, 231, 132, 255} to its original state - amountNormal->resetPalette(26); - amountPositive->resetPalette(26); - amountNegative->resetPalette(26); - amountEffNeutral->resetPalette(26); + amountNormal->adjustPalette(shifterNormal, ignoredMask); + amountPositive->adjustPalette(shifterPositive, ignoredMask); + amountNegative->adjustPalette(shifterNegative, ignoredMask); + amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask); std::vector stacks = owner.curInt->cb->battleGetAllStacks(true); for(const CStack * s : stacks) @@ -187,7 +184,7 @@ void BattleStacksController::stackReset(const CStack * stack) void BattleStacksController::stackAdded(const CStack * stack, bool instant) { // Tower shooters have only their upper half visible - static const int turretCreatureAnimationHeight = 225; + static const int turretCreatureAnimationHeight = 232; stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position @@ -201,6 +198,11 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant) stackAnimation[stack->ID] = AnimationControls::getAnimation(turretCreature); stackAnimation[stack->ID]->pos.h = turretCreatureAnimationHeight; + stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth(); + + // FIXME: workaround for visible animation of Medusa tails (animation disabled in H3) + if (turretCreature->idNumber == CreatureID::MEDUSA ) + stackAnimation[stack->ID]->pos.w = 250; coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition); } @@ -209,10 +211,10 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant) stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->getCreature()); stackAnimation[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->ID]); stackAnimation[stack->ID]->pos.h = stackAnimation[stack->ID]->getHeight(); + stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth(); } stackAnimation[stack->ID]->pos.x = coords.x; stackAnimation[stack->ID]->pos.y = coords.y; - stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth(); stackAnimation[stack->ID]->setType(ECreatureAnimType::HOLDING); if (!instant) @@ -683,6 +685,15 @@ void BattleStacksController::stackActivated(const CStack *stack) owner.activateStack(); } +void BattleStacksController::deactivateStack() +{ + if (!activeStack) { + return; + } + stackToActivate = activeStack; + setActiveStack(nullptr); +} + void BattleStacksController::activateStack() { if ( !currentAnimations.empty()) @@ -840,7 +851,7 @@ std::vector BattleStacksController::selectHoveredStacks() const CSpell *spell = nullptr; spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); caster = owner.actionsController->getCurrentSpellcaster(); if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 3cce90334..8fe4f6ab0 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -118,7 +118,9 @@ public: void startAction(const BattleAction* action); void endAction(const BattleAction* action); - void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all + void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack + + void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack void setActiveStack(const CStack *stack); void setSelectedStack(const CStack *stack); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 865a410d0..f8270ec15 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -38,7 +38,8 @@ #include "../windows/settings/SettingsMainWindow.h" BattleWindow::BattleWindow(BattleInterface & owner): - owner(owner) + owner(owner), + defaultAction(PossiblePlayerBattleAction::INVALID) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos.w = 800; @@ -326,7 +327,7 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) return; std::string iconName = variables["actionIconDefault"].String(); - switch(action) + switch(action.get()) { case PossiblePlayerBattleAction::ATTACK: iconName = variables["actionIconAttack"].String(); diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index ee4ba1449..afc4ff4af 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -12,6 +12,7 @@ #include "../gui/CIntObject.h" #include "../gui/InterfaceObjectConfigurable.h" #include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/PossiblePlayerBattleAction.h" VCMI_LIB_NAMESPACE_BEGIN class CStack; diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index e355b7d5b..c534f02b2 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -343,13 +343,14 @@ static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over) void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) { - target[0] = genShadow(shadowAlpha / 2); + target.resize(8); + target[0] = genShadow(0); target[1] = genShadow(shadowAlpha / 2); - target[2] = genShadow(shadowAlpha); - target[3] = genShadow(shadowAlpha); - target[4] = genBorderColor(getBorderStrength(elapsedTime), border); - target[5] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); - target[6] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); + // colors 2 & 3 are not used in creatures + target[4] = genShadow(shadowAlpha); + target[5] = genBorderColor(getBorderStrength(elapsedTime), border); + target[6] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); + target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); } void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) @@ -371,8 +372,8 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, IImage::SpecialPalette SpecialPalette; genSpecialPalette(SpecialPalette); - image->setSpecialPallete(SpecialPalette); - image->adjustPalette(shifter, 8); + image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); + image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 1ebc32c11..3135104fe 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -192,7 +192,7 @@ void CGuiHandler::updateTime() for (auto & elem : hlp) { if(!vstd::contains(timeinterested,elem)) continue; - (elem)->onTimer(ms); + (elem)->tick(ms); } } diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 8e5721e44..79ed430cd 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -29,7 +29,6 @@ CIntObject::CIntObject(int used_, Point pos_): active(active_m) { hovered = captureAllKeys = strongInterest = false; - toNextTick = timerDelay = 0; used = used_; recActions = defActions = GH.defActionsDef; @@ -60,24 +59,6 @@ CIntObject::~CIntObject() parent_m->removeChild(this); } -void CIntObject::setTimer(int msToTrigger) -{ - if (!(active & TIME)) - activate(TIME); - toNextTick = timerDelay = msToTrigger; - used |= TIME; -} - -void CIntObject::onTimer(int timePassed) -{ - toNextTick -= timePassed; - if (toNextTick < 0) - { - toNextTick += timerDelay; - tick(); - } -} - void CIntObject::show(SDL_Surface * to) { if(defActions & UPDATE) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 80017d32b..ff26c96d4 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -65,14 +65,8 @@ class CIntObject : public IShowActivatable //interface object { ui16 used;//change via addUsed() or delUsed - //time handling - int toNextTick; - int timerDelay; - std::map currentMouseState; - void onTimer(int timePassed); - //non-const versions of fields to allow changing them in CIntObject CIntObject *parent_m; //parent object ui16 active_m; @@ -129,8 +123,7 @@ public: virtual void mouseMoved (const Point & cursorPosition){} //time handling - void setTimer(int msToTrigger);//set timer delay and activate timer if needed. - virtual void tick(){} + virtual void tick(uint32_t msPassed){} //mouse wheel virtual void wheelScrolled(bool down, bool in){} diff --git a/client/mapView/IMapRendererObserver.h b/client/mapView/IMapRendererObserver.h index e4b51f8b4..10e36807b 100644 --- a/client/mapView/IMapRendererObserver.h +++ b/client/mapView/IMapRendererObserver.h @@ -26,27 +26,27 @@ public: virtual bool hasOngoingAnimations() = 0; /// Plays fade-in animation and adds object to map - virtual void onObjectFadeIn(const CGObjectInstance * obj) {} + virtual void onObjectFadeIn(const CGObjectInstance * obj) = 0; /// Plays fade-out animation and removed object from map - virtual void onObjectFadeOut(const CGObjectInstance * obj) {} + virtual void onObjectFadeOut(const CGObjectInstance * obj) = 0; /// Adds object to map instantly, with no animation - virtual void onObjectInstantAdd(const CGObjectInstance * obj) {} + virtual void onObjectInstantAdd(const CGObjectInstance * obj) = 0; /// Removes object from map instantly, with no animation - virtual void onObjectInstantRemove(const CGObjectInstance * obj) {} + virtual void onObjectInstantRemove(const CGObjectInstance * obj) = 0; /// Perform hero movement animation, moving hero across terrain - virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {} + virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; /// Perform initialization of hero teleportation animation with terrain fade animation - virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {} - virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) {} + virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){}; - virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){}; + virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){}; - virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest){}; + virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; }; diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 79483b4df..730ec0b6b 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -60,14 +60,14 @@ BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) void BasicMapView::render(Canvas & target, bool fullUpdate) { Canvas targetClipped(target, pos); - - controller->update(GH.mainFPSmng->getElapsedMilliseconds()); tilesCache->update(controller->getContext()); tilesCache->render(controller->getContext(), targetClipped, fullUpdate); } void BasicMapView::show(SDL_Surface * to) { + controller->update(GH.mainFPSmng->getElapsedMilliseconds()); + Canvas target(to); CSDL_Ext::CClipRectGuard guard(to, pos); render(target, false); @@ -75,6 +75,8 @@ void BasicMapView::show(SDL_Surface * to) void BasicMapView::showAll(SDL_Surface * to) { + controller->update(0); + Canvas target(to); CSDL_Ext::CClipRectGuard guard(to, pos); render(target, true); diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index 854edd036..40402cb8f 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -24,7 +24,6 @@ MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr & model) : model(model) , owner(owner) - , curHoveredTile(-1, -1, -1) , isSwiping(false) { pos.w = model->getPixelsVisibleDimensions().x; @@ -47,17 +46,6 @@ void MapViewActions::setContext(const std::shared_ptr & con this->context = context; } -void MapViewActions::activate() -{ - CIntObject::activate(); -} - -void MapViewActions::deactivate() -{ - CIntObject::deactivate(); - curHoveredTile = int3(-1, -1, -1); //we lost info about hovered tile when disabling -} - void MapViewActions::clickLeft(tribool down, bool previousState) { if(indeterminate(down)) @@ -159,11 +147,7 @@ void MapViewActions::handleHover(const Point & cursorPosition) return; } - if(tile != curHoveredTile) - { - curHoveredTile = tile; - adventureInt->onTileHovered(tile); - } + adventureInt->onTileHovered(tile); } void MapViewActions::hover(bool on) diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index c2655f7b0..dc6d84e16 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -23,8 +23,6 @@ class MapViewActions : public CIntObject Point swipeInitialViewPos; Point swipeInitialRealPos; - int3 curHoveredTile; - MapView & owner; std::shared_ptr model; std::shared_ptr context; @@ -39,8 +37,6 @@ public: void setContext(const std::shared_ptr & context); - void activate() override; - void deactivate() override; void clickLeft(tribool down, bool previousState) override; void clickRight(tribool down, bool previousState) override; void clickMiddle(tribool down, bool previousState) override; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 5a8276612..c8b6599c7 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -251,6 +251,26 @@ void MapViewController::fadeInObject(const CGObjectInstance * obj) void MapViewController::removeObject(const CGObjectInstance * obj) { + if (obj->ID == Obj::BOAT) + { + auto * boat = dynamic_cast(obj); + if (boat->hero) + { + view->invalidate(context, boat->hero->id); + state->removeObject(boat->hero); + } + } + + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + { + view->invalidate(context, hero->boat->id); + state->removeObject(hero->boat); + } + } + view->invalidate(context, obj->id); state->removeObject(obj); } @@ -265,7 +285,7 @@ void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int { if(isEventVisible(obj, from, dest)) { - onObjectFadeOut(obj); + fadeOutObject(obj); setViewCenter(obj->getSightCenter()); } else @@ -288,7 +308,7 @@ void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const i { if(isEventVisible(obj, from, dest)) { - onObjectFadeIn(obj); + fadeInObject(obj); setViewCenter(obj->getSightCenter()); } addObject(obj); diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index ac92ad0d6..8fbf66c67 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -229,5 +229,6 @@ IMapObjectObserver::IMapObjectObserver() IMapObjectObserver::~IMapObjectObserver() { - CGI->mh->removeMapObserver(this); + if (CGI && CGI->mh) + CGI->mh->removeMapObserver(this); } diff --git a/client/render/IImage.h b/client/render/IImage.h index 0facf0ee9..8f53888ba 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -40,7 +40,8 @@ enum class EImageBlitMode : uint8_t class IImage { public: - using SpecialPalette = std::array; + using SpecialPalette = std::vector; + static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011; //draws image on surface "where" at position virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0; @@ -65,7 +66,7 @@ public: //only indexed bitmaps, 16 colors maximum virtual void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) = 0; - virtual void adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) = 0; + virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0; virtual void resetPalette(int colorID) = 0; virtual void resetPalette() = 0; @@ -73,7 +74,7 @@ public: virtual void setBlitMode(EImageBlitMode mode) = 0; //only indexed bitmaps with 7 special colors - virtual void setSpecialPallete(const SpecialPalette & SpecialPalette) = 0; + virtual void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) = 0; virtual void horizontalFlip() = 0; virtual void verticalFlip() = 0; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index e7c97a530..bd944dd40 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -308,7 +308,7 @@ void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32 } } -void SDLImage::adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) +void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) { if(originalPalette == nullptr) return; @@ -316,8 +316,11 @@ void SDLImage::adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) SDL_Palette* palette = surf->format->palette; // Note: here we skip first colors in the palette that are predefined in H3 images - for(int i = colorsToSkip; i < palette->ncolors; i++) + for(int i = 0; i < palette->ncolors; i++) { + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) + continue; + palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]); } } @@ -340,11 +343,17 @@ void SDLImage::resetPalette( int colorID ) SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1); } -void SDLImage::setSpecialPallete(const IImage::SpecialPalette & SpecialPalette) +void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) { if(surf->format->palette) { - CSDL_Ext::setColors(surf, const_cast(SpecialPalette.data()), 1, 7); + size_t last = std::min(specialPalette.size(), surf->format->palette->ncolors); + + for (size_t i = 0; i < last; ++i) + { + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) + surf->format->palette->colors[i] = specialPalette[i]; + } } } diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 437daceab..5b7894c3a 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -66,14 +66,14 @@ public: void verticalFlip() override; void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, size_t colorsToSkip) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; void resetPalette(int colorID) override; void resetPalette() override; void setAlpha(uint8_t value) override; void setBlitMode(EImageBlitMode mode) override; - void setSpecialPallete(const SpecialPalette & SpecialPalette) override; + void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) override; friend class SDLImageLoader; diff --git a/client/widgets/ObjectLists.cpp b/client/widgets/ObjectLists.cpp index dd26f08e8..17ab35cd8 100644 --- a/client/widgets/ObjectLists.cpp +++ b/client/widgets/ObjectLists.cpp @@ -35,7 +35,8 @@ std::shared_ptr CObjectList::createItem(size_t index) item->recActions = defActions; addChild(item.get()); - item->activate(); + if (active) + item->activate(); return item; } diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 5da9c4941..63030010a 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1210,7 +1210,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst CCastleInterface::~CCastleInterface() { - adventureInt->onAudioResumed(); + if (adventureInt) // may happen on exiting client with open castle interface + adventureInt->onAudioResumed(); if(LOCPLINT->castleInt == this) LOCPLINT->castleInt = nullptr; } diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 979b93b48..d73cb20c4 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -924,6 +924,9 @@ void CMarketplaceWindow::artifactsChanged(bool Left) toRemove.insert(item); removeItems(toRemove); + + // clear set to erase final instance of shared_ptr - we want to redraw screen only after it has been deleted + toRemove.clear(); redraw(); } diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index b9072f76a..a75cb7b97 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -85,6 +85,7 @@ std::shared_ptr CWindowObject::createBg(std::string imageName, bool pl return nullptr; auto image = std::make_shared(imageName); + image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); if(playerColored) image->colorize(LOCPLINT->playerID); return image; diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index db6360cea..602278338 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -258,6 +258,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/battle/IBattleInfoCallback.h ${MAIN_LIB_DIR}/battle/IBattleState.h ${MAIN_LIB_DIR}/battle/IUnitInfo.h + ${MAIN_LIB_DIR}/battle/PossiblePlayerBattleAction.h ${MAIN_LIB_DIR}/battle/ReachabilityInfo.h ${MAIN_LIB_DIR}/battle/SideInBattle.h ${MAIN_LIB_DIR}/battle/SiegeInfo.h diff --git a/config/battleEffects.json b/config/battleEffects.json index 7f08e608e..498290cd4 100644 --- a/config/battleEffects.json +++ b/config/battleEffects.json @@ -68,7 +68,7 @@ "alpha" : 0.0 }, { - "time" : 0.2 + "time" : 0.5 }, ], "teleportFadeOut" : [ @@ -76,7 +76,7 @@ "time" : 0.0 }, { - "time" : 0.2, + "time" : 0.5, "alpha" : 0.0 }, ], diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 64ab8ef22..125857289 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -64,12 +64,12 @@ }, "language" : { "type":"string", - "enum" : [ "chinese", "english", "german", "polish", "russian", "spanish", "ukrainian" ], + "enum" : [ "english", "chinese", "german", "polish", "russian", "spanish", "ukrainian" ], "default" : "english" }, "gameDataLanguage" : { "type":"string", - "enum" : [ "auto", "chinese", "english", "german", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ], + "enum" : [ "auto", "english", "chinese", "german", "korean", "polish", "russian", "spanish", "ukrainian", "other_cp1250", "other_cp1251", "other_cp1252" ], "default" : "auto" }, "lastSave" : { diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index a6abadb69..e6bcc32fa 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -145,7 +145,7 @@ Contact - + Kontakt @@ -242,7 +242,7 @@ Adventure Map AI - + AI mapy przygody @@ -261,18 +261,18 @@ Artificial Intelligence - + Sztuczna Inteligencja Mod Repositories - + Repozytoria modów Update now - + Zaktualizuj teraz @@ -285,37 +285,37 @@ Cursor - + Kursor Heroes III Data Language - + Język plików Heroes III Default - + Domyślny Hardware - + Sprzętowy Software - + Programowy Heroes III Translation - + Tłumaczenie Heroes III Check on startup - + Sprawdzaj przy uruchomieniu @@ -408,27 +408,27 @@ Active - Aktywny + Aktywny Disabled - + Wyłączone Enable - Włącz + Włącz Not Installed - + Nie zainstalowano Install - Zainstaluj + Zainstaluj @@ -436,134 +436,134 @@ Language - + Język Heroes III Data - + Pliki Heroes III Mods Preset - + Zestaw modów Your Heroes III data files have been successfully found. - + Twoje pliki Heroes III zostały pomyślnie znalezione. Optionally, you can install additional mods either now or at any point later: - + Opcjonalnie możesz zainstalować dodatkowe modyfikacje teraz lub później: Install support for playing Heroes III in resolutions other than 800x600. - + Zapinstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600. Install compatible version of addon Horn of the Abyss: fan-made Heroes III expansion, ported by VCMI team - + Zainstaluj kompatybilną wersję fanowskiego dodatku Horn of the Abyss przeportowaną przez zespół VCMI Install compatible version of addon "In The Wake of Gods": fan-made Heroes III expansion - + Zainstaluj kompatybilną wersję fanowskiego dodatku "In The Wake Of Gods" Finish - + Zakończ Step %v out of %m - + Krok %v z %m Choose your language - + Wybierz język Next - + Dalej Find Heroes III data files - + Znajdź pliki Heroes III Open help in browser - + Otwórz pomoc w przeglądarce Search again - + Szukaj ponownie If you don't have installed Heroes III copy, it is possible to use our automatic installation tool 'vcmibuilder' to extract data from GoG.com installer. Visit our wiki for detailed instructions. - + Jeśli nie masz zainstalowanej kopii Heroes III istnieje możliwość użycia naszego automatycznego narzędzia instalacyjnego 'vcmibuilder' by wyodrębnić dane z instalatora GoG.com. Odwiedź nasze wiki po szczegółowe instrukcje. VCMI requires Heroes III data files in one of the locations listed above. Please copy Heroes III data in one of these directories. - + VCMI wymaga plików Heroes III w jednej z wymienionych wyżej lokalizacji. Proszę, skopiuj pliki Heroes III do jednego z tych katalogów. Heroes III data files - + Pliki Heroes III Alternatively, you can select directory with installed Heroes III data and VCMI will copy exisiting data automatically. - + Możesz też wybrać folder z zainstalowanym Heroes III i VCMI automatycznie skopiuje istniejące dane. Copy existing data - + Skopiuj istniejące dane Your Heroes III language has been successfully detected. - + Twój język Heroes III został pomyślnie wykryty. Automatic detection of language failed. Please select language of your Heroes III copy - + Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III Heroes III language - + Język Heroes III Back - + Wstecz Install VCMI Mod Preset - + Zainstaluj zestaw modyfikacji Install translation of Heroes III to your language - + Zainstaluj tłumaczenie Heroes III dla twojego języka @@ -663,12 +663,12 @@ People in lobby - + Ludzie w lobby Lobby chat - + Czat lobby @@ -683,17 +683,17 @@ Resolve - + Rozwiąż New game - + Nowa gra Load game - + Wczytaj grę @@ -733,12 +733,12 @@ Disconnect - + Rozłącz No issues detected - + Nie znaleziono problemów @@ -779,7 +779,7 @@ Map Editor - + Edytor map diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 1f4b7e022..d803ebae9 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -684,7 +684,7 @@ void CModInfo::loadLocalData(const JsonNode & data) { bool validated = false; implicitlyEnabled = true; - explicitlyEnabled = true; + explicitlyEnabled = !config["keepDisabled"].Bool(); checksum = 0; if (data.getType() == JsonNode::JsonType::DATA_BOOL) { diff --git a/lib/GameConstants.h b/lib/GameConstants.h index d9265ffbf..d7b2c9f69 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1120,6 +1120,7 @@ public: LICHES = 64, BONE_DRAGON = 68, TROGLODYTES = 70, + MEDUSA = 76, HYDRA = 110, CHAOS_HYDRA = 111, AIR_ELEMENTAL = 112, @@ -1245,7 +1246,7 @@ class ObstacleInfo; class Obstacle : public BaseForID { INSTID_LIKE_CLASS_COMMON(Obstacle, si32) - + DLL_LINKAGE const ObstacleInfo * getInfo() const; DLL_LINKAGE operator std::string() const; DLL_LINKAGE static Obstacle fromString(const std::string & identifier); @@ -1295,7 +1296,6 @@ enum class EHealPower : ui8 // Typedef declarations typedef ui8 TFaction; typedef si64 TExpType; -typedef std::pair TDmgRange; typedef si32 TBonusSubtype; typedef si32 TQuantity; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 311c9fb34..bb473ef48 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -663,14 +663,14 @@ const IBonusBearer * BattleInfo::asBearer() const return this; } -int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const +int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const { - if(damage.first != damage.second) + if(damage.min != damage.max) { int64_t sum = 0; auto howManyToAv = std::min(10, attackerCount); - auto rangeGen = rng.getInt64Range(damage.first, damage.second); + auto rangeGen = rng.getInt64Range(damage.min, damage.max); for(int32_t g = 0; g < howManyToAv; ++g) sum += rangeGen(); @@ -679,7 +679,7 @@ int64_t BattleInfo::getActualDamage(const TDmgRange & damage, int32_t attackerCo } else { - return damage.first; + return damage.min; } } diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index ab53fed25..02dcafe45 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -97,7 +97,7 @@ public: uint32_t nextUnitId() const override; - int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; ////////////////////////////////////////////////////////////////////////// // IBattleState diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index adb20351c..c0198c530 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -15,6 +15,7 @@ #include "../CStack.h" #include "BattleInfo.h" #include "DamageCalculator.h" +#include "PossiblePlayerBattleAction.h" #include "../NetPacks.h" #include "../spells/CSpellHandler.h" #include "../mapObjects/CGTownInstance.h" @@ -218,11 +219,14 @@ std::vector CBattleInfoCallback::getClientActionsFor { if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? { - if(stack->hasBonusOfType(Bonus::SPELLCASTER) && data.creatureSpellToCast != -1) + if(stack->hasBonusOfType(Bonus::SPELLCASTER)) { - const CSpell *spell = SpellID(data.creatureSpellToCast).toSpell(); - PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); - allowedActionList.push_back(act); + for (auto const & spellID : data.creatureSpellsToCast) + { + const CSpell *spell = spellID.toSpell(); + PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); + allowedActionList.push_back(act); + } } if(stack->hasBonusOfType(Bonus::RANDOM_SPELLCASTER)) allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); @@ -251,7 +255,7 @@ std::vector CBattleInfoCallback::getClientActionsFor PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const { RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID); - PossiblePlayerBattleAction spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; + auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); @@ -264,7 +268,7 @@ PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * s else if(ti.type == spells::AimType::OBSTACLE) spellSelMode = PossiblePlayerBattleAction::OBSTACLE; - return spellSelMode; + return PossiblePlayerBattleAction(spellSelMode, spell->id); } std::set CBattleInfoCallback::battleGetAttackedHexes(const CStack* attacker, BattleHex destinationTile, BattleHex attackerPos) const @@ -715,57 +719,64 @@ bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHe return false; } -TDmgRange CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const +DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const { DamageCalculator calculator(*this, info); return calculator.calculateDmgRange(); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); auto reachability = battleGetDistances(attacker, attacker->getPosition()); int movementDistance = reachability[attackerPosition]; return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); const bool shooting = battleCanShoot(attacker, defender->getPosition()); const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); return battleEstimateDamage(bai, retaliationDmg); } -TDmgRange CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg) const +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const { - RETURN_IF_NOT_BATTLE(std::make_pair(0, 0)); + RETURN_IF_NOT_BATTLE({}); - TDmgRange ret = calculateDmgRange(bai); + DamageEstimation ret = calculateDmgRange(bai); if(retaliationDmg) { if(bai.shooting) { //FIXME: handle RANGED_RETALIATION - retaliationDmg->first = retaliationDmg->second = 0; + *retaliationDmg = DamageEstimation(); } else { //TODO: rewrite using boost::numeric::interval //TODO: rewire once more using interval-based fuzzy arithmetic - int64_t TDmgRange::* pairElems[] = {&TDmgRange::first, &TDmgRange::second}; - for (int i=0; i<2; ++i) + auto const & estimateRetaliation = [&]( int64_t damage) { auto retaliationAttack = bai.reverse(); - int64_t dmg = ret.*pairElems[i]; auto state = retaliationAttack.attacker->acquireState(); - state->damage(dmg); + state->damage(damage); retaliationAttack.attacker = state.get(); - retaliationDmg->*pairElems[!i] = calculateDmgRange(retaliationAttack).*pairElems[!i]; - } + return calculateDmgRange(retaliationAttack); + }; + + DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); + DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); + + retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); + retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); + + retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); + retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); } } @@ -993,12 +1004,19 @@ std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePe for(auto &oi : battleGetAllObstacles(whichSidePerspective)) { - if(battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) + if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) + continue; + + for(const auto & hex : oi->getStoppingTile()) { - range::copy(oi->getStoppingTile(), vstd::set_inserter(ret)); + if(hex == ESiegeHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) + { + if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) + continue; // this tile is disabled by drawbridge on top of it + } + ret.insert(hex); } } - return ret; } @@ -1646,12 +1664,16 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const int totalWeight = 0; for(const auto & b : *bl) { - totalWeight += std::max(b->additionalInfo[0], 1); //minimal chance to cast is 1 + totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them } + + if (totalWeight == 0) + return SpellID::NONE; + int randomPos = rand.nextInt(totalWeight - 1); for(const auto & b : *bl) { - randomPos -= std::max(b->additionalInfo[0], 1); + randomPos -= std::max(b->additionalInfo[0], 0); if(randomPos < 0) { return SpellID(b->subtype); diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index 5cc2c637d..dccdac50c 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -24,6 +24,7 @@ class CSpell; struct CObstacleInstance; class IBonusBearer; class CRandomGenerator; +class PossiblePlayerBattleAction; namespace spells { @@ -42,35 +43,9 @@ struct DLL_LINKAGE AttackableTiles } }; -enum class PossiblePlayerBattleAction // actions performed at l-click -{ - INVALID = -1, - CREATURE_INFO, - HERO_INFO, - MOVE_TACTICS, - CHOOSE_TACTICS_STACK, - - MOVE_STACK, - ATTACK, - WALK_AND_ATTACK, - ATTACK_AND_RETURN, - SHOOT, - CATAPULT, - HEAL, - - NO_LOCATION, // massive spells that affect every possible target, automatic casts - ANY_LOCATION, - OBSTACLE, - TELEPORT, - SACRIFICE, - RANDOM_GENIE_SPELL, // random spell on a friendly creature - FREE_LOCATION, // used with Force Field and Fire Wall - all tiles affected by spell must be free - AIMED_SPELL_CREATURE, // spell targeted at creature -}; - struct DLL_LINKAGE BattleClientInterfaceData { - si32 creatureSpellToCast; + std::vector creatureSpellsToCast; ui8 tacticsMode; }; @@ -117,14 +92,14 @@ public: bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack std::set battleAdjacentUnits(const battle::Unit * unit) const; - TDmgRange calculateDmgRange(const BattleAttackInfo & info) const; //charge - number of hexes travelled before attack (for champion's jousting); returns pair + DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; /// estimates damage dealt by attacker to defender; /// only non-random bonuses are considered in estimation /// returns pair - TDmgRange battleEstimateDamage(const BattleAttackInfo & bai, TDmgRange * retaliationDmg = nullptr) const; - TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, TDmgRange * retaliationDmg = nullptr) const; - TDmgRange battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, TDmgRange * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const; bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index e4c03ee97..ee130edb6 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -112,7 +112,10 @@ public: int32_t getFirstHPleft() const; int32_t getResurrected() const; + /// returns total remaining health int64_t available() const; + + /// returns total initial health int64_t total() const; void takeResurrected(); diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 6b6e490b9..80d31bf48 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -20,10 +20,10 @@ VCMI_LIB_NAMESPACE_BEGIN -TDmgRange DamageCalculator::getBaseDamageSingle() const +DamageRange DamageCalculator::getBaseDamageSingle() const { - double minDmg = 0.0; - double maxDmg = 0.0; + int64_t minDmg = 0.0; + int64_t maxDmg = 0.0; minDmg = info.attacker->getMinDamage(info.shooting); maxDmg = info.attacker->getMaxDamage(info.shooting); @@ -63,7 +63,7 @@ TDmgRange DamageCalculator::getBaseDamageSingle() const return { minDmg, maxDmg }; } -TDmgRange DamageCalculator::getBaseDamageBlessCurse() const +DamageRange DamageCalculator::getBaseDamageBlessCurse() const { const std::string cachingStrForcedMinDamage = "type_ALWAYS_MINIMUM_DAMAGE"; static const auto selectorForcedMinDamage = Selector::type()(Bonus::ALWAYS_MINIMUM_DAMAGE); @@ -76,10 +76,10 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const int curseBlessAdditiveModifier = blessEffects->totalValue() - curseEffects->totalValue(); - TDmgRange baseDamage = getBaseDamageSingle(); - TDmgRange modifiedDamage = { - std::max(static_cast(1), baseDamage.first + curseBlessAdditiveModifier), - std::max(static_cast(1), baseDamage.second + curseBlessAdditiveModifier) + DamageRange baseDamage = getBaseDamageSingle(); + DamageRange modifiedDamage = { + std::max(static_cast(1), baseDamage.min + curseBlessAdditiveModifier), + std::max(static_cast(1), baseDamage.max + curseBlessAdditiveModifier) }; if(curseEffects->size() && blessEffects->size() ) @@ -91,29 +91,29 @@ TDmgRange DamageCalculator::getBaseDamageBlessCurse() const if(curseEffects->size()) { return { - modifiedDamage.first, - modifiedDamage.first + modifiedDamage.min, + modifiedDamage.min }; } if(blessEffects->size()) { return { - modifiedDamage.second, - modifiedDamage.second + modifiedDamage.max, + modifiedDamage.max }; } return modifiedDamage; } -TDmgRange DamageCalculator::getBaseDamageStack() const +DamageRange DamageCalculator::getBaseDamageStack() const { auto stackSize = info.attacker->getCount(); auto baseDamage = getBaseDamageBlessCurse(); return { - baseDamage.first * stackSize, - baseDamage.second * stackSize + baseDamage.min * stackSize, + baseDamage.max * stackSize }; } @@ -450,6 +450,25 @@ std::vector DamageCalculator::getDefenseFactors() const }; } +DamageRange DamageCalculator::getCasualties(const DamageRange & damageDealt) const +{ + return { + getCasualties(damageDealt.min), + getCasualties(damageDealt.max), + }; +} + +int64_t DamageCalculator::getCasualties(int64_t damageDealt) const +{ + if (damageDealt < info.defender->getFirstHPleft()) + return 0; + + int64_t damageLeft = damageDealt - info.defender->getFirstHPleft(); + int64_t killsLeft = damageLeft / info.defender->MaxHealth(); + + return 1 + killsLeft; +} + int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const { auto noLimit = Selector::effectRange()(Bonus::NO_LIMIT); @@ -461,9 +480,9 @@ int DamageCalculator::battleBonusValue(const IBonusBearer * bearer, const CSelec return bearer->getBonuses(selector, noLimit.Or(limitMatches))->totalValue(); }; -TDmgRange DamageCalculator::calculateDmgRange() const +DamageEstimation DamageCalculator::calculateDmgRange() const { - TDmgRange result = getBaseDamageStack(); + DamageRange damageBase = getBaseDamageStack(); auto attackFactors = getAttackFactors(); auto defenseFactors = getDefenseFactors(); @@ -485,10 +504,16 @@ TDmgRange DamageCalculator::calculateDmgRange() const double resultingFactor = std::min(8.0, attackFactorTotal) * std::max( 0.01, defenseFactorTotal); - return { - std::max( 1.0, std::floor(result.first * resultingFactor)), - std::max( 1.0, std::floor(result.second * resultingFactor)) + info.defender->getTotalHealth(); + + DamageRange damageDealt { + std::max( 1.0, std::floor(damageBase.min * resultingFactor)), + std::max( 1.0, std::floor(damageBase.max * resultingFactor)) }; + + DamageRange killsDealt = getCasualties(damageDealt); + + return DamageEstimation{damageDealt, killsDealt}; } VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/DamageCalculator.h b/lib/battle/DamageCalculator.h index ad8111dc2..9548f950f 100644 --- a/lib/battle/DamageCalculator.h +++ b/lib/battle/DamageCalculator.h @@ -18,6 +18,8 @@ class CBattleInfoCallback; class IBonusBearer; class CSelector; struct BattleAttackInfo; +struct DamageRange; +struct DamageEstimation; class DLL_LINKAGE DamageCalculator { @@ -26,9 +28,12 @@ class DLL_LINKAGE DamageCalculator int battleBonusValue(const IBonusBearer * bearer, const CSelector & selector) const; - TDmgRange getBaseDamageSingle() const; - TDmgRange getBaseDamageBlessCurse() const; - TDmgRange getBaseDamageStack() const; + DamageRange getCasualties(const DamageRange & damageDealt) const; + int64_t getCasualties(int64_t damageDealt) const; + + DamageRange getBaseDamageSingle() const; + DamageRange getBaseDamageBlessCurse() const; + DamageRange getBaseDamageStack() const; int getActorAttackBase() const; int getActorAttackEffective() const; @@ -66,7 +71,7 @@ public: info(info) {} - TDmgRange calculateDmgRange() const; + DamageEstimation calculateDmgRange() const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 38e57f0dc..8f8ab8c80 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -26,6 +26,18 @@ namespace battle using UnitFilter = std::function; } +struct DamageRange +{ + int64_t min = 0; + int64_t max = 0; +}; + +struct DamageEstimation +{ + DamageRange damage; + DamageRange kills; +}; + #if SCRIPTING_ENABLED namespace scripting { diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 7165ed5b2..ab98a6d89 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -66,7 +66,7 @@ public: virtual uint32_t nextUnitId() const = 0; - virtual int64_t getActualDamage(const TDmgRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; + virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; }; class DLL_LINKAGE IBattleState : public IBattleInfo diff --git a/lib/battle/PossiblePlayerBattleAction.h b/lib/battle/PossiblePlayerBattleAction.h new file mode 100644 index 000000000..db5249567 --- /dev/null +++ b/lib/battle/PossiblePlayerBattleAction.h @@ -0,0 +1,78 @@ +/* + * CBattleInfoCallback.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ + +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class PossiblePlayerBattleAction // actions performed at l-click +{ +public: + enum Actions { + INVALID = -1, + CREATURE_INFO, + HERO_INFO, + MOVE_TACTICS, + CHOOSE_TACTICS_STACK, + + MOVE_STACK, + ATTACK, + WALK_AND_ATTACK, + ATTACK_AND_RETURN, + SHOOT, + CATAPULT, + HEAL, + + RANDOM_GENIE_SPELL, // random spell on a friendly creature + + NO_LOCATION, // massive spells that affect every possible target, automatic casts + ANY_LOCATION, + OBSTACLE, + TELEPORT, + SACRIFICE, + FREE_LOCATION, // used with Force Field and Fire Wall - all tiles affected by spell must be free + AIMED_SPELL_CREATURE, // spell targeted at creature + }; + +private: + Actions action; + SpellID spellToCast; + +public: + bool spellcast() const + { + return action == ANY_LOCATION || action == NO_LOCATION || action == OBSTACLE || action == TELEPORT || + action == SACRIFICE || action == FREE_LOCATION || action == AIMED_SPELL_CREATURE; + } + + Actions get() const + { + return action; + } + + SpellID spell() const + { + return spellToCast; + } + + PossiblePlayerBattleAction(Actions action, SpellID spellToCast = SpellID::NONE): + action(static_cast(action)), + spellToCast(spellToCast) + { + assert((spellToCast != SpellID::NONE) == spellcast()); + } + + bool operator == (const PossiblePlayerBattleAction & other) const + { + return action == other.action && spellToCast == other.spellToCast; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/Unit.h b/lib/battle/Unit.h index 5c0c75904..424e4f628 100644 --- a/lib/battle/Unit.h +++ b/lib/battle/Unit.h @@ -70,10 +70,19 @@ public: virtual bool canShoot() const = 0; virtual bool isShooter() const = 0; + /// returns initial size of this unit virtual int32_t getCount() const = 0; + + /// returns remaining health of first unit virtual int32_t getFirstHPleft() const = 0; + + /// returns total amount of killed in this unit virtual int32_t getKilled() const = 0; + + /// returns total health that unit still has virtual int64_t getAvailableHealth() const = 0; + + /// returns total health that unit had initially virtual int64_t getTotalHealth() const = 0; virtual int getTotalAttacks(bool ranged) const = 0; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index bb12ba10d..a41e8d1e7 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -12,7 +12,7 @@ #include "CGTownInstance.h" #include "CObjectClassesHandler.h" #include "../spells/CSpellHandler.h" - +#include "../battle/IBattleInfoCallback.h" #include "../NetPacks.h" #include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" @@ -807,7 +807,7 @@ void CGTownInstance::addTownBonuses() } } -TDmgRange CGTownInstance::getTowerDamageRange() const +DamageRange CGTownInstance::getTowerDamageRange() const { assert(hasBuilt(BuildingID::CASTLE)); @@ -825,7 +825,7 @@ TDmgRange CGTownInstance::getTowerDamageRange() const }; } -TDmgRange CGTownInstance::getKeepDamageRange() const +DamageRange CGTownInstance::getKeepDamageRange() const { assert(hasBuilt(BuildingID::CITADEL)); diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 47b497431..2eb63be11 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -20,6 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CCastleEvent; class CGTownInstance; class CGDwelling; +struct DamageRange; class DLL_LINKAGE CSpecObjInfo { @@ -332,10 +333,10 @@ public: void deleteTownBonus(BuildingID::EBuildingID bid); /// Returns damage range for secondary towers of this town - TDmgRange getTowerDamageRange() const; + DamageRange getTowerDamageRange() const; /// Returns damage range for central tower(keep) of this town - TDmgRange getKeepDamageRange() const; + DamageRange getKeepDamageRange() const; const CTown * getTown() const ; diff --git a/lib/mapObjects/CObjectClassesHandler.cpp b/lib/mapObjects/CObjectClassesHandler.cpp index 0cc0029cb..b86520095 100644 --- a/lib/mapObjects/CObjectClassesHandler.cpp +++ b/lib/mapObjects/CObjectClassesHandler.cpp @@ -399,12 +399,43 @@ void CObjectClassesHandler::afterLoadFinalization() } } + generateExtraMonolithsForRMG(); +} + +void CObjectClassesHandler::generateExtraMonolithsForRMG() +{ //duplicate existing two-way portals to make reserve for RMG auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects; - size_t portalCount = portalVec.size(); + //FIXME: Monoliths in this vector can be already not useful for every terrain + const size_t portalCount = portalVec.size(); - for (size_t i = portalCount; i < 100; ++i) - portalVec.push_back(portalVec[static_cast(i % portalCount)]); + //Invalid portals will be skipped and portalVec size stays unchanged + for (size_t i = portalCount; portalVec.size() < 100; ++i) + { + auto index = static_cast(i % portalCount); + auto portal = portalVec[index]; + auto templates = portal->getTemplates(); + if (templates.empty() || !templates[0]->canBePlacedAtAnyTerrain()) + { + continue; //Do not clone HoTA water-only portals or any others we can't use + } + + //deep copy of noncopyable object :? + auto newPortal = std::make_shared>(); + newPortal->rmgInfo = portal->getRMGInfo(); + newPortal->base = portal->base; //not needed? + newPortal->templates = portal->getTemplates(); + newPortal->sounds = portal->getSounds(); + newPortal->aiValue = portal->getAiValue(); + newPortal->battlefield = portal->battlefield; //getter is not initialized at this point + newPortal->modScope = portal->modScope; //private + newPortal->typeName = portal->getTypeName(); + newPortal->subTypeName = std::string("monolith") + std::to_string(portalVec.size()); + newPortal->type = portal->getIndex(); + + newPortal->subtype = portalVec.size(); //indexes must be unique, they are returned as a set + portalVec.push_back(newPortal); + } } std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const diff --git a/lib/mapObjects/CObjectClassesHandler.h b/lib/mapObjects/CObjectClassesHandler.h index fed6b1dde..ef5035935 100644 --- a/lib/mapObjects/CObjectClassesHandler.h +++ b/lib/mapObjects/CObjectClassesHandler.h @@ -292,6 +292,8 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); + void generateExtraMonolithsForRMG(); + public: CObjectClassesHandler(); ~CObjectClassesHandler(); diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 845be7a94..788dfbadf 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -102,6 +102,11 @@ public: return visitDir & 2; }; + inline bool canBePlacedAtAnyTerrain() const + { + return anyTerrain; + }; + // Checks if object can be placed on specific terrain bool canBePlacedAt(TerrainId terrain) const; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 2478687ea..4f13b446d 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -367,10 +367,24 @@ void CMapGenerator::addHeaderInfo() int CMapGenerator::getNextMonlithIndex() { - if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size()) - throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); - else - return monolithIndex++; + while (true) + { + if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size()) + throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); + else + { + //Skip modded Monoliths which can't beplaced on every terrain + auto templates = VLC->objtypeh->getHandlerFor(Obj::MONOLITH_TWO_WAY, monolithIndex)->getTemplates(); + if (templates.empty() || !templates[0]->canBePlacedAtAnyTerrain()) + { + monolithIndex++; + } + else + { + return monolithIndex++; + } + } + } } int CMapGenerator::getPrisonsRemaning() const diff --git a/lib/rmg/ConnectionsPlacer.cpp b/lib/rmg/ConnectionsPlacer.cpp index f5131935c..20ce47f14 100644 --- a/lib/rmg/ConnectionsPlacer.cpp +++ b/lib/rmg/ConnectionsPlacer.cpp @@ -100,15 +100,20 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con borderPos = *RandomGeneratorUtil::nextItem(directConnectionIterator->second, generator.rand); guardPos = zone.areaPossible().nearest(borderPos); assert(borderPos != guardPos); - - auto safetyGap = rmg::Area({guardPos}); - safetyGap.unite(safetyGap.getBorderOutside()); - safetyGap.intersect(zone.areaPossible()); - if(!safetyGap.empty()) + + float dist = map.getTile(guardPos).getNearestObjectDistance(); + if (dist >= 3) //Don't place guards at adjacent tiles { - safetyGap.intersect(otherZone->areaPossible()); - if(safetyGap.empty()) - break; //successfull position + + auto safetyGap = rmg::Area({ guardPos }); + safetyGap.unite(safetyGap.getBorderOutside()); + safetyGap.intersect(zone.areaPossible()); + if (!safetyGap.empty()) + { + safetyGap.intersect(otherZone->areaPossible()); + if (safetyGap.empty()) + break; //successfull position + } } //failed position @@ -150,6 +155,8 @@ void ConnectionsPlacer::selfSideDirectConnection(const rmg::ZoneConnection & con rmg::Object monster(*monsterType); monster.setPosition(guardPos); manager.placeObject(monster, false, true); + //Place objects away from the monster in the other zone, too + otherZone->getModificator()->updateDistances(monster); } else { @@ -225,8 +232,10 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2 ](const int3 & tile) { auto ti = map.getTile(tile); + auto otherTi = map.getTile(tile - zShift); float dist = ti.getNearestObjectDistance(); - if(dist < minDist) + float otherDist = otherTi.getNearestObjectDistance(); + if(dist < minDist || otherDist < minDist) return -1.f; rmg::Area toPlace(rmgGate1.getArea() + rmgGate1.getAccessibleArea()); @@ -234,8 +243,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c path2 = managerOther.placeAndConnectObject(toPlace, rmgGate2, minDist, guarded2, true, ObjectManager::OptimizeType::NONE); - return path2.valid() ? 1.f : -1.f; - }, guarded1, true, ObjectManager::OptimizeType::NONE); + return path2.valid() ? (dist + otherDist) : -1.f; + }, guarded1, true, ObjectManager::OptimizeType::DISTANCE); if(path1.valid() && path2.valid()) { diff --git a/lib/rmg/WaterProxy.cpp b/lib/rmg/WaterProxy.cpp index 5dbf679ac..809b24a47 100644 --- a/lib/rmg/WaterProxy.cpp +++ b/lib/rmg/WaterProxy.cpp @@ -209,24 +209,28 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, RouteInfo & info) auto * manager = zone.getModificator(); if(!manager) return false; - + auto subObjects = VLC->objtypeh->knownSubObjects(Obj::BOAT); auto * boat = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(subObjects, generator.rand))->create()); rmg::Object rmgObject(*boat); rmgObject.setTemplate(zone.getTerrainType()); - + auto waterAvailable = zone.areaPossible() + zone.freePaths(); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles coast.intersect(land.areaPossible() + land.freePaths()); //having only available land tiles - auto boardingPositions = coast.getSubarea([&waterAvailable](const int3 & tile) //tiles where boarding is possible - { - rmg::Area a({tile}); - a = a.getBorderOutside(); - a.intersect(waterAvailable); - return !a.empty(); - }); - + auto boardingPositions = coast.getSubarea([&waterAvailable, this](const int3 & tile) //tiles where boarding is possible + { + //We don't want place boat right to any land object, especiallly the zone guard + if (map.getTile(tile).getNearestObjectDistance() <= 3) + return false; + + rmg::Area a({tile}); + a = a.getBorderOutside(); + a.intersect(waterAvailable); + return !a.empty(); + }); + while(!boardingPositions.empty()) { auto boardingPosition = *boardingPositions.getTiles().begin(); @@ -239,27 +243,28 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, RouteInfo & info) boardingPositions.erase(boardingPosition); continue; } - + //try to place boat at water, create paths on water and land - auto path = manager->placeAndConnectObject(shipPositions, rmgObject, 2, false, true, ObjectManager::OptimizeType::NONE); + auto path = manager->placeAndConnectObject(shipPositions, rmgObject, 4, false, true, ObjectManager::OptimizeType::NONE); auto landPath = land.searchPath(boardingPosition, false); if(!path.valid() || !landPath.valid()) { boardingPositions.erase(boardingPosition); continue; } - + info.blocked = rmgObject.getArea(); info.visitable = rmgObject.getVisitablePosition(); info.boarding = boardingPosition; info.water = shipPositions; - + zone.connectPath(path); land.connectPath(landPath); manager->placeObject(rmgObject, false, true); + land.getModificator()->updateDistances(rmgObject); //Keep land objects away from the boat break; } - + return !boardingPositions.empty(); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9557e7939..18ba84335 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1233,7 +1233,7 @@ int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptrcurB->calculateDmgRange(bai); - bsa.damageAmount = gs->curB->getActualDamage(range, attackerState->getCount(), getRandomGenerator()); + bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator()); CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties } @@ -4910,8 +4910,8 @@ bool CGameHandler::makeBattleAction(BattleAction &ba) const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); SpellID spellID = SpellID(ba.actionSubtype); - std::shared_ptr randSpellcaster = stack->getBonusLocalFirst(Selector::type()(Bonus::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonusLocalFirst(Selector::typeSubtype(Bonus::SPELLCASTER, spellID)); + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(Bonus::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(Bonus::SPELLCASTER, spellID)); //TODO special bonus for genies ability if (randSpellcaster && battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0)