1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-27 00:41:08 +02:00

Merge remote-tracking branch 'origin/develop' into custom_objects_per_zone

# Conflicts:
#	lib/rmg/CRmgTemplate.cpp
This commit is contained in:
Tomasz Zieliński
2024-09-14 10:19:22 +02:00
717 changed files with 28274 additions and 13108 deletions

View File

@ -183,6 +183,11 @@ jobs:
distribution: 'temurin' distribution: 'temurin'
java-version: '11' java-version: '11'
# a hack to build ID for x64 build in order for Google Play to allow upload of both 32 and 64 bit builds
- name: Bump Android x64 build ID
if: ${{ matrix.platform == 'android-64' }}
run: perl -i -pe 's/versionCode (\d+)/$x=$1+1; "versionCode $x"/e' android/vcmi-app/build.gradle
- name: Build Number - name: Build Number
run: | run: |
source '${{github.workspace}}/CI/get_package_name.sh' source '${{github.workspace}}/CI/get_package_name.sh'

2
.gitmodules vendored
View File

@ -1,7 +1,7 @@
[submodule "test/googletest"] [submodule "test/googletest"]
path = test/googletest path = test/googletest
url = https://github.com/google/googletest url = https://github.com/google/googletest
branch = v1.13.x branch = v1.15.x
[submodule "AI/FuzzyLite"] [submodule "AI/FuzzyLite"]
path = AI/FuzzyLite path = AI/FuzzyLite
url = https://github.com/fuzzylite/fuzzylite.git url = https://github.com/fuzzylite/fuzzylite.git

View File

@ -12,6 +12,10 @@
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used, // Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely // CUnitState should be private and CStack should be removed completely
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/ObstacleCasterProxy.h"
#include "../../lib/battle/CObstacleInstance.h"
uint64_t averageDmg(const DamageRange & range) uint64_t averageDmg(const DamageRange & range)
{ {
@ -25,9 +29,57 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit
damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount(); damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
} }
void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side)
{ {
for(const auto & obst : hb->battleGetAllObstacles(side))
{
auto spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(obst.get());
if(!spellObstacle || !obst->triggersEffects())
continue;
auto triggerAbility = VLC->spells()->getById(obst->getTrigger());
auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
if(!triggerIsNegative)
continue;
const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide);
auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
auto affectedHexes = obst->getAffectedTiles();
auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool {
return u->alive() && !u->isTurret() && u->getPosition().isValid();
});
for(auto stack : stacks)
{
std::shared_ptr<HypotheticBattle> inner = std::make_shared<HypotheticBattle>(hb->env, hb);
auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell());
auto updated = inner->getForUpdate(stack->unitId());
spells::Target target;
target.push_back(spells::Destination(updated.get()));
cast.castEval(inner->getServerCallback(), target);
auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth();
for(auto hex : affectedHexes)
{
obstacleDamage[hex][stack->unitId()] = damageDealt;
}
}
}
}
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
{
if(parent == nullptr)
{
buildObstacleDamageCache(hb, side);
}
auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool
{ {
return u->isValidTarget(); return u->isValidTarget();
@ -70,6 +122,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit
return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
} }
int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender)
{
if(parent)
return parent->getObstacleDamage(hex, defender);
auto damages = obstacleDamage.find(hex);
if(damages == obstacleDamage.end())
return 0;
auto damage = damages->second.find(defender->unitId());
return damage == damages->second.end()
? 0
: damage->second;
}
int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb) int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{ {
if(parent) if(parent)
@ -93,6 +162,8 @@ int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const batt
AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: from(from), dest(dest), attack(attack) : from(from), dest(dest), attack(attack)
{ {
this->attack.attackerPos = from;
this->attack.defenderPos = dest;
} }
float AttackPossibility::damageDiff() const float AttackPossibility::damageDiff() const
@ -199,6 +270,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting) if(attackInfo.shooting)
return 0; return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker; auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex); auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
@ -206,9 +279,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true); auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker)) if(!st || !state->battleMatchOwner(st, attacker))
continue; continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st)) if(!state->battleCanShoot(st))
continue; continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true); BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex; rangeAttackInfo.defenderPos = hex;
@ -218,9 +295,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain; res += gain * cachedDmg / std::max<uint64_t>(1, averageDmg(rangeDmg.damage));
} }
return res; return res;
@ -243,7 +321,7 @@ AttackPossibility AttackPossibility::evaluate(
std::vector<BattleHex> defenderHex; std::vector<BattleHex> defenderHex;
if(attackInfo.shooting) if(attackInfo.shooting)
defenderHex = defender->getHexes(); defenderHex.push_back(defender->getPosition());
else else
defenderHex = CStack::meleeAttackHexes(attacker, defender, hex); defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
@ -261,63 +339,114 @@ AttackPossibility AttackPossibility::evaluate(
if (!attackInfo.shooting) if (!attackInfo.shooting)
ap.attackerState->setPosition(hex); ap.attackerState->setPosition(hex);
std::vector<const battle::Unit*> units; std::vector<const battle::Unit *> defenderUnits;
std::vector<const battle::Unit *> retaliatedUnits = {attacker};
std::vector<const battle::Unit *> affectedUnits;
if (attackInfo.shooting) if (attackInfo.shooting)
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
else else
units = state->getAttackedBattleUnits(attacker, defHex, false, hex); {
defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, false, hex, defender->getPosition());
retaliatedUnits = state->getAttackedBattleUnits(defender, attacker, hex, false, defender->getPosition(), hex);
// attacker can not melle-attack itself but still can hit that place where it was before moving
vstd::erase_if(defenderUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); });
if(!vstd::contains_if(retaliatedUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); }))
{
retaliatedUnits.push_back(attacker);
}
auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker);
if(obstacleDamage > 0)
{
ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state);
ap.attackerState->damage(obstacleDamage);
}
}
// ensure the defender is also affected // ensure the defender is also affected
bool addDefender = true; if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); }))
for(auto unit : units)
{ {
if (unit->unitId() == defender->unitId()) defenderUnits.push_back(defender);
{
addDefender = false;
break;
}
} }
if(addDefender) affectedUnits = defenderUnits;
units.push_back(defender); vstd::concatenate(affectedUnits, retaliatedUnits);
for(auto u : units) logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex.hex, defHex.hex);
std::map<uint32_t, std::shared_ptr<battle::CUnitState>> defenderStates;
for(auto u : affectedUnits)
{ {
if(!ap.attackerState->alive()) if(u->unitId() == attacker->unitId())
break; continue;
auto defenderState = u->acquireState(); auto defenderState = u->acquireState();
ap.affectedUnits.push_back(defenderState); ap.affectedUnits.push_back(defenderState);
defenderStates[u->unitId()] = defenderState;
}
for(int i = 0; i < totalAttacks; i++) for(int i = 0; i < totalAttacks; i++)
{ {
if(!ap.attackerState->alive() || !defenderStates[defender->unitId()]->alive())
break;
for(auto u : defenderUnits)
{
auto defenderState = defenderStates.at(u->unitId());
int64_t damageDealt; int64_t damageDealt;
int64_t damageReceived;
float defenderDamageReduce; float defenderDamageReduce;
float attackerDamageReduce; float attackerDamageReduce;
DamageEstimation retaliation; DamageEstimation retaliation;
auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage); damageDealt = averageDmg(attackDmg.damage);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state); vstd::amin(damageDealt, defenderState->getAvailableHealth());
defenderDamageReduce = calculateDamageReduce(attacker, u, damageDealt, damageCache, state);
ap.attackerState->afterAttack(attackInfo.shooting, false); ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation //FIXME: use ranged retaliation
damageReceived = 0;
attackerDamageReduce = 0; attackerDamageReduce = 0;
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{ {
damageReceived = averageDmg(retaliation.damage); for(auto retaliated : retaliatedUnits)
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state); {
if(retaliated->unitId() == attacker->unitId())
{
int64_t damageReceived = averageDmg(retaliation.damage);
vstd::amin(damageReceived, ap.attackerState->getAvailableHealth());
attackerDamageReduce = calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
ap.attackerState->damage(damageReceived);
}
else
{
auto retaliationCollateral = state->battleEstimateDamage(defender, retaliated, 0);
int64_t damageReceived = averageDmg(retaliationCollateral.damage);
vstd::amin(damageReceived, retaliated->getAvailableHealth());
if(defender->unitSide() == retaliated->unitSide())
defenderDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
else
ap.collateralDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
defenderStates.at(retaliated->unitId())->damage(damageReceived);
}
}
defenderState->afterAttack(attackInfo.shooting, true); defenderState->afterAttack(attackInfo.shooting, true);
} }
@ -331,20 +460,29 @@ AttackPossibility AttackPossibility::evaluate(
if(attackerSide == u->unitSide()) if(attackerSide == u->unitSide())
ap.collateralDamageReduce += defenderDamageReduce; ap.collateralDamageReduce += defenderDamageReduce;
if(u->unitId() == defender->unitId() || if(u->unitId() == defender->unitId()
(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex))) || (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
{ {
//FIXME: handle RANGED_RETALIATION ? //FIXME: handle RANGED_RETALIATION ?
ap.attackerDamageReduce += attackerDamageReduce; ap.attackerDamageReduce += attackerDamageReduce;
} }
ap.attackerState->damage(damageReceived);
defenderState->damage(damageDealt); defenderState->damage(damageDealt);
if (!ap.attackerState->alive() || !defenderState->alive()) if(u->unitId() == defender->unitId())
break; {
ap.defenderDead = !defenderState->alive();
} }
} }
}
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
attackInfo.attacker->unitType()->getJsonKey(),
attackInfo.defender->unitType()->getJsonKey(),
(int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(),
ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg);
#endif
if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue()) if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
bestAp = ap; bestAp = ap;

View File

@ -18,16 +18,20 @@ class DamageCache
{ {
private: private:
std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache; std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache;
std::map<BattleHex, std::unordered_map<uint32_t, int64_t>> obstacleDamage;
DamageCache * parent; DamageCache * parent;
void buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
public: public:
DamageCache() : parent(nullptr) {} DamageCache() : parent(nullptr) {}
DamageCache(DamageCache * parent) : parent(parent) {} DamageCache(DamageCache * parent) : parent(parent) {}
void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb); void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb); int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
int64_t getObstacleDamage(BattleHex hex, const battle::Unit * defender);
int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb); int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side); void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
}; };
/// <summary> /// <summary>
@ -49,6 +53,7 @@ public:
float attackerDamageReduce = 0; //usually by counter-attack float attackerDamageReduce = 0; //usually by counter-attack
float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0; int64_t shootersBlockedDmg = 0;
bool defenderDead = false;
AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);

View File

@ -23,15 +23,17 @@
#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/StartInfo.h"
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used, // Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely // CUnitState should be private and CStack should be removed completely
#include "../../lib/logging/VisualLogger.h"
#define LOGL(text) print(text) #define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
CBattleAI::CBattleAI() CBattleAI::CBattleAI()
: side(-1), : side(BattleSide::NONE),
wasWaitingForRealize(false), wasWaitingForRealize(false),
wasUnlockingGs(false) wasUnlockingGs(false)
{ {
@ -47,6 +49,17 @@ CBattleAI::~CBattleAI()
} }
} }
void logHexNumbers()
{
#if BATTLE_TRACE_LEVEL >= 1
logVisual->updateWithLock("hexes", [](IVisualLogBuilder & b)
{
for(BattleHex hex = BattleHex(0); hex < GameConstants::BFIELD_SIZE; hex = BattleHex(hex + 1))
b.addText(hex, std::to_string(hex.hex));
});
#endif
}
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{ {
env = ENV; env = ENV;
@ -57,6 +70,8 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
CB->waitTillRealize = false; CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false; CB->unlockGsWhenWaiting = false;
movesSkippedByDefense = 0; movesSkippedByDefense = 0;
logHexNumbers();
} }
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
@ -86,7 +101,7 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
} }
static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side) static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, BattleSide side)
{ {
auto stacks = cb->battleGetAllStacks(); auto stacks = cb->battleGetAllStacks();
auto our = 0; auto our = 0;
@ -108,6 +123,11 @@ static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy; return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
} }
int getSimulationTurnsCount(const StartInfo * startInfo)
{
return startInfo->difficulty < 4 ? 2 : 10;
}
void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{ {
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
@ -140,7 +160,10 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
logAi->trace("Build evaluator and targets"); logAi->trace("Build evaluator and targets");
#endif #endif
BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); BattleEvaluator evaluator(
env, cb, stack, playerID, battleID, side,
getStrengthRatio(cb->getBattle(battleID), side),
getSimulationTurnsCount(env->game()->getStartInfo()));
result = evaluator.selectStackAction(stack); result = evaluator.selectStackAction(stack);
@ -206,7 +229,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
{ {
auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) if(wallState != EWallState::NONE && wallState != EWallState::DESTROYED)
{ {
targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
break; break;
@ -229,7 +252,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
return attack; return attack;
} }
void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed)
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
side = Side; side = Side;

View File

@ -27,7 +27,7 @@ struct CurrentOffensivePotential
std::map<const CStack *, PotentialTargets> ourAttacks; std::map<const CStack *, PotentialTargets> ourAttacks;
std::map<const CStack *, PotentialTargets> enemyAttacks; std::map<const CStack *, PotentialTargets> enemyAttacks;
CurrentOffensivePotential(ui8 side) CurrentOffensivePotential(BattleSide side)
{ {
for(auto stack : cbc->battleGetStacks()) for(auto stack : cbc->battleGetStacks())
{ {
@ -54,7 +54,7 @@ struct CurrentOffensivePotential
class CBattleAI : public CBattleGameInterface class CBattleAI : public CBattleGameInterface
{ {
int side; BattleSide side;
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
@ -80,7 +80,7 @@ public:
BattleAction useCatapult(const BattleID & battleID, const CStack *stack); BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
BattleAction useHealingTent(const BattleID & battleID, const CStack *stack); BattleAction useHealingTent(const BattleID & battleID, const CStack *stack);
void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
@ -93,7 +93,7 @@ public:
//void battleSpellCast(const BattleSpellCast *sc) override; //void battleSpellCast(const BattleSpellCast *sc) override;
//void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
//void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right //void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side) override; //called by engine when battle starts; side=0 - left, side=1 - right
//void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
AutocombatPreferences autobattlePreferences = AutocombatPreferences(); AutocombatPreferences autobattlePreferences = AutocombatPreferences();
}; };

View File

@ -17,6 +17,7 @@
#include "../../lib/CStopWatch.h" #include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h" #include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/entities/building/TownFortifications.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
@ -49,6 +50,43 @@ SpellTypes spellType(const CSpell * spell)
return SpellTypes::OTHER; return SpellTypes::OTHER;
} }
BattleEvaluator::BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
BattleSide side,
float strengthRatio,
int simulationTurnsCount)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount),
cachedAttack(), playerID(playerID), side(side), env(env),
cb(cb), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
}
BattleEvaluator::BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
BattleSide side,
float strengthRatio,
int simulationTurnsCount)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount),
cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb),
damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
{
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
}
std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
{ {
std::vector<BattleHex> result; std::vector<BattleHex> result;
@ -139,8 +177,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
auto & bestAttack = evaluationResult.bestAttack; auto & bestAttack = evaluationResult.bestAttack;
cachedAttack = bestAttack; cachedAttack.ap = bestAttack;
cachedScore = evaluationResult.score; cachedAttack.score = evaluationResult.score;
cachedAttack.turn = 0;
cachedAttack.waited = evaluationResult.wait;
//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@ -167,7 +207,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score score
); );
if (moveTarget.scorePerTurn <= score) if (moveTarget.score <= score)
{ {
if(evaluationResult.wait) if(evaluationResult.wait)
{ {
@ -186,37 +226,64 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
{ {
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);
} }
else
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
{ {
activeActionMade = true; return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); });
bool isTargetOutsideFort = bestAttack.dest.getY() < GameConstants::BFIELD_WIDTH - 4;
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& !bestAttack.attack.shooting
&& hb->battleGetFortifications().hasMoat
&& !enemyMellee.empty()
&& isTargetOutsideFort;
if(siegeDefense)
{
logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex);
BattleAttackInfo bai(stack, stack, 0, false);
AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai);
float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb);
if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0))
{
return BattleAction::makeDefend(stack);
} }
} }
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from);
}
} }
} }
} }
//ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code. //ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
if(moveTarget.scorePerTurn > score) if(moveTarget.score > score)
{ {
score = moveTarget.score; score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack; cachedAttack.ap = moveTarget.cachedAttack;
cachedScore = score; cachedAttack.score = score;
cachedAttack.turn = moveTarget.turnsToRich;
if(stack->waited()) if(stack->waited())
{ {
logAi->debug( logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f", "Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(), stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(), moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex, moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score, moveTarget.score);
moveTarget.scorePerTurn);
return goTowardsNearest(stack, moveTarget.positions); return goTowardsNearest(stack, moveTarget.positions, *targets);
} }
else else
{ {
cachedAttack.waited = true;
return BattleAction::makeWait(stack); return BattleAction::makeWait(stack);
} }
} }
@ -224,7 +291,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(score <= EvaluationResult::INEFFECTIVE_SCORE if(score <= EvaluationResult::INEFFECTIVE_SCORE
&& !stack->hasBonusOfType(BonusType::FLYING) && !stack->hasBonusOfType(BonusType::FLYING)
&& stack->unitSide() == BattleSide::ATTACKER && stack->unitSide() == BattleSide::ATTACKER
&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL) && cb->getBattle(battleID)->battleGetFortifications().hasMoat)
{ {
auto brokenWallMoat = getBrokenWallMoatHexes(); auto brokenWallMoat = getBrokenWallMoatHexes();
@ -235,7 +302,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else else
return goTowardsNearest(stack, brokenWallMoat); return goTowardsNearest(stack, brokenWallMoat, *targets);
} }
} }
@ -249,7 +316,32 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
} }
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets)
{
auto additionalScore = 0;
std::optional<AttackPossibility> attackOnTheWay;
for(auto & target : targets.possibleAttacks)
{
if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore)
{
additionalScore = target.attackValue();
attackOnTheWay = target;
}
}
if(attackOnTheWay)
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from);
}
else
{
return BattleAction::makeMove(stack, hex);
}
}
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets)
{ {
auto reachability = cb->getBattle(battleID)->getReachability(stack); auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
@ -261,49 +353,38 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
std::vector<BattleHex> targetHexes = hexes; std::vector<BattleHex> targetHexes = hexes;
for(int i = 0; i < 5; i++) vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
{
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{ {
return reachability.distances[h1] < reachability.distances[h2]; return reachability.distances[h1] < reachability.distances[h2];
}); });
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
logAi->trace("No richable hexes.");
return BattleAction::makeDefend(stack);
}
// this turn
for(auto hex : targetHexes) for(auto hex : targetHexes)
{ {
if(vstd::contains(avHexes, hex)) if(vstd::contains(avHexes, hex))
{ {
return BattleAction::makeMove(stack, hex); return moveOrAttack(stack, hex, targets);
} }
if(stack->coversPos(hex)) if(stack->coversPos(hex))
{ {
logAi->warn("Warning: already standing on neighbouring tile!"); logAi->warn("Warning: already standing on neighbouring hex!");
//We shouldn't even be here... //We shouldn't even be here...
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);
} }
} }
if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE) // not this turn
{
break;
}
std::vector<BattleHex> copy = targetHexes;
for(auto hex : copy)
vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
vstd::erase_if(targetHexes, [](const BattleHex & hex) {return !hex.isValid();});
vstd::removeDuplicates(targetHexes);
}
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
return BattleAction::makeDefend(stack);
}
scoreEvaluator.updateReachabilityMap(hb); scoreEvaluator.updateReachabilityMap(hb);
if(stack->hasBonusOfType(BonusType::FLYING)) if(stack->hasBonusOfType(BonusType::FLYING))
@ -343,7 +424,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
}); });
return BattleAction::makeMove(stack, *nearestAvailableHex); return moveOrAttack(stack, *nearestAvailableHex, targets);
} }
else else
{ {
@ -357,11 +438,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
if(vstd::contains(avHexes, currentDest) if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest); {
return moveOrAttack(stack, currentDest, targets);
}
currentDest = reachability.predecessors[currentDest]; currentDest = reachability.predecessors[currentDest];
} }
} }
logAi->error("We should either detect that hexes are unreachable or make a move!");
return BattleAction::makeDefend(stack);
} }
bool BattleEvaluator::canCastSpell() bool BattleEvaluator::canCastSpell()
@ -391,7 +477,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
vstd::erase_if(possibleSpells, [](const CSpell *s) vstd::erase_if(possibleSpells, [](const CSpell *s)
{ {
return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; return spellType(s) != SpellTypes::BATTLE;
}); });
LOGFL("I know how %d of them works.", possibleSpells.size()); LOGFL("I know how %d of them works.", possibleSpells.size());
@ -402,9 +488,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
{ {
spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell); spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
if(spell->getTargetType() == spells::AimType::LOCATION)
continue;
const bool FAST = true; const bool FAST = true;
for(auto & target : temp.findPotentialTargets(FAST)) for(auto & target : temp.findPotentialTargets(FAST))
@ -573,7 +656,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
auto & ps = possibleCasts[i]; auto & ps = possibleCasts[i];
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
if(ps.dest.empty())
logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
else
{
auto psFirst = ps.dest.front();
auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
}
#endif #endif
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID)); auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
@ -591,38 +682,56 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
DamageCache safeCopy = damageCache; DamageCache safeCopy = damageCache;
DamageCache innerCache(&safeCopy); DamageCache innerCache(&safeCopy);
innerCache.buildDamageCache(state, side); innerCache.buildDamageCache(state, side);
if(needFullEval || !cachedAttack) if(cachedAttack.ap && cachedAttack.waited)
{
state->makeWait(activeStack);
}
if(needFullEval || !cachedAttack.ap)
{ {
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Full evaluation is started due to stack speed affected."); logAi->trace("Full evaluation is started due to stack speed affected.");
#endif #endif
PotentialTargets innerTargets(activeStack, innerCache, state); PotentialTargets innerTargets(activeStack, innerCache, state);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio); BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
innerEvaluator.updateReachabilityMap(state);
auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
if(!innerTargets.possibleAttacks.empty()) if(!innerTargets.possibleAttacks.empty())
{ {
innerEvaluator.updateReachabilityMap(state);
auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
ps.value = newStackAction.score; ps.value = std::max(moveTarget.score, newStackAction.score);
} }
else else
{ {
ps.value = 0; ps.value = moveTarget.score;
} }
} }
else else
{ {
ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId());
auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId());
auto updatedBai = BattleAttackInfo(
updatedAttacker.get(),
updatedDefender.get(),
cachedAttack.ap->attack.chargeDistance,
cachedAttack.ap->attack.shooting);
auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
} }
for(const auto & unit : allUnits) for(const auto & unit : allUnits)
{ {
if (!unit->isValidTarget()) if(!unit->isValidTarget(true))
continue; continue;
auto newHealth = unit->getAvailableHealth(); auto newHealth = unit->getAvailableHealth();
@ -645,13 +754,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
if(ourUnit * goodEffect == 1) if(ourUnit * goodEffect == 1)
{ {
if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost())) auto isMagical = state->getForUpdate(unit->unitId())->summoned
|| unit->isClone()
|| unit->isGhost();
if(ourUnit && goodEffect && isMagical)
continue; continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
} }
else else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); // discourage AI making collateral damage with spells
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace( logAi->trace(
@ -662,6 +776,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
#endif #endif
} }
} }
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Total score: %2f", ps.value); logAi->trace("Total score: %2f", ps.value);
#endif #endif
@ -672,13 +787,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
LOGFL("Evaluation took %d ms", timer.getDiff()); LOGFL("Evaluation took %d ms", timer.getDiff());
auto pscValue = [](const PossibleSpellcast &ps) -> float auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float
{ {
return ps.value; return ps.value;
}; });
auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
if(castToPerform.value > cachedScore) if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score))
{ {
LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
BattleAction spellcast; BattleAction spellcast;
@ -686,7 +800,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spellcast.spell = castToPerform.spell->id; spellcast.spell = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest); spellcast.setTarget(castToPerform.dest);
spellcast.side = side; spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2; spellcast.stackNumber = -1;
cb->battleMakeSpellAction(battleID, spellcast); cb->battleMakeSpellAction(battleID, spellcast);
activeActionMade = true; activeActionMade = true;

View File

@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END
class EnemyInfo; class EnemyInfo;
struct CachedAttack
{
std::optional<AttackPossibility> ap;
float score = EvaluationResult::INEFFECTIVE_SCORE;
uint8_t turn = 255;
bool waited = false;
};
class BattleEvaluator class BattleEvaluator
{ {
std::unique_ptr<PotentialTargets> targets; std::unique_ptr<PotentialTargets> targets;
@ -30,23 +38,24 @@ class BattleEvaluator
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
bool activeActionMade = false; bool activeActionMade = false;
std::optional<AttackPossibility> cachedAttack; CachedAttack cachedAttack;
PlayerColor playerID; PlayerColor playerID;
BattleID battleID; BattleID battleID;
int side; BattleSide side;
float cachedScore;
DamageCache damageCache; DamageCache damageCache;
float strengthRatio; float strengthRatio;
int simulationTurnsCount;
public: public:
BattleAction selectStackAction(const CStack * stack); BattleAction selectStackAction(const CStack * stack);
bool attemptCastingSpell(const CStack * stack); bool attemptCastingSpell(const CStack * stack);
bool canCastSpell(); bool canCastSpell();
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack); std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes); BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
std::vector<BattleHex> getBrokenWallMoatHexes() const; std::vector<BattleHex> getBrokenWallMoatHexes() const;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void print(const std::string & text) const; void print(const std::string & text) const;
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
BattleEvaluator( BattleEvaluator(
std::shared_ptr<Environment> env, std::shared_ptr<Environment> env,
@ -54,16 +63,9 @@ public:
const battle::Unit * activeStack, const battle::Unit * activeStack,
PlayerColor playerID, PlayerColor playerID,
BattleID battleID, BattleID battleID,
int side, BattleSide side,
float strengthRatio) float strengthRatio,
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID) int simulationTurnsCount);
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
BattleEvaluator( BattleEvaluator(
std::shared_ptr<Environment> env, std::shared_ptr<Environment> env,
@ -73,11 +75,7 @@ public:
const battle::Unit * activeStack, const battle::Unit * activeStack,
PlayerColor playerID, PlayerColor playerID,
BattleID battleID, BattleID battleID,
int side, BattleSide side,
float strengthRatio) float strengthRatio,
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID) int simulationTurnsCount);
{
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
}; };

View File

@ -18,7 +18,7 @@ AttackerValue::AttackerValue()
} }
MoveTarget::MoveTarget() MoveTarget::MoveTarget()
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
{ {
turnsToRich = 1; turnsToRich = 1;
} }
@ -28,102 +28,97 @@ float BattleExchangeVariant::trackAttack(
std::shared_ptr<HypotheticBattle> hb, std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache) DamageCache & damageCache)
{ {
if(!ap.attackerState)
{
logAi->trace("Skipping fake ap attack");
return 0;
}
auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; float attackValue = ap.attackValue();
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
float attackValue = 0;
auto affectedUnits = ap.affectedUnits; auto affectedUnits = ap.affectedUnits;
dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce;
dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg;
attackerValue[attacker->unitId()].value = attackValue;
affectedUnits.push_back(ap.attackerState); affectedUnits.push_back(ap.attackerState);
for(auto affectedUnit : affectedUnits) for(auto affectedUnit : affectedUnits)
{ {
auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth();
if(damageDealt > 0)
{
unitToUpdate->damage(damageDealt);
}
if(unitToUpdate->unitSide() == attacker->unitSide()) if(unitToUpdate->unitSide() == attacker->unitSide())
{ {
if(unitToUpdate->unitId() == attacker->unitId()) if(unitToUpdate->unitId() == attacker->unitId())
{ {
auto defender = hb->getForUpdate(ap.attack.defender->unitId()); unitToUpdate->afterAttack(ap.attack.shooting, false);
if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate())
continue;
auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb);
auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb);
attackValue -= attackerDamageReduce;
dpsScore.ourDamageReduce += attackerDamageReduce;
attackerValue[unitToUpdate->unitId()].isRetaliated = true;
unitToUpdate->damage(retaliationDamage);
defender->afterAttack(false, true);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap retaliation, %s, dps: %2f, score: %2f", "%s -> %s, ap retaliation, %s, dps: %lld",
defender->getDescription(), hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
unitToUpdate->getDescription(), ap.attack.attacker->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
retaliationDamage, damageDealt);
attackerDamageReduce);
#endif #endif
} }
else else
{ {
auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb);
attackValue -= collateralDamageReduce;
dpsScore.ourDamageReduce += collateralDamageReduce;
unitToUpdate->damage(collateralDamage);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap collateral, %s, dps: %2f, score: %2f", "%s, ap collateral, dps: %lld",
attacker->getDescription(),
unitToUpdate->getDescription(), unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee", damageDealt);
collateralDamage,
collateralDamageReduce);
#endif #endif
} }
} }
else else
{ {
int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); if(unitToUpdate->unitId() == ap.attack.defender->unitId())
float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb); {
if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate())
attackValue += defenderDamageReduce; {
dpsScore.enemyDamageReduce += defenderDamageReduce; unitToUpdate->afterAttack(ap.attack.shooting, true);
attackerValue[attacker->unitId()].value += defenderDamageReduce; }
unitToUpdate->damage(attackDamage);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap attack, %s, dps: %2f, score: %2f", "%s -> %s, ap attack, %s, dps: %lld",
attacker->getDescription(), attacker->getDescription(),
unitToUpdate->getDescription(), ap.attack.defender->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
attackDamage, damageDealt);
defenderDamageReduce);
#endif #endif
} }
else
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s, ap enemy collateral, dps: %lld",
unitToUpdate->getDescription(),
damageDealt);
#endif
}
}
} }
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg); logAi->trace(
"ap score: our: %2f, enemy: %2f, collateral: %2f, blocked: %2f",
ap.attackerDamageReduce,
ap.defenderDamageReduce,
ap.collateralDamageReduce,
ap.shootersBlockedDmg);
#endif #endif
attackValue += ap.shootersBlockedDmg;
dpsScore.enemyDamageReduce += ap.shootersBlockedDmg;
attacker->afterAttack(ap.attack.shooting, false);
return attackValue; return attackValue;
} }
@ -230,8 +225,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb); auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
hbWaited->getForUpdate(activeStack->unitId())->waiting = true; hbWaited->makeWait(activeStack);
hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
updateReachabilityMap(hbWaited); updateReachabilityMap(hbWaited);
@ -259,6 +253,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
updateReachabilityMap(hb); updateReachabilityMap(hb);
if(result.bestAttack.attack.shooting if(result.bestAttack.attack.shooting
&& !result.bestAttack.defenderDead
&& !activeStack->waited() && !activeStack->waited()
&& hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
{ {
@ -269,8 +264,9 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
for(auto & ap : targets.possibleAttacks) for(auto & ap : targets.possibleAttacks)
{ {
float score = evaluateExchange(ap, 0, targets, damageCache, hb); float score = evaluateExchange(ap, 0, targets, damageCache, hb);
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait)) if(score > result.score || sameScoreButWaited)
{ {
result.score = score; result.score = score;
result.bestAttack = ap; result.bestAttack = ap;
@ -285,6 +281,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
return result; return result;
} }
ReachabilityInfo getReachabilityWithEnemyBypass(
const battle::Unit * activeStack,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> state)
{
ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition());
if(!params.flying)
{
for(const auto * unit : state->battleAliveUnits())
{
if(unit->unitSide() == activeStack->unitSide())
continue;
auto dmg = damageCache.getOriginalDamage(activeStack, unit, state);
auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, (int64_t)1);
vstd::amin(turnsToKill, 100);
for(auto & hex : unit->getHexes())
if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns
params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange();
}
params.bypassEnemyStacks = true;
}
return state->getReachability(params);
}
MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
@ -294,6 +320,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
MoveTarget result; MoveTarget result;
BattleExchangeVariant ev; BattleExchangeVariant ev;
logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size());
if(targets.unreachableEnemies.empty()) if(targets.unreachableEnemies.empty())
return result; return result;
@ -304,17 +332,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
updateReachabilityMap(hb); updateReachabilityMap(hb);
auto dists = cb->getReachability(activeStack); auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb);
auto flying = activeStack->hasBonusOfType(BonusType::FLYING);
for(const battle::Unit * enemy : targets.unreachableEnemies) for(const battle::Unit * enemy : targets.unreachableEnemies)
{ {
std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy); logAi->trace(
auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t "Checking movement towards %d of %s",
{ enemy->getCount(),
return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); enemy->creatureId().toCreature()->getNameSingularTranslated());
});
auto distance = dists.distToNearestNeighbour(activeStack, closestStack); auto distance = dists.distToNearestNeighbour(activeStack, enemy);
if(distance >= GameConstants::BFIELD_SIZE) if(distance >= GameConstants::BFIELD_SIZE)
continue; continue;
@ -323,30 +351,87 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
continue; continue;
auto turnsToRich = (distance - 1) / speed + 1; auto turnsToRich = (distance - 1) / speed + 1;
auto hexes = closestStack->getSurroundingHexes(); auto hexes = enemy->getSurroundingHexes();
auto enemySpeed = closestStack->getMovementRange(); auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed); auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = speedRatio > 1 ? 1 : speedRatio; auto multiplier = speedRatio > 1 ? 1 : speedRatio;
if(enemy->canShoot()) for(auto & hex : hexes)
multiplier *= 1.5f;
for(auto hex : hexes)
{ {
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack));
auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce);
if(result.scorePerTurn < scoreValue(scorePerTurn)) score.enemyDamageReduce *= multiplier;
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
#endif
if(result.score < scoreValue(score)
|| (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
{ {
result.scorePerTurn = scoreValue(scorePerTurn);
result.score = scoreValue(score); result.score = scoreValue(score);
result.positions = closestStack->getAttackableHexes(activeStack); result.positions.clear();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score");
#endif
for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
{
BattleHex enemyHex = initialEnemyHex;
while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
{
enemyHex = dists.predecessors.at(enemyHex);
if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
{
auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
if(defenderToBypass)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Found target to bypass at %d", enemyHex.hex);
#endif
auto attackHex = dists.predecessors[enemyHex];
auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
auto adjacentStacks = getAdjacentUnits(enemy);
adjacentStacks.push_back(defenderToBypass);
vstd::removeDuplicates(adjacentStacks);
auto bypassScore = calculateExchange(
attackBypass,
dists.distances[attackHex],
targets,
damageCache,
hb,
adjacentStacks);
if(scoreValue(bypassScore) > result.score)
{
result.score = scoreValue(bypassScore);
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score after bypass %f", scoreValue(bypassScore));
#endif
}
}
}
}
result.positions.push_back(enemyHex);
}
result.cachedAttack = attack; result.cachedAttack = attack;
result.turnsToRich = turnsToRich; result.turnsToRich = turnsToRich;
} }
@ -390,7 +475,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap, const AttackPossibility & ap,
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{ {
ReachabilityData result; ReachabilityData result;
@ -398,13 +484,29 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
if(!ap.attack.shooting) hexes.push_back(ap.from); if(!ap.attack.shooting) hexes.push_back(ap.from);
std::vector<const battle::Unit *> allReachableUnits; std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
for(auto hex : hexes) for(auto hex : hexes)
{ {
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
} }
if(!ap.attack.attacker->isTurret())
{
for(auto hex : ap.attack.attacker->getHexes())
{
auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
for(auto unit : unitsReachingAttacker)
{
if(unit->unitSide() != ap.attack.attacker->unitSide())
{
allReachableUnits.push_back(unit);
result.enemyUnitsReachingAttacker.insert(unit->unitId());
}
}
}
}
vstd::removeDuplicates(allReachableUnits); vstd::removeDuplicates(allReachableUnits);
auto copy = allReachableUnits; auto copy = allReachableUnits;
@ -440,7 +542,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : allReachableUnits) for(auto unit : allReachableUnits)
{ {
auto accessible = !unit->canShoot(); auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
if(!accessible) if(!accessible)
{ {
@ -464,14 +566,14 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : turnOrder[turn]) for(auto unit : turnOrder[turn])
{ {
if(vstd::contains(allReachableUnits, unit)) if(vstd::contains(allReachableUnits, unit))
result.units.push_back(unit); result.units[turn].push_back(unit);
}
} }
vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool
{ {
return !hb->battleGetUnitByID(u->unitId())->alive(); return !hb->battleGetUnitByID(u->unitId())->alive();
}); });
}
return result; return result;
} }
@ -502,13 +604,14 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) const std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{ {
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
#endif #endif
if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE if(cb->battleGetMySide() == BattleSide::LEFT_SIDE
&& cb->battleGetGateState() == EGateState::BLOCKED && cb->battleGetGateState() == EGateState::BLOCKED
&& ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
{ {
@ -521,7 +624,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender); enemyStacks.push_back(ap.attack.defender);
ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits);
if(exchangeUnits.units.empty()) if(exchangeUnits.units.empty())
{ {
@ -531,7 +634,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb); auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v; BattleExchangeVariant v;
for(auto unit : exchangeUnits.units) for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
{
for(auto unit : exchangeUnits.units.at(exchangeTurn))
{ {
if(unit->isTurret()) if(unit->isTurret())
continue; continue;
@ -549,6 +654,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
#endif #endif
} }
} }
}
auto melleeAttackers = ourStacks; auto melleeAttackers = ourStacks;
@ -560,13 +666,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
bool canUseAp = true; bool canUseAp = true;
for(auto activeUnit : exchangeUnits.units) std::set<uint32_t> blockedShooters;
int totalTurnsCount = simulationTurnsCount >= turn + turnOrder.size()
? simulationTurnsCount
: turn + turnOrder.size();
for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++)
{
bool isMovingTurm = exchangeTurn < turn;
int queueTurn = exchangeTurn >= exchangeUnits.units.size()
? exchangeUnits.units.size() - 1
: exchangeTurn;
for(auto activeUnit : exchangeUnits.units.at(queueTurn))
{ {
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get())
&& !vstd::contains(blockedShooters, attacker->unitId());
if(!attacker->alive()) if(!attacker->alive())
{ {
@ -577,10 +698,23 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
continue; continue;
} }
if(isMovingTurm && !shooting
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Attacker is moving");
#endif
continue;
}
auto targetUnit = ap.attack.defender; auto targetUnit = ap.attack.defender;
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{ {
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector for %s", attacker->getDescription());
#endif
auto estimateAttack = [&](const battle::Unit * u) -> float auto estimateAttack = [&](const battle::Unit * u) -> float
{ {
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
@ -593,7 +727,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
hb, hb,
true); true);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
#endif #endif
@ -607,6 +741,17 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
return vstd::contains(exchangeUnits.shooters, u); return vstd::contains(exchangeUnits.shooters, u);
}); });
if(!isOur
&& exchangeTurn == 0
&& exchangeUnits.units.at(exchangeTurn).at(0)->unitId() != ap.attack.attacker->unitId()
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool
{
return u->unitId() == ap.attack.attacker->unitId();
});
}
if(!unitsInOppositeQueueExceptInaccessible.empty()) if(!unitsInOppositeQueueExceptInaccessible.empty())
{ {
targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
@ -646,7 +791,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting); const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
@ -665,6 +809,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
} }
if(!shooting)
blockedShooters.insert(defender->unitId());
canUseAp = false; canUseAp = false;
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
@ -678,6 +825,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
}); });
} }
exchangeBattle->nextRound();
}
// avoid blocking path for stronger stack by weaker stack // avoid blocking path for stronger stack by weaker stack
// the method checks if all stacks can be placed around enemy // the method checks if all stacks can be placed around enemy
std::map<BattleHex, battle::Units> reachabilityMap; std::map<BattleHex, battle::Units> reachabilityMap;
@ -687,11 +837,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
for(auto hex : hexes) for(auto hex : hexes)
reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
auto score = v.getScore();
if(simulationTurnsCount < totalTurnsCount)
{
float scalingRatio = simulationTurnsCount / static_cast<float>(totalTurnsCount);
score.enemyDamageReduce *= scalingRatio;
score.ourDamageReduce *= scalingRatio;
}
if(turn > 0)
{
auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn);
score.enemyDamageReduce *= turnMultiplier;
}
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce);
#endif #endif
return v.getScore(); return score;
} }
bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)
@ -739,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
{ {
std::vector<const battle::Unit *> result; std::vector<const battle::Unit *> result;
for(int i = 0; i < turnOrder.size(); i++, turn++) for(int i = 0; i < turnOrder.size(); i++)
{ {
auto & turnQueue = turnOrder[i]; auto & turnQueue = turnOrder[i];
HypotheticBattle turnBattle(env.get(), cb); HypotheticBattle turnBattle(env.get(), cb);
@ -764,7 +931,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit); ReachabilityInfo unitReachability = reachabilityIter != reachabilityCache.end() ? reachabilityIter->second : turnBattle.getReachability(unit);
bool reachable = unitReachability.distances[hex] <= radius; bool reachable = unitReachability.distances.at(hex) <= radius;
if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
{ {
@ -774,7 +941,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
{ {
for(BattleHex neighbor : hex.neighbouringTiles()) for(BattleHex neighbor : hex.neighbouringTiles())
{ {
reachable = unitReachability.distances[neighbor] <= radius; reachable = unitReachability.distances.at(neighbor) <= radius;
if(reachable) break; if(reachable) break;
} }
@ -824,7 +991,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
{ {
bool enemyUnit = false; bool enemyUnit = false;
bool reachable = unitReachability.distances[hex] <= unitSpeed; bool reachable = unitReachability.distances.at(hex) <= unitSpeed;
if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
{ {
@ -836,7 +1003,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
for(BattleHex neighbor : hex.neighbouringTiles()) for(BattleHex neighbor : hex.neighbouringTiles())
{ {
reachable = unitReachability.distances[neighbor] <= unitSpeed; reachable = unitReachability.distances.at(neighbor) <= unitSpeed;
if(reachable) break; if(reachable) break;
} }

View File

@ -54,7 +54,6 @@ struct AttackerValue
struct MoveTarget struct MoveTarget
{ {
float score; float score;
float scorePerTurn;
std::vector<BattleHex> positions; std::vector<BattleHex> positions;
std::optional<AttackPossibility> cachedAttack; std::optional<AttackPossibility> cachedAttack;
uint8_t turnsToRich; uint8_t turnsToRich;
@ -64,7 +63,7 @@ struct MoveTarget
struct EvaluationResult struct EvaluationResult
{ {
static const int64_t INEFFECTIVE_SCORE = -10000; static const int64_t INEFFECTIVE_SCORE = -100000000;
AttackPossibility bestAttack; AttackPossibility bestAttack;
MoveTarget bestMove; MoveTarget bestMove;
@ -113,13 +112,15 @@ private:
struct ReachabilityData struct ReachabilityData
{ {
std::vector<const battle::Unit *> units; std::map<int, std::vector<const battle::Unit *>> units;
// shooters which are within mellee attack and mellee units // shooters which are within mellee attack and mellee units
std::vector<const battle::Unit *> melleeAccessible; std::vector<const battle::Unit *> melleeAccessible;
// far shooters // far shooters
std::vector<const battle::Unit *> shooters; std::vector<const battle::Unit *> shooters;
std::set<uint32_t> enemyUnitsReachingAttacker;
}; };
class BattleExchangeEvaluator class BattleExchangeEvaluator
@ -131,6 +132,7 @@ private:
std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap; std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
std::vector<battle::Units> turnOrder; std::vector<battle::Units> turnOrder;
float negativeEffectMultiplier; float negativeEffectMultiplier;
int simulationTurnsCount;
float scoreValue(const BattleScore & score) const; float scoreValue(const BattleScore & score) const;
@ -139,7 +141,8 @@ private:
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) const; std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits = {}) const;
bool canBeHitThisTurn(const AttackPossibility & ap); bool canBeHitThisTurn(const AttackPossibility & ap);
@ -147,8 +150,9 @@ public:
BattleExchangeEvaluator( BattleExchangeEvaluator(
std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<CBattleInfoCallback> cb,
std::shared_ptr<Environment> env, std::shared_ptr<Environment> env,
float strengthRatio): cb(cb), env(env) { float strengthRatio,
negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio; int simulationTurnsCount): cb(cb), env(env), simulationTurnsCount(simulationTurnsCount){
negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio * strengthRatio;
} }
EvaluationResult findBestTarget( EvaluationResult findBestTarget(
@ -171,7 +175,8 @@ public:
const AttackPossibility & ap, const AttackPossibility & ap,
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const; std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits = {}) const;
bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);

View File

@ -37,11 +37,7 @@ else()
endif() endif()
target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb) target_link_libraries(BattleAI PRIVATE vcmi)
vcmi_set_output_dir(BattleAI "AI") vcmi_set_output_dir(BattleAI "AI")
enable_pch(BattleAI) enable_pch(BattleAI)
if(APPLE_IOS AND NOT USING_CONAN)
install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
endif()

View File

@ -10,6 +10,7 @@
#include "StdInc.h" #include "StdInc.h"
#include "PotentialTargets.h" #include "PotentialTargets.h"
#include "../../lib/CStack.h"//todo: remove #include "../../lib/CStack.h"//todo: remove
#include "../../lib/mapObjects/CGTownInstance.h"
PotentialTargets::PotentialTargets( PotentialTargets::PotentialTargets(
const battle::Unit * attacker, const battle::Unit * attacker,

View File

@ -12,6 +12,7 @@
#include <vcmi/events/EventBus.h> #include <vcmi/events/EventBus.h>
#include "../../lib/battle/BattleLayout.h"
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
#include "../../lib/ScriptHandler.h" #include "../../lib/ScriptHandler.h"
#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForClientBattle.h"
@ -116,7 +117,7 @@ uint32_t StackWithBonuses::unitId() const
return id; return id;
} }
ui8 StackWithBonuses::unitSide() const BattleSide StackWithBonuses::unitSide() const
{ {
return side; return side;
} }
@ -132,10 +133,10 @@ SlotID StackWithBonuses::unitSlot() const
} }
TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit, TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit,
const CBonusSystemNode * root, const std::string & cachingStr) const const std::string & cachingStr) const
{ {
auto ret = std::make_shared<BonusList>(); auto ret = std::make_shared<BonusList>();
TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr); TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, cachingStr);
vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr<Bonus> & b) vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr<Bonus> & b)
{ {
@ -467,7 +468,7 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
return (damage.min + damage.max) / 2; return (damage.min + damage.max) / 2;
} }
std::vector<SpellID> HypotheticBattle::getUsedSpells(ui8 side) const std::vector<SpellID> HypotheticBattle::getUsedSpells(BattleSide side) const
{ {
// TODO // TODO
return {}; return {};
@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const
return int3(-1, -1, -1); return int3(-1, -1, -1);
} }
bool HypotheticBattle::isCreatureBank() const BattleLayout HypotheticBattle::getLayout() const
{ {
// TODO return subject->getBattle()->getLayout();
return false;
} }
int64_t HypotheticBattle::getTreeVersion() const int64_t HypotheticBattle::getTreeVersion() const
@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
return serverCallback.get(); return serverCallback.get();
} }
void HypotheticBattle::makeWait(const battle::Unit * activeStack)
{
auto unit = getForUpdate(activeStack->unitId());
resetActiveUnit();
unit->waiting = true;
unit->waitedThisTurn = true;
}
HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_) HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
:owner(owner_) :owner(owner_)
{ {
} }
void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem) void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)

View File

@ -24,7 +24,7 @@ class HypotheticBattle;
class RNGStub final : public vstd::RNG class RNGStub final : public vstd::RNG
{ {
public: public:
virtual int nextInt() override int nextInt() override
{ {
return 0; return 0;
} }
@ -85,13 +85,13 @@ public:
int32_t unitBaseAmount() const override; int32_t unitBaseAmount() const override;
uint32_t unitId() const override; uint32_t unitId() const override;
ui8 unitSide() const override; BattleSide unitSide() const override;
PlayerColor unitOwner() const override; PlayerColor unitOwner() const override;
SlotID unitSlot() const override; SlotID unitSlot() const override;
///IBonusBearer ///IBonusBearer
TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit,
const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override; const std::string & cachingStr = "") const override;
int64_t getTreeVersion() const override; int64_t getTreeVersion() const override;
@ -111,7 +111,7 @@ private:
const CCreature * type; const CCreature * type;
ui32 baseAmount; ui32 baseAmount;
uint32_t id; uint32_t id;
ui8 side; BattleSide side;
PlayerColor player; PlayerColor player;
SlotID slot; SlotID slot;
}; };
@ -158,12 +158,19 @@ public:
uint32_t nextUnitId() const override; uint32_t nextUnitId() const override;
int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
std::vector<SpellID> getUsedSpells(ui8 side) const override; std::vector<SpellID> getUsedSpells(BattleSide side) const override;
int3 getLocation() const override; int3 getLocation() const override;
bool isCreatureBank() const override; BattleLayout getLayout() const override;
int64_t getTreeVersion() const; int64_t getTreeVersion() const;
void makeWait(const battle::Unit * activeStack);
void resetActiveUnit()
{
activeUnitId = -1;
}
#if SCRIPTING_ENABLED #if SCRIPTING_ENABLED
scripting::Pool * getContextPool() const override; scripting::Pool * getContextPool() const override;
#endif #endif

View File

@ -8,10 +8,6 @@ else()
option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF) option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF)
endif() endif()
if(TBB_FOUND AND MSVC)
install_vcpkg_imported_tgt(TBB::tbb)
endif()
#FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas #FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas
if(MINGW) if(MINGW)
add_compile_options(-Wno-unknown-pragmas) add_compile_options(-Wno-unknown-pragmas)

View File

@ -18,11 +18,9 @@
#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/CGameState.h"
#include "../../lib/serializer/CTypeList.h" #include "../../lib/serializer/CTypeList.h"
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClient.h"
#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForClientBattle.h"
#include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/networkPacks/PacksForServer.h"
@ -570,6 +568,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
LOG_TRACE(logAi); LOG_TRACE(logAi);
myCb = CB; myCb = CB;
cbc = CB; cbc = CB;
this->env = env;
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
playerID = *myCb->getPlayerID(); playerID = *myCb->getPlayerID();
@ -1148,7 +1147,7 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
} }
} }
void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
@ -1490,7 +1489,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
//TODO trade only as much as needed //TODO trade only as much as needed
if (toGive) //don't try to sell 0 resources if (toGive) //don't try to sell 0 resources
{ {
cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive);
acquiredResources = static_cast<int>(toGet * (it->resVal / toGive)); acquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName()); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName());
} }

View File

@ -156,7 +156,7 @@ public:
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();

View File

@ -18,7 +18,7 @@
#include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/mapping/CMapDefines.h" #include "../../lib/mapping/CMapDefines.h"
#include "../../lib/gameState/QuestInfo.h" #include "../../lib/gameState/QuestInfo.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include <vcmi/CreatureService.h> #include <vcmi/CreatureService.h>

View File

@ -31,16 +31,14 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
} }
} }
BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST}; for(int level = 0; level < developmentInfo.town->town->creatures.size(); level++)
for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++)
{ {
logAi->trace("Checking dwelling level %d", level); logAi->trace("Checking dwelling level %d", level);
BuildingInfo nextToBuild = BuildingInfo(); BuildingInfo nextToBuild = BuildingInfo();
for(BuildingID prefix : prefixes) for(int upgradeIndex : {1, 0})
{ {
BuildingID building = BuildingID(prefix + level); BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building)) if(!vstd::contains(buildings, building))
continue; // no such building in town continue; // no such building in town
@ -211,8 +209,8 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST) if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{ {
creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN; creatureLevel = BuildingID::getLevelFromDwelling(toBuild);
creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN; creatureUpgrade = BuildingID::getUpgradedFromDwelling(toBuild);
} }
else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR) else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{ {
@ -316,9 +314,7 @@ void BuildAnalyzer::updateDailyIncome()
const CGMine* mine = dynamic_cast<const CGMine*>(obj); const CGMine* mine = dynamic_cast<const CGMine*>(obj);
if(mine) if(mine)
{ dailyIncome += mine->dailyIncome();
dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
}
} }
for(const CGTownInstance* town : towns) for(const CGTownInstance* town : towns)

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../pforeach.h" #include "../pforeach.h"
#include "../../../lib/CRandomGenerator.h" #include "../../../lib/CRandomGenerator.h"
#include "../../../lib/logging/VisualLogger.h"
namespace NKAI namespace NKAI
{ {
@ -24,6 +25,41 @@ double HitMapInfo::value() const
return danger / std::sqrt(turn / 3.0f + 1); return danger / std::sqrt(turn / 3.0f + 1);
} }
void logHitmap(PlayerColor playerID, DangerHitMapAnalyzer & data)
{
#if NKAI_TRACE_LEVEL >= 1
logVisual->updateWithLock(playerID.toString() + ".danger.max", [&data](IVisualLogBuilder & b)
{
foreach_tile_pos([&b, &data](const int3 & pos)
{
auto & treat = data.getTileThreat(pos).maximumDanger;
b.addText(pos, std::to_string(treat.danger));
if(treat.hero.validAndSet())
{
b.addText(pos, std::to_string(treat.turn));
b.addText(pos, treat.hero->getNameTranslated());
}
});
});
logVisual->updateWithLock(playerID.toString() + ".danger.fast", [&data](IVisualLogBuilder & b)
{
foreach_tile_pos([&b, &data](const int3 & pos)
{
auto & treat = data.getTileThreat(pos).fastestDanger;
b.addText(pos, std::to_string(treat.danger));
if(treat.hero.validAndSet())
{
b.addText(pos, std::to_string(treat.turn));
b.addText(pos, treat.hero->getNameTranslated());
}
});
});
#endif
}
void DangerHitMapAnalyzer::updateHitMap() void DangerHitMapAnalyzer::updateHitMap()
{ {
if(hitMapUpToDate) if(hitMapUpToDate)
@ -53,6 +89,14 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN; heroes[hero->tempOwner][hero] = HeroRole::MAIN;
} }
if(obj->ID == Obj::TOWN)
{
auto town = dynamic_cast<const CGTownInstance *>(obj);
if(town->garrisonHero)
heroes[town->garrisonHero->tempOwner][town->garrisonHero] = HeroRole::MAIN;
}
} }
auto ourTowns = cb->getTownsInfo(); auto ourTowns = cb->getTownsInfo();
@ -144,6 +188,8 @@ void DangerHitMapAnalyzer::updateHitMap()
} }
logAi->trace("Danger hit map updated in %ld", timeElapsed(start)); logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
logHitmap(ai->playerID, *this);
} }
void DangerHitMapAnalyzer::calculateTileOwners() void DangerHitMapAnalyzer::calculateTileOwners()

View File

@ -12,7 +12,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/CHeroHandler.h" #include "../../../lib/CHeroHandler.h"
#include "../../../lib/GameSettings.h" #include "../../../lib/IGameSettings.h"
namespace NKAI namespace NKAI
{ {
@ -196,8 +196,8 @@ bool HeroManager::heroCapReached() const
return heroCount >= ALLOWED_ROAMING_HEROES return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= ai->settings->getMaxRoamingHeroes() || heroCount >= ai->settings->getMaxRoamingHeroes()
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); || heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
} }
float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const

View File

@ -212,7 +212,7 @@ void CaptureObjectsBehavior::decomposeObjects(
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects)); vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
} }
std::lock_guard<std::mutex> lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead std::lock_guard lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead
vstd::concatenate(result, tasksLocal); vstd::concatenate(result, tasksLocal);
}); });
} }

View File

@ -157,11 +157,7 @@ else()
endif() endif()
target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb) target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite)
vcmi_set_output_dir(Nullkiller "AI") vcmi_set_output_dir(Nullkiller "AI")
enable_pch(Nullkiller) enable_pch(Nullkiller)
if(APPLE_IOS AND NOT USING_CONAN)
install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
endif()

View File

@ -208,12 +208,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers); enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed); enemySpeed->setValue(enemyStructure.maxSpeed);
bool bank = dynamic_cast<const CBank *>(enemy);
if(bank)
bankPresent->setValue(1);
else
bankPresent->setValue(0);
const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy); const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort) if(fort)
castleWalls->setValue(fort->fortLevel()); castleWalls->setValue(fort->fortLevel());

View File

@ -20,25 +20,6 @@
namespace NKAI namespace NKAI
{ {
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
ui64 totalStrength = 0;
ui8 totalChance = 0;
for(auto config : bankInfo->getPossibleGuards(bank->cb))
{
totalStrength += config.second.totalStrength * config.first;
totalChance += config.first;
}
return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
}
ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards) ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards)
{ {
auto cb = ai->cb.get(); auto cb = ai->cb.get();
@ -158,30 +139,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
return 0; return 0;
[[fallthrough]]; [[fallthrough]];
} }
case Obj::MONSTER: default:
case Obj::GARRISON:
case Obj::GARRISON2:
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR4:
case Obj::MINE:
case Obj::ABANDONED_MINE:
case Obj::PANDORAS_BOX:
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship
{ {
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj); const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
if (a)
return a->getArmyStrength(); return a->getArmyStrength();
} else
case Obj::PYRAMID:
{
return estimateBankDanger(dynamic_cast<const CBank *>(obj));
}
default:
return 0; return 0;
} }
} }
}
} }

View File

@ -10,12 +10,6 @@
#pragma once #pragma once
#include "FuzzyEngines.h" #include "FuzzyEngines.h"
VCMI_LIB_NAMESPACE_BEGIN
class CBank;
VCMI_LIB_NAMESPACE_END
namespace NKAI namespace NKAI
{ {
@ -30,8 +24,6 @@ private:
public: public:
FuzzyHelper(const Nullkiller * ai): ai(ai) {} FuzzyHelper(const Nullkiller * ai): ai(ai) {}
ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
ui64 evaluateDanger(const CGObjectInstance * obj); ui64 evaluateDanger(const CGObjectInstance * obj);
ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true); ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true);
}; };

View File

@ -118,36 +118,15 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250);
} }
TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero)
{
//Fixme: unused variable hero
auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
auto resources = bankInfo->getPossibleResourcesReward();
TResources result = TResources();
int sum = 0;
for(auto & reward : resources)
{
result += reward.data * reward.chance;
sum += reward.chance;
}
return sum > 1 ? result / sum : result;
}
int32_t getResourcesGoldReward(const TResources & res) int32_t getResourcesGoldReward(const TResources & res)
{ {
int32_t result = 0; int32_t result = 0;
for(EGameResID r = EGameResID(0); r < EGameResID::COUNT; r.advance(1)) for(auto r : GameResID::ALL_RESOURCES())
{ {
if(res[r] > 0) if(res[r] > 0)
{
result += r == EGameResID::GOLD ? res[r] : res[r] * 100; result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
} }
}
return result; return result;
} }
@ -305,22 +284,13 @@ uint64_t RewardEvaluator::getArmyReward(
{ {
case Obj::HILL_FORT: case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
case Obj::CREATURE_BANK:
return getCreatureBankArmyReward(target, hero);
case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT:
case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::WARRIORS_TOMB:
return 1000;
case Obj::ARTIFACT: case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType); return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType);
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::HERO: case Obj::HERO:
return relations == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength() ? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@ -350,7 +320,7 @@ uint64_t RewardEvaluator::getArmyReward(
{ {
for(auto artID : info.reward.artifacts) for(auto artID : info.reward.artifacts)
{ {
const CArtifact * art = dynamic_cast<const CArtifact *>(VLC->artifacts()->getById(artID)); const auto * art = dynamic_cast<const CArtifact *>(VLC->artifacts()->getById(artID));
rewardValue += evaluateArtifactArmyValue(art); rewardValue += evaluateArtifactArmyValue(art);
} }
@ -358,7 +328,7 @@ uint64_t RewardEvaluator::getArmyReward(
if(!info.reward.creatures.empty()) if(!info.reward.creatures.empty())
{ {
for(auto stackInfo : info.reward.creatures) for(const auto & stackInfo : info.reward.creatures)
{ {
rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount(); rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount();
} }
@ -555,13 +525,6 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
return getResourceRequirementStrength(res); return getResourceRequirementStrength(res);
} }
case Obj::CREATURE_BANK:
{
auto resourceReward = getCreatureBankResources(target, nullptr);
return getResourceRequirementStrength(resourceReward);
}
case Obj::TOWN: case Obj::TOWN:
{ {
if(ai->buildAnalyzer->getDevelopmentInfo().empty()) if(ai->buildAnalyzer->getDevelopmentInfo().empty())
@ -672,8 +635,6 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps) //Can contains experience, spells, or skills (only on custom maps)
return 2.5f; return 2.5f;
case Obj::PYRAMID:
return 6.0f;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level ? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
@ -780,22 +741,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
auto * mine = dynamic_cast<const CGMine*>(target); auto * mine = dynamic_cast<const CGMine*>(target);
return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
} }
case Obj::MYSTICAL_GARDEN:
case Obj::WINDMILL:
return 100;
case Obj::CAMPFIRE:
return 800;
case Obj::WAGON:
return 100;
case Obj::CREATURE_BANK:
return getResourcesGoldReward(getCreatureBankResources(target, hero));
case Obj::CRYPT:
case Obj::DERELICT_SHIP:
return 3000;
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::SEA_CHEST:
return 1500;
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
return 2500; return 2500;
case Obj::PRISON: case Obj::PRISON:

View File

@ -68,7 +68,12 @@ void RecruitHero::accept(AIGateway * ai)
throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!"); throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!");
cb->recruitHero(t, heroToHire); cb->recruitHero(t, heroToHire);
{
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
ai->nullkiller->heroManager->update(); ai->nullkiller->heroManager->update();
}
if(t->visitingHero) if(t->visitingHero)
ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get()); ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());

View File

@ -44,7 +44,7 @@ namespace AIPathfinding
Nullkiller * ai, Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage, std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects) bool allowBypassObjects)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage) :PathfinderConfig(nodeStorage, cb, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
{ {
options.canUseCast = true; options.canUseCast = true;
options.allowLayerTransitioningAfterBattle = true; options.allowLayerTransitioningAfterBattle = true;

View File

@ -321,7 +321,7 @@ void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat) void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat)
{ {
std::lock_guard<std::mutex> lock(syncLock); std::lock_guard lock(syncLock);
auto internalCb = temporaryActorHeroes.front()->cb; auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get(); auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();

View File

@ -18,7 +18,7 @@
#include "../../lib/CRandomGenerator.h" #include "../../lib/CRandomGenerator.h"
CStupidAI::CStupidAI() CStupidAI::CStupidAI()
: side(-1) : side(BattleSide::NONE)
, wasWaitingForRealize(false) , wasWaitingForRealize(false)
, wasUnlockingGs(false) , wasUnlockingGs(false)
{ {
@ -262,7 +262,7 @@ void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStack
print("battleStacksEffectsSet called"); print("battleStacksEffectsSet called");
} }
void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed)
{ {
print("battleStart called"); print("battleStart called");
side = Side; side = Side;

View File

@ -17,7 +17,7 @@ class EnemyInfo;
class CStupidAI : public CBattleGameInterface class CStupidAI : public CBattleGameInterface
{ {
int side; BattleSide side;
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
@ -47,7 +47,7 @@ public:
void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
private: private:

View File

@ -16,7 +16,6 @@
#include "../../lib/UnlockGuard.h" #include "../../lib/UnlockGuard.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.h" #include "../../lib/mapping/CMapDefines.h"

View File

@ -143,9 +143,9 @@ static const std::vector<BuildingID> basicGoldSource = { BuildingID::TOWN_HALL,
static const std::vector<BuildingID> defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE }; static const std::vector<BuildingID> defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE };
static const std::vector<BuildingID> capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL }; static const std::vector<BuildingID> capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL };
static const std::vector<BuildingID> unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, static const std::vector<BuildingID> unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7, BuildingID::DWELL_LVL_8 };
static const std::vector<BuildingID> unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP, static const std::vector<BuildingID> unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP }; BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP, BuildingID::DWELL_LVL_8_UP };
static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR }; static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 }; BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
@ -196,7 +196,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
return true; return true;
//workaround for mantis #2696 - build capitol with separate algorithm if it is available //workaround for mantis #2696 - build capitol with separate algorithm if it is available
if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL) if(t->hasBuilt(BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
{ {
if(tryBuildNextStructure(t, capitolAndRequirements)) if(tryBuildNextStructure(t, capitolAndRequirements))
return true; return true;

View File

@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers); enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed); enemySpeed->setValue(enemyStructure.maxSpeed);
bool bank = dynamic_cast<const CBank *>(enemy);
if(bank)
bankPresent->setValue(1);
else
bankPresent->setValue(0);
const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy); const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort) if(fort)
castleWalls->setValue(fort->fortLevel()); castleWalls->setValue(fort->fortLevel());

View File

@ -16,7 +16,6 @@
#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGCreature.h"
#include "../../lib/mapObjects/CGDwelling.h" #include "../../lib/mapObjects/CGDwelling.h"
#include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/gameState/InfoAboutArmy.h"
@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
return result; return result;
} }
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
ui64 totalStrength = 0;
ui8 totalChance = 0;
for(auto config : bankInfo->getPossibleGuards(bank->cb))
{
totalStrength += config.second.totalStrength * config.first;
totalChance += config.first;
}
return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
}
float FuzzyHelper::evaluate(Goals::VisitTile & g) float FuzzyHelper::evaluate(Goals::VisitTile & g)
{ {
if(g.parent) if(g.parent)
@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
cb->getTownInfo(obj, iat); cb->getTownInfo(obj, iat);
return iat.army.getStrength(); return iat.army.getStrength();
} }
case Obj::MONSTER: default:
{
//TODO!!!!!!!!
const CGCreature * cre = dynamic_cast<const CGCreature *>(obj);
return cre->getArmyStrength();
}
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR4:
{
const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
return d->getArmyStrength();
}
case Obj::MINE:
case Obj::ABANDONED_MINE:
{ {
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj); const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
if (a)
return a->getArmyStrength(); return a->getArmyStrength();
} else
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship
case Obj::PYRAMID:
return estimateBankDanger(dynamic_cast<const CBank *>(obj));
default:
return 0; return 0;
} }
} }
}

View File

@ -10,12 +10,6 @@
#pragma once #pragma once
#include "FuzzyEngines.h" #include "FuzzyEngines.h"
VCMI_LIB_NAMESPACE_BEGIN
class CBank;
VCMI_LIB_NAMESPACE_END
class DLL_EXPORT FuzzyHelper class DLL_EXPORT FuzzyHelper
{ {
public: public:
@ -42,8 +36,6 @@ public:
float evaluate(Goals::AbstractGoal & g); float evaluate(Goals::AbstractGoal & g);
void setPriority(Goals::TSubgoal & g); void setPriority(Goals::TSubgoal & g);
ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
Goals::TSubgoal chooseSolution(Goals::TGoalVec vec); Goals::TSubgoal chooseSolution(Goals::TGoalVec vec);
//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec); //std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec);

View File

@ -109,7 +109,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals()
if(upgradeNumber < 0) if(upgradeNumber < 0)
continue; continue;
BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * GameConstants::CREATURES_PER_TOWN); BuildingID bid(BuildingID::DWELL_FIRST + creature->getLevel() - 1 + upgradeNumber * t->town->creatures.size());
if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) //this assumes only creatures with dwellings are assigned to faction if(t->hasBuilt(bid) && ai->ah->freeResources().canAfford(creature->getFullRecruitCost())) //this assumes only creatures with dwellings are assigned to faction
{ {
solutions.push_back(sptr(BuyArmy(t, creature->getAIValue() * this->value).setobjid(objid))); solutions.push_back(sptr(BuyArmy(t, creature->getAIValue() * this->value).setobjid(objid)));

View File

@ -39,7 +39,7 @@ namespace AIPathfinding
CPlayerSpecificInfoCallback * cb, CPlayerSpecificInfoCallback * cb,
VCAI * ai, VCAI * ai,
std::shared_ptr<AINodeStorage> nodeStorage) std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero()) :PathfinderConfig(nodeStorage, cb, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero())
{ {
options.ignoreGuards = false; options.ignoreGuards = false;
options.useEmbarkAndDisembark = true; options.useEmbarkAndDisembark = true;

View File

@ -21,7 +21,7 @@
#include "../../lib/mapObjects/ObjectTemplate.h" #include "../../lib/mapObjects/ObjectTemplate.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/CGameState.h"
#include "../../lib/bonuses/Limiters.h" #include "../../lib/bonuses/Limiters.h"
#include "../../lib/bonuses/Updaters.h" #include "../../lib/bonuses/Updaters.h"
@ -31,8 +31,6 @@
#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForClientBattle.h"
#include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/networkPacks/PacksForServer.h"
#include "../../lib/serializer/CTypeList.h" #include "../../lib/serializer/CTypeList.h"
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "AIhelper.h" #include "AIhelper.h"
@ -1319,7 +1317,7 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
return false; return false;
if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
return false; return false;
if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false; return false;
if(!cb->getAvailableHeroes(t).size()) if(!cb->getAvailableHeroes(t).size())
return false; return false;
@ -1566,7 +1564,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
} }
void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed)
{ {
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
@ -2130,7 +2128,7 @@ void VCAI::tryRealize(Goals::Trade & g) //trade
//TODO trade only as much as needed //TODO trade only as much as needed
if (toGive) //don't try to sell 0 resources if (toGive) //don't try to sell 0 resources
{ {
cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive);
acquiredResources = static_cast<int>(toGet * (it->resVal / toGive)); acquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName()); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName());
} }
@ -2752,8 +2750,6 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
if(dynamic_cast<const CGDwelling *>(obj)) if(dynamic_cast<const CGDwelling *>(obj))
return true; return true;
if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
return true;
switch(obj->ID) switch(obj->ID)
{ {
@ -2854,12 +2850,12 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
case Obj::MAGIC_WELL: case Obj::MAGIC_WELL:
return h->mana < h->manaLimit(); return h->mana < h->manaLimit();
case Obj::PRISON: case Obj::PRISON:
return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); return ai->myCb->getHeroesInfo().size() < cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
case Obj::TAVERN: case Obj::TAVERN:
{ {
//TODO: make AI actually recruit heroes //TODO: make AI actually recruit heroes
//TODO: only on request //TODO: only on request
if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) if(ai->myCb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false; return false;
else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST)
return false; return false;

View File

@ -187,7 +187,7 @@ public:
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;

View File

@ -217,6 +217,16 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID)
return true; return true;
} }
bool CCallback::visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)
{
if(town->tempOwner!=player)
return false;
VisitTownBuilding pack(town->id, buildingID);
sendRequest(&pack);
return true;
}
void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action)
{ {
assert(action.actionType == EActionType::HERO_SPELL); assert(action.actionType == EActionType::HERO_SPELL);
@ -256,15 +266,15 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
sendRequest(&pack); sendRequest(&pack);
} }
void CCallback::trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)
{ {
trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); trade(marketId, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero);
} }
void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero) void CCallback::trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)
{ {
TradeOnMarketplace pack; TradeOnMarketplace pack;
pack.marketId = dynamic_cast<const CGObjectInstance *>(market)->id; pack.marketId = marketId;
pack.heroId = hero ? hero->id : ObjectInstanceID(); pack.heroId = hero ? hero->id : ObjectInstanceID();
pack.mode = mode; pack.mode = mode;
pack.r1 = id1; pack.r1 = id1;

View File

@ -34,7 +34,6 @@ class IBattleEventsReceiver;
class IGameEventsReceiver; class IGameEventsReceiver;
struct ArtifactLocation; struct ArtifactLocation;
class BattleStateInfoForRetreat; class BattleStateInfoForRetreat;
class IMarket;
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END
@ -76,12 +75,13 @@ public:
//town //town
virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE)=0; virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE)=0;
virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
virtual bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID)=0;
virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0;
virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
virtual void swapGarrisonHero(const CGTownInstance *town)=0; virtual void swapGarrisonHero(const CGTownInstance *town)=0;
virtual void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
virtual void trade(const IMarket * market, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0; virtual void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)=0;
virtual int selectionMade(int selection, QueryID queryID) =0; virtual int selectionMade(int selection, QueryID queryID) =0;
virtual int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) =0; virtual int sendQueryReply(std::optional<int32_t> reply, QueryID queryID) =0;
@ -182,14 +182,15 @@ public:
void manageHeroCostume(ObjectInstanceID hero, size_t costumeIdx, bool saveCostume) override; void manageHeroCostume(ObjectInstanceID hero, size_t costumeIdx, bool saveCostume) override;
void eraseArtifactByClient(const ArtifactLocation & al) override; void eraseArtifactByClient(const ArtifactLocation & al) override;
bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override;
bool visitTownBuilding(const CGTownInstance *town, BuildingID buildingID) override;
void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override;
bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override; bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override;
bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override;
void endTurn() override; void endTurn() override;
void swapGarrisonHero(const CGTownInstance *town) override; void swapGarrisonHero(const CGTownInstance *town) override;
void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; void trade(const ObjectInstanceID marketId, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
void trade(const IMarket * market, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override; void trade(const ObjectInstanceID marketId, EMarketMode mode, const std::vector<TradeItemSell> & id1, const std::vector<TradeItemBuy> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override;
void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero, const HeroTypeID & nextHero=HeroTypeID::NONE) override;
void save(const std::string &fname) override; void save(const std::string &fname) override;

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DEPS_FILENAME=armeabi-v7a DEPS_FILENAME=dependencies-android-32
. CI/android/before_install.sh . CI/android/before_install.sh

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DEPS_FILENAME=aarch64-v8a DEPS_FILENAME=dependencies-android-64
. CI/android/before_install.sh . CI/android/before_install.sh

View File

@ -4,6 +4,4 @@ echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/25.2.9519653" >> $GITHUB_ENV
brew install ninja brew install ninja
mkdir ~/.conan ; cd ~/.conan . CI/install_conan_dependencies.sh "$DEPS_FILENAME"
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \
| tar -xf - --xz

View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
RELEASE_TAG="1.2"
FILENAME="$1"
DOWNLOAD_URL="https://github.com/vcmi/vcmi-dependencies/releases/download/$RELEASE_TAG/$FILENAME.txz"
mkdir ~/.conan
cd ~/.conan
curl -L "$DOWNLOAD_URL" | tar -xf - --xz

View File

@ -2,6 +2,4 @@
echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
mkdir ~/.conan ; cd ~/.conan . CI/install_conan_dependencies.sh "dependencies-ios"
curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2.1/ios-arm64.txz' \
| tar -xf -

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DEPS_FILENAME=intel-cross-arm DEPS_FILENAME=dependencies-mac-arm
. CI/mac/before_install.sh . CI/mac/before_install.sh

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash #!/usr/bin/env bash
DEPS_FILENAME=intel DEPS_FILENAME=dependencies-mac-intel
. CI/mac/before_install.sh . CI/mac/before_install.sh

View File

@ -4,6 +4,4 @@ echo DEVELOPER_DIR=/Applications/Xcode_14.2.app >> $GITHUB_ENV
brew install ninja brew install ninja
mkdir ~/.conan ; cd ~/.conan . CI/install_conan_dependencies.sh "$DEPS_FILENAME"
curl -L "https://github.com/vcmi/vcmi-deps-macos/releases/download/1.2.1/$DEPS_FILENAME.txz" \
| tar -xf -

View File

@ -11,6 +11,4 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-
curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \
&& sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb; && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
mkdir ~/.conan ; cd ~/.conan . CI/install_conan_dependencies.sh "dependencies-mingw-32"
curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w32.tgz" \
| tar -xzf -

View File

@ -11,6 +11,4 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-
curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-3_all.deb \ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-x86-64-dev_10.0.0-3_all.deb \
&& sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb; && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
mkdir ~/.conan ; cd ~/.conan . CI/install_conan_dependencies.sh "dependencies-mingw"
curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w64.tgz" \
| tar -xzf -

View File

@ -403,7 +403,10 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32)
if(CMAKE_BUILD_TYPE STREQUAL "Debug") if(CMAKE_BUILD_TYPE STREQUAL "Debug")
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32) if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32)
# For gcc 14+ we can use -fhardened instead # For gcc 14+ we can use -fhardened instead
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection -fcf-protection=full") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection")
if (CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x86_64")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcf-protection=full")
endif()
endif() endif()
endif() endif()
@ -519,7 +522,7 @@ if(ENABLE_LAUNCHER OR ENABLE_EDITOR)
endif() endif()
endif() endif()
if(ENABLE_CLIENT) if(NOT ENABLE_MINIMAL_LIB)
find_package(TBB REQUIRED) find_package(TBB REQUIRED)
endif() endif()
@ -677,6 +680,7 @@ endif()
if (ENABLE_CLIENT) if (ENABLE_CLIENT)
add_subdirectory(client) add_subdirectory(client)
add_subdirectory(clientapp)
endif() endif()
if(ENABLE_SERVER) if(ENABLE_SERVER)
@ -722,6 +726,10 @@ endif()
if(WIN32) if(WIN32)
if(TBB_FOUND AND MSVC)
install_vcpkg_imported_tgt(TBB::tbb)
endif()
if(USING_CONAN) if(USING_CONAN)
#Conan imports enabled #Conan imports enabled
vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}") vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")

View File

@ -1,12 +1,28 @@
# 1.5.5 -> 1.6.0 # 1.5.5 -> 1.6.0 (in development)
# General ### General
* Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods. * Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods.
* Added option to start vcmi server on randomly selected TCP port * Added option to start vcmi server on randomly selected TCP port
* Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems * Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems
* It is now possible to generate game statistics using `!statistic` command in game chat
# Interface ### Stability
* Fixed possible crash on connecting bluetooth mouse during gameplay on Android
* VCMI will now write more detailed information to log file on crash due to uncaught exception
### Multiplayer
* Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction
### Mechanics
* Arrow tower will now prefer to attack more units that are viewed most dangerous instead of simply attacking top-most unit
* Score in campaigns will now be correctly tracked for games loaded from a save
* Fixed incorrect direction of Dragon Breath attack in some cases if wide creature attacks another wide creature
* Map events and town events are now triggered on start of turn of player affected by event, in line with H3 instead of triggering on start of new day for all players
### Interface
* Implemented spell quick selection panel in combat * Implemented spell quick selection panel in combat
* Implemented adventure map overlay accessible via Alt key that highlights all interactive objects on screen
* Added hotkeys to reorder list of owned towns or heroes
* The number of units resurrected using the Life Drain ability is now written to the combat log. * The number of units resurrected using the Life Drain ability is now written to the combat log.
* Fixed playback of audio stream with different formats from video files in some Heroes 3 versions * Fixed playback of audio stream with different formats from video files in some Heroes 3 versions
* Video playback will not be replaced by a black square when another dialogue box is on top of the video. * Video playback will not be replaced by a black square when another dialogue box is on top of the video.
@ -19,17 +35,81 @@
* Semi-transparent shadows now correctly update their transparency during fading effects, such as resource pickups * Semi-transparent shadows now correctly update their transparency during fading effects, such as resource pickups
* Game will now save all names for human player in hotseat mode * Game will now save all names for human player in hotseat mode
* Added unassigned by default shortcuts for toggling visibility of visitable and blocked tiles * Added unassigned by default shortcuts for toggling visibility of visitable and blocked tiles
* Spellbook button in battle is now blocked if hero has no spellbook
* Adventure map will no longer scroll if window is not in focus
### Random Maps Generator
* Implemented connection option 'forcePortal'
* It is now possible to connect zone to itself using pair of portals
### AI ### AI
* Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex * Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex
* Fixed several cases where Nullkiller AI can count same dangerous object twice, doubling expected army loss. * Fixed several cases where Nullkiller AI can count same dangerous object twice, doubling expected army loss.
* Nullkiller is now capable of visiting configurable objects from mods * Nullkiller is now capable of visiting configurable objects from mods
* Nullkiller now uses whirlpools for map movement
* AI can now correctly estimate effect of Dragon Breath and other similar abilities
### Map Editor
* Implemented tracking of building requirements for Building Dialog
* Added build/demolish/enable/disable all buildings options to Building Dialog in town properties
* It is now possible to set spells allowed or required to be present in Mages Guild
* It is now possible to add timed events to a town
* Fixed editor not marking mod as dependency if spells from mod are used in town Mages Guild or in hero starting spells
### Modding ### Modding
* Fixed multiple issues with configurable town buildings
* Added documentation for configurable town buildings. See docs/Moddders/Entities_Format/Town_Buildings_Format.md
* Replaced some of hardcoded town buildings with configurable buildings. These building types are now deprecated and will be removed in future.
* Added support for custom music and opening sound for a battlefield * Added support for custom music and opening sound for a battlefield
* Added support for multiple music tracks for towns * Added support for multiple music tracks for towns
* Added support for multiple music tracks for terrains on adventure map * Added support for multiple music tracks for terrains on adventure map
* Fixed several cases where vcmi will report errors in json without specifying filename of invalid file * Fixed several cases where vcmi will report errors in json without specifying filename of invalid file
* It is now possible to use .zip archive for VCMI campaigns instead of raw gzip stream
* Added support for custom region definitions (such as background images) for VCMI campaigns
* It is now possible to change minimal values of primary skills for heroes
* Added support for HotA bank building from Factory
* Added support for HotA-style 8th creature in town
# 1.5.6 -> 1.5.7
* Fixed game freeze if player is attacked in online multiplayer game by another player when he has unread dialogs, such as new week notification
* Fixed possible game crash after being attacked by enemy with artifact that blocks spellcasting
* Fixed heroes on map limit game setting not respected when moving hero from town garrison.
* Add workaround to fix possible crash on attempt to start previously generated random map that has players without owned heroes or towns
* Fixed crash on right-clicking spell icon when receiving unlearnable spells from Pandora
* Fixed possible text overflow in match information box in online lobby
* Fixed overlapping text in lobby login window
* Fixed excessive removal of open dialogs such as new week or map events on new turn
* Fixed objects like Mystical Gardens not resetting their state on new week correctly
# 1.5.5 -> 1.5.6
### Stability
* Fixed possible crash on transferring hero to next campaign scenario if hero has combined artifact some components of which can be transferred
* Fixed possible crash on transferring hero to next campaign scenario that has creature with faction limiter in his army
* Fixed possible crash on application shutdown due to incorrect destruction order of UI entities
### Multiplayer
* Mod compatibility issues when joining a lobby room now use color coding to make them less easy to miss.
* Incompatible mods are now placed before compatible mods when joining lobby room.
* Fixed text overflow in online lobby interface
* Fixed jittering simultaneous turns slider after moving it twice over short period
* Fixed non-functioning slider in invite to game room dialog
### Interface
* Fixed some shortcuts that were not active during the enemy's turn, such as Thieves' Guild.
* Game now correctly uses melee damage calculation when forcing a melee attack with a shooter.
* Game will now close all open dialogs on start of our turn, to avoid bugs like locked right-click popups
### Map Objects
* Spells the hero can't learn are no longer hidden when received from a rewardable object, such as the Pandora Box
* Spells that cannot be learned are now displayed with gray text in the name of the spell.
* Configurable objects with scouted state such as Witch Hut in HotA now correctly show their reward on right click after vising them but refusing to accept reward
* Right-click tooltip on map dwelling now always shows produced creatures. Player that owns the dwelling can also see number of creatures available for recruit
### Modding
* Fixed possible crash on invalid SPELL_LIKE_ATTACK bonus
* Added compatibility check when loading maps with old names for boats
# 1.5.4 -> 1.5.5 # 1.5.4 -> 1.5.5

View File

@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
# define STRONG_INLINE inline # define STRONG_INLINE inline
#endif #endif
// Required for building boost::stacktrace on macOS.
// See https://github.com/boostorg/stacktrace/issues/88
#if defined(VCMI_APPLE)
#define _GNU_SOURCE
#endif
#define _USE_MATH_DEFINES #define _USE_MATH_DEFINES
#include <algorithm> #include <algorithm>
@ -700,6 +706,33 @@ namespace vstd
return a + (b - a) * f; return a + (b - a) * f;
} }
/// Divides dividend by divisor and rounds result up
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndCeil(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return (dividend + divisor - 1) / divisor;
}
/// Divides dividend by divisor and rounds result to nearest
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndRound(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return (dividend + divisor / 2 - 1) / divisor;
}
/// Divides dividend by divisor and rounds result down
/// For use with integer-only arithmetic
template<typename Integer1, typename Integer2>
Integer1 divideAndFloor(const Integer1 & dividend, const Integer2 & divisor)
{
static_assert(std::is_integral_v<Integer1> && std::is_integral_v<Integer2>, "This function should only be used with integral types");
return dividend / divisor;
}
template<typename Floating> template<typename Floating>
bool isAlmostZero(const Floating & value) bool isAlmostZero(const Floating & value)
{ {

View File

@ -72,6 +72,11 @@
"vcmi.lobby.noUnderground" : "无地下部分", "vcmi.lobby.noUnderground" : "无地下部分",
"vcmi.lobby.sortDate" : "以修改时间排序地图", "vcmi.lobby.sortDate" : "以修改时间排序地图",
"vcmi.lobby.backToLobby" : "返回大厅", "vcmi.lobby.backToLobby" : "返回大厅",
"vcmi.lobby.author" : "作者",
"vcmi.lobby.handicap" : "障碍",
"vcmi.lobby.handicap.resource" : "给予玩家起始资源以外的更多资源,允许负值,但总量不会低于0(玩家永远不会能以负资源开始游戏)。",
"vcmi.lobby.handicap.income" : "按百分比改变玩家的各种收入,向上取整。",
"vcmi.lobby.handicap.growth" : "改变玩家拥有的城镇的生物增长率,向上取整。",
"vcmi.lobby.login.title" : "VCMI大厅", "vcmi.lobby.login.title" : "VCMI大厅",
"vcmi.lobby.login.username" : "用户名:", "vcmi.lobby.login.username" : "用户名:",
@ -157,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "其他设置", // unused right now "vcmi.systemOptions.otherGroup" : "其他设置", // unused right now
"vcmi.systemOptions.townsGroup" : "城镇画面", "vcmi.systemOptions.townsGroup" : "城镇画面",
"vcmi.statisticWindow.statistics" : "统计",
"vcmi.statisticWindow.tsvCopy" : "复制到剪切板",
"vcmi.statisticWindow.selectView" : "选择视角",
"vcmi.statisticWindow.value" : "值",
"vcmi.statisticWindow.title.overview" : "概况",
"vcmi.statisticWindow.title.resources" : "资源",
"vcmi.statisticWindow.title.income" : "收入",
"vcmi.statisticWindow.title.numberOfHeroes" : "英雄数量",
"vcmi.statisticWindow.title.numberOfTowns" : "城镇数量",
"vcmi.statisticWindow.title.numberOfArtifacts" : "宝物数量",
"vcmi.statisticWindow.title.numberOfDwellings" : "野外巢穴数量",
"vcmi.statisticWindow.title.numberOfMines" : "矿井数量",
"vcmi.statisticWindow.title.armyStrength" : "部队强度",
"vcmi.statisticWindow.title.experience" : "经验",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "部队花费",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "建造花费",
"vcmi.statisticWindow.title.mapExplored" : "地图探索比例",
"vcmi.statisticWindow.param.playerName" : "玩家名称",
"vcmi.statisticWindow.param.daysSurvived" : "存活天数",
"vcmi.statisticWindow.param.maxHeroLevel" : "最大英雄等级",
"vcmi.statisticWindow.param.battleWinRatioHero" : "胜率(对英雄)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "胜率(对中立生物)",
"vcmi.statisticWindow.param.battlesHero" : "战斗(对英雄)",
"vcmi.statisticWindow.param.battlesNeutral" : "战斗(对中立生物)",
"vcmi.statisticWindow.param.maxArmyStrength" : "最大部队强度",
"vcmi.statisticWindow.param.tradeVolume" : "交易量",
"vcmi.statisticWindow.param.obeliskVisited" : "方尖塔访问",
"vcmi.statisticWindow.icon.townCaptured" : "占领城镇",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "击败对手最强英雄",
"vcmi.statisticWindow.icon.grailFound" : "找到神器",
"vcmi.statisticWindow.icon.defeated" : "被击败",
"vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ", "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以无边框全屏模式运行。该模式下,游戏会始终和桌面分辨率保持一致,无视设置的分辨率。 ",
"vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (独占)", "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (独占)",
@ -237,6 +274,8 @@
"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
"vcmi.battleOptions.endWithAutocombat.hover": "结束战斗", "vcmi.battleOptions.endWithAutocombat.hover": "结束战斗",
"vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程", "vcmi.battleOptions.endWithAutocombat.help": "{结束战斗}\n\n以自动战斗立即结束剩余战斗过程",
"vcmi.battleOptions.showQuickSpell.hover": "展示快捷法术面板",
"vcmi.battleOptions.showQuickSpell.help": "{展示快捷法术面板}\n\n展示快捷选择法术的面板。",
"vcmi.adventureMap.revisitObject.hover" : "重新访问", "vcmi.adventureMap.revisitObject.hover" : "重新访问",
"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。", "vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
@ -285,17 +324,9 @@
"vcmi.townHall.missingBase" : "必须先建造基础建筑 %s", "vcmi.townHall.missingBase" : "必须先建造基础建筑 %s",
"vcmi.townHall.noCreaturesToRecruit" : "没有可供招募的生物。", "vcmi.townHall.noCreaturesToRecruit" : "没有可供招募的生物。",
"vcmi.townHall.greetingManaVortex" : "接近%s时,你会全身充满活力,并且你的魔法值会加倍。",
"vcmi.townHall.greetingKnowledge" : "你研究了%s的浮雕,洞察了魔法的秘密(知识+1)。", "vcmi.townStructure.bank.borrow" : "你走进银行。一位银行职员看到你,说道:“我们为您提供了一个特别优惠。您可以向我们借2500金币,期限为5天。您每天需要偿还500金币。”",
"vcmi.townHall.greetingSpellPower" : "%s教你如何运用魔法力量(力量+1)。", "vcmi.townStructure.bank.payBack" : "你走进银行。一位银行职员看到你,说道:“您已经获得了贷款。还清之前,不能再申请新的贷款。”",
"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.anyOf" : "以下任一前提:",
"vcmi.logicalExpressions.allOf" : "以下所有前提:", "vcmi.logicalExpressions.allOf" : "以下所有前提:",
@ -629,5 +660,7 @@
"core.bonus.WATER_IMMUNITY.name": "水系免疫", "core.bonus.WATER_IMMUNITY.name": "水系免疫",
"core.bonus.WATER_IMMUNITY.description": "免疫水系魔法", "core.bonus.WATER_IMMUNITY.description": "免疫水系魔法",
"core.bonus.WIDE_BREATH.name": "弧形焰息", "core.bonus.WIDE_BREATH.name": "弧形焰息",
"core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)" "core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)",
"core.bonus.DISINTEGRATE.name": "解体",
"core.bonus.DISINTEGRATE.description": "死亡后不会留下尸体"
} }

View File

@ -278,17 +278,6 @@
"vcmi.townHall.missingBase" : "Základní budova %s musí být postavena jako první", "vcmi.townHall.missingBase" : "Základní budova %s musí být postavena jako první",
"vcmi.townHall.noCreaturesToRecruit" : "Žádné jednotky k vycvičení!", "vcmi.townHall.noCreaturesToRecruit" : "Žádné jednotky k vycvičení!",
"vcmi.townHall.greetingManaVortex" : "Při pobytu u místa %s se vaše tělo naplnilo novou energií. Máte dvojnásobné množství maximální magické energie.",
"vcmi.townHall.greetingKnowledge" : "Studujete glyfy na the %s a porozumíte fungování různých magií (+1 Znalosti).",
"vcmi.townHall.greetingSpellPower" : "%s vás učí nové cesty zaměření vaší magické síly (+1 Síla kouzel).",
"vcmi.townHall.greetingExperience" : "Návštěva %s vás naučila spoustu nových dovedností (+1000 zkušeností).",
"vcmi.townHall.greetingAttack" : "Čas strávený poblíž místa zvaného %s vám dovolil se naučit efektivnější bojové dovednosti (+1 Útočná síla).",
"vcmi.townHall.greetingDefence" : "Trávíte čas na místě zvaném %s, zkušení bojovníci vás u toho naučili nové metody obrany (+1 Obranná síla).",
"vcmi.townHall.hasNotProduced" : "%s - zatím nic nevyrobeno.",
"vcmi.townHall.hasProduced" : "%s - vyrobeno %d %s tento týden.",
"vcmi.townHall.greetingCustomBonus" : "%s vám dává +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " do další bitvy.",
"vcmi.townHall.greetingInTownMagicWell" : "%s - obnoveno na maximum vaši magickou energii.",
"vcmi.logicalExpressions.anyOf" : "Něco z následujících:", "vcmi.logicalExpressions.anyOf" : "Něco z následujících:",
"vcmi.logicalExpressions.allOf" : "Všechny následující:", "vcmi.logicalExpressions.allOf" : "Všechny následující:",

View File

@ -73,6 +73,10 @@
"vcmi.lobby.sortDate" : "Sorts maps by change date", "vcmi.lobby.sortDate" : "Sorts maps by change date",
"vcmi.lobby.backToLobby" : "Return to lobby", "vcmi.lobby.backToLobby" : "Return to lobby",
"vcmi.lobby.author" : "Author", "vcmi.lobby.author" : "Author",
"vcmi.lobby.handicap" : "Handicap",
"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
"vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.title" : "VCMI Online Lobby",
"vcmi.lobby.login.username" : "Username:", "vcmi.lobby.login.username" : "Username:",
@ -158,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now "vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now
"vcmi.systemOptions.townsGroup" : "Town Screen", "vcmi.systemOptions.townsGroup" : "Town Screen",
"vcmi.statisticWindow.statistics" : "Statistics",
"vcmi.statisticWindow.tsvCopy" : "Data to clipboard",
"vcmi.statisticWindow.selectView" : "Select view",
"vcmi.statisticWindow.value" : "Value",
"vcmi.statisticWindow.title.overview" : "Overview",
"vcmi.statisticWindow.title.resources" : "Resources",
"vcmi.statisticWindow.title.income" : "Income",
"vcmi.statisticWindow.title.numberOfHeroes" : "No. of heroes",
"vcmi.statisticWindow.title.numberOfTowns" : "No. of towns",
"vcmi.statisticWindow.title.numberOfArtifacts" : "No. of artifacts",
"vcmi.statisticWindow.title.numberOfDwellings" : "No. of dwellings",
"vcmi.statisticWindow.title.numberOfMines" : "No. of mines",
"vcmi.statisticWindow.title.armyStrength" : "Army strength",
"vcmi.statisticWindow.title.experience" : "Experience",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Army costs",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Building costs",
"vcmi.statisticWindow.title.mapExplored" : "Map explore ratio",
"vcmi.statisticWindow.param.playerName" : "Player name",
"vcmi.statisticWindow.param.daysSurvived" : "Days survived",
"vcmi.statisticWindow.param.maxHeroLevel" : "Max hero level",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Win ratio (vs. hero)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Win ratio (vs. neutral)",
"vcmi.statisticWindow.param.battlesHero" : "Battles (vs. hero)",
"vcmi.statisticWindow.param.battlesNeutral" : "Battles (vs. neutral)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Max total army strength",
"vcmi.statisticWindow.param.tradeVolume" : "Trade volume",
"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk visited",
"vcmi.statisticWindow.icon.townCaptured" : "Town captured",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Strongest hero of opponent defeated",
"vcmi.statisticWindow.icon.grailFound" : "Grail found",
"vcmi.statisticWindow.icon.defeated" : "Defeated",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)",
@ -288,17 +324,9 @@
"vcmi.townHall.missingBase" : "Base building %s must be built first", "vcmi.townHall.missingBase" : "Base building %s must be built first",
"vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!", "vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!",
"vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.",
"vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", "vcmi.townStructure.bank.borrow" : "You enter the bank. A banker sees you and says: \"We have made a special offer for you. You can take a loan of 2500 gold from us for 5 days. You will have to repay 500 gold every day.\"",
"vcmi.townHall.greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", "vcmi.townStructure.bank.payBack" : "You enter the bank. A banker sees you and says: \"You have already got your loan. Pay it back before taking a new one.\"",
"vcmi.townHall.greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).",
"vcmi.townHall.greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).",
"vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).",
"vcmi.townHall.hasNotProduced" : "The %s has not produced anything yet.",
"vcmi.townHall.hasProduced" : "The %s produced %d %s this week.",
"vcmi.townHall.greetingCustomBonus" : "%s gives you +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " until next battle.",
"vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.",
"vcmi.logicalExpressions.anyOf" : "Any of the following:", "vcmi.logicalExpressions.anyOf" : "Any of the following:",
"vcmi.logicalExpressions.allOf" : "All of the following:", "vcmi.logicalExpressions.allOf" : "All of the following:",
@ -632,5 +660,7 @@
"core.bonus.WATER_IMMUNITY.name": "Water immunity", "core.bonus.WATER_IMMUNITY.name": "Water immunity",
"core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic", "core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic",
"core.bonus.WIDE_BREATH.name": "Wide breath", "core.bonus.WIDE_BREATH.name": "Wide breath",
"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)" "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)",
"core.bonus.DISINTEGRATE.name": "Disintegrate",
"core.bonus.DISINTEGRATE.description": "No corpse remains after death"
} }

View File

@ -135,17 +135,6 @@
"vcmi.townHall.missingBase" : "Le bâtiment de base %s doit être construit avant", "vcmi.townHall.missingBase" : "Le bâtiment de base %s doit être construit avant",
"vcmi.townHall.noCreaturesToRecruit" : "Il n'y a aucune créature à recruter !", "vcmi.townHall.noCreaturesToRecruit" : "Il n'y a aucune créature à recruter !",
"vcmi.townHall.greetingManaVortex" : "Alors que vous approchez du %s, votre corps est rempli d'une nouvelle énergie. Vous avez doublé vos points de sort normaux.",
"vcmi.townHall.greetingKnowledge" : "Vous étudiez les glyphes sur le %s et découvrez le fonctionnement de diverses magies (+1 Connaissance).",
"vcmi.townHall.greetingSpellPower" : "Le %s vous apprend de nouvelles façons de concentrer vos pouvoirs magiques (+1 Pouvoir).",
"vcmi.townHall.greetingExperience" : "Une visite au %s vous apprend de nombreuses nouvelles compétences (+1000 Expérience).",
"vcmi.townHall.greetingAttack" : "Un peu de temps passé au %s vous permet d'apprendre des compétences de combat plus efficaces (+1 compétence d'attaque).",
"vcmi.townHall.greetingDefence" : "En passant du temps dans le %s, les guerriers expérimentés qui s'y trouvent vous enseignent des compétences défensives supplémentaires (+1 Défense).",
"vcmi.townHall.hasNotProduced" : "Le %s n'a encore rien produit.",
"vcmi.townHall.hasProduced" : "Le %s a produit %d %s cette semaine.",
"vcmi.townHall.greetingCustomBonus" : "%s vous offre +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " jusqu'à la prochaine bataille.",
"vcmi.townHall.greetingInTownMagicWell" : "%s a restauré vos points de sort au maximum.",
"vcmi.logicalExpressions.anyOf" : "L'un des éléments suivants :", "vcmi.logicalExpressions.anyOf" : "L'un des éléments suivants :",
"vcmi.logicalExpressions.allOf" : "Tous les éléments suivants :", "vcmi.logicalExpressions.allOf" : "Tous les éléments suivants :",

View File

@ -73,6 +73,10 @@
"vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum", "vcmi.lobby.sortDate" : "Ordnet Karten nach Änderungsdatum",
"vcmi.lobby.backToLobby" : "Zur Lobby zurückkehren", "vcmi.lobby.backToLobby" : "Zur Lobby zurückkehren",
"vcmi.lobby.author" : "Author", "vcmi.lobby.author" : "Author",
"vcmi.lobby.handicap" : "Handicap",
"vcmi.lobby.handicap.resource" : "Gibt den Spielern entsprechende Ressourcen zum Start zusätzlich zu den normalen Startressourcen. Negative Werte sind erlaubt, werden aber insgesamt auf 0 begrenzt (der Spieler beginnt nie mit negativen Ressourcen).",
"vcmi.lobby.handicap.income" : "Verändert die verschiedenen Einkommen des Spielers um den Prozentsatz. Wird aufgerundet.",
"vcmi.lobby.handicap.growth" : "Verändert die Wachstumsrate der Kreaturen in den Städten, die der Spieler besitzt. Wird aufgerundet.",
"vcmi.lobby.login.title" : "VCMI Online Lobby", "vcmi.lobby.login.title" : "VCMI Online Lobby",
"vcmi.lobby.login.username" : "Benutzername:", "vcmi.lobby.login.username" : "Benutzername:",
@ -158,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
"vcmi.statisticWindow.statistics" : "Statistik",
"vcmi.statisticWindow.tsvCopy" : "In Zwischenablage",
"vcmi.statisticWindow.selectView" : "Ansicht wählen",
"vcmi.statisticWindow.value" : "Wert",
"vcmi.statisticWindow.title.overview" : "Überblick",
"vcmi.statisticWindow.title.resources" : "Ressourcen",
"vcmi.statisticWindow.title.income" : "Einkommen",
"vcmi.statisticWindow.title.numberOfHeroes" : "Nr. der Helden",
"vcmi.statisticWindow.title.numberOfTowns" : "Nr. der Städte",
"vcmi.statisticWindow.title.numberOfArtifacts" : "Nr. der Artefakte",
"vcmi.statisticWindow.title.numberOfDwellings" : "Nr. der Behausungen",
"vcmi.statisticWindow.title.numberOfMines" : "Nr. der Minen",
"vcmi.statisticWindow.title.armyStrength" : "Armeestärke",
"vcmi.statisticWindow.title.experience" : "Erfahrung",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Armeekosten",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Gebäudekosten",
"vcmi.statisticWindow.title.mapExplored" : "Maperkundungsrate",
"vcmi.statisticWindow.param.playerName" : "Spielername",
"vcmi.statisticWindow.param.daysSurvived" : "Tage überlebt",
"vcmi.statisticWindow.param.maxHeroLevel" : "Max Heldenlevel",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Sieg Verh. (Helden)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Sieg Verh. (Neutral)",
"vcmi.statisticWindow.param.battlesHero" : "Kämpfe (Helden)",
"vcmi.statisticWindow.param.battlesNeutral" : "Kämpfe (Neutral)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Max Gesamt-Armeestärke",
"vcmi.statisticWindow.param.tradeVolume" : "Handelsvol.",
"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk besucht",
"vcmi.statisticWindow.icon.townCaptured" : "Stadt erobert",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Stärksten Helden eines Gegners besiegt",
"vcmi.statisticWindow.icon.grailFound" : "Gral gefunden",
"vcmi.statisticWindow.icon.defeated" : "Besiegt",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)",
@ -288,17 +324,6 @@
"vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden", "vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden",
"vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!", "vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!",
"vcmi.townHall.greetingManaVortex" : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.",
"vcmi.townHall.greetingKnowledge" : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).",
"vcmi.townHall.greetingSpellPower" : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).",
"vcmi.townHall.greetingExperience" : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).",
"vcmi.townHall.greetingAttack" : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).",
"vcmi.townHall.greetingDefence" : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).",
"vcmi.townHall.hasNotProduced" : "Die %s hat noch nichts produziert.",
"vcmi.townHall.hasProduced" : "Die %s hat diese Woche %d %s produziert.",
"vcmi.townHall.greetingCustomBonus" : "%s gibt Ihnen +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " bis zur nächsten Schlacht.",
"vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.",
"vcmi.logicalExpressions.anyOf" : "Eines der folgenden:", "vcmi.logicalExpressions.anyOf" : "Eines der folgenden:",
"vcmi.logicalExpressions.allOf" : "Alles der folgenden:", "vcmi.logicalExpressions.allOf" : "Alles der folgenden:",

View File

@ -18,8 +18,8 @@
"vcmi.adventureMap.noTownWithTavern" : "Brak dostępnego miasta z karczmą!", "vcmi.adventureMap.noTownWithTavern" : "Brak dostępnego miasta z karczmą!",
"vcmi.adventureMap.spellUnknownProblem" : "Nieznany problem z zaklęciem, brak dodatkowych informacji.", "vcmi.adventureMap.spellUnknownProblem" : "Nieznany problem z zaklęciem, brak dodatkowych informacji.",
"vcmi.adventureMap.playerAttacked" : "Gracz został zaatakowany: %s", "vcmi.adventureMap.playerAttacked" : "Gracz został zaatakowany: %s",
"vcmi.adventureMap.moveCostDetails" : "Punkty ruchu - Koszt: %TURNS tury + %POINTS punkty, Pozostałe punkty: %REMAINING", "vcmi.adventureMap.moveCostDetails" : "Punkty ruchu - Koszt: %TURNS tury + %POINTS punktów, Pozostanie: %REMAINING punktów",
"vcmi.adventureMap.moveCostDetailsNoTurns" : "Punkty ruchu - Koszt: %POINTS punkty, Pozostałe punkty: %REMAINING", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Punkty ruchu - Koszt: %POINTS punktów, Pozostanie: %REMAINING punktów",
"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!", "vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!",
"vcmi.capitalColors.0" : "Czerwony", "vcmi.capitalColors.0" : "Czerwony",
@ -72,6 +72,11 @@
"vcmi.lobby.noUnderground" : "brak podziemi", "vcmi.lobby.noUnderground" : "brak podziemi",
"vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji", "vcmi.lobby.sortDate" : "Sortuj mapy według daty modyfikacji",
"vcmi.lobby.backToLobby" : "Wróc do lobby", "vcmi.lobby.backToLobby" : "Wróc do lobby",
"vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Balans",
"vcmi.lobby.handicap.resource" : "Przyznaje graczom zasoby na start, oprócz normalnych zasobów początkowych. Wartości ujemne są dozwolone, ale łącznie gracz nie może zaczynać na minus.",
"vcmi.lobby.handicap.income" : "Ustala procentowy współczynnik przychodu gracza. Zaokrąglone w górę.",
"vcmi.lobby.handicap.growth" : "Ustala współczynnik populacji stworzeń w miastach należących do gracza. Zaokrąglone w górę.",
"vcmi.lobby.login.title" : "Lobby sieciowe VCMI", "vcmi.lobby.login.title" : "Lobby sieciowe VCMI",
"vcmi.lobby.login.username" : "Użytkownik:", "vcmi.lobby.login.username" : "Użytkownik:",
@ -79,7 +84,7 @@
"vcmi.lobby.login.error" : "Błąd połączenia: %s", "vcmi.lobby.login.error" : "Błąd połączenia: %s",
"vcmi.lobby.login.create" : "Nowe konto", "vcmi.lobby.login.create" : "Nowe konto",
"vcmi.lobby.login.login" : "Login", "vcmi.lobby.login.login" : "Login",
"vcmi.lobby.login.as" : "Zaloguj jako %s", "vcmi.lobby.login.as" : "Logowanie jako %s",
"vcmi.lobby.header.rooms" : "Pokoje - %d", "vcmi.lobby.header.rooms" : "Pokoje - %d",
"vcmi.lobby.header.channels" : "Kanały tekstowe", "vcmi.lobby.header.channels" : "Kanały tekstowe",
"vcmi.lobby.header.chat.global" : "Chat globalny - %s", // %s -> language name "vcmi.lobby.header.chat.global" : "Chat globalny - %s", // %s -> language name
@ -88,7 +93,7 @@
"vcmi.lobby.header.history" : "Twoje poprzednie gry", "vcmi.lobby.header.history" : "Twoje poprzednie gry",
"vcmi.lobby.header.players" : "Graczy online - %d", "vcmi.lobby.header.players" : "Graczy online - %d",
"vcmi.lobby.match.solo" : "Gra jednoosobowa", "vcmi.lobby.match.solo" : "Gra jednoosobowa",
"vcmi.lobby.match.duel" : "Graj z %s", // %s -> nickname of another player "vcmi.lobby.match.duel" : "vs. %s", // %s -> nickname of another player
"vcmi.lobby.match.multi" : "%d graczy", "vcmi.lobby.match.multi" : "%d graczy",
"vcmi.lobby.room.create" : "Stwórz nowy pokój", "vcmi.lobby.room.create" : "Stwórz nowy pokój",
"vcmi.lobby.room.players.limit" : "Limit graczy", "vcmi.lobby.room.players.limit" : "Limit graczy",
@ -125,20 +130,20 @@
"vcmi.lobby.mod.state.version" : "Niepoprawna wersja", "vcmi.lobby.mod.state.version" : "Niepoprawna wersja",
"vcmi.lobby.mod.state.excessive" : "Musi być wyłączony", "vcmi.lobby.mod.state.excessive" : "Musi być wyłączony",
"vcmi.lobby.mod.state.missing" : "Nie zainstalowany", "vcmi.lobby.mod.state.missing" : "Nie zainstalowany",
"vcmi.lobby.pvp.coin.hover" : "Moneta", "vcmi.lobby.pvp.coin.hover" : "Rzut monetą",
"vcmi.lobby.pvp.coin.help" : "Rzut monetą", "vcmi.lobby.pvp.coin.help" : "Wyświetli symulację rzutu monetą na czacie, wskazując 0 lub 1 w zależności od rezultatu rzutu.",
"vcmi.lobby.pvp.randomTown.hover" : "Losowe miasto", "vcmi.lobby.pvp.randomTown.hover" : "Wylosuj miasto",
"vcmi.lobby.pvp.randomTown.help" : "Wyświetli nazwę wylosowanego miasta na czacie", "vcmi.lobby.pvp.randomTown.help" : "Wyświetli nazwę wylosowanego miasta na czacie, które nie zostało zablokowane na liście",
"vcmi.lobby.pvp.randomTownVs.hover" : "Losowe miasto vs.", "vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta",
"vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie", "vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście",
"vcmi.lobby.pvp.versus" : "vs.", "vcmi.lobby.pvp.versus" : "vs.",
"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s", "vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.", "vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
"vcmi.server.errors.disconnected" : "{Błąd sieciowy}\n\nUtracono połączenie z serwerem!",
"vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
"vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}",
"vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}",
"vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?",
"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
@ -157,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now "vcmi.systemOptions.otherGroup" : "Inne ustawienia", // unused right now
"vcmi.systemOptions.townsGroup" : "Ekran miasta", "vcmi.systemOptions.townsGroup" : "Ekran miasta",
"vcmi.statisticWindow.statistics" : "Statystyki",
"vcmi.statisticWindow.tsvCopy" : "Kopiuj do schowka",
"vcmi.statisticWindow.selectView" : "Tryb widoku",
"vcmi.statisticWindow.value" : "Wartość",
"vcmi.statisticWindow.title.overview" : "Przegląd",
"vcmi.statisticWindow.title.resources" : "Surowce",
"vcmi.statisticWindow.title.income" : "Przychód",
"vcmi.statisticWindow.title.numberOfHeroes" : "Lb. bohaterów",
"vcmi.statisticWindow.title.numberOfTowns" : "Lb. miast",
"vcmi.statisticWindow.title.numberOfArtifacts" : "Lb. artefaktów",
"vcmi.statisticWindow.title.numberOfDwellings" : "Lb. siedlisk",
"vcmi.statisticWindow.title.numberOfMines" : "Lb. kopalni",
"vcmi.statisticWindow.title.armyStrength" : "Siła armii",
"vcmi.statisticWindow.title.experience" : "Doświadczenie",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Koszty armii",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Koszty budowy",
"vcmi.statisticWindow.title.mapExplored" : "Odkrycie mapy",
"vcmi.statisticWindow.param.playerName" : "Imię",
"vcmi.statisticWindow.param.daysSurvived" : "Przeżytych dni",
"vcmi.statisticWindow.param.maxHeroLevel" : "Maks. poziom bohatera",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Zwycięstw (vs. bohaterom)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Zwycięstw (vs. neutralnym)",
"vcmi.statisticWindow.param.battlesHero" : "Walk (vs. bohaterom)",
"vcmi.statisticWindow.param.battlesNeutral" : "Walk (vs. neutralnym)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Maks. siła armii",
"vcmi.statisticWindow.param.tradeVolume" : "Wielkość handlu",
"vcmi.statisticWindow.param.obeliskVisited" : "Lb. obelisków",
"vcmi.statisticWindow.icon.townCaptured" : "Miasto zdobyte",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Najsilniejszy bohater przeciwnika pokonany",
"vcmi.statisticWindow.icon.grailFound" : "Gral znaleziony",
"vcmi.statisticWindow.icon.defeated" : "Pokonany",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Pełny ekran (bez ramek)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Pełny ekran w trybie okna}\n\nVCMI będzie działać w trybie okna pełnoekranowego. W tym trybie gra będzie zawsze używać rozdzielczości pulpitu, ignorując wybraną rozdzielczość.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Pełny ekran (tradycyjny)",
@ -237,6 +274,8 @@
"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.",
"vcmi.battleOptions.endWithAutocombat.hover": "Natychmiastowe auto-walki", "vcmi.battleOptions.endWithAutocombat.hover": "Natychmiastowe auto-walki",
"vcmi.battleOptions.endWithAutocombat.help": "{Natychmiastowe auto-walki}\n\nAuto-walka natychmiastowo toczy walkę do samego końca", "vcmi.battleOptions.endWithAutocombat.help": "{Natychmiastowe auto-walki}\n\nAuto-walka natychmiastowo toczy walkę do samego końca",
"vcmi.battleOptions.showQuickSpell.hover": "Szybki dostęp do magii",
"vcmi.battleOptions.showQuickSpell.help": "{Szybki dostęp do magii}\n\nPokazuje panel szybkiego dostępu do czarów",
"vcmi.adventureMap.revisitObject.hover" : "Odwiedź obiekt ponownie", "vcmi.adventureMap.revisitObject.hover" : "Odwiedź obiekt ponownie",
"vcmi.adventureMap.revisitObject.help" : "{Odwiedź obiekt ponownie}\n\nJeżeli bohater aktualnie stoi na polu odwiedzającym obiekt za pomocą tego przycisku może go odwiedzić ponownie.", "vcmi.adventureMap.revisitObject.help" : "{Odwiedź obiekt ponownie}\n\nJeżeli bohater aktualnie stoi na polu odwiedzającym obiekt za pomocą tego przycisku może go odwiedzić ponownie.",
@ -285,17 +324,9 @@
"vcmi.townHall.missingBase" : "Podstawowy budynek %s musi zostać najpierw wybudowany", "vcmi.townHall.missingBase" : "Podstawowy budynek %s musi zostać najpierw wybudowany",
"vcmi.townHall.noCreaturesToRecruit" : "Brak stworzeń do rekrutacji!", "vcmi.townHall.noCreaturesToRecruit" : "Brak stworzeń do rekrutacji!",
"vcmi.townHall.greetingManaVortex" : "Zbliżając się do %s czujesz jak twoje ciało wypełnia energia. Ilość pkt. magii, które posiadasz, zwiększa się dwukrotnie.",
"vcmi.townHall.greetingKnowledge" : "Studiując napisy na %s odkrywasz nowe aspekty stosowania magii (wiedza +1).", "vcmi.townStructure.bank.borrow" : "Wchodzisz do banku. Bankier cię widzi i mówi: \"Złożyliśmy ci specjalną ofertę. Możesz wziąć od nas pożyczkę w wysokości 2500 złota na 5 dni. Będziesz musiał spłacać 500 złota każdego dnia.\"",
"vcmi.townHall.greetingSpellPower" : "Odwiedzając %s dowiadujesz się, jak zwiększyć potęgę swojej mocy magicznej (moc +1).", "vcmi.townStructure.bank.payBack" : "Wchodzisz do banku. Bankier cię widzi i mówi: „Już dostałeś pożyczkę. Spłać ją zanim weźmiesz nową.\"",
"vcmi.townHall.greetingExperience" : "Wizyta w %s zwiększa twoje doświadczenie (doświadczenie +1000).",
"vcmi.townHall.greetingAttack" : "Krótka wizyka w %s umożliwia ci polepszenie technik walki (atak +1).",
"vcmi.townHall.greetingDefence" : "Odwiedzasz %s. Doświadczeni żołnierze, którzy tam przebywają, uczą cię sztuki skutecznej obrony (obrona +1).",
"vcmi.townHall.hasNotProduced" : "%s nic jeszcze nie wyprodukował.",
"vcmi.townHall.hasProduced" : "%s wyprodukował w tym tygodniu: %d %s.",
"vcmi.townHall.greetingCustomBonus" : "%s daje tobie +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " do następnej bitwy.",
"vcmi.townHall.greetingInTownMagicWell" : "%s przywraca ci wszystkie punkty magii.",
"vcmi.logicalExpressions.anyOf" : "Dowolne spośród:", "vcmi.logicalExpressions.anyOf" : "Dowolne spośród:",
"vcmi.logicalExpressions.allOf" : "Wszystkie spośród:", "vcmi.logicalExpressions.allOf" : "Wszystkie spośród:",
@ -487,7 +518,7 @@
"core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowy odwet", "core.bonus.ADDITIONAL_RETALIATION.name": "Dodatkowy odwet",
"core.bonus.ADDITIONAL_RETALIATION.description": "${val} dodatkowy kontratak", "core.bonus.ADDITIONAL_RETALIATION.description": "${val} dodatkowy kontratak",
"core.bonus.AIR_IMMUNITY.name": "Odporność: Powietrze", "core.bonus.AIR_IMMUNITY.name": "Odporność: Powietrze",
"core.bonus.AIR_IMMUNITY.description": "Odporny na wszystkie czary szkoły powietrza", "core.bonus.AIR_IMMUNITY.description": "Odporny na magię powietrza",
"core.bonus.ATTACKS_ALL_ADJACENT.name": "Obrotowy atak", "core.bonus.ATTACKS_ALL_ADJACENT.name": "Obrotowy atak",
"core.bonus.ATTACKS_ALL_ADJACENT.description": "Atakuje wszystkich sąsiadujących wrogów", "core.bonus.ATTACKS_ALL_ADJACENT.description": "Atakuje wszystkich sąsiadujących wrogów",
"core.bonus.BLOCKS_RETALIATION.name": "Bez kontrataku", "core.bonus.BLOCKS_RETALIATION.name": "Bez kontrataku",
@ -509,13 +540,13 @@
"core.bonus.DEFENSIVE_STANCE.name": "Bonus do obrony", "core.bonus.DEFENSIVE_STANCE.name": "Bonus do obrony",
"core.bonus.DEFENSIVE_STANCE.description": "+${val} Obrony kiedy broni", "core.bonus.DEFENSIVE_STANCE.description": "+${val} Obrony kiedy broni",
"core.bonus.DESTRUCTION.name": "Destrukcja", "core.bonus.DESTRUCTION.name": "Destrukcja",
"core.bonus.DESTRUCTION.description": "Ma ${val}% szans na zabicie dodatkowych jednostek po ataku", "core.bonus.DESTRUCTION.description": "${val}% szans na zabicie dodatkowych jednostek po ataku",
"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Uderzenie Śmierci", "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Uderzenie Śmierci",
"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia", "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% szans na podwójne obrażenia",
"core.bonus.DRAGON_NATURE.name": "Smok", "core.bonus.DRAGON_NATURE.name": "Smok",
"core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę", "core.bonus.DRAGON_NATURE.description": "Stworzenie posiada smoczą naturę",
"core.bonus.EARTH_IMMUNITY.name": "Odporność: Ziemia", "core.bonus.EARTH_IMMUNITY.name": "Odporność: Ziemia",
"core.bonus.EARTH_IMMUNITY.description": "Odporny na wszystkie czary szkoły ziemi", "core.bonus.EARTH_IMMUNITY.description": "Odporny na magię ziemi",
"core.bonus.ENCHANTER.name": "Czarodziej", "core.bonus.ENCHANTER.name": "Czarodziej",
"core.bonus.ENCHANTER.description": "Rzuca czar ${subtype.spell}", "core.bonus.ENCHANTER.description": "Rzuca czar ${subtype.spell}",
"core.bonus.ENCHANTED.name": "Zaczarowany", "core.bonus.ENCHANTED.name": "Zaczarowany",
@ -525,7 +556,7 @@
"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Osłabienie Obrony (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Osłabienie Obrony (${val}%)",
"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Osłabia obronę wroga podczas ataku", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Osłabia obronę wroga podczas ataku",
"core.bonus.FIRE_IMMUNITY.name": "Odporność: Ogień", "core.bonus.FIRE_IMMUNITY.name": "Odporność: Ogień",
"core.bonus.FIRE_IMMUNITY.description": "Odporny na wszystkie czary szkoły ognia", "core.bonus.FIRE_IMMUNITY.description": "Odporny na magię ognia",
"core.bonus.FIRE_SHIELD.name": "Ognista tarcza (${val}%)", "core.bonus.FIRE_SHIELD.name": "Ognista tarcza (${val}%)",
"core.bonus.FIRE_SHIELD.description": "Odbija część obrażeń z walki wręcz", "core.bonus.FIRE_SHIELD.description": "Odbija część obrażeń z walki wręcz",
"core.bonus.FIRST_STRIKE.name": "Pierwsze Uderzenie", "core.bonus.FIRST_STRIKE.name": "Pierwsze Uderzenie",
@ -543,7 +574,7 @@
"core.bonus.GARGOYLE.name": "Gargulec", "core.bonus.GARGOYLE.name": "Gargulec",
"core.bonus.GARGOYLE.description": "Nie może się wskrzesić i uleczyć", "core.bonus.GARGOYLE.description": "Nie może się wskrzesić i uleczyć",
"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Redukcja obrażeń (${val}%)", "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Redukcja obrażeń (${val}%)",
"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obrażenia fizyczne z dystansu lub walki wręcz", "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Zmiejsza obr. fizyczne z dystansu lub wręcz",
"core.bonus.HATE.name": "${subtype.creature}", "core.bonus.HATE.name": "${subtype.creature}",
"core.bonus.HATE.description": "+${val}% dodatkowych obrażeń", "core.bonus.HATE.description": "+${val}% dodatkowych obrażeń",
"core.bonus.HEALER.name": "Uzdrowiciel", "core.bonus.HEALER.name": "Uzdrowiciel",
@ -556,8 +587,8 @@
"core.bonus.KING.description": "czar POGROMCA stopnia ${val}+", "core.bonus.KING.description": "czar POGROMCA stopnia ${val}+",
"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność: Czary 1-${val}", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odporność: Czary 1-${val}",
"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poz.", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odporny na czary 1-${val} poz.",
"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Ograniczony zasięg strzelania", "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Nie może strzelać do",
"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nie może strzelać do celów będących dalej niż ${val} heksów", "core.bonus.LIMITED_SHOOTING_RANGE.description" : "celów będących dalej niż ${val} heksów",
"core.bonus.LIFE_DRAIN.name": "Wysysa życie (${val}%)", "core.bonus.LIFE_DRAIN.name": "Wysysa życie (${val}%)",
"core.bonus.LIFE_DRAIN.description": "Wysysa ${val}% zadanych obrażeń", "core.bonus.LIFE_DRAIN.description": "Wysysa ${val}% zadanych obrażeń",
"core.bonus.MANA_CHANNELING.name": "Transfer many ${val}%", "core.bonus.MANA_CHANNELING.name": "Transfer many ${val}%",
@ -591,13 +622,13 @@
"core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót", "core.bonus.RETURN_AFTER_STRIKE.name": "Atak i Powrót",
"core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz", "core.bonus.RETURN_AFTER_STRIKE.description": "Wraca po ataku wręcz",
"core.bonus.REVENGE.name": "Odwet", "core.bonus.REVENGE.name": "Odwet",
"core.bonus.REVENGE.description": "Zadaje dodatkowe obrażenia zależne od strat własnych oddziału", "core.bonus.REVENGE.description": "Zadaje dodat. obr. zależne od strat własnych oddziału",
"core.bonus.SHOOTER.name": "Dystansowy", "core.bonus.SHOOTER.name": "Dystansowy",
"core.bonus.SHOOTER.description": "Stworzenie może strzelać", "core.bonus.SHOOTER.description": "Stworzenie może strzelać",
"core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła", "core.bonus.SHOOTS_ALL_ADJACENT.name": "Ostrzeliwuje wszystko dookoła",
"core.bonus.SHOOTS_ALL_ADJACENT.description": "Ataki dystansowe tego stworzenia uderzają we wszystkie cele na małym obszarze", "core.bonus.SHOOTS_ALL_ADJACENT.description": "Ataki dyst. tego stworzenia uderzają we wszystkie cele na małym obszarze",
"core.bonus.SOUL_STEAL.name": "Kradzież dusz", "core.bonus.SOUL_STEAL.name": "Kradzież dusz",
"core.bonus.SOUL_STEAL.description": "Zdobywa ${val} nowych stworzeń za każdego zabitego wroga", "core.bonus.SOUL_STEAL.description": "+${val} nowych stworzeń za każdego zabitego wroga",
"core.bonus.SPELLCASTER.name": "Czarodziej", "core.bonus.SPELLCASTER.name": "Czarodziej",
"core.bonus.SPELLCASTER.description": "Może rzucić ${subtype.spell}", "core.bonus.SPELLCASTER.description": "Może rzucić ${subtype.spell}",
"core.bonus.SPELL_AFTER_ATTACK.name": "${val}% szans na czar", "core.bonus.SPELL_AFTER_ATTACK.name": "${val}% szans na czar",
@ -629,5 +660,7 @@
"core.bonus.WATER_IMMUNITY.name": "Odporność: Woda", "core.bonus.WATER_IMMUNITY.name": "Odporność: Woda",
"core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody", "core.bonus.WATER_IMMUNITY.description": "Odporny na wszystkie czary szkoły wody",
"core.bonus.WIDE_BREATH.name": "Szerokie zionięcie", "core.bonus.WIDE_BREATH.name": "Szerokie zionięcie",
"core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)" "core.bonus.WIDE_BREATH.description": "Szeroki atak zionięciem (wiele heksów)",
"core.bonus.DISINTEGRATE.name": "Rozpadanie",
"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup"
} }

View File

@ -72,6 +72,11 @@
"vcmi.lobby.noUnderground" : "sem subterrâneo", "vcmi.lobby.noUnderground" : "sem subterrâneo",
"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração", "vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
"vcmi.lobby.backToLobby" : "Voltar para a sala de espera", "vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
"vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Desvant.",
"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI", "vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
"vcmi.lobby.login.username" : "Nome de usuário:", "vcmi.lobby.login.username" : "Nome de usuário:",
@ -157,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Outras Configurações", // não utilizado no momento "vcmi.systemOptions.otherGroup" : "Outras Configurações", // não utilizado no momento
"vcmi.systemOptions.townsGroup" : "Tela da Cidade", "vcmi.systemOptions.townsGroup" : "Tela da Cidade",
"vcmi.statisticWindow.statistics" : "Estatísticas",
"vcmi.statisticWindow.tsvCopy" : "Copiar dados",
"vcmi.statisticWindow.selectView" : "Selecionar visualização",
"vcmi.statisticWindow.value" : "Valor",
"vcmi.statisticWindow.title.overview" : "Visão geral",
"vcmi.statisticWindow.title.resources" : "Recursos",
"vcmi.statisticWindow.title.income" : "Renda",
"vcmi.statisticWindow.title.numberOfHeroes" : "Nº de heróis",
"vcmi.statisticWindow.title.numberOfTowns" : "Nº de cidades",
"vcmi.statisticWindow.title.numberOfArtifacts" : "Nº de artefatos",
"vcmi.statisticWindow.title.numberOfDwellings" : "Nº de moradias",
"vcmi.statisticWindow.title.numberOfMines" : "Nº de minas",
"vcmi.statisticWindow.title.armyStrength" : "Força do exército",
"vcmi.statisticWindow.title.experience" : "Experiência",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Custo do exército",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Custo de construção",
"vcmi.statisticWindow.title.mapExplored" : "Exploração do mapa",
"vcmi.statisticWindow.param.playerName" : "Nome do jogador",
"vcmi.statisticWindow.param.daysSurvived" : "Dias sobrevividos",
"vcmi.statisticWindow.param.maxHeroLevel" : "Nível máximo do herói",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Taxa de vitória (vs. herói)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Taxa de vitória (vs. neutro)",
"vcmi.statisticWindow.param.battlesHero" : "Batalhas (vs. herói)",
"vcmi.statisticWindow.param.battlesNeutral" : "Batalhas (vs. neutro)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Força máxima do exército",
"vcmi.statisticWindow.param.tradeVolume" : "Volume de comércio",
"vcmi.statisticWindow.param.obeliskVisited" : "Obelisco visitado",
"vcmi.statisticWindow.icon.townCaptured" : "Cidade capturada",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Herói mais forte do oponente derrotado",
"vcmi.statisticWindow.icon.grailFound" : "Graal encontrado",
"vcmi.statisticWindow.icon.defeated" : "Derrotado",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Tela Cheia (sem bordas)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Tela Cheia (sem bordas)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Tela Cheia sem Bordas}\n\nSe selecionado, o VCMI será executado em modo de tela cheia sem bordas. Neste modo, o jogo sempre usará a mesma resolução que a área de trabalho, ignorando a resolução selecionada.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Tela Cheia sem Bordas}\n\nSe selecionado, o VCMI será executado em modo de tela cheia sem bordas. Neste modo, o jogo sempre usará a mesma resolução que a área de trabalho, ignorando a resolução selecionada.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Tela Cheia (exclusiva)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Tela Cheia (exclusiva)",
@ -287,17 +324,9 @@
"vcmi.townHall.missingBase" : "A construção base %s deve ser construída primeiro", "vcmi.townHall.missingBase" : "A construção base %s deve ser construída primeiro",
"vcmi.townHall.noCreaturesToRecruit" : "Não há criaturas para recrutar!", "vcmi.townHall.noCreaturesToRecruit" : "Não há criaturas para recrutar!",
"vcmi.townHall.greetingManaVortex" : "Ao se aproximar de %s, seu corpo é preenchido com nova energia. Você dobrou seus pontos de mana normais.",
"vcmi.townHall.greetingKnowledge" : "Estudando os glifos de %s, você adquire uma visão dos segredos sobre o funcionamento de várias magias (+1 de Conhecimento).", "vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode tomar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"",
"vcmi.townHall.greetingSpellPower" : "%s ensina novas maneiras de concentrar seus poderes mágicos (+1 de Força).", "vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de tomar um novo.\"",
"vcmi.townHall.greetingExperience" : "Uma visita em %s ensina muitas habilidades novas (+1000 de Experiência).",
"vcmi.townHall.greetingAttack" : "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Ataque).",
"vcmi.townHall.greetingDefence" : "Ao passar um tempo em %s, os guerreiros experientes lá dentro te ensinam habilidades defensivas adicionais (+1 de Defesa).",
"vcmi.townHall.hasNotProduced" : "%s ainda não produziu nada.",
"vcmi.townHall.hasProduced" : "%s produziu %d %s nesta semana.",
"vcmi.townHall.greetingCustomBonus" : "%s dá +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " até a próxima batalha.",
"vcmi.townHall.greetingInTownMagicWell" : "%s restaurou seus pontos de mana para o máximo.",
"vcmi.logicalExpressions.anyOf" : "Qualquer um dos seguintes:", "vcmi.logicalExpressions.anyOf" : "Qualquer um dos seguintes:",
"vcmi.logicalExpressions.allOf" : "Todos os seguintes:", "vcmi.logicalExpressions.allOf" : "Todos os seguintes:",

View File

@ -162,17 +162,6 @@
"vcmi.townHall.missingBase" : "Сначала необходимо построить: %s", "vcmi.townHall.missingBase" : "Сначала необходимо построить: %s",
"vcmi.townHall.noCreaturesToRecruit" : "Нет существ для найма!", "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.anyOf" : "Любое из:",
"vcmi.logicalExpressions.allOf" : "Все перечисленное:", "vcmi.logicalExpressions.allOf" : "Все перечисленное:",

View File

@ -219,17 +219,6 @@
"vcmi.townHall.missingBase" : "Primero se debe construir el edificio base %s", "vcmi.townHall.missingBase" : "Primero se debe construir el edificio base %s",
"vcmi.townHall.noCreaturesToRecruit" : "¡No hay criaturas para reclutar!", "vcmi.townHall.noCreaturesToRecruit" : "¡No hay criaturas para reclutar!",
"vcmi.townHall.greetingManaVortex" : "Al acercarte a %s, tu cuerpo se llena de nueva energía. Has duplicado tus puntos de hechizo normales.",
"vcmi.townHall.greetingKnowledge" : "Estudias los glifos en %s y obtienes una visión de los entresijos de varias magias (+1 conocimiento).",
"vcmi.townHall.greetingSpellPower" : "El %s te enseña nuevas formas de enfocar tus poderes mágicos (+1 Poder).",
"vcmi.townHall.greetingExperience" : "Una visita a %s te enseña muchas habilidades nuevas (+1000 Experiencia).",
"vcmi.townHall.greetingAttack" : "El tiempo dedicado en %s te permite aprender habilidades de combate más efectivas (+1 habilidad de ataque).",
"vcmi.townHall.greetingDefence" : "Pasando tiempo en %s, los guerreros experimentados allí te enseñan habilidades defensivas adicionales (+1 Defensa).",
"vcmi.townHall.hasNotProduced" : "%s aún no ha producido nada.",
"vcmi.townHall.hasProduced" : "%s ha producido %d %s esta semana.",
"vcmi.townHall.greetingCustomBonus" : "%s te da +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " hasta la próxima batalla.",
"vcmi.townHall.greetingInTownMagicWell" : "%s ha restaurado tus puntos de hechizo al máximo.",
"vcmi.logicalExpressions.anyOf" : "Cualquiera de lo siguiente:", "vcmi.logicalExpressions.anyOf" : "Cualquiera de lo siguiente:",
"vcmi.logicalExpressions.allOf" : "Todo lo siguiente:", "vcmi.logicalExpressions.allOf" : "Todo lo siguiente:",

View File

@ -285,17 +285,6 @@
"vcmi.townHall.missingBase" : "Спочатку необхідно звести початкову будівлю: %s", "vcmi.townHall.missingBase" : "Спочатку необхідно звести початкову будівлю: %s",
"vcmi.townHall.noCreaturesToRecruit" : "Немає істот, яких можна завербувати!", "vcmi.townHall.noCreaturesToRecruit" : "Немає істот, яких можна завербувати!",
"vcmi.townHall.greetingManaVortex" : "Неподалік %s ваше тіло наповнюється новою силою. Ваша звична магічна енергія сьогодні подвоєна.",
"vcmi.townHall.greetingKnowledge" : "Ви вивчили знаки на %s, і на вас зійшло прозріння у справах магії. (+1 Knowledge).",
"vcmi.townHall.greetingSpellPower" : "В %s вас навчили новим методам концентрації магічної сили. (+1 Power).",
"vcmi.townHall.greetingExperience" : "Відвідавши %s, ви дізналися багато нового. (+1000 Experience).",
"vcmi.townHall.greetingAttack" : "Перебування у %s дозволило вам краще використовувати бойові навички (+1 Attack Skill).",
"vcmi.townHall.greetingDefence" : "У %s досвідчені воїни виклали вам свої захисні вміння. (+1 Defense).",
"vcmi.townHall.hasNotProduced" : "Поки що %s нічого не створило.",
"vcmi.townHall.hasProduced" : "Цього тижня %s створило %d одиниць, цього разу це %s.",
"vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " до наступної битви.",
"vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.",
"vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:", "vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:",
"vcmi.logicalExpressions.allOf" : "Все з перерахованого:", "vcmi.logicalExpressions.allOf" : "Все з перерахованого:",

View File

@ -159,17 +159,6 @@
"vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước", "vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước",
"vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!", "vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!",
"vcmi.townHall.greetingManaVortex": "%s giúp cơ thể bạn tràn đầy năng lượng mới. Bạn được gấp đôi năng lượng tối đa.",
"vcmi.townHall.greetingKnowledge": "Bạn học chữ khắc trên %s và thấu hiểu cách vận hành của nhiều ma thuật (+1 Trí).",
"vcmi.townHall.greetingSpellPower": "%s dạy bạn hướng mới tập trung sức mạnh ma thuật (+1 Lực).",
"vcmi.townHall.greetingExperience": "Viếng thăm %s dạy bạn nhiều kĩ năng mới (+1000 Kinh nghiệm).",
"vcmi.townHall.greetingAttack": "Thời gian ở %s giúp bạn học nhiều kĩ năng chiến đấu hiệu quả (+1 Công).",
"vcmi.townHall.greetingDefence": "Thời gian ở %s, các chiến binh lão luyện tại đó dạy bạn nhiều kĩ năng phòng thủ (+1 Thủ).",
"vcmi.townHall.hasNotProduced": "%s chưa tạo được cái gì.",
"vcmi.townHall.hasProduced": "%s tạo %d %s tuần này.",
"vcmi.townHall.greetingCustomBonus": "%s cho bạn +%d %s%s",
"vcmi.townHall.greetingCustomUntil": " đến trận đánh tiếp theo.",
"vcmi.townHall.greetingInTownMagicWell": "%s đã hồi phục năng lượng tối đa của bạn.",
"vcmi.logicalExpressions.anyOf": "Bất kì cái sau:", "vcmi.logicalExpressions.anyOf": "Bất kì cái sau:",
"vcmi.logicalExpressions.allOf": "Tất cả cái sau:", "vcmi.logicalExpressions.allOf": "Tất cả cái sau:",

View File

@ -92,9 +92,9 @@ const ObstacleService * CGameInfo::obstacles() const
return globalServices->obstacles(); return globalServices->obstacles();
} }
const IGameSettings * CGameInfo::settings() const const IGameSettings * CGameInfo::engineSettings() const
{ {
return globalServices->settings(); return globalServices->engineSettings();
} }
spells::effects::Registry * CGameInfo::spellEffects() spells::effects::Registry * CGameInfo::spellEffects()

View File

@ -70,7 +70,7 @@ public:
const SkillService * skills() const override; const SkillService * skills() const override;
const BattleFieldService * battlefields() const override; const BattleFieldService * battlefields() const override;
const ObstacleService * obstacles() const override; const ObstacleService * obstacles() const override;
const IGameSettings * settings() const override; const IGameSettings * engineSettings() const override;
const spells::effects::Registry * spellEffects() const override; const spells::effects::Registry * spellEffects() const override;
spells::effects::Registry * spellEffects() override; spells::effects::Registry * spellEffects() override;

View File

@ -20,8 +20,8 @@ extern SDL_Surface *screen; // main screen surface
extern SDL_Surface *screen2; // and hlp surface (used to store not-active interfaces layer) extern SDL_Surface *screen2; // and hlp surface (used to store not-active interfaces layer)
extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed extern SDL_Surface *screenBuf; // points to screen (if only advmapint is present) or screen2 (else) - should be used when updating controls which are not regularly redrawed
void handleQuit(bool ask = true);
/// Notify user about encountered fatal error and terminate the game /// Notify user about encountered fatal error and terminate the game
/// Defined in clientapp EntryPoint
/// TODO: decide on better location for this method /// TODO: decide on better location for this method
[[noreturn]] void handleFatalError(const std::string & message, bool terminate); [[noreturn]] void handleFatalError(const std::string & message, bool terminate);
void handleQuit(bool ask = true);

View File

@ -1,4 +1,4 @@
set(client_SRCS set(vcmiclientcommon_SRCS
StdInc.cpp StdInc.cpp
../CCallback.cpp ../CCallback.cpp
@ -27,6 +27,7 @@ set(client_SRCS
battle/BattleStacksController.cpp battle/BattleStacksController.cpp
battle/BattleWindow.cpp battle/BattleWindow.cpp
battle/CreatureAnimation.cpp battle/CreatureAnimation.cpp
battle/BattleOverlayLogVisualizer.cpp
eventsSDL/NotificationHandler.cpp eventsSDL/NotificationHandler.cpp
eventsSDL/InputHandler.cpp eventsSDL/InputHandler.cpp
@ -64,6 +65,7 @@ set(client_SRCS
mainmenu/CPrologEpilogVideo.cpp mainmenu/CPrologEpilogVideo.cpp
mainmenu/CreditsScreen.cpp mainmenu/CreditsScreen.cpp
mainmenu/CHighScoreScreen.cpp mainmenu/CHighScoreScreen.cpp
mainmenu/CStatisticScreen.cpp
mapView/MapRenderer.cpp mapView/MapRenderer.cpp
mapView/MapRendererContext.cpp mapView/MapRendererContext.cpp
@ -74,12 +76,14 @@ set(client_SRCS
mapView/MapViewController.cpp mapView/MapViewController.cpp
mapView/MapViewModel.cpp mapView/MapViewModel.cpp
mapView/mapHandler.cpp mapView/mapHandler.cpp
mapView/MapOverlayLogVisualizer.cpp
media/CAudioBase.cpp media/CAudioBase.cpp
media/CMusicHandler.cpp media/CMusicHandler.cpp
media/CSoundHandler.cpp media/CSoundHandler.cpp
media/CVideoHandler.cpp media/CVideoHandler.cpp
render/AssetGenerator.cpp
render/CAnimation.cpp render/CAnimation.cpp
render/CBitmapHandler.cpp render/CBitmapHandler.cpp
render/CDefFile.cpp render/CDefFile.cpp
@ -95,6 +99,7 @@ set(client_SRCS
renderSDL/CTrueTypeFont.cpp renderSDL/CTrueTypeFont.cpp
renderSDL/CursorHardware.cpp renderSDL/CursorHardware.cpp
renderSDL/CursorSoftware.cpp renderSDL/CursorSoftware.cpp
renderSDL/ImageScaled.cpp
renderSDL/RenderHandler.cpp renderSDL/RenderHandler.cpp
renderSDL/SDLImage.cpp renderSDL/SDLImage.cpp
renderSDL/SDLImageLoader.cpp renderSDL/SDLImageLoader.cpp
@ -169,9 +174,10 @@ set(client_SRCS
windows/settings/BattleOptionsTab.cpp windows/settings/BattleOptionsTab.cpp
windows/settings/AdventureOptionsTab.cpp windows/settings/AdventureOptionsTab.cpp
xBRZ/xbrz.cpp
ArtifactsUIController.cpp ArtifactsUIController.cpp
CGameInfo.cpp CGameInfo.cpp
CMT.cpp
CPlayerInterface.cpp CPlayerInterface.cpp
PlayerLocalState.cpp PlayerLocalState.cpp
CServerHandler.cpp CServerHandler.cpp
@ -184,7 +190,7 @@ set(client_SRCS
ServerRunner.cpp ServerRunner.cpp
) )
set(client_HEADERS set(vcmiclientcommon_HEADERS
StdInc.h StdInc.h
adventureMap/AdventureMapInterface.h adventureMap/AdventureMapInterface.h
@ -214,6 +220,7 @@ set(client_HEADERS
battle/BattleStacksController.h battle/BattleStacksController.h
battle/BattleWindow.h battle/BattleWindow.h
battle/CreatureAnimation.h battle/CreatureAnimation.h
battle/BattleOverlayLogVisualizer.h
eventsSDL/NotificationHandler.h eventsSDL/NotificationHandler.h
eventsSDL/InputHandler.h eventsSDL/InputHandler.h
@ -254,6 +261,7 @@ set(client_HEADERS
mainmenu/CPrologEpilogVideo.h mainmenu/CPrologEpilogVideo.h
mainmenu/CreditsScreen.h mainmenu/CreditsScreen.h
mainmenu/CHighScoreScreen.h mainmenu/CHighScoreScreen.h
mainmenu/CStatisticScreen.h
mapView/IMapRendererContext.h mapView/IMapRendererContext.h
mapView/IMapRendererObserver.h mapView/IMapRendererObserver.h
@ -266,6 +274,7 @@ set(client_HEADERS
mapView/MapViewController.h mapView/MapViewController.h
mapView/MapViewModel.h mapView/MapViewModel.h
mapView/mapHandler.h mapView/mapHandler.h
mapView/MapOverlayLogVisualizer.h
media/CAudioBase.h media/CAudioBase.h
media/CEmptyVideoPlayer.h media/CEmptyVideoPlayer.h
@ -276,6 +285,7 @@ set(client_HEADERS
media/ISoundPlayer.h media/ISoundPlayer.h
media/IVideoPlayer.h media/IVideoPlayer.h
render/AssetGenerator.h
render/CAnimation.h render/CAnimation.h
render/CBitmapHandler.h render/CBitmapHandler.h
render/CDefFile.h render/CDefFile.h
@ -297,6 +307,7 @@ set(client_HEADERS
renderSDL/CTrueTypeFont.h renderSDL/CTrueTypeFont.h
renderSDL/CursorHardware.h renderSDL/CursorHardware.h
renderSDL/CursorSoftware.h renderSDL/CursorSoftware.h
renderSDL/ImageScaled.h
renderSDL/RenderHandler.h renderSDL/RenderHandler.h
renderSDL/SDLImage.h renderSDL/SDLImage.h
renderSDL/SDLImageLoader.h renderSDL/SDLImageLoader.h
@ -374,6 +385,9 @@ set(client_HEADERS
windows/settings/BattleOptionsTab.h windows/settings/BattleOptionsTab.h
windows/settings/AdventureOptionsTab.h windows/settings/AdventureOptionsTab.h
xBRZ/xbrz.h
xBRZ/xbrz_tools.h
ArtifactsUIController.h ArtifactsUIController.h
CGameInfo.h CGameInfo.h
CMT.h CMT.h
@ -392,76 +406,50 @@ set(client_HEADERS
) )
if(APPLE_IOS) if(APPLE_IOS)
set(client_SRCS ${client_SRCS} set(vcmiclientcommon_SRCS ${vcmiclientcommon_SRCS}
CFocusableHelper.cpp
ios/GameChatKeyboardHandler.m
ios/main.m
ios/startSDL.mm
ios/utils.mm ios/utils.mm
) )
set(client_HEADERS ${client_HEADERS} set(vcmiclientcommon_HEADERS ${vcmiclientcommon_HEADERS}
CFocusableHelper.h
ios/GameChatKeyboardHandler.h
ios/startSDL.h
ios/utils.h ios/utils.h
) )
endif() endif()
assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc) assign_source_group(${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
add_library(vcmiclientcommon STATIC ${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
if(ANDROID)
add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS})
set_target_properties(vcmiclient PROPERTIES
OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
)
else()
add_executable(vcmiclient ${client_SRCS} ${client_HEADERS})
endif()
if(NOT ENABLE_STATIC_LIBS) if(NOT ENABLE_STATIC_LIBS)
add_dependencies(vcmiclient add_dependencies(vcmiclientcommon
BattleAI BattleAI
EmptyAI EmptyAI
StupidAI StupidAI
VCAI VCAI
) )
if(ENABLE_NULLKILLER_AI) if(ENABLE_NULLKILLER_AI)
add_dependencies(vcmiclient Nullkiller) add_dependencies(vcmiclientcommon Nullkiller)
endif() endif()
endif() endif()
if(APPLE_IOS) if(APPLE_IOS)
if(ENABLE_ERM) if(ENABLE_ERM)
add_dependencies(vcmiclient vcmiERM) add_dependencies(vcmiclientcommon vcmiERM)
endif() endif()
if(ENABLE_LUA) if(ENABLE_LUA)
add_dependencies(vcmiclient vcmiLua) add_dependencies(vcmiclientcommon vcmiLua)
endif() endif()
endif() endif()
if(WIN32) if(WIN32)
target_sources(vcmiclient PRIVATE "VCMI_client.rc") set_target_properties(vcmiclientcommon
set_target_properties(vcmiclient
PROPERTIES PROPERTIES
OUTPUT_NAME "VCMI_client" OUTPUT_NAME "VCMI_vcmiclientcommon"
PROJECT_LABEL "VCMI_client" PROJECT_LABEL "VCMI_vcmiclientcommon"
) )
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient) set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclientcommon)
if(NOT ENABLE_DEBUG_CONSOLE) if(NOT ENABLE_DEBUG_CONSOLE)
set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE) target_link_libraries(vcmiclientcommon SDL2::SDL2main)
target_link_libraries(vcmiclient SDL2::SDL2main)
endif()
target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
# TODO: very hacky, find proper solution to copy AI dlls into bin dir
if(MSVC)
add_custom_command(TARGET vcmiclient POST_BUILD
WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
)
endif() endif()
target_compile_definitions(vcmiclientcommon PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
elseif(APPLE_IOS) elseif(APPLE_IOS)
target_link_libraries(vcmiclient PRIVATE target_link_libraries(vcmiclientcommon PRIVATE
iOS_utils iOS_utils
# FFmpeg # FFmpeg
@ -473,101 +461,31 @@ elseif(APPLE_IOS)
"-framework CoreMedia" "-framework CoreMedia"
"-framework VideoToolbox" "-framework VideoToolbox"
) )
set_target_properties(vcmiclient PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
)
foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png)
set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE})
target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH})
set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
# workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0")
set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX)
endif()
endforeach()
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
endif() endif()
target_link_libraries(vcmiclient PRIVATE vcmiservercommon) target_link_libraries(vcmiclientcommon PRIVATE vcmiservercommon)
if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER)
target_link_libraries(vcmiclient PRIVATE vcmilauncher)
endif()
target_link_libraries(vcmiclient PRIVATE target_link_libraries(vcmiclientcommon PUBLIC
vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
) )
if(ffmpeg_LIBRARIES) if(ffmpeg_LIBRARIES)
target_link_libraries(vcmiclient PRIVATE target_link_libraries(vcmiclientcommon PRIVATE
${ffmpeg_LIBRARIES} ${ffmpeg_LIBRARIES}
) )
else() else()
target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO) target_compile_definitions(vcmiclientcommon PRIVATE DISABLE_VIDEO)
endif() endif()
target_include_directories(vcmiclient PUBLIC target_include_directories(vcmiclientcommon PUBLIC
${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}
) )
if (ffmpeg_INCLUDE_DIRS) if (ffmpeg_INCLUDE_DIRS)
target_include_directories(vcmiclient PRIVATE target_include_directories(vcmiclientcommon PRIVATE
${ffmpeg_INCLUDE_DIRS} ${ffmpeg_INCLUDE_DIRS}
) )
endif() endif()
vcmi_set_output_dir(vcmiclient "") vcmi_set_output_dir(vcmiclientcommon "")
enable_pch(vcmiclient) enable_pch(vcmiclientcommon)
if(APPLE_IOS)
vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
add_custom_command(TARGET vcmiclient POST_BUILD
COMMAND ios/set_build_version.sh "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --component "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}" --config "$<CONFIG>" --prefix "$<TARGET_BUNDLE_CONTENT_DIR:vcmiclient>"
COMMAND ios/rpath_remove_symlinks.sh
COMMAND ios/codesign.sh
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
install(TARGETS vcmiclient DESTINATION Payload COMPONENT app) # for ipa generation with cpack
elseif(ANDROID)
find_program(androidDeployQt androiddeployqt
PATHS "${qtBinDir}"
)
vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}/${LIB_DIR}")
add_custom_target(android_deploy ALL
COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" --config "$<CONFIG>" --prefix "${androidQtBuildDir}"
COMMAND "${androidDeployQt}" --input "${CMAKE_BINARY_DIR}/androiddeployqt.json" --output "${androidQtBuildDir}" --android-platform "android-${ANDROID_TARGET_SDK_VERSION}" --verbose $<$<NOT:$<CONFIG:Debug>>:--release> ${ANDROIDDEPLOYQT_OPTIONS}
COMMAND_EXPAND_LISTS
VERBATIM
COMMENT "Create android package"
)
add_dependencies(android_deploy vcmiclient)
else()
install(TARGETS vcmiclient DESTINATION ${BIN_DIR})
endif()
#install icons and desktop file on Linux
if(NOT WIN32 AND NOT APPLE AND NOT ANDROID)
#FIXME: move to client makefile?
foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048)
install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png"
DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps"
RENAME vcmiclient.png
)
endforeach()
install(FILES icons/vcmiclient.svg
DESTINATION share/icons/hicolor/scalable/apps
RENAME vcmiclient.svg
)
install(FILES icons/vcmiclient.desktop
DESTINATION share/applications
)
endif()

View File

@ -13,7 +13,6 @@
#include <vcmi/Artifact.h> #include <vcmi/Artifact.h>
#include "CGameInfo.h" #include "CGameInfo.h"
#include "CMT.h"
#include "CServerHandler.h" #include "CServerHandler.h"
#include "HeroMovementController.h" #include "HeroMovementController.h"
#include "PlayerLocalState.h" #include "PlayerLocalState.h"
@ -100,9 +99,8 @@
#include "../lib/pathfinder/CGPathNode.h" #include "../lib/pathfinder/CGPathNode.h"
#include "../lib/serializer/BinaryDeserializer.h"
#include "../lib/serializer/BinarySerializer.h"
#include "../lib/serializer/CTypeList.h" #include "../lib/serializer/CTypeList.h"
#include "../lib/serializer/ESerializationVersion.h"
#include "../lib/spells/CSpellHandler.h" #include "../lib/spells/CSpellHandler.h"
@ -137,7 +135,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
{ {
logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString());
GH.defActionsDef = 0;
LOCPLINT = this; LOCPLINT = this;
playerID=Player; playerID=Player;
human=true; human=true;
@ -171,13 +168,8 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
adventureInt.reset(new AdventureMapInterface()); adventureInt.reset(new AdventureMapInterface());
} }
void CPlayerInterface::playerEndsTurn(PlayerColor player) void CPlayerInterface::closeAllDialogs()
{ {
EVENT_HANDLER_CALLED_BY_CLIENT;
if (player == playerID)
{
makingTurn = false;
// remove all active dialogs that do not expect query answer // remove all active dialogs that do not expect query answer
for (;;) for (;;)
{ {
@ -200,6 +192,15 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player)
castleInt->close(); castleInt->close();
castleInt = nullptr; castleInt = nullptr;
}
void CPlayerInterface::playerEndsTurn(PlayerColor player)
{
EVENT_HANDLER_CALLED_BY_CLIENT;
if (player == playerID)
{
makingTurn = false;
closeAllDialogs();
// remove all pending dialogs that do not expect query answer // remove all pending dialogs that do not expect query answer
vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){ vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
@ -286,6 +287,7 @@ void CPlayerInterface::gamePause(bool pause)
void CPlayerInterface::yourTurn(QueryID queryID) void CPlayerInterface::yourTurn(QueryID queryID)
{ {
closeAllDialogs();
CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP);
EVENT_HANDLER_CALLED_BY_CLIENT; EVENT_HANDLER_CALLED_BY_CLIENT;
@ -620,12 +622,10 @@ void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreat
{ {
movementController->onBattleStarted(); movementController->onBattleStarted();
//Don't wait for dialogs when we are non-active hot-seat player
if (LOCPLINT == this)
waitForAllDialogs(); waitForAllDialogs();
} }
void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed)
{ {
EVENT_HANDLER_CALLED_BY_CLIENT; EVENT_HANDLER_CALLED_BY_CLIENT;
@ -645,8 +645,6 @@ void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet
cb->registerBattleInterface(autofightingAI); cb->registerBattleInterface(autofightingAI);
} }
//Don't wait for dialogs when we are non-active hot-seat player
if (LOCPLINT == this)
waitForAllDialogs(); waitForAllDialogs();
BATTLE_EVENT_POSSIBLE_RETURN; BATTLE_EVENT_POSSIBLE_RETURN;
@ -1014,7 +1012,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector
} }
std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components); std::shared_ptr<CInfoWindow> temp = CInfoWindow::create(text, playerID, components);
if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) if ((makingTurn || (battleInt && battleInt->curInt && battleInt->curInt.get() == this)) && GH.windows().count() > 0 && LOCPLINT == this)
{ {
CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID)); CCS->soundh->playSound(static_cast<soundBase::soundID>(soundID));
showingDialog->setBusy(); showingDialog->setBusy();
@ -1146,7 +1144,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
if(t) if(t)
{ {
auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE); auto image = GH.renderHandler().loadImage(AnimationPath::builtin("ITPA"), t->town->clientInfo.icons[t->hasFort()][false] + 2, 0, EImageBlitMode::OPAQUE);
image->scaleFast(Point(35, 23)); image->scaleTo(Point(35, 23));
images.push_back(image); images.push_back(image);
} }
} }
@ -1477,7 +1475,7 @@ void CPlayerInterface::update()
return; return;
//if there are any waiting dialogs, show them //if there are any waiting dialogs, show them
if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->isBusy()) if (makingTurn && !dialogs.empty() && !showingDialog->isBusy())
{ {
showingDialog->setBusy(); showingDialog->setBusy();
GH.windows().pushWindow(dialogs.front()); GH.windows().pushWindow(dialogs.front());
@ -1640,15 +1638,6 @@ void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInsta
cb->selectionMade(0, queryID); cb->selectionMade(0, queryID);
}; };
if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && dynamic_cast<const CGArtifactsAltar*>(market) == nullptr)
{
// compatibility check, safe to remove for 1.6
// 1.4 saves loaded in 1.5 will not be able to visit Altar of Sacrifice due to Altar now requiring different map object class
static_assert(ESerializationVersion::RELEASE_143 < ESerializationVersion::CURRENT, "Please remove this compatibility check once it no longer needed");
onWindowClosed();
return;
}
if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP); GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
@ -1656,7 +1645,16 @@ void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInsta
else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed); GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed);
else if (!market->availableModes().empty()) else if (!market->availableModes().empty())
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, market->availableModes().front()); for(auto mode = EMarketMode::RESOURCE_RESOURCE; mode != EMarketMode::MARKET_AFTER_LAST_PLACEHOLDER; mode = vstd::next(mode, 1))
{
if(vstd::contains(market->availableModes(), mode))
{
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, mode);
break;
}
}
else
onWindowClosed();
} }
void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
@ -1665,7 +1663,7 @@ void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroI
auto onWindowClosed = [this, queryID](){ auto onWindowClosed = [this, queryID](){
cb->selectionMade(0, queryID); cb->selectionMade(0, queryID);
}; };
GH.windows().createAndPushWindow<CUniversityWindow>(visitor, market, onWindowClosed); GH.windows().createAndPushWindow<CUniversityWindow>(visitor, BuildingID::NONE, market, onWindowClosed);
} }
void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor)
@ -1761,6 +1759,9 @@ void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
void CPlayerInterface::waitForAllDialogs() void CPlayerInterface::waitForAllDialogs()
{ {
if (!makingTurn)
return;
while(!dialogs.empty()) while(!dialogs.empty())
{ {
auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
@ -1776,7 +1777,6 @@ void CPlayerInterface::proposeLoadingGame()
[]() []()
{ {
CSH->endGameplay(); CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("load"); CMM->menu->switchToTab("load");
}, },
nullptr nullptr

View File

@ -160,7 +160,7 @@ protected: // Call-ins from server, should not be called directly, but only via
void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect
void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override; void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units) override; void battleUnitsChanged(const BattleID & battleID, const std::vector<UnitChanges> & units) override;
void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override; void battleObstaclesChanged(const BattleID & battleID, const std::vector<ObstacleChanges> & obstacles) override;
void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
@ -197,6 +197,7 @@ public: // public interface for use by client via LOCPLINT access
void performAutosave(); void performAutosave();
void gamePause(bool pause); void gamePause(bool pause);
void endNetwork(); void endNetwork();
void closeAllDialogs();
///returns true if all events are processed internally ///returns true if all events are processed internally
bool capturedAllEvents(); bool capturedAllEvents();

View File

@ -21,7 +21,11 @@
#include "globalLobby/GlobalLobbyClient.h" #include "globalLobby/GlobalLobbyClient.h"
#include "lobby/CSelectionBase.h" #include "lobby/CSelectionBase.h"
#include "lobby/CLobbyScreen.h" #include "lobby/CLobbyScreen.h"
#include "lobby/CBonusSelection.h"
#include "windows/InfoWindows.h" #include "windows/InfoWindows.h"
#include "media/CMusicHandler.h"
#include "media/IVideoPlayer.h"
#include "mainmenu/CMainMenu.h" #include "mainmenu/CMainMenu.h"
#include "mainmenu/CPrologEpilogVideo.h" #include "mainmenu/CPrologEpilogVideo.h"
@ -35,6 +39,8 @@
#include "../lib/TurnTimerInfo.h" #include "../lib/TurnTimerInfo.h"
#include "../lib/VCMIDirs.h" #include "../lib/VCMIDirs.h"
#include "../lib/campaign/CampaignState.h" #include "../lib/campaign/CampaignState.h"
#include "../lib/gameState/CGameState.h"
#include "../lib/gameState/HighScore.h"
#include "../lib/CPlayerState.h" #include "../lib/CPlayerState.h"
#include "../lib/mapping/CMapInfo.h" #include "../lib/mapping/CMapInfo.h"
#include "../lib/mapObjects/CGTownInstance.h" #include "../lib/mapObjects/CGTownInstance.h"
@ -43,73 +49,16 @@
#include "../lib/rmg/CMapGenOptions.h" #include "../lib/rmg/CMapGenOptions.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/Filesystem.h"
#include "../lib/registerTypes/RegisterTypesLobbyPacks.h"
#include "../lib/serializer/CMemorySerializer.h" #include "../lib/serializer/CMemorySerializer.h"
#include "../lib/UnlockGuard.h" #include "../lib/UnlockGuard.h"
#include <boost/uuid/uuid.hpp> #include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp> #include <boost/uuid/uuid_io.hpp>
#include <boost/uuid/uuid_generators.hpp> #include <boost/uuid/uuid_generators.hpp>
#include "../lib/serializer/Cast.h"
#include "LobbyClientNetPackVisitors.h" #include "LobbyClientNetPackVisitors.h"
#include <vcmi/events/EventBus.h> #include <vcmi/events/EventBus.h>
template<typename T> class CApplyOnLobby;
class CBaseForLobbyApply
{
public:
virtual bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const = 0;
virtual void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const = 0;
virtual ~CBaseForLobbyApply(){};
template<typename U> static CBaseForLobbyApply * getApplier(const U * t = nullptr)
{
return new CApplyOnLobby<U>();
}
};
template<typename T> class CApplyOnLobby : public CBaseForLobbyApply
{
public:
bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override
{
auto & ref = static_cast<T&>(pack);
ApplyOnLobbyHandlerNetPackVisitor visitor(*handler);
logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ref).name());
ref.visit(visitor);
return visitor.getResult();
}
void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override
{
auto & ref = static_cast<T &>(pack);
ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby);
logNetwork->trace("\tApply on lobby from queue: %s", typeid(ref).name());
ref.visit(visitor);
}
};
template<> class CApplyOnLobby<CPack>: public CBaseForLobbyApply
{
public:
bool applyOnLobbyHandler(CServerHandler * handler, CPackForLobby & pack) const override
{
logGlobal->error("Cannot apply plain CPack!");
assert(0);
return false;
}
void applyOnLobbyScreen(CLobbyScreen * lobby, CServerHandler * handler, CPackForLobby & pack) const override
{
logGlobal->error("Cannot apply plain CPack!");
assert(0);
}
};
CServerHandler::~CServerHandler() CServerHandler::~CServerHandler()
{ {
if (serverRunner) if (serverRunner)
@ -147,7 +96,6 @@ CServerHandler::CServerHandler()
: networkHandler(INetworkHandler::createHandler()) : networkHandler(INetworkHandler::createHandler())
, lobbyClient(std::make_unique<GlobalLobbyClient>()) , lobbyClient(std::make_unique<GlobalLobbyClient>())
, gameChat(std::make_unique<GameChatHandler>()) , gameChat(std::make_unique<GameChatHandler>())
, applier(std::make_unique<CApplier<CBaseForLobbyApply>>())
, threadNetwork(&CServerHandler::threadRunNetwork, this) , threadNetwork(&CServerHandler::threadRunNetwork, this)
, state(EClientState::NONE) , state(EClientState::NONE)
, serverPort(0) , serverPort(0)
@ -158,12 +106,6 @@ CServerHandler::CServerHandler()
, client(nullptr) , client(nullptr)
{ {
uuid = boost::uuids::to_string(boost::uuids::random_generator()()); uuid = boost::uuids::to_string(boost::uuids::random_generator()());
registerTypesLobbyPacks(*applier);
}
void CServerHandler::setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc)
{
campaignScoreCalculator = newHighScoreCalc;
} }
void CServerHandler::threadRunNetwork() void CServerHandler::threadRunNetwork()
@ -324,8 +266,8 @@ void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netCon
void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack) void CServerHandler::applyPackOnLobbyScreen(CPackForLobby & pack)
{ {
const CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(&pack)); //find the applier ApplyOnLobbyScreenNetPackVisitor visitor(*this, dynamic_cast<CLobbyScreen *>(SEL));
apply->applyOnLobbyScreen(dynamic_cast<CLobbyScreen *>(SEL), this, pack); pack.visit(visitor);
GH.windows().totalRedraw(); GH.windows().totalRedraw();
} }
@ -497,6 +439,14 @@ void CServerHandler::setPlayerName(PlayerColor color, const std::string & name)
sendLobbyPack(lspn); sendLobbyPack(lspn);
} }
void CServerHandler::setPlayerHandicap(PlayerColor color, Handicap handicap) const
{
LobbySetPlayerHandicap lsph;
lsph.color = color;
lsph.handicap = handicap;
sendLobbyPack(lsph);
}
void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const
{ {
LobbyChangePlayerOption lcpo; LobbyChangePlayerOption lcpo;
@ -585,6 +535,9 @@ void CServerHandler::sendGuiAction(ui8 action) const
void CServerHandler::sendRestartGame() const void CServerHandler::sendRestartGame() const
{ {
if(si->campState && !si->campState->getLoadingBackground().empty())
GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
else
GH.windows().createAndPushWindow<CLoadingScreen>(); GH.windows().createAndPushWindow<CLoadingScreen>();
LobbyRestartGame endGame; LobbyRestartGame endGame;
@ -629,7 +582,12 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const
verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool());
if(!settings["session"]["headless"].Bool()) if(!settings["session"]["headless"].Bool())
{
if(si->campState && !si->campState->getLoadingBackground().empty())
GH.windows().createAndPushWindow<CLoadingScreen>(si->campState->getLoadingBackground());
else
GH.windows().createAndPushWindow<CLoadingScreen>(); GH.windows().createAndPushWindow<CLoadingScreen>();
}
LobbyPrepareStartGame lpsg; LobbyPrepareStartGame lpsg;
sendLobbyPack(lpsg); sendLobbyPack(lpsg);
@ -655,7 +613,7 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
break; break;
case EStartMode::CAMPAIGN: case EStartMode::CAMPAIGN:
if(si->campState->conqueredScenarios().empty()) if(si->campState->conqueredScenarios().empty())
campaignScoreCalculator.reset(); si->campState->highscoreParameters.clear();
client->newGame(gameState); client->newGame(gameState);
break; break;
case EStartMode::LOAD_GAME: case EStartMode::LOAD_GAME:
@ -669,43 +627,13 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
setState(EClientState::GAMEPLAY); setState(EClientState::GAMEPLAY);
} }
HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory) void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic)
{ {
const auto * gs = client->gameState(); HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory);
const auto * playerState = gs->getPlayerState(player);
HighScoreParameter param;
param.difficulty = gs->getStartInfo()->difficulty;
param.day = gs->getDate();
param.townAmount = gs->howManyTowns(player);
param.usedCheat = gs->getPlayerState(player)->cheated;
param.hasGrail = false;
for(const CGHeroInstance * h : playerState->heroes)
if(h->hasArt(ArtifactID::GRAIL))
param.hasGrail = true;
for(const CGTownInstance * t : playerState->towns)
if(t->builtBuildings.count(BuildingID::GRAIL))
param.hasGrail = true;
param.allDefeated = true;
for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
{
auto ps = gs->getPlayerState(otherPlayer, false);
if(ps && otherPlayer != player && !ps->checkVanquished())
param.allDefeated = false;
}
param.scenarioName = gs->getMapHeader()->name.toString();
param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
return param;
}
void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory)
{
HighScoreParameter param = prepareHighScores(player, victory);
if(victory && client->gameState()->getStartInfo()->campState) if(victory && client->gameState()->getStartInfo()->campState)
{ {
startCampaignScenario(param, client->gameState()->getStartInfo()->campState); startCampaignScenario(param, client->gameState()->getStartInfo()->campState, statistic);
} }
else else
{ {
@ -714,9 +642,8 @@ void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victo
scenarioHighScores.isCampaign = false; scenarioHighScores.isCampaign = false;
endGameplay(); endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("main"); CMM->menu->switchToTab("main");
GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores); GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores, statistic);
} }
} }
@ -749,26 +676,23 @@ void CServerHandler::restartGameplay()
logicConnection->enterLobbyConnectionMode(); logicConnection->enterLobbyConnectionMode();
} }
void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs) void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic)
{ {
std::shared_ptr<CampaignState> ourCampaign = cs; std::shared_ptr<CampaignState> ourCampaign = cs;
if (!cs) if (!cs)
ourCampaign = si->campState; ourCampaign = si->campState;
if(campaignScoreCalculator == nullptr)
{
campaignScoreCalculator = std::make_shared<HighScoreCalculation>();
campaignScoreCalculator->isCampaign = true;
campaignScoreCalculator->parameters.clear();
}
param.campaignName = cs->getNameTranslated(); param.campaignName = cs->getNameTranslated();
campaignScoreCalculator->parameters.push_back(param); cs->highscoreParameters.push_back(param);
auto campaignScoreCalculator = std::make_shared<HighScoreCalculation>();
campaignScoreCalculator->isCampaign = true;
campaignScoreCalculator->parameters = cs->highscoreParameters;
endGameplay(); endGameplay();
auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
auto finisher = [this, ourCampaign]() auto finisher = [ourCampaign, campaignScoreCalculator, statistic]()
{ {
if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished()) if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
{ {
@ -784,7 +708,15 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
else else
{ {
CMM->openCampaignScreen(ourCampaign->campaignSet); CMM->openCampaignScreen(ourCampaign->campaignSet);
GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator); if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), false))
{
CCS->musich->stopMusic();
GH.windows().createAndPushWindow<CampaignRimVideo>(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), [campaignScoreCalculator, statistic](){
GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator, statistic);
});
}
else
GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator, statistic);
} }
}; };
@ -948,7 +880,6 @@ void CServerHandler::onDisconnected(const std::shared_ptr<INetworkConnection> &
if(client) if(client)
{ {
endGameplay(); endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("main"); CMM->menu->switchToTab("main");
showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected")); showServerError(CGI->generaltexth->translate("vcmi.server.errors.disconnected"));
} }
@ -991,7 +922,10 @@ void CServerHandler::waitForServerShutdown()
void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) void CServerHandler::visitForLobby(CPackForLobby & lobbyPack)
{ {
if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, lobbyPack)) ApplyOnLobbyHandlerNetPackVisitor visitor(*this);
lobbyPack.visit(visitor);
if(visitor.getResult())
{ {
if(!settings["session"]["headless"].Bool()) if(!settings["session"]["headless"].Bool())
applyPackOnLobbyScreen(lobbyPack); applyPackOnLobbyScreen(lobbyPack);

View File

@ -13,6 +13,7 @@
#include "../lib/network/NetworkInterface.h" #include "../lib/network/NetworkInterface.h"
#include "../lib/StartInfo.h" #include "../lib/StartInfo.h"
#include "../lib/gameState/GameStatistics.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -28,7 +29,7 @@ struct CPack;
struct CPackForLobby; struct CPackForLobby;
struct CPackForClient; struct CPackForClient;
template<typename T> class CApplier; class HighScoreParameter;
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END
@ -38,9 +39,6 @@ class GlobalLobbyClient;
class GameChatHandler; class GameChatHandler;
class IServerRunner; class IServerRunner;
class HighScoreCalculation;
class HighScoreParameter;
enum class ESelectionScreen : ui8; enum class ESelectionScreen : ui8;
enum class ELoadMode : ui8; enum class ELoadMode : ui8;
@ -81,6 +79,7 @@ public:
virtual void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const = 0; virtual void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const = 0;
virtual void setPlayer(PlayerColor color) const = 0; virtual void setPlayer(PlayerColor color) const = 0;
virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0; virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0;
virtual void setPlayerHandicap(PlayerColor color, Handicap handicap) const = 0;
virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0;
virtual void setDifficulty(int to) const = 0; virtual void setDifficulty(int to) const = 0;
virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0;
@ -101,11 +100,9 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
std::shared_ptr<INetworkConnection> networkConnection; std::shared_ptr<INetworkConnection> networkConnection;
std::unique_ptr<GlobalLobbyClient> lobbyClient; std::unique_ptr<GlobalLobbyClient> lobbyClient;
std::unique_ptr<GameChatHandler> gameChat; std::unique_ptr<GameChatHandler> gameChat;
std::unique_ptr<CApplier<CBaseForLobbyApply>> applier;
std::unique_ptr<IServerRunner> serverRunner; std::unique_ptr<IServerRunner> serverRunner;
std::shared_ptr<CMapInfo> mapToStart; std::shared_ptr<CMapInfo> mapToStart;
std::vector<std::string> localPlayerNames; std::vector<std::string> localPlayerNames;
std::shared_ptr<HighScoreCalculation> campaignScoreCalculator;
boost::thread threadNetwork; boost::thread threadNetwork;
@ -127,8 +124,6 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
bool isServerLocal() const; bool isServerLocal() const;
HighScoreParameter prepareHighScores(PlayerColor player, bool victory);
public: public:
/// High-level connection overlay that is capable of (de)serializing network data /// High-level connection overlay that is capable of (de)serializing network data
std::shared_ptr<CConnection> logicConnection; std::shared_ptr<CConnection> logicConnection;
@ -191,6 +186,7 @@ public:
void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const override; void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const override;
void setPlayer(PlayerColor color) const override; void setPlayer(PlayerColor color) const override;
void setPlayerName(PlayerColor color, const std::string & name) const override; void setPlayerName(PlayerColor color, const std::string & name) const override;
void setPlayerHandicap(PlayerColor color, Handicap handicap) const override;
void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override;
void setDifficulty(int to) const override; void setDifficulty(int to) const override;
void setTurnTimerInfo(const TurnTimerInfo &) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override;
@ -206,11 +202,11 @@ public:
void debugStartTest(std::string filename, bool save = false); void debugStartTest(std::string filename, bool save = false);
void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
void showHighScoresAndEndGameplay(PlayerColor player, bool victory); void showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic);
void endNetwork(); void endNetwork();
void endGameplay(); void endGameplay();
void restartGameplay(); void restartGameplay();
void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {}); void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic);
void showServerError(const std::string & txt) const; void showServerError(const std::string & txt) const;
// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle
@ -219,7 +215,6 @@ public:
void visitForLobby(CPackForLobby & lobbyPack); void visitForLobby(CPackForLobby & lobbyPack);
void visitForClient(CPackForClient & clientPack); void visitForClient(CPackForClient & clientPack);
void setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc);
}; };
extern CServerHandler * CSH; extern CServerHandler * CSH;

View File

@ -29,13 +29,10 @@
#include "../lib/VCMIDirs.h" #include "../lib/VCMIDirs.h"
#include "../lib/UnlockGuard.h" #include "../lib/UnlockGuard.h"
#include "../lib/battle/BattleInfo.h" #include "../lib/battle/BattleInfo.h"
#include "../lib/serializer/BinaryDeserializer.h"
#include "../lib/serializer/BinarySerializer.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMapService.h"
#include "../lib/pathfinder/CGPathNode.h" #include "../lib/pathfinder/CGPathNode.h"
#include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/Filesystem.h"
#include "../lib/registerTypes/RegisterTypesClientPacks.h"
#include <memory> #include <memory>
#include <vcmi/events/EventBus.h> #include <vcmi/events/EventBus.h>
@ -50,53 +47,6 @@
ThreadSafeVector<int> CClient::waitingRequest; ThreadSafeVector<int> CClient::waitingRequest;
template<typename T> class CApplyOnCL;
class CBaseForCLApply
{
public:
virtual void applyOnClAfter(CClient * cl, CPack * pack) const =0;
virtual void applyOnClBefore(CClient * cl, CPack * pack) const =0;
virtual ~CBaseForCLApply(){}
template<typename U> static CBaseForCLApply * getApplier(const U * t = nullptr)
{
return new CApplyOnCL<U>();
}
};
template<typename T> class CApplyOnCL : public CBaseForCLApply
{
public:
void applyOnClAfter(CClient * cl, CPack * pack) const override
{
T * ptr = static_cast<T *>(pack);
ApplyClientNetPackVisitor visitor(*cl, *cl->gameState());
ptr->visit(visitor);
}
void applyOnClBefore(CClient * cl, CPack * pack) const override
{
T * ptr = static_cast<T *>(pack);
ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState());
ptr->visit(visitor);
}
};
template<> class CApplyOnCL<CPack>: public CBaseForCLApply
{
public:
void applyOnClAfter(CClient * cl, CPack * pack) const override
{
logGlobal->error("Cannot apply on CL plain CPack!");
assert(0);
}
void applyOnClBefore(CClient * cl, CPack * pack) const override
{
logGlobal->error("Cannot apply on CL plain CPack!");
assert(0);
}
};
CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_) CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr<CCallback> mainCallback_)
: player(player_), : player(player_),
cl(cl_), cl(cl_),
@ -130,12 +80,9 @@ const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const
return mainCallback.get(); return mainCallback.get();
} }
CClient::CClient() CClient::CClient()
{ {
waitingRequest.clear(); waitingRequest.clear();
applier = std::make_shared<CApplier<CBaseForCLApply>>();
registerTypesClientPacks(*applier);
gs = nullptr; gs = nullptr;
} }
@ -400,25 +347,21 @@ void CClient::installNewBattleInterface(std::shared_ptr<CBattleGameInterface> ba
} }
} }
void CClient::handlePack(CPack * pack) void CClient::handlePack(CPackForClient * pack)
{ {
CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier ApplyClientNetPackVisitor afterVisitor(*this, *gameState());
if(apply) ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState());
{
apply->applyOnClBefore(this, pack); pack->visit(beforeVisitor);
logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name()); logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name());
{ {
boost::unique_lock lock(CGameState::mutex); boost::unique_lock lock(CGameState::mutex);
gs->apply(pack); gs->apply(pack);
} }
logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name());
apply->applyOnClAfter(this, pack); pack->visit(afterVisitor);
logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name()); logNetwork->trace("\tMade second apply on cl: %s", typeid(*pack).name());
}
else
{
logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(*pack).name());
}
delete pack; delete pack;
} }
@ -443,8 +386,8 @@ void CClient::battleStarted(const BattleInfo * info)
{ {
std::shared_ptr<CPlayerInterface> att; std::shared_ptr<CPlayerInterface> att;
std::shared_ptr<CPlayerInterface> def; std::shared_ptr<CPlayerInterface> def;
auto & leftSide = info->sides[0]; const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE);
auto & rightSide = info->sides[1]; const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE);
for(auto & battleCb : battleCallbacks) for(auto & battleCb : battleCallbacks)
{ {
@ -453,17 +396,17 @@ void CClient::battleStarted(const BattleInfo * info)
} }
//If quick combat is not, do not prepare interfaces for battleint //If quick combat is not, do not prepare interfaces for battleint
auto callBattleStart = [&](PlayerColor color, ui8 side) auto callBattleStart = [&](PlayerColor color, BattleSide side)
{ {
if(vstd::contains(battleints, color)) if(vstd::contains(battleints, color))
battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
}; };
callBattleStart(leftSide.color, 0); callBattleStart(leftSide.color, BattleSide::LEFT_SIDE);
callBattleStart(rightSide.color, 1); callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE);
callBattleStart(PlayerColor::UNFLAGGABLE, 1); callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
callBattleStart(PlayerColor::SPECTATOR, 1); callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE);
if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human)
att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]); att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]);
@ -480,9 +423,9 @@ void CClient::battleStarted(const BattleInfo * info)
{ {
auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID); auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID);
if(interface->playerID == info->sides[info->tacticsSide].color) if(interface->playerID == info->getSide(info->tacticsSide).color)
{ {
auto action = BattleAction::makeEndOFTacticPhase(*side); auto action = BattleAction::makeEndOFTacticPhase(side);
interface->cb->battleMakeTacticAction(info->battleID, action); interface->cb->battleMakeTacticAction(info->battleID, action);
} }
} }
@ -514,7 +457,7 @@ void CClient::battleStarted(const BattleInfo * info)
if(info->tacticDistance) if(info->tacticDistance)
{ {
auto tacticianColor = info->sides[info->tacticsSide].color; auto tacticianColor = info->getSide(info->tacticsSide).color;
if (vstd::contains(battleints, tacticianColor)) if (vstd::contains(battleints, tacticianColor))
battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance);
@ -523,9 +466,11 @@ void CClient::battleStarted(const BattleInfo * info)
void CClient::battleFinished(const BattleID & battleID) void CClient::battleFinished(const BattleID & battleID)
{ {
for(auto & side : gs->getBattle(battleID)->sides) for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER })
if(battleCallbacks.count(side.color)) {
battleCallbacks[side.color]->onBattleEnded(battleID); if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color))
battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID);
}
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID);

View File

@ -21,14 +21,10 @@ struct CPackForServer;
class IBattleEventsReceiver; class IBattleEventsReceiver;
class CBattleGameInterface; class CBattleGameInterface;
class CGameInterface; class CGameInterface;
class BinaryDeserializer;
class BinarySerializer;
class BattleAction; class BattleAction;
class BattleInfo; class BattleInfo;
struct BankConfig; struct BankConfig;
template<typename T> class CApplier;
#if SCRIPTING_ENABLED #if SCRIPTING_ENABLED
namespace scripting namespace scripting
{ {
@ -147,7 +143,7 @@ public:
static ThreadSafeVector<int> waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) static ThreadSafeVector<int> waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction)
void handlePack(CPack * pack); //applies the given pack and deletes it void handlePack(CPackForClient * pack); //applies the given pack and deletes it
int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request
void battleStarted(const BattleInfo * info); void battleStarted(const BattleInfo * info);
@ -168,7 +164,7 @@ public:
void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {};
void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {};
void showBlockingDialog(BlockingDialog * iw) override {}; void showBlockingDialog(const IObjectInterface * caller, BlockingDialog * iw) override {};
void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {};
void showTeleportDialog(TeleportDialog * iw) override {}; void showTeleportDialog(TeleportDialog * iw) override {};
void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
@ -188,7 +184,8 @@ public:
void removeAfterVisit(const CGObjectInstance * object) override {}; void removeAfterVisit(const CGObjectInstance * object) override {};
bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;};
bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} bool giveHeroNewArtifact(const CGHeroInstance * h, const ArtifactID & artId, const ArtifactPosition & pos) override {return false;};
bool giveHeroNewScroll(const CGHeroInstance * h, const SpellID & spellId, const ArtifactPosition & pos) override {return false;};
bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional<bool> askAssemble) override {return false;}; bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional<bool> askAssemble) override {return false;};
void removeArtifact(const ArtifactLocation & al) override {}; void removeArtifact(const ArtifactLocation & al) override {};
bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; bool moveArtifact(const PlayerColor & player, const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;};
@ -196,9 +193,8 @@ public:
void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero void startBattle(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, const BattleLayout & layout, const CGTownInstance * town) override {}; //use hero=nullptr for no hero
void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used void startBattle(const CArmedInstance * army1, const CArmedInstance * army2) override {}; //if any of armies is hero, hero will be used
void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
void giveHeroBonus(GiveBonus * bonus) override {}; void giveHeroBonus(GiveBonus * bonus) override {};
void setMovePoints(SetMovePoints * smp) override {}; void setMovePoints(SetMovePoints * smp) override {};
@ -211,7 +207,7 @@ public:
void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {} void changeFogOfWar(const std::unordered_set<int3> & tiles, PlayerColor player, ETileVisibility mode) override {}
void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}; void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {};
void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}; void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {};
@ -237,8 +233,6 @@ private:
#endif #endif
std::unique_ptr<events::EventBus> clientEventBus; std::unique_ptr<events::EventBus> clientEventBus;
std::shared_ptr<CApplier<CBaseForCLApply>> applier;
mutable boost::mutex pathCacheMutex; mutable boost::mutex pathCacheMutex;
std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache; std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;

View File

@ -18,6 +18,7 @@
#include "gui/CGuiHandler.h" #include "gui/CGuiHandler.h"
#include "gui/WindowHandler.h" #include "gui/WindowHandler.h"
#include "render/IRenderHandler.h" #include "render/IRenderHandler.h"
#include "render/AssetGenerator.h"
#include "ClientNetPackVisitors.h" #include "ClientNetPackVisitors.h"
#include "../lib/CConfigHandler.h" #include "../lib/CConfigHandler.h"
#include "../lib/gameState/CGameState.h" #include "../lib/gameState/CGameState.h"
@ -38,7 +39,6 @@
#include "../lib/CHeroHandler.h" #include "../lib/CHeroHandler.h"
#include "../lib/VCMIDirs.h" #include "../lib/VCMIDirs.h"
#include "../lib/logging/VisualLogger.h" #include "../lib/logging/VisualLogger.h"
#include "CMT.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#ifdef SCRIPTING_ENABLED #ifdef SCRIPTING_ENABLED
@ -502,6 +502,12 @@ void ClientCommandManager::handleVsLog(std::istringstream & singleWordBuffer)
logVisual->setKey(key); logVisual->setKey(key);
} }
void ClientCommandManager::handleGenerateAssets()
{
AssetGenerator::generateAll();
printCommandMessage("All assets generated");
}
void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType) void ClientCommandManager::printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType)
{ {
switch(messageType) switch(messageType)
@ -624,6 +630,9 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
else if(commandName == "vslog") else if(commandName == "vslog")
handleVsLog(singleWordBuffer); handleVsLog(singleWordBuffer);
else if(message=="generate assets")
handleGenerateAssets();
else else
{ {
if (!commandName.empty() && !vstd::iswithin(commandName[0], 0, ' ')) // filter-out debugger/IDE noise if (!commandName.empty() && !vstd::iswithin(commandName[0], 0, ' ')) // filter-out debugger/IDE noise

View File

@ -84,6 +84,9 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
// shows object graph // shows object graph
void handleVsLog(std::istringstream & singleWordBuffer); void handleVsLog(std::istringstream & singleWordBuffer);
// generate all assets
void handleGenerateAssets();
// Prints in Chat the given message // Prints in Chat the given message
void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET); void printCommandMessage(const std::string &commandMessage, ELogLevel::ELogLevel messageType = ELogLevel::NOT_SET);
void giveTurn(const PlayerColor &color); void giveTurn(const PlayerColor &color);

View File

@ -47,7 +47,7 @@ public:
void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override; void visitBulkRebalanceStacks(BulkRebalanceStacks & pack) override;
void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) override; void visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) override;
void visitPutArtifact(PutArtifact & pack) override; void visitPutArtifact(PutArtifact & pack) override;
void visitEraseArtifact(EraseArtifact & pack) override; void visitEraseArtifact(BulkEraseArtifacts & pack) override;
void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override; void visitBulkMoveArtifacts(BulkMoveArtifacts & pack) override;
void visitAssembledArtifact(AssembledArtifact & pack) override; void visitAssembledArtifact(AssembledArtifact & pack) override;
void visitDisassembledArtifact(DisassembledArtifact & pack) override; void visitDisassembledArtifact(DisassembledArtifact & pack) override;

View File

@ -29,7 +29,6 @@
#include "../CCallback.h" #include "../CCallback.h"
#include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/Filesystem.h"
#include "../lib/filesystem/FileInfo.h" #include "../lib/filesystem/FileInfo.h"
#include "../lib/serializer/BinarySerializer.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/texts/CGeneralTextHandler.h"
#include "../lib/CHeroHandler.h" #include "../lib/CHeroHandler.h"
@ -108,8 +107,8 @@ void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & bat
return; return;
} }
callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward<Args2>(args)...); callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->getSide(BattleSide::ATTACKER).color, ptr, std::forward<Args2>(args)...);
callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward<Args2>(args)...); callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->getSide(BattleSide::DEFENDER).color, ptr, std::forward<Args2>(args)...);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt)
{ {
callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward<Args2>(args)...); callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward<Args2>(args)...);
@ -291,9 +290,10 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al); callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
} }
void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack)
{ {
callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al); for(const auto & slotErase : pack.posPack)
callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
} }
void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
@ -362,6 +362,14 @@ void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
{ {
cl.invalidatePaths(); cl.invalidatePaths();
if (pack.newWeekNotification)
{
const auto & newWeek = *pack.newWeekNotification;
std::string str = newWeek.text.toString();
callAllInterfaces(cl, &CGameInterface::showInfoDialog, newWeek.type, str, newWeek.components,(soundBase::soundID)newWeek.soundID);
}
} }
void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
@ -420,7 +428,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
adventureInt.reset(); adventureInt.reset();
} }
CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory()); CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic);
} }
// In auto testing pack.mode we always close client if red pack.player won or lose // In auto testing pack.mode we always close client if red pack.player won or lose
@ -769,12 +777,12 @@ void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog
void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack) void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack)
{ {
// Cannot use the usual code because curB is not set yet // Cannot use the usual code because curB is not set yet
callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, callOnlyThatBattleInterface(cl, pack.info->getSide(BattleSide::ATTACKER).color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject,
pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero);
callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, callOnlyThatBattleInterface(cl, pack.info->getSide(BattleSide::DEFENDER).color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject,
pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero);
callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->getSide(BattleSide::ATTACKER).armyObject, pack.info->getSide(BattleSide::DEFENDER).armyObject,
pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); pack.info->tile, pack.info->getSide(BattleSide::ATTACKER).hero, pack.info->getSide(BattleSide::DEFENDER).hero);
} }
void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack)
@ -801,9 +809,9 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack &
PlayerColor playerToCall; //pack.player that will move activated stack PlayerColor playerToCall; //pack.player that will move activated stack
if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
{ {
playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner() playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner()
? gs.getBattle(pack.battleID)->sides[1].color ? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color
: gs.getBattle(pack.battleID)->sides[0].color); : gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color;
} }
else else
{ {
@ -999,7 +1007,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
case EOpenWindowMode::UNIVERSITY_WINDOW: case EOpenWindowMode::UNIVERSITY_WINDOW:
{ {
//displays University window (when hero enters University on adventure map) //displays University window (when hero enters University on adventure map)
const auto * market = dynamic_cast<const IMarket*>(cl.getObj(ObjectInstanceID(pack.object))); const auto * market = cl.getMarket(ObjectInstanceID(pack.object));
const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID);
} }
@ -1009,7 +1017,7 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack)
//displays Thieves' Guild window (when hero enters Den of Thieves) //displays Thieves' Guild window (when hero enters Den of Thieves)
const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object));
const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor));
const auto *market = dynamic_cast<const IMarket*>(obj); const auto market = cl.getMarket(pack.object);
callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID);
} }
break; break;

View File

@ -31,10 +31,13 @@
#include "gui/WindowHandler.h" #include "gui/WindowHandler.h"
#include "widgets/Buttons.h" #include "widgets/Buttons.h"
#include "widgets/TextControls.h" #include "widgets/TextControls.h"
#include "media/CMusicHandler.h"
#include "media/IVideoPlayer.h"
#include "../lib/CConfigHandler.h" #include "../lib/CConfigHandler.h"
#include "../lib/texts/CGeneralTextHandler.h" #include "../lib/texts/CGeneralTextHandler.h"
#include "../lib/serializer/Connection.h" #include "../lib/serializer/Connection.h"
#include "../lib/campaign/CampaignState.h"
void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
{ {
@ -202,8 +205,19 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN) if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
{ {
lobby->bonusSel = std::make_shared<CBonusSelection>(); auto bonusSel = std::make_shared<CBonusSelection>();
GH.windows().pushWindow(lobby->bonusSel); lobby->bonusSel = bonusSel;
if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), false))
{
CCS->musich->stopMusic();
GH.windows().createAndPushWindow<CampaignRimVideo>(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), [bonusSel](){
if(!CSH->si->campState->getMusic().empty())
CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false);
GH.windows().pushWindow(bonusSel);
});
}
else
GH.windows().pushWindow(bonusSel);
} }
if(lobby->bonusSel) if(lobby->bonusSel)

View File

@ -15,11 +15,19 @@
#include "../lib/CThreadHelper.h" #include "../lib/CThreadHelper.h"
#include "../server/CVCMIServer.h" #include "../server/CVCMIServer.h"
#ifndef VCMI_MOBILE #ifdef ENABLE_SERVER_PROCESS
#if BOOST_VERSION >= 108600
// TODO: upgrade code to use v2 API instead of deprecated v1
#include <boost/process/v1/child.hpp>
#include <boost/process/v1/io.hpp>
#else
#include <boost/process/child.hpp> #include <boost/process/child.hpp>
#include <boost/process/io.hpp> #include <boost/process/io.hpp>
#endif #endif
#endif
#include <future> #include <future>
ServerThreadRunner::ServerThreadRunner() = default; ServerThreadRunner::ServerThreadRunner() = default;
@ -66,7 +74,7 @@ int ServerThreadRunner::exitCode()
return 0; return 0;
} }
#ifndef VCMI_MOBILE #ifdef ENABLE_SERVER_PROCESS
ServerProcessRunner::ServerProcessRunner() = default; ServerProcessRunner::ServerProcessRunner() = default;
ServerProcessRunner::~ServerProcessRunner() = default; ServerProcessRunner::~ServerProcessRunner() = default;

View File

@ -44,10 +44,23 @@ public:
}; };
#ifndef VCMI_MOBILE #ifndef VCMI_MOBILE
// Enable support for running vcmiserver as separate process. Unavailable on mobile systems
#define ENABLE_SERVER_PROCESS
#endif
#ifdef ENABLE_SERVER_PROCESS
#if BOOST_VERSION >= 108600
namespace boost::process {
inline namespace v1 {
class child;
}
}
#else
namespace boost::process { namespace boost::process {
class child; class child;
} }
#endif
/// Class that runs server instance as a child process /// Class that runs server instance as a child process
/// Available only on desktop systems where process management is allowed /// Available only on desktop systems where process management is allowed

View File

@ -33,12 +33,13 @@
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/IImage.h" #include "../render/IImage.h"
#include "../render/IRenderHandler.h" #include "../render/IRenderHandler.h"
#include "../render/IScreenHandler.h"
#include "../CMT.h" #include "../CMT.h"
#include "../PlayerLocalState.h" #include "../PlayerLocalState.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../../CCallback.h" #include "../../CCallback.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include "../../lib/StartInfo.h" #include "../../lib/StartInfo.h"
#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/CGeneralTextHandler.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
@ -58,7 +59,7 @@ AdventureMapInterface::AdventureMapInterface():
scrollingWasBlocked(false), scrollingWasBlocked(false),
backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer())
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
pos.x = pos.y = 0; pos.x = pos.y = 0;
pos.w = GH.screenDimensions().x; pos.w = GH.screenDimensions().x;
pos.h = GH.screenDimensions().y; pos.h = GH.screenDimensions().y;
@ -232,7 +233,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
bool cursorInScrollArea = scrollDelta != Point(0,0); bool cursorInScrollArea = scrollDelta != Point(0,0);
bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked;
bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool() || !GH.screenHandler().hasFocus();
if (!scrollingWasActive && scrollingBlocked) if (!scrollingWasActive && scrollingBlocked)
{ {
@ -375,7 +376,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma
mapAudio->onEnemyTurnStarted(); mapAudio->onEnemyTurnStarted();
widget->getMinimap()->setAIRadar(!isHuman); widget->getMinimap()->setAIRadar(!isHuman);
widget->getInfoBar()->startEnemyTurn(playerID); widget->getInfoBar()->startEnemyTurn(playerID);
setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); setState(isHuman ? EAdventureState::MAKING_TURN : EAdventureState::AI_PLAYER_TURN);
} }
void AdventureMapInterface::setState(EAdventureState state) void AdventureMapInterface::setState(EAdventureState state)
@ -452,7 +453,7 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID)
widget->getInfoBar()->showDate(); widget->getInfoBar()->showDate();
onHeroChanged(nullptr); onHeroChanged(nullptr);
Canvas canvas = Canvas::createFromSurface(screen); Canvas canvas = Canvas::createFromSurface(screen, CanvasScalingPolicy::AUTO);
showAll(canvas); showAll(canvas);
mapAudio->onPlayerTurnStarted(); mapAudio->onPlayerTurnStarted();
@ -616,7 +617,7 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
case SpellID::DIMENSION_DOOR: case SpellID::DIMENSION_DOOR:
if(isValidAdventureSpellTarget(targetPosition)) if(isValidAdventureSpellTarget(targetPosition))
{ {
if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition)) if(LOCPLINT->cb->getSettings().getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition))
CCS->curh->set(Cursor::Map::T1_ATTACK); CCS->curh->set(Cursor::Map::T1_ATTACK);
else else
CCS->curh->set(Cursor::Map::TELEPORT); CCS->curh->set(Cursor::Map::TELEPORT);
@ -898,7 +899,7 @@ void AdventureMapInterface::hotkeyZoom(int delta, bool useDeadZone)
void AdventureMapInterface::onScreenResize() void AdventureMapInterface::onScreenResize()
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
// remember our activation state and reactive after reconstruction // remember our activation state and reactive after reconstruction
// since othervice activate() calls for created elements will bypass virtual dispatch // since othervice activate() calls for created elements will bypass virtual dispatch

View File

@ -325,7 +325,6 @@ void AdventureMapShortcuts::toMainMenu()
[]() []()
{ {
CSH->endGameplay(); CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("main"); CMM->menu->switchToTab("main");
}, },
0 0
@ -339,7 +338,6 @@ void AdventureMapShortcuts::newGame()
[]() []()
{ {
CSH->endGameplay(); CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("new"); CMM->menu->switchToTab("new");
}, },
nullptr nullptr
@ -532,7 +530,6 @@ bool AdventureMapShortcuts::optionCanVisitObject()
auto * hero = LOCPLINT->localState->getCurrentHero(); auto * hero = LOCPLINT->localState->getCurrentHero();
auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos()); auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos());
assert(vstd::contains(objects,hero));
return objects.size() > 1; // there is object other than our hero return objects.size() > 1; // there is object other than our hero
} }
@ -577,16 +574,15 @@ bool AdventureMapShortcuts::optionInWorldView()
bool AdventureMapShortcuts::optionSidePanelActive() bool AdventureMapShortcuts::optionSidePanelActive()
{ {
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
} }
bool AdventureMapShortcuts::optionMapScrollingActive() bool AdventureMapShortcuts::optionMapScrollingActive()
{ {
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN; return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW;
} }
bool AdventureMapShortcuts::optionMapViewActive() bool AdventureMapShortcuts::optionMapViewActive()
{ {
return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::CASTING_SPELL;
|| state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
} }

View File

@ -381,7 +381,7 @@ CAdventureMapIcon::CAdventureMapIcon(const Point & position, const AnimationPath
: index(index) : index(index)
, iconsPerPlayer(iconsPerPlayer) , iconsPerPlayer(iconsPerPlayer)
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
pos += position; pos += position;
image = std::make_shared<CAnimImage>(animation, index); image = std::make_shared<CAnimImage>(animation, index);
} }

View File

@ -28,7 +28,7 @@
AdventureOptions::AdventureOptions() AdventureOptions::AdventureOptions()
: CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS"))
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); viewWorld = std::make_shared<CButton>(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); }); viewWorld->addCallback([] { LOCPLINT->viewWorldMap(); });

View File

@ -15,7 +15,6 @@ enum class EAdventureState
HOTSEAT_WAIT, HOTSEAT_WAIT,
MAKING_TURN, MAKING_TURN,
AI_PLAYER_TURN, AI_PLAYER_TURN,
OTHER_HUMAN_PLAYER_TURN,
CASTING_SPELL, CASTING_SPELL,
WORLD_VIEW WORLD_VIEW
}; };

View File

@ -51,7 +51,7 @@ CInfoBar::EmptyVisibleInfo::EmptyVisibleInfo()
CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATHR")); background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATHR"));
if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool())
@ -62,7 +62,7 @@ CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero)
CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town) CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATCS")); background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATCS"));
if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool())
@ -73,7 +73,7 @@ CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town)
CInfoBar::VisibleDateInfo::VisibleDateInfo() CInfoBar::VisibleDateInfo::VisibleDateInfo()
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
animation = std::make_shared<CShowableAnim>(1, 0, getNewDayName(), CShowableAnim::PLAY_ONCE, 180);// H3 uses around 175-180 ms per frame animation = std::make_shared<CShowableAnim>(1, 0, getNewDayName(), CShowableAnim::PLAY_ONCE, 180);// H3 uses around 175-180 ms per frame
animation->setDuration(1500); animation->setDuration(1500);
@ -114,7 +114,7 @@ AnimationPath CInfoBar::VisibleDateInfo::getNewDayName()
CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player) CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATNX")); background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATNX"));
banner = std::make_shared<CAnimImage>(AnimationPath::builtin("CREST58"), player.getNum(), 0, 20, 51); banner = std::make_shared<CAnimImage>(AnimationPath::builtin("CREST58"), player.getNum(), 0, 20, 51);
sand = std::make_shared<CShowableAnim>(99, 51, AnimationPath::builtin("HOURSAND"), 0, 100); // H3 uses around 100 ms per frame sand = std::make_shared<CShowableAnim>(99, 51, AnimationPath::builtin("HOURSAND"), 0, 100); // H3 uses around 100 ms per frame
@ -123,7 +123,7 @@ CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player)
CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo()
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
//get amount of halls of each level //get amount of halls of each level
std::vector<int> halls(4, 0); std::vector<int> halls(4, 0);
for(auto town : LOCPLINT->localState->getOwnedTowns()) for(auto town : LOCPLINT->localState->getOwnedTowns())
@ -180,7 +180,7 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo()
CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector<Component> & compsToDisplay, std::string message, int textH, bool tiny) CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector<Component> & compsToDisplay, std::string message, int textH, bool tiny)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATOT"), 1, 0); background = std::make_shared<CPicture>(ImagePath::builtin("ADSTATOT"), 1, 0);
auto fullRect = Rect(CInfoBar::offset, CInfoBar::offset, data_width - 2 * CInfoBar::offset, data_height - 2 * CInfoBar::offset); auto fullRect = Rect(CInfoBar::offset, CInfoBar::offset, data_width - 2 * CInfoBar::offset, data_height - 2 * CInfoBar::offset);
@ -250,14 +250,14 @@ void CInfoBar::playNewDaySound()
void CInfoBar::reset() void CInfoBar::reset()
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
state = EMPTY; state = EMPTY;
visibleInfo = std::make_shared<EmptyVisibleInfo>(); visibleInfo = std::make_shared<EmptyVisibleInfo>();
} }
void CInfoBar::showSelection() void CInfoBar::showSelection()
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
if(LOCPLINT->localState->getCurrentHero()) if(LOCPLINT->localState->getCurrentHero())
{ {
showHeroSelection(LOCPLINT->localState->getCurrentHero()); showHeroSelection(LOCPLINT->localState->getCurrentHero());
@ -325,7 +325,7 @@ CInfoBar::CInfoBar(const Rect & position)
state(EMPTY), state(EMPTY),
listener(settings.listen["gameTweaks"]["infoBarCreatureManagement"]) listener(settings.listen["gameTweaks"]["infoBarCreatureManagement"])
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
pos.w = position.w; pos.w = position.w;
pos.h = position.h; pos.h = position.h;
listener(std::bind(&CInfoBar::OnInfoBarCreatureManagementChanged, this)); listener(std::bind(&CInfoBar::OnInfoBarCreatureManagementChanged, this));
@ -349,7 +349,7 @@ void CInfoBar::setTimer(uint32_t msToTrigger)
void CInfoBar::showDate() void CInfoBar::showDate()
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
playNewDaySound(); playNewDaySound();
state = DATE; state = DATE;
visibleInfo = std::make_shared<VisibleDateInfo>(); visibleInfo = std::make_shared<VisibleDateInfo>();
@ -475,7 +475,7 @@ void CInfoBar::popAll()
void CInfoBar::popComponents(bool remove) void CInfoBar::popComponents(bool remove)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
if(remove && !componentsQueue.empty()) if(remove && !componentsQueue.empty())
componentsQueue.pop(); componentsQueue.pop();
if(!componentsQueue.empty()) if(!componentsQueue.empty())
@ -492,7 +492,7 @@ void CInfoBar::popComponents(bool remove)
void CInfoBar::pushComponents(const std::vector<Component> & comps, std::string message, int textH, bool tiny, int timer) void CInfoBar::pushComponents(const std::vector<Component> & comps, std::string message, int textH, bool tiny, int timer)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
componentsQueue.emplace(VisibleComponentInfo::Cache(comps, message, textH, tiny), timer); componentsQueue.emplace(VisibleComponentInfo::Cache(comps, message, textH, tiny), timer);
} }
@ -503,7 +503,7 @@ bool CInfoBar::showingComponents()
void CInfoBar::startEnemyTurn(PlayerColor color) void CInfoBar::startEnemyTurn(PlayerColor color)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
state = AITURN; state = AITURN;
visibleInfo = std::make_shared<VisibleEnemyTurnInfo>(color); visibleInfo = std::make_shared<VisibleEnemyTurnInfo>(color);
redraw(); redraw();
@ -511,7 +511,7 @@ void CInfoBar::startEnemyTurn(PlayerColor color)
void CInfoBar::showHeroSelection(const CGHeroInstance * hero) void CInfoBar::showHeroSelection(const CGHeroInstance * hero)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
if(!hero) if(!hero)
{ {
reset(); reset();
@ -526,7 +526,7 @@ void CInfoBar::showHeroSelection(const CGHeroInstance * hero)
void CInfoBar::showTownSelection(const CGTownInstance * town) void CInfoBar::showTownSelection(const CGTownInstance * town)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
if(!town) if(!town)
{ {
reset(); reset();
@ -541,7 +541,7 @@ void CInfoBar::showTownSelection(const CGTownInstance * town)
void CInfoBar::showGameStatus() void CInfoBar::showGameStatus()
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
state = GAME; state = GAME;
visibleInfo = std::make_shared<VisibleGameStatusInfo>(); visibleInfo = std::make_shared<VisibleGameStatusInfo>();
setTimer(3000); setTimer(3000);

View File

@ -18,26 +18,29 @@
#include "../widgets/ObjectLists.h" #include "../widgets/ObjectLists.h"
#include "../widgets/RadialMenu.h" #include "../widgets/RadialMenu.h"
#include "../windows/InfoWindows.h" #include "../windows/InfoWindows.h"
#include "../windows/CCastleInterface.h"
#include "../CGameInfo.h" #include "../CGameInfo.h"
#include "../CPlayerInterface.h" #include "../CPlayerInterface.h"
#include "../PlayerLocalState.h" #include "../PlayerLocalState.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../render/Canvas.h" #include "../render/Canvas.h"
#include "../render/Colors.h" #include "../render/Colors.h"
#include "../../lib/texts/CGeneralTextHandler.h" #include "../../lib/texts/CGeneralTextHandler.h"
#include "../../lib/CHeroHandler.h" #include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../CCallback.h"
CList::CListItem::CListItem(CList * Parent) CList::CListItem::CListItem(CList * Parent)
: CIntObject(LCLICK | SHOW_POPUP | HOVER), : CIntObject(LCLICK | SHOW_POPUP | HOVER),
parent(Parent), parent(Parent),
selection() selection()
{ {
defActions = 255-DISPOSE;
} }
CList::CListItem::~CListItem() = default; CList::CListItem::~CListItem() = default;
@ -71,7 +74,7 @@ void CList::CListItem::hover(bool on)
void CList::CListItem::onSelect(bool on) void CList::CListItem::onSelect(bool on)
{ {
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
selection.reset(); selection.reset();
if(on) if(on)
selection = genSelection(); selection = genSelection();
@ -96,7 +99,7 @@ void CList::showAll(Canvas & to)
void CList::createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount) void CList::createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
listBox = std::make_shared<CListBox>(std::bind(&CList::createItem, this, _1), firstItemPosition, itemPositionDelta, size, listAmount); listBox = std::make_shared<CListBox>(std::bind(&CList::createItem, this, _1), firstItemPosition, itemPositionDelta, size, listAmount);
} }
@ -207,7 +210,7 @@ void CList::selectPrev()
CHeroList::CEmptyHeroItem::CEmptyHeroItem() CHeroList::CEmptyHeroItem::CEmptyHeroItem()
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1);
portrait = std::make_shared<CPicture>(ImagePath::builtin("HPSXXX"), movement->pos.w + 1, 0); portrait = std::make_shared<CPicture>(ImagePath::builtin("HPSXXX"), movement->pos.w + 1, 0);
mana = std::make_shared<CAnimImage>(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1 ); mana = std::make_shared<CAnimImage>(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1 );
@ -220,7 +223,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero)
: CListItem(parent), : CListItem(parent),
hero(Hero) hero(Hero)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1);
portrait = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex(), 0, movement->pos.w + 1); portrait = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex(), 0, movement->pos.w + 1);
mana = std::make_shared<CAnimImage>(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1); mana = std::make_shared<CAnimImage>(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1);
@ -230,7 +233,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero)
update(); update();
addUsedEvents(GESTURE); addUsedEvents(GESTURE | KEYBOARD);
} }
void CHeroList::CHeroItem::update() void CHeroList::CHeroItem::update()
@ -301,6 +304,55 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const
GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements, true); GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements, true);
} }
void CHeroList::CHeroItem::keyPressed(EShortcut key)
{
if(!hero)
return;
if(parent->selected != this->shared_from_this())
return;
auto & heroes = LOCPLINT->localState->getWanderingHeroes();
if(key == EShortcut::LIST_HERO_DISMISS)
{
LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], [=](){ LOCPLINT->cb->dismissHero(hero); }, nullptr);
return;
}
if(heroes.size() < 2)
return;
size_t heroPos = vstd::find_pos(heroes, hero);
const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes.at(heroPos - 1);
const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes.at(heroPos + 1);
switch(key)
{
case EShortcut::LIST_HERO_UP:
if(heroUpper)
LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1);
break;
case EShortcut::LIST_HERO_DOWN:
if(heroLower)
LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1);
break;
case EShortcut::LIST_HERO_TOP:
if(heroUpper)
for (size_t i = heroPos; i > 0; i--)
LOCPLINT->localState->swapWanderingHero(i, i - 1);
break;
case EShortcut::LIST_HERO_BOTTOM:
if(heroLower)
for (int i = heroPos; i < heroes.size() - 1; i++)
LOCPLINT->localState->swapWanderingHero(i, i + 1);
break;
}
}
std::shared_ptr<CIntObject> CHeroList::createItem(size_t index) std::shared_ptr<CIntObject> CHeroList::createItem(size_t index)
{ {
if (LOCPLINT->localState->getWanderingHeroes().size() > index) if (LOCPLINT->localState->getWanderingHeroes().size() > index)
@ -365,12 +417,12 @@ CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
CListItem(parent), CListItem(parent),
town(Town) town(Town)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0); picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0);
pos = picture->pos; pos = picture->pos;
update(); update();
addUsedEvents(GESTURE); addUsedEvents(GESTURE | KEYBOARD);
} }
std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection() std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
@ -380,7 +432,7 @@ std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
void CTownList::CTownItem::update() void CTownList::CTownItem::update()
{ {
size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->built >= LOCPLINT->cb->getSettings().getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)];
picture->setFrame(iconIndex + 2); picture->setFrame(iconIndex + 2);
redraw(); redraw();
@ -419,24 +471,77 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1; int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1;
int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1;
auto updateList = [](){
for (auto ki : GH.windows().findWindows<CCastleInterface>())
ki->townChange(); //update list
};
std::vector<RadialMenuConfig> menuElements = { std::vector<RadialMenuConfig> menuElements = {
{ RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [townIndex]() { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [updateList, townIndex]()
{ {
for (int i = townIndex; i > 0; i--) for (int i = townIndex; i > 0; i--)
LOCPLINT->localState->swapOwnedTowns(i, i - 1); LOCPLINT->localState->swapOwnedTowns(i, i - 1);
updateList();
} }, } },
{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } }, { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [updateList, townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); updateList(); } },
{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } }, { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [updateList, townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); updateList(); } },
{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [townIndex, towns]() { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [updateList, townIndex, towns]()
{ {
for (int i = townIndex; i < towns.size() - 1; i++) for (int i = townIndex; i < towns.size() - 1; i++)
LOCPLINT->localState->swapOwnedTowns(i, i + 1); LOCPLINT->localState->swapOwnedTowns(i, i + 1);
updateList();
} }, } },
}; };
GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements, true); GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements, true);
} }
void CTownList::CTownItem::keyPressed(EShortcut key)
{
if(parent->selected != this->shared_from_this())
return;
const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
size_t townIndex = vstd::find_pos(towns, town);
if(townIndex + 1 > towns.size() || !towns.at(townIndex))
return;
if(towns.size() < 2)
return;
int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1;
int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1;
switch(key)
{
case EShortcut::LIST_TOWN_UP:
if(townUpperPos > -1)
LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos);
break;
case EShortcut::LIST_TOWN_DOWN:
if(townLowerPos > -1)
LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos);
break;
case EShortcut::LIST_TOWN_TOP:
if(townUpperPos > -1)
for (int i = townIndex; i > 0; i--)
LOCPLINT->localState->swapOwnedTowns(i, i - 1);
break;
case EShortcut::LIST_TOWN_BOTTOM:
if(townLowerPos > -1)
for (int i = townIndex; i < towns.size() - 1; i++)
LOCPLINT->localState->swapOwnedTowns(i, i + 1);
break;
}
for (auto ki : GH.windows().findWindows<CCastleInterface>())
ki->townChange(); //update list
}
std::string CTownList::CTownItem::getHoverText() std::string CTownList::CTownItem::getHoverText()
{ {
return town->getObjectName(); return town->getObjectName();

View File

@ -29,9 +29,10 @@ class CList : public Scrollable
protected: protected:
class CListItem : public CIntObject, public std::enable_shared_from_this<CListItem> class CListItem : public CIntObject, public std::enable_shared_from_this<CListItem>
{ {
CList * parent;
std::shared_ptr<CIntObject> selection; std::shared_ptr<CIntObject> selection;
public: public:
CList * parent;
CListItem(CList * parent); CListItem(CList * parent);
~CListItem(); ~CListItem();
@ -55,9 +56,6 @@ protected:
private: private:
const size_t size; const size_t size;
//for selection\deselection
std::shared_ptr<CListItem> selected;
void select(std::shared_ptr<CListItem> which); void select(std::shared_ptr<CListItem> which);
friend class CListItem; friend class CListItem;
@ -81,6 +79,9 @@ protected:
void update(); void update();
public: public:
//for selection\deselection
std::shared_ptr<CListItem> selected;
/// functions that will be called when selection changes /// functions that will be called when selection changes
CFunctionList<void()> onSelect; CFunctionList<void()> onSelect;
@ -128,6 +129,7 @@ class CHeroList : public CList
void open() override; void open() override;
void showTooltip() override; void showTooltip() override;
void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
void keyPressed(EShortcut key) override;
std::string getHoverText() override; std::string getHoverText() override;
}; };
@ -162,6 +164,7 @@ class CTownList : public CList
void open() override; void open() override;
void showTooltip() override; void showTooltip() override;
void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
void keyPressed(EShortcut key) override;
std::string getHoverText() override; std::string getHoverText() override;
}; };

View File

@ -73,7 +73,7 @@ void CMinimapInstance::redrawMinimap()
CMinimapInstance::CMinimapInstance(CMinimap *Parent, int Level): CMinimapInstance::CMinimapInstance(CMinimap *Parent, int Level):
parent(Parent), parent(Parent),
minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y))), minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y), CanvasScalingPolicy::IGNORE)),
level(Level) level(Level)
{ {
pos.w = parent->pos.w; pos.w = parent->pos.w;
@ -90,7 +90,7 @@ CMinimap::CMinimap(const Rect & position)
: CIntObject(LCLICK | SHOW_POPUP | DRAG | MOVE | GESTURE, position.topLeft()), : CIntObject(LCLICK | SHOW_POPUP | DRAG | MOVE | GESTURE, position.topLeft()),
level(0) level(0)
{ {
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
double maxSideLengthSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y); double maxSideLengthSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y);
double maxSideLengthDst = std::max(position.w, position.h); double maxSideLengthDst = std::max(position.w, position.h);
@ -202,7 +202,7 @@ void CMinimap::update()
if(aiShield->recActions & UPDATE) //AI turn is going on. There is no need to update minimap if(aiShield->recActions & UPDATE) //AI turn is going on. There is no need to update minimap
return; return;
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
minimap = std::make_shared<CMinimapInstance>(this, level); minimap = std::make_shared<CMinimapInstance>(this, level);
redraw(); redraw();
} }

View File

@ -29,7 +29,7 @@ CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position)
pos.x += position.x; pos.x += position.x;
pos.y += position.y; pos.y += position.y;
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); OBJECT_CONSTRUCTION;
background = std::make_shared<CPicture>(imageName, 0, 0); background = std::make_shared<CPicture>(imageName, 0, 0);
background->setPlayerColor(LOCPLINT->playerID); background->setPlayerColor(LOCPLINT->playerID);

View File

@ -35,7 +35,7 @@ TurnTimerWidget::TurnTimerWidget(const Point & position, PlayerColor player)
, lastSoundCheckSeconds(0) , lastSoundCheckSeconds(0)
, isBattleMode(player.isValidPlayer()) , isBattleMode(player.isValidPlayer())
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
pos += position; pos += position;
pos.w = 0; pos.w = 0;

View File

@ -312,8 +312,8 @@ void BattleActionsController::castThisSpell(SpellID spellID)
heroSpellToCast = std::make_shared<BattleAction>(); heroSpellToCast = std::make_shared<BattleAction>();
heroSpellToCast->actionType = EActionType::HERO_SPELL; heroSpellToCast->actionType = EActionType::HERO_SPELL;
heroSpellToCast->spell = spellID; heroSpellToCast->spell = spellID;
heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; heroSpellToCast->stackNumber = -1;
heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; heroSpellToCast->side = owner.curInt->cb->getBattle(owner.getBattleID())->battleGetMySide();
//choosing possible targets //choosing possible targets
const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
@ -499,9 +499,12 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK:
case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
{ {
const auto * attacker = owner.stacksController->getActiveStack();
BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0;
DamageEstimation retaliation; DamageEstimation retaliation;
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex, &retaliation); BattleAttackInfo attackInfo(attacker, targetStack, distance, false );
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount()); estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount()); estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount(); bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount();
@ -514,7 +517,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
const auto * shooter = owner.stacksController->getActiveStack(); const auto * shooter = owner.stacksController->getActiveStack();
DamageEstimation retaliation; DamageEstimation retaliation;
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition(), &retaliation); BattleAttackInfo attackInfo(shooter, targetStack, 0, true );
DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(attackInfo, &retaliation);
estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount()); estimation.kills.max = std::min<int64_t>(estimation.kills.max, targetStack->getCount());
estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount()); estimation.kills.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available());

View File

@ -161,7 +161,7 @@ ECreatureAnimType AttackAnimation::findValidGroup( const std::vector<ECreatureAn
const CCreature * AttackAnimation::getCreature() const const CCreature * AttackAnimation::getCreature() const
{ {
if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS) if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
return owner.siegeController->getTurretCreature(); return owner.siegeController->getTurretCreature(attackingStack->initialPosition);
else else
return attackingStack->unitType(); return attackingStack->unitType();
} }

View File

@ -110,7 +110,7 @@ static const std::map<int, int> hexEdgeMaskToFrameIndex =
BattleFieldController::BattleFieldController(BattleInterface & owner): BattleFieldController::BattleFieldController(BattleInterface & owner):
owner(owner) owner(owner)
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
//preparing cells and hexes //preparing cells and hexes
cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY);
@ -124,6 +124,8 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY); rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY);
shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY); shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY);
cellShade->setShadowEnabled(true);
if(!owner.siegeController) if(!owner.siegeController)
{ {
auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); auto bfieldType = owner.getBattle()->battleGetBattlefieldType();
@ -142,7 +144,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
pos.w = background->width(); pos.w = background->width();
pos.h = background->height(); pos.h = background->height();
backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height())); backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()), CanvasScalingPolicy::AUTO);
updateAccessibleHexes(); updateAccessibleHexes();
addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE);
@ -156,7 +158,7 @@ void BattleFieldController::activate()
void BattleFieldController::createHeroes() void BattleFieldController::createHeroes()
{ {
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; OBJECT_CONSTRUCTION;
// create heroes as part of our constructor for correct positioning inside battlefield // create heroes as part of our constructor for correct positioning inside battlefield
if(owner.attackingHeroInstance) if(owner.attackingHeroInstance)

View File

@ -85,7 +85,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *
this->army2 = army2; this->army2 = army2;
const CGTownInstance *town = getBattle()->battleGetDefendedTown(); const CGTownInstance *town = getBattle()->battleGetDefendedTown();
if(town && town->hasFort()) if(town && town->fortificationsLevel().wallsHealth > 0)
siegeController.reset(new BattleSiegeController(*this, town)); siegeController.reset(new BattleSiegeController(*this, town));
windowObject = std::make_shared<BattleWindow>(*this); windowObject = std::make_shared<BattleWindow>(*this);
@ -229,19 +229,19 @@ void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedI
{ {
stacksController->stacksAreAttacked(attackedInfos); stacksController->stacksAreAttacked(attackedInfos);
std::array<int, 2> killedBySide = {0, 0}; BattleSideArray<int> killedBySide;
for(const StackAttackedInfo & attackedInfo : attackedInfos) for(const StackAttackedInfo & attackedInfo : attackedInfos)
{ {
ui8 side = attackedInfo.defender->unitSide(); BattleSide side = attackedInfo.defender->unitSide();
killedBySide.at(side) += attackedInfo.amountKilled; killedBySide.at(side) += attackedInfo.amountKilled;
} }
for(ui8 side = 0; side < 2; side++) for(BattleSide side : { BattleSide::ATTACKER, BattleSide::DEFENDER })
{ {
if(killedBySide.at(side) > killedBySide.at(1-side)) if(killedBySide.at(side) > killedBySide.at(getBattle()->otherSide(side)))
setHeroAnimation(side, EHeroAnimType::DEFEAT); setHeroAnimation(side, EHeroAnimType::DEFEAT);
else if(killedBySide.at(side) < killedBySide.at(1-side)) else if(killedBySide.at(side) < killedBySide.at(getBattle()->otherSide(side)))
setHeroAnimation(side, EHeroAnimType::VICTORY); setHeroAnimation(side, EHeroAnimType::VICTORY);
} }
} }
@ -271,14 +271,14 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID sp
} }
auto side = getBattle()->playerToSide(curInt->playerID); auto side = getBattle()->playerToSide(curInt->playerID);
if(!side) if(side == BattleSide::NONE)
{ {
logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); logGlobal->error("Player %s is not in battle", curInt->playerID.toString());
return; return;
} }
BattleAction ba; BattleAction ba;
ba.side = side.value(); ba.side = side;
ba.actionType = action; ba.actionType = action;
ba.aimToHex(tile); ba.aimToHex(tile);
ba.spell = spell; ba.spell = spell;
@ -409,7 +409,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
} }
else else
{ {
auto hero = sc->side ? defendingHero : attackingHero; auto hero = sc->side == BattleSide::DEFENDER ? defendingHero : attackingHero;
assert(hero); assert(hero);
addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]()
@ -466,11 +466,11 @@ void BattleInterface::spellCast(const BattleSpellCast * sc)
{ {
Point leftHero = Point(15, 30); Point leftHero = Point(15, 30);
Point rightHero = Point(755, 30); Point rightHero = Point(755, 30);
bool side = sc->side; BattleSide side = sc->side;
addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side == BattleSide::DEFENDER ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero));
stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side == BattleSide::DEFENDER ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero));
}); });
} }
@ -483,7 +483,7 @@ void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse)
fieldController->redrawBackgroundWithHexes(); fieldController->redrawBackgroundWithHexes();
} }
void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) void BattleInterface::setHeroAnimation(BattleSide side, EHeroAnimType phase)
{ {
if(side == BattleSide::ATTACKER) if(side == BattleSide::ATTACKER)
{ {
@ -656,7 +656,7 @@ void BattleInterface::tacticPhaseEnd()
tacticsMode = false; tacticsMode = false;
auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID);
auto action = BattleAction::makeEndOFTacticPhase(*side); auto action = BattleAction::makeEndOFTacticPhase(side);
tacticianInterface->cb->battleMakeTacticAction(battleID, action); tacticianInterface->cb->battleMakeTacticAction(battleID, action);
} }

View File

@ -170,7 +170,7 @@ public:
void showInterface(Canvas & to); void showInterface(Canvas & to);
void setHeroAnimation(ui8 side, EHeroAnimType phase); void setHeroAnimation(BattleSide side, EHeroAnimType phase);
void executeSpellCast(); //called when a hero casts a spell void executeSpellCast(); //called when a hero casts a spell

Some files were not shown because too many files have changed in this diff Show More