1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-23 00:28: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'
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
run: |
source '${{github.workspace}}/CI/get_package_name.sh'

2
.gitmodules vendored
View File

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

View File

@ -12,6 +12,10 @@
#include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/ObstacleCasterProxy.h"
#include "../../lib/battle/CObstacleInstance.h"
uint64_t averageDmg(const DamageRange & range)
{
@ -25,9 +29,57 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit
damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
}
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side)
void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide 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
{
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();
}
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)
{
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)
: from(from), dest(dest), attack(attack)
{
this->attack.attackerPos = from;
this->attack.defenderPos = dest;
}
float AttackPossibility::damageDiff() const
@ -199,6 +270,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting)
return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes)
@ -206,9 +279,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker))
continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st))
continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex;
@ -218,9 +295,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain;
res += gain * cachedDmg / std::max<uint64_t>(1, averageDmg(rangeDmg.damage));
}
return res;
@ -243,7 +321,7 @@ AttackPossibility AttackPossibility::evaluate(
std::vector<BattleHex> defenderHex;
if(attackInfo.shooting)
defenderHex = defender->getHexes();
defenderHex.push_back(defender->getPosition());
else
defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
@ -261,63 +339,114 @@ AttackPossibility AttackPossibility::evaluate(
if (!attackInfo.shooting)
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)
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
else
units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
{
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
bool addDefender = true;
for(auto unit : units)
if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); }))
{
if (unit->unitId() == defender->unitId())
{
addDefender = false;
break;
}
defenderUnits.push_back(defender);
}
if(addDefender)
units.push_back(defender);
affectedUnits = defenderUnits;
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())
break;
if(u->unitId() == attacker->unitId())
continue;
auto defenderState = u->acquireState();
ap.affectedUnits.push_back(defenderState);
defenderStates[u->unitId()] = defenderState;
}
for(int i = 0; i < totalAttacks; i++)
{
if(!ap.attackerState->alive() || !defenderStates[defender->unitId()]->alive())
break;
for(auto u : defenderUnits)
{
auto defenderState = defenderStates.at(u->unitId());
int64_t damageDealt;
int64_t damageReceived;
float defenderDamageReduce;
float attackerDamageReduce;
DamageEstimation retaliation;
auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state);
vstd::amin(damageDealt, defenderState->getAvailableHealth());
defenderDamageReduce = calculateDamageReduce(attacker, u, damageDealt, damageCache, state);
ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation
damageReceived = 0;
attackerDamageReduce = 0;
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{
damageReceived = averageDmg(retaliation.damage);
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state);
for(auto retaliated : retaliatedUnits)
{
if(retaliated->unitId() == attacker->unitId())
{
int64_t damageReceived = averageDmg(retaliation.damage);
vstd::amin(damageReceived, ap.attackerState->getAvailableHealth());
attackerDamageReduce = calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
ap.attackerState->damage(damageReceived);
}
else
{
auto retaliationCollateral = state->battleEstimateDamage(defender, retaliated, 0);
int64_t damageReceived = averageDmg(retaliationCollateral.damage);
vstd::amin(damageReceived, retaliated->getAvailableHealth());
if(defender->unitSide() == retaliated->unitSide())
defenderDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
else
ap.collateralDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
defenderStates.at(retaliated->unitId())->damage(damageReceived);
}
}
defenderState->afterAttack(attackInfo.shooting, true);
}
@ -331,20 +460,29 @@ AttackPossibility AttackPossibility::evaluate(
if(attackerSide == u->unitSide())
ap.collateralDamageReduce += defenderDamageReduce;
if(u->unitId() == defender->unitId() ||
(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
if(u->unitId() == defender->unitId()
|| (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
{
//FIXME: handle RANGED_RETALIATION ?
ap.attackerDamageReduce += attackerDamageReduce;
}
ap.attackerState->damage(damageReceived);
defenderState->damage(damageDealt);
if (!ap.attackerState->alive() || !defenderState->alive())
break;
if(u->unitId() == defender->unitId())
{
ap.defenderDead = !defenderState->alive();
}
}
}
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
attackInfo.attacker->unitType()->getJsonKey(),
attackInfo.defender->unitType()->getJsonKey(),
(int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(),
ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg);
#endif
if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
bestAp = ap;

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@
#include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/entities/building/TownFortifications.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
@ -49,6 +50,43 @@ SpellTypes spellType(const CSpell * spell)
return SpellTypes::OTHER;
}
BattleEvaluator::BattleEvaluator(
std::shared_ptr<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> result;
@ -139,8 +177,10 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
auto & bestAttack = evaluationResult.bestAttack;
cachedAttack = bestAttack;
cachedScore = evaluationResult.score;
cachedAttack.ap = bestAttack;
cachedAttack.score = evaluationResult.score;
cachedAttack.turn = 0;
cachedAttack.waited = evaluationResult.wait;
//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@ -167,7 +207,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score
);
if (moveTarget.scorePerTurn <= score)
if (moveTarget.score <= score)
{
if(evaluationResult.wait)
{
@ -186,37 +226,64 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
{
return BattleAction::makeDefend(stack);
}
else
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit * u) -> bool
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
});
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.
if(moveTarget.scorePerTurn > score)
if(moveTarget.score > score)
{
score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack;
cachedScore = score;
cachedAttack.ap = moveTarget.cachedAttack;
cachedAttack.score = score;
cachedAttack.turn = moveTarget.turnsToRich;
if(stack->waited())
{
logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f",
"Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score,
moveTarget.scorePerTurn);
moveTarget.score);
return goTowardsNearest(stack, moveTarget.positions);
return goTowardsNearest(stack, moveTarget.positions, *targets);
}
else
{
cachedAttack.waited = true;
return BattleAction::makeWait(stack);
}
}
@ -224,7 +291,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(score <= EvaluationResult::INEFFECTIVE_SCORE
&& !stack->hasBonusOfType(BonusType::FLYING)
&& stack->unitSide() == BattleSide::ATTACKER
&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
&& cb->getBattle(battleID)->battleGetFortifications().hasMoat)
{
auto brokenWallMoat = getBrokenWallMoatHexes();
@ -235,7 +302,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else
return goTowardsNearest(stack, brokenWallMoat);
return goTowardsNearest(stack, brokenWallMoat, *targets);
}
}
@ -249,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();
}
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 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;
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
{
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)
{
if(vstd::contains(avHexes, hex))
{
return BattleAction::makeMove(stack, hex);
return moveOrAttack(stack, hex, targets);
}
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...
return BattleAction::makeDefend(stack);
}
}
if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE)
{
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);
}
// not this turn
scoreEvaluator.updateReachabilityMap(hb);
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 BattleAction::makeMove(stack, *nearestAvailableHex);
return moveOrAttack(stack, *nearestAvailableHex, targets);
}
else
{
@ -357,11 +438,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest);
{
return moveOrAttack(stack, currentDest, targets);
}
currentDest = reachability.predecessors[currentDest];
}
}
logAi->error("We should either detect that hexes are unreachable or make a move!");
return BattleAction::makeDefend(stack);
}
bool BattleEvaluator::canCastSpell()
@ -391,7 +477,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
vstd::erase_if(possibleSpells, [](const CSpell *s)
{
return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION;
return spellType(s) != SpellTypes::BATTLE;
});
LOGFL("I know how %d of them works.", possibleSpells.size());
@ -402,9 +488,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
{
spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
if(spell->getTargetType() == spells::AimType::LOCATION)
continue;
const bool FAST = true;
for(auto & target : temp.findPotentialTargets(FAST))
@ -573,7 +656,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
auto & ps = possibleCasts[i];
#if BATTLE_TRACE_LEVEL >= 1
if(ps.dest.empty())
logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
else
{
auto psFirst = ps.dest.front();
auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
}
#endif
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
@ -591,38 +682,56 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
DamageCache safeCopy = damageCache;
DamageCache innerCache(&safeCopy);
innerCache.buildDamageCache(state, side);
if(needFullEval || !cachedAttack)
if(cachedAttack.ap && cachedAttack.waited)
{
state->makeWait(activeStack);
}
if(needFullEval || !cachedAttack.ap)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Full evaluation is started due to stack speed affected.");
#endif
PotentialTargets innerTargets(activeStack, innerCache, state);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
innerEvaluator.updateReachabilityMap(state);
auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
if(!innerTargets.possibleAttacks.empty())
{
innerEvaluator.updateReachabilityMap(state);
auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
ps.value = newStackAction.score;
ps.value = std::max(moveTarget.score, newStackAction.score);
}
else
{
ps.value = 0;
ps.value = moveTarget.score;
}
}
else
{
ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId());
auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId());
auto updatedBai = BattleAttackInfo(
updatedAttacker.get(),
updatedDefender.get(),
cachedAttack.ap->attack.chargeDistance,
cachedAttack.ap->attack.shooting);
auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
}
for(const auto & unit : allUnits)
{
if (!unit->isValidTarget())
if(!unit->isValidTarget(true))
continue;
auto newHealth = unit->getAvailableHealth();
@ -645,13 +754,18 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
if(ourUnit * goodEffect == 1)
{
if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
auto isMagical = state->getForUpdate(unit->unitId())->summoned
|| unit->isClone()
|| unit->isGhost();
if(ourUnit && goodEffect && isMagical)
continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
}
else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
// discourage AI making collateral damage with spells
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace(
@ -662,6 +776,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
#endif
}
}
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Total score: %2f", ps.value);
#endif
@ -672,13 +787,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
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;
};
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);
BattleAction spellcast;
@ -686,7 +800,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spellcast.spell = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest);
spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2;
spellcast.stackNumber = -1;
cb->battleMakeSpellAction(battleID, spellcast);
activeActionMade = true;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,11 +18,9 @@
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/gameState/CGameState.h"
#include "../../lib/serializer/CTypeList.h"
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "../../lib/networkPacks/PacksForClient.h"
#include "../../lib/networkPacks/PacksForClientBattle.h"
#include "../../lib/networkPacks/PacksForServer.h"
@ -570,6 +568,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
LOG_TRACE(logAi);
myCb = CB;
cbc = CB;
this->env = env;
NET_EVENT_HANDLER;
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;
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
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));
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;
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 makeTurn();

View File

@ -18,7 +18,7 @@
#include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/mapping/CMapDefines.h"
#include "../../lib/gameState/QuestInfo.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.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 < GameConstants::CREATURES_PER_TOWN; level++)
for(int level = 0; level < developmentInfo.town->town->creatures.size(); level++)
{
logAi->trace("Checking dwelling level %d", level);
BuildingInfo nextToBuild = BuildingInfo();
for(BuildingID prefix : prefixes)
for(int upgradeIndex : {1, 0})
{
BuildingID building = BuildingID(prefix + level);
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building))
continue; // no such building in town
@ -211,8 +209,8 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{
creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
creatureLevel = BuildingID::getLevelFromDwelling(toBuild);
creatureUpgrade = BuildingID::getUpgradedFromDwelling(toBuild);
}
else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{
@ -316,9 +314,7 @@ void BuildAnalyzer::updateDailyIncome()
const CGMine* mine = dynamic_cast<const CGMine*>(obj);
if(mine)
{
dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
}
dailyIncome += mine->dailyIncome();
}
for(const CGTownInstance* town : towns)

View File

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

View File

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

View File

@ -212,7 +212,7 @@ void CaptureObjectsBehavior::decomposeObjects(
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);
});
}

View File

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

View File

@ -208,12 +208,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers);
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);
if(fort)
castleWalls->setValue(fort->fortLevel());

View File

@ -20,25 +20,6 @@
namespace NKAI
{
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<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)
{
auto cb = ai->cb.get();
@ -158,30 +139,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
return 0;
[[fallthrough]];
}
case Obj::MONSTER:
case Obj::GARRISON:
case Obj::GARRISON2:
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR4:
case Obj::MINE:
case Obj::ABANDONED_MINE:
case Obj::PANDORAS_BOX:
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship
default:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
if (a)
return a->getArmyStrength();
}
case Obj::PYRAMID:
{
return estimateBankDanger(dynamic_cast<const CBank *>(obj));
}
default:
else
return 0;
}
}
}
}

View File

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

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);
}
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 result = 0;
for(EGameResID r = EGameResID(0); r < EGameResID::COUNT; r.advance(1))
for(auto r : GameResID::ALL_RESOURCES())
{
if(res[r] > 0)
{
result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
}
}
return result;
}
@ -305,22 +284,13 @@ uint64_t RewardEvaluator::getArmyReward(
{
case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
case Obj::CREATURE_BANK:
return getCreatureBankArmyReward(target, hero);
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT:
case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::WARRIORS_TOMB:
return 1000;
case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->artType);
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::HERO:
return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@ -350,7 +320,7 @@ uint64_t RewardEvaluator::getArmyReward(
{
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);
}
@ -358,7 +328,7 @@ uint64_t RewardEvaluator::getArmyReward(
if(!info.reward.creatures.empty())
{
for(auto stackInfo : info.reward.creatures)
for(const auto & stackInfo : info.reward.creatures)
{
rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount();
}
@ -555,13 +525,6 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
return getResourceRequirementStrength(res);
}
case Obj::CREATURE_BANK:
{
auto resourceReward = getCreatureBankResources(target, nullptr);
return getResourceRequirementStrength(resourceReward);
}
case Obj::TOWN:
{
if(ai->buildAnalyzer->getDevelopmentInfo().empty())
@ -672,8 +635,6 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps)
return 2.5f;
case Obj::PYRAMID:
return 6.0f;
case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? 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);
return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
}
case Obj::MYSTICAL_GARDEN:
case Obj::WINDMILL:
return 100;
case Obj::CAMPFIRE:
return 800;
case Obj::WAGON:
return 100;
case Obj::CREATURE_BANK:
return getResourcesGoldReward(getCreatureBankResources(target, hero));
case Obj::CRYPT:
case Obj::DERELICT_SHIP:
return 3000;
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::SEA_CHEST:
return 1500;
case Obj::PANDORAS_BOX:
return 2500;
case Obj::PRISON:

View File

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

View File

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

View File

@ -321,7 +321,7 @@ void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
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 objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();

View File

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

View File

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

View File

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

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> 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,
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,
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> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
@ -196,7 +196,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
return true;
//workaround for mantis #2696 - build capitol with separate algorithm if it is available
if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
if(t->hasBuilt(BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
{
if(tryBuildNextStructure(t, capitolAndRequirements))
return true;

View File

@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers);
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);
if(fort)
castleWalls->setValue(fort->fortLevel());

View File

@ -16,7 +16,6 @@
#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGCreature.h"
#include "../../lib/mapObjects/CGDwelling.h"
#include "../../lib/gameState/InfoAboutArmy.h"
@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
return result;
}
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<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)
{
if(g.parent)
@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
cb->getTownInfo(obj, iat);
return iat.army.getStrength();
}
case Obj::MONSTER:
{
//TODO!!!!!!!!
const CGCreature * cre = dynamic_cast<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:
default:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
if (a)
return a->getArmyStrength();
}
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:
else
return 0;
}
}
}

View File

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

View File

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

View File

@ -39,7 +39,7 @@ namespace AIPathfinding
CPlayerSpecificInfoCallback * cb,
VCAI * ai,
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.useEmbarkAndDisembark = true;

View File

@ -21,7 +21,7 @@
#include "../../lib/mapObjects/ObjectTemplate.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/gameState/CGameState.h"
#include "../../lib/bonuses/Limiters.h"
#include "../../lib/bonuses/Updaters.h"
@ -31,8 +31,6 @@
#include "../../lib/networkPacks/PacksForClientBattle.h"
#include "../../lib/networkPacks/PacksForServer.h"
#include "../../lib/serializer/CTypeList.h"
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "AIhelper.h"
@ -1319,7 +1317,7 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
return false;
if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
return false;
if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false;
if(!cb->getAvailableHeroes(t).size())
return false;
@ -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;
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
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));
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))
return true;
if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
return true;
switch(obj->ID)
{
@ -2854,12 +2850,12 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj)
case Obj::MAGIC_WELL:
return h->mana < h->manaLimit();
case Obj::PRISON:
return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
return ai->myCb->getHeroesInfo().size() < cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
case Obj::TAVERN:
{
//TODO: make AI actually recruit heroes
//TODO: only on request
if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
if(ai->myCb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
return false;
else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST)
return false;

View File

@ -187,7 +187,7 @@ public:
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) 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;
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;
}
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)
{
assert(action.actionType == EActionType::HERO_SPELL);
@ -256,15 +266,15 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
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;
pack.marketId = dynamic_cast<const CGObjectInstance *>(market)->id;
pack.marketId = marketId;
pack.heroId = hero ? hero->id : ObjectInstanceID();
pack.mode = mode;
pack.r1 = id1;

View File

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

View File

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

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
DEPS_FILENAME=aarch64-v8a
DEPS_FILENAME=dependencies-android-64
. 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
mkdir ~/.conan ; cd ~/.conan
curl -L "https://github.com/vcmi/vcmi-dependencies/releases/download/android-1.1/$DEPS_FILENAME.txz" \
| tar -xf - --xz
. CI/install_conan_dependencies.sh "$DEPS_FILENAME"

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
mkdir ~/.conan ; cd ~/.conan
curl -L 'https://github.com/vcmi/vcmi-ios-deps/releases/download/1.2.1/ios-arm64.txz' \
| tar -xf -
. CI/install_conan_dependencies.sh "dependencies-ios"

View File

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

View File

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

View File

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

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 \
&& sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
mkdir ~/.conan ; cd ~/.conan
curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w32.tgz" \
| tar -xzf -
. CI/install_conan_dependencies.sh "dependencies-mingw-32"

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 \
&& sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
mkdir ~/.conan ; cd ~/.conan
curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.2/vcmi-deps-windows-conan-w64.tgz" \
| tar -xzf -
. CI/install_conan_dependencies.sh "dependencies-mingw"

View File

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

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.
* 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
* 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 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.
* 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.
@ -19,17 +35,81 @@
* 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
* 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
* 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.
* 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
* 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 multiple music tracks for towns
* Added support for multiple music tracks for terrains on adventure map
* Fixed several cases where vcmi will report errors in json without specifying filename of invalid file
* 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

View File

@ -102,6 +102,12 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
# define STRONG_INLINE inline
#endif
// Required for building boost::stacktrace on macOS.
// See https://github.com/boostorg/stacktrace/issues/88
#if defined(VCMI_APPLE)
#define _GNU_SOURCE
#endif
#define _USE_MATH_DEFINES
#include <algorithm>
@ -700,6 +706,33 @@ namespace vstd
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>
bool isAlmostZero(const Floating & value)
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,11 @@
"vcmi.lobby.noUnderground" : "sem subterrâneo",
"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
"vcmi.lobby.author" : "Autor",
"vcmi.lobby.handicap" : "Desvant.",
"vcmi.lobby.handicap.resource" : "Fornece aos jogadores recursos apropriados para começar, além dos recursos iniciais normais. Valores negativos são permitidos, mas são limitados a 0 no total (o jogador nunca começa com recursos negativos).",
"vcmi.lobby.handicap.income" : "Altera as várias rendas do jogador em porcentagem. Arredondado para cima.",
"vcmi.lobby.handicap.growth" : "Altera a taxa de produção das criaturas nas cidades possuídas pelo jogador. Arredondado para cima.",
"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
"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.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.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)",
@ -287,17 +324,9 @@
"vcmi.townHall.missingBase" : "A construção base %s deve ser construída primeiro",
"vcmi.townHall.noCreaturesToRecruit" : "Não há criaturas para recrutar!",
"vcmi.townHall.greetingManaVortex" : "Ao se aproximar de %s, seu corpo é preenchido com nova energia. Você dobrou seus pontos de mana normais.",
"vcmi.townHall.greetingKnowledge" : "Estudando os glifos de %s, você adquire uma visão dos segredos sobre o funcionamento de várias magias (+1 de Conhecimento).",
"vcmi.townHall.greetingSpellPower" : "%s ensina novas maneiras de concentrar seus poderes mágicos (+1 de Força).",
"vcmi.townHall.greetingExperience" : "Uma visita em %s ensina muitas habilidades novas (+1000 de Experiência).",
"vcmi.townHall.greetingAttack" : "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Ataque).",
"vcmi.townHall.greetingDefence" : "Ao passar um tempo em %s, os guerreiros experientes lá dentro te ensinam habilidades defensivas adicionais (+1 de Defesa).",
"vcmi.townHall.hasNotProduced" : "%s ainda não produziu nada.",
"vcmi.townHall.hasProduced" : "%s produziu %d %s nesta semana.",
"vcmi.townHall.greetingCustomBonus" : "%s dá +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " até a próxima batalha.",
"vcmi.townHall.greetingInTownMagicWell" : "%s restaurou seus pontos de mana para o máximo.",
"vcmi.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.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.logicalExpressions.anyOf" : "Qualquer um dos seguintes:",
"vcmi.logicalExpressions.allOf" : "Todos os seguintes:",

View File

@ -162,17 +162,6 @@
"vcmi.townHall.missingBase" : "Сначала необходимо построить: %s",
"vcmi.townHall.noCreaturesToRecruit" : "Нет существ для найма!",
"vcmi.townHall.greetingManaVortex" : "Близ %s ваше тело наполняется новой силой. Ваша обычная магическая энергия ныне удвоена.",
"vcmi.townHall.greetingKnowledge" : "Вы изучили знаки %s, на вас снизошло прозрение в деле магии (+1 Знания).",
"vcmi.townHall.greetingSpellPower" : "В %s вас научили новым способам концентрации магической силы (+1 Силы)",
"vcmi.townHall.greetingExperience" : "Посетив %s, вы узнали много нового (+1000 опыта).",
"vcmi.townHall.greetingAttack" : "Пребывание в %s позволило вам лучше использовать боевые навыки (+1 Атаки).",
"vcmi.townHall.greetingDefence" : "В %s искушенные воины преподали вам свои защитные умения (+1 Защиты).",
"vcmi.townHall.hasNotProduced" : "В %s еще ничего не произведено.",
"vcmi.townHall.hasProduced" : "В %s на этой неделе произведено: %d %s",
"vcmi.townHall.greetingCustomBonus" : "%s дает вам +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " до следующей битвы.",
"vcmi.townHall.greetingInTownMagicWell" : "%s восстанавливает ваши очки заклинаний до максимума.",
"vcmi.logicalExpressions.anyOf" : "Любое из:",
"vcmi.logicalExpressions.allOf" : "Все перечисленное:",

View File

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

View File

@ -285,17 +285,6 @@
"vcmi.townHall.missingBase" : "Спочатку необхідно звести початкову будівлю: %s",
"vcmi.townHall.noCreaturesToRecruit" : "Немає істот, яких можна завербувати!",
"vcmi.townHall.greetingManaVortex" : "Неподалік %s ваше тіло наповнюється новою силою. Ваша звична магічна енергія сьогодні подвоєна.",
"vcmi.townHall.greetingKnowledge" : "Ви вивчили знаки на %s, і на вас зійшло прозріння у справах магії. (+1 Knowledge).",
"vcmi.townHall.greetingSpellPower" : "В %s вас навчили новим методам концентрації магічної сили. (+1 Power).",
"vcmi.townHall.greetingExperience" : "Відвідавши %s, ви дізналися багато нового. (+1000 Experience).",
"vcmi.townHall.greetingAttack" : "Перебування у %s дозволило вам краще використовувати бойові навички (+1 Attack Skill).",
"vcmi.townHall.greetingDefence" : "У %s досвідчені воїни виклали вам свої захисні вміння. (+1 Defense).",
"vcmi.townHall.hasNotProduced" : "Поки що %s нічого не створило.",
"vcmi.townHall.hasProduced" : "Цього тижня %s створило %d одиниць, цього разу це %s.",
"vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s",
"vcmi.townHall.greetingCustomUntil" : " до наступної битви.",
"vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.",
"vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:",
"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.noCreaturesToRecruit": "Không có quái để chiêu mộ!",
"vcmi.townHall.greetingManaVortex": "%s giúp cơ thể bạn tràn đầy năng lượng mới. Bạn được gấp đôi năng lượng tối đa.",
"vcmi.townHall.greetingKnowledge": "Bạn học chữ khắc trên %s và thấu hiểu cách vận hành của nhiều ma thuật (+1 Trí).",
"vcmi.townHall.greetingSpellPower": "%s dạy bạn hướng mới tập trung sức mạnh ma thuật (+1 Lực).",
"vcmi.townHall.greetingExperience": "Viếng thăm %s dạy bạn nhiều kĩ năng mới (+1000 Kinh nghiệm).",
"vcmi.townHall.greetingAttack": "Thời gian ở %s giúp bạn học nhiều kĩ năng chiến đấu hiệu quả (+1 Công).",
"vcmi.townHall.greetingDefence": "Thời gian ở %s, các chiến binh lão luyện tại đó dạy bạn nhiều kĩ năng phòng thủ (+1 Thủ).",
"vcmi.townHall.hasNotProduced": "%s chưa tạo được cái gì.",
"vcmi.townHall.hasProduced": "%s tạo %d %s tuần này.",
"vcmi.townHall.greetingCustomBonus": "%s cho bạn +%d %s%s",
"vcmi.townHall.greetingCustomUntil": " đến trận đánh tiếp theo.",
"vcmi.townHall.greetingInTownMagicWell": "%s đã hồi phục năng lượng tối đa của bạn.",
"vcmi.logicalExpressions.anyOf": "Bất kì cái sau:",
"vcmi.logicalExpressions.allOf": "Tất cả cái sau:",

View File

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

View File

@ -70,7 +70,7 @@ public:
const SkillService * skills() const override;
const BattleFieldService * battlefields() const override;
const ObstacleService * obstacles() const override;
const IGameSettings * settings() const override;
const IGameSettings * engineSettings() const override;
const spells::effects::Registry * spellEffects() const 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 *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
/// Defined in clientapp EntryPoint
/// TODO: decide on better location for this method
[[noreturn]] void handleFatalError(const std::string & message, bool terminate);
void handleQuit(bool ask = true);

View File

@ -1,4 +1,4 @@
set(client_SRCS
set(vcmiclientcommon_SRCS
StdInc.cpp
../CCallback.cpp
@ -27,6 +27,7 @@ set(client_SRCS
battle/BattleStacksController.cpp
battle/BattleWindow.cpp
battle/CreatureAnimation.cpp
battle/BattleOverlayLogVisualizer.cpp
eventsSDL/NotificationHandler.cpp
eventsSDL/InputHandler.cpp
@ -64,6 +65,7 @@ set(client_SRCS
mainmenu/CPrologEpilogVideo.cpp
mainmenu/CreditsScreen.cpp
mainmenu/CHighScoreScreen.cpp
mainmenu/CStatisticScreen.cpp
mapView/MapRenderer.cpp
mapView/MapRendererContext.cpp
@ -74,12 +76,14 @@ set(client_SRCS
mapView/MapViewController.cpp
mapView/MapViewModel.cpp
mapView/mapHandler.cpp
mapView/MapOverlayLogVisualizer.cpp
media/CAudioBase.cpp
media/CMusicHandler.cpp
media/CSoundHandler.cpp
media/CVideoHandler.cpp
render/AssetGenerator.cpp
render/CAnimation.cpp
render/CBitmapHandler.cpp
render/CDefFile.cpp
@ -95,6 +99,7 @@ set(client_SRCS
renderSDL/CTrueTypeFont.cpp
renderSDL/CursorHardware.cpp
renderSDL/CursorSoftware.cpp
renderSDL/ImageScaled.cpp
renderSDL/RenderHandler.cpp
renderSDL/SDLImage.cpp
renderSDL/SDLImageLoader.cpp
@ -169,9 +174,10 @@ set(client_SRCS
windows/settings/BattleOptionsTab.cpp
windows/settings/AdventureOptionsTab.cpp
xBRZ/xbrz.cpp
ArtifactsUIController.cpp
CGameInfo.cpp
CMT.cpp
CPlayerInterface.cpp
PlayerLocalState.cpp
CServerHandler.cpp
@ -184,7 +190,7 @@ set(client_SRCS
ServerRunner.cpp
)
set(client_HEADERS
set(vcmiclientcommon_HEADERS
StdInc.h
adventureMap/AdventureMapInterface.h
@ -214,6 +220,7 @@ set(client_HEADERS
battle/BattleStacksController.h
battle/BattleWindow.h
battle/CreatureAnimation.h
battle/BattleOverlayLogVisualizer.h
eventsSDL/NotificationHandler.h
eventsSDL/InputHandler.h
@ -254,6 +261,7 @@ set(client_HEADERS
mainmenu/CPrologEpilogVideo.h
mainmenu/CreditsScreen.h
mainmenu/CHighScoreScreen.h
mainmenu/CStatisticScreen.h
mapView/IMapRendererContext.h
mapView/IMapRendererObserver.h
@ -266,6 +274,7 @@ set(client_HEADERS
mapView/MapViewController.h
mapView/MapViewModel.h
mapView/mapHandler.h
mapView/MapOverlayLogVisualizer.h
media/CAudioBase.h
media/CEmptyVideoPlayer.h
@ -276,6 +285,7 @@ set(client_HEADERS
media/ISoundPlayer.h
media/IVideoPlayer.h
render/AssetGenerator.h
render/CAnimation.h
render/CBitmapHandler.h
render/CDefFile.h
@ -297,6 +307,7 @@ set(client_HEADERS
renderSDL/CTrueTypeFont.h
renderSDL/CursorHardware.h
renderSDL/CursorSoftware.h
renderSDL/ImageScaled.h
renderSDL/RenderHandler.h
renderSDL/SDLImage.h
renderSDL/SDLImageLoader.h
@ -374,6 +385,9 @@ set(client_HEADERS
windows/settings/BattleOptionsTab.h
windows/settings/AdventureOptionsTab.h
xBRZ/xbrz.h
xBRZ/xbrz_tools.h
ArtifactsUIController.h
CGameInfo.h
CMT.h
@ -392,76 +406,50 @@ set(client_HEADERS
)
if(APPLE_IOS)
set(client_SRCS ${client_SRCS}
CFocusableHelper.cpp
ios/GameChatKeyboardHandler.m
ios/main.m
ios/startSDL.mm
set(vcmiclientcommon_SRCS ${vcmiclientcommon_SRCS}
ios/utils.mm
)
set(client_HEADERS ${client_HEADERS}
CFocusableHelper.h
ios/GameChatKeyboardHandler.h
ios/startSDL.h
set(vcmiclientcommon_HEADERS ${vcmiclientcommon_HEADERS}
ios/utils.h
)
endif()
assign_source_group(${client_SRCS} ${client_HEADERS} VCMI_client.rc)
if(ANDROID)
add_library(vcmiclient SHARED ${client_SRCS} ${client_HEADERS})
set_target_properties(vcmiclient PROPERTIES
OUTPUT_NAME "vcmiclient_${ANDROID_ABI}" # required by Qt
)
else()
add_executable(vcmiclient ${client_SRCS} ${client_HEADERS})
endif()
assign_source_group(${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
add_library(vcmiclientcommon STATIC ${vcmiclientcommon_SRCS} ${vcmiclientcommon_HEADERS})
if(NOT ENABLE_STATIC_LIBS)
add_dependencies(vcmiclient
add_dependencies(vcmiclientcommon
BattleAI
EmptyAI
StupidAI
VCAI
)
if(ENABLE_NULLKILLER_AI)
add_dependencies(vcmiclient Nullkiller)
add_dependencies(vcmiclientcommon Nullkiller)
endif()
endif()
if(APPLE_IOS)
if(ENABLE_ERM)
add_dependencies(vcmiclient vcmiERM)
add_dependencies(vcmiclientcommon vcmiERM)
endif()
if(ENABLE_LUA)
add_dependencies(vcmiclient vcmiLua)
add_dependencies(vcmiclientcommon vcmiLua)
endif()
endif()
if(WIN32)
target_sources(vcmiclient PRIVATE "VCMI_client.rc")
set_target_properties(vcmiclient
set_target_properties(vcmiclientcommon
PROPERTIES
OUTPUT_NAME "VCMI_client"
PROJECT_LABEL "VCMI_client"
OUTPUT_NAME "VCMI_vcmiclientcommon"
PROJECT_LABEL "VCMI_vcmiclientcommon"
)
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclient)
set_property(DIRECTORY ${CMAKE_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vcmiclientcommon)
if(NOT ENABLE_DEBUG_CONSOLE)
set_target_properties(vcmiclient PROPERTIES WIN32_EXECUTABLE)
target_link_libraries(vcmiclient SDL2::SDL2main)
endif()
target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
# TODO: very hacky, find proper solution to copy AI dlls into bin dir
if(MSVC)
add_custom_command(TARGET vcmiclient POST_BUILD
WORKING_DIRECTORY "$<TARGET_FILE_DIR:vcmiclient>"
COMMAND ${CMAKE_COMMAND} -E copy AI/fuzzylite.dll fuzzylite.dll
COMMAND ${CMAKE_COMMAND} -E copy AI/tbb12.dll tbb12.dll
)
target_link_libraries(vcmiclientcommon SDL2::SDL2main)
endif()
target_compile_definitions(vcmiclientcommon PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
elseif(APPLE_IOS)
target_link_libraries(vcmiclient PRIVATE
target_link_libraries(vcmiclientcommon PRIVATE
iOS_utils
# FFmpeg
@ -473,101 +461,31 @@ elseif(APPLE_IOS)
"-framework CoreMedia"
"-framework VideoToolbox"
)
set_target_properties(vcmiclient PROPERTIES
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/ios/Info.plist"
XCODE_ATTRIBUTE_LD_RUNPATH_SEARCH_PATHS "@executable_path/Frameworks"
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "$(CODE_SIGNING_ALLOWED_FOR_APPS)"
XCODE_ATTRIBUTE_ASSETCATALOG_COMPILER_APPICON_NAME AppIcon
)
foreach(XCODE_RESOURCE LaunchScreen.storyboard Images.xcassets Settings.bundle vcmi_logo.png)
set(XCODE_RESOURCE_PATH ios/${XCODE_RESOURCE})
target_sources(vcmiclient PRIVATE ${XCODE_RESOURCE_PATH})
set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
# workaround to prevent CMAKE_SKIP_PRECOMPILE_HEADERS being added as compile flag
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.22.0" AND CMAKE_VERSION VERSION_LESS "3.25.0")
set_source_files_properties(${XCODE_RESOURCE_PATH} PROPERTIES LANGUAGE CXX)
endif()
endforeach()
set(CMAKE_EXE_LINKER_FLAGS "-Wl,-e,_client_main")
endif()
target_link_libraries(vcmiclient PRIVATE vcmiservercommon)
if(ENABLE_SINGLE_APP_BUILD AND ENABLE_LAUNCHER)
target_link_libraries(vcmiclient PRIVATE vcmilauncher)
endif()
target_link_libraries(vcmiclientcommon PRIVATE vcmiservercommon)
target_link_libraries(vcmiclient PRIVATE
target_link_libraries(vcmiclientcommon PUBLIC
vcmi SDL2::SDL2 SDL2::Image SDL2::Mixer SDL2::TTF
)
if(ffmpeg_LIBRARIES)
target_link_libraries(vcmiclient PRIVATE
target_link_libraries(vcmiclientcommon PRIVATE
${ffmpeg_LIBRARIES}
)
else()
target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO)
target_compile_definitions(vcmiclientcommon PRIVATE DISABLE_VIDEO)
endif()
target_include_directories(vcmiclient PUBLIC
target_include_directories(vcmiclientcommon PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
if (ffmpeg_INCLUDE_DIRS)
target_include_directories(vcmiclient PRIVATE
target_include_directories(vcmiclientcommon PRIVATE
${ffmpeg_INCLUDE_DIRS}
)
endif()
vcmi_set_output_dir(vcmiclient "")
enable_pch(vcmiclient)
if(APPLE_IOS)
vcmi_install_conan_deps("\${CMAKE_INSTALL_PREFIX}")
add_custom_command(TARGET vcmiclient POST_BUILD
COMMAND ios/set_build_version.sh "$<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()
vcmi_set_output_dir(vcmiclientcommon "")
enable_pch(vcmiclientcommon)

View File

@ -13,7 +13,6 @@
#include <vcmi/Artifact.h>
#include "CGameInfo.h"
#include "CMT.h"
#include "CServerHandler.h"
#include "HeroMovementController.h"
#include "PlayerLocalState.h"
@ -100,9 +99,8 @@
#include "../lib/pathfinder/CGPathNode.h"
#include "../lib/serializer/BinaryDeserializer.h"
#include "../lib/serializer/BinarySerializer.h"
#include "../lib/serializer/CTypeList.h"
#include "../lib/serializer/ESerializationVersion.h"
#include "../lib/spells/CSpellHandler.h"
@ -137,7 +135,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player):
{
logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString());
GH.defActionsDef = 0;
LOCPLINT = this;
playerID=Player;
human=true;
@ -171,13 +168,8 @@ void CPlayerInterface::initGameInterface(std::shared_ptr<Environment> ENV, std::
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
for (;;)
{
@ -200,6 +192,15 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player)
castleInt->close();
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
vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
@ -286,6 +287,7 @@ void CPlayerInterface::gamePause(bool pause)
void CPlayerInterface::yourTurn(QueryID queryID)
{
closeAllDialogs();
CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP);
EVENT_HANDLER_CALLED_BY_CLIENT;
@ -620,12 +622,10 @@ void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreat
{
movementController->onBattleStarted();
//Don't wait for dialogs when we are non-active hot-seat player
if (LOCPLINT == this)
waitForAllDialogs();
}
void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
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;
@ -645,8 +645,6 @@ void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet
cb->registerBattleInterface(autofightingAI);
}
//Don't wait for dialogs when we are non-active hot-seat player
if (LOCPLINT == this)
waitForAllDialogs();
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);
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));
showingDialog->setBusy();
@ -1146,7 +1144,7 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component
if(t)
{
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);
}
}
@ -1477,7 +1475,7 @@ void CPlayerInterface::update()
return;
//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();
GH.windows().pushWindow(dialogs.front());
@ -1633,30 +1631,30 @@ void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID)
battleInt->newRoundFirst();
}
void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
void CPlayerInterface::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
{
EVENT_HANDLER_CALLED_BY_CLIENT;
auto onWindowClosed = [this, queryID](){
cb->selectionMade(0, queryID);
};
if (market->allowsTrade(EMarketMode::ARTIFACT_EXP) && dynamic_cast<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)
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP);
else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed);
else if(!market->availableModes().empty())
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, market->availableModes().front());
else if (!market->availableModes().empty())
for(auto mode = EMarketMode::RESOURCE_RESOURCE; mode != EMarketMode::MARKET_AFTER_LAST_PLACEHOLDER; mode = vstd::next(mode, 1))
{
if(vstd::contains(market->availableModes(), mode))
{
GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, mode);
break;
}
}
else
onWindowClosed();
}
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](){
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)
@ -1761,6 +1759,9 @@ void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
void CPlayerInterface::waitForAllDialogs()
{
if (!makingTurn)
return;
while(!dialogs.empty())
{
auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex);
@ -1776,7 +1777,6 @@ void CPlayerInterface::proposeLoadingGame()
[]()
{
CSH->endGameplay();
GH.defActionsDef = 63;
CMM->menu->switchToTab("load");
},
nullptr

View File

@ -120,7 +120,7 @@ protected: // Call-ins from server, should not be called directly, but only via
void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override;
void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override;
void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell
@ -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 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 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 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
@ -197,6 +197,7 @@ public: // public interface for use by client via LOCPLINT access
void performAutosave();
void gamePause(bool pause);
void endNetwork();
void closeAllDialogs();
///returns true if all events are processed internally
bool capturedAllEvents();

View File

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

View File

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

View File

@ -29,13 +29,10 @@
#include "../lib/VCMIDirs.h"
#include "../lib/UnlockGuard.h"
#include "../lib/battle/BattleInfo.h"
#include "../lib/serializer/BinaryDeserializer.h"
#include "../lib/serializer/BinarySerializer.h"
#include "../lib/serializer/Connection.h"
#include "../lib/mapping/CMapService.h"
#include "../lib/pathfinder/CGPathNode.h"
#include "../lib/filesystem/Filesystem.h"
#include "../lib/registerTypes/RegisterTypesClientPacks.h"
#include <memory>
#include <vcmi/events/EventBus.h>
@ -50,53 +47,6 @@
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_)
: player(player_),
cl(cl_),
@ -130,12 +80,9 @@ const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const
return mainCallback.get();
}
CClient::CClient()
{
waitingRequest.clear();
applier = std::make_shared<CApplier<CBaseForCLApply>>();
registerTypesClientPacks(*applier);
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
if(apply)
{
apply->applyOnClBefore(this, pack);
ApplyClientNetPackVisitor afterVisitor(*this, *gameState());
ApplyFirstClientNetPackVisitor beforeVisitor(*this, *gameState());
pack->visit(beforeVisitor);
logNetwork->trace("\tMade first apply on cl: %s", typeid(*pack).name());
{
boost::unique_lock lock(CGameState::mutex);
gs->apply(pack);
}
logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name());
apply->applyOnClAfter(this, pack);
pack->visit(afterVisitor);
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;
}
@ -443,8 +386,8 @@ void CClient::battleStarted(const BattleInfo * info)
{
std::shared_ptr<CPlayerInterface> att;
std::shared_ptr<CPlayerInterface> def;
auto & leftSide = info->sides[0];
auto & rightSide = info->sides[1];
const auto & leftSide = info->getSide(BattleSide::LEFT_SIDE);
const auto & rightSide = info->getSide(BattleSide::RIGHT_SIDE);
for(auto & battleCb : battleCallbacks)
{
@ -453,17 +396,17 @@ void CClient::battleStarted(const BattleInfo * info)
}
//If quick combat is not, do not prepare interfaces for battleint
auto callBattleStart = [&](PlayerColor color, ui8 side)
auto callBattleStart = [&](PlayerColor color, BattleSide side)
{
if(vstd::contains(battleints, color))
battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
};
callBattleStart(leftSide.color, 0);
callBattleStart(rightSide.color, 1);
callBattleStart(PlayerColor::UNFLAGGABLE, 1);
callBattleStart(leftSide.color, BattleSide::LEFT_SIDE);
callBattleStart(rightSide.color, BattleSide::RIGHT_SIDE);
callBattleStart(PlayerColor::UNFLAGGABLE, BattleSide::RIGHT_SIDE);
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
callBattleStart(PlayerColor::SPECTATOR, 1);
callBattleStart(PlayerColor::SPECTATOR, BattleSide::RIGHT_SIDE);
if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human)
att = std::dynamic_pointer_cast<CPlayerInterface>(playerint[leftSide.color]);
@ -480,9 +423,9 @@ void CClient::battleStarted(const BattleInfo * info)
{
auto side = interface->cb->getBattle(info->battleID)->playerToSide(interface->playerID);
if(interface->playerID == info->sides[info->tacticsSide].color)
if(interface->playerID == info->getSide(info->tacticsSide).color)
{
auto action = BattleAction::makeEndOFTacticPhase(*side);
auto action = BattleAction::makeEndOFTacticPhase(side);
interface->cb->battleMakeTacticAction(info->battleID, action);
}
}
@ -514,7 +457,7 @@ void CClient::battleStarted(const BattleInfo * info)
if(info->tacticDistance)
{
auto tacticianColor = info->sides[info->tacticsSide].color;
auto tacticianColor = info->getSide(info->tacticsSide).color;
if (vstd::contains(battleints, tacticianColor))
battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance);
@ -523,9 +466,11 @@ void CClient::battleStarted(const BattleInfo * info)
void CClient::battleFinished(const BattleID & battleID)
{
for(auto & side : gs->getBattle(battleID)->sides)
if(battleCallbacks.count(side.color))
battleCallbacks[side.color]->onBattleEnded(battleID);
for(auto side : { BattleSide::ATTACKER, BattleSide::DEFENDER })
{
if(battleCallbacks.count(gs->getBattle(battleID)->getSide(side).color))
battleCallbacks[gs->getBattle(battleID)->getSide(side).color]->onBattleEnded(battleID);
}
if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool())
battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID);

View File

@ -21,14 +21,10 @@ struct CPackForServer;
class IBattleEventsReceiver;
class CBattleGameInterface;
class CGameInterface;
class BinaryDeserializer;
class BinarySerializer;
class BattleAction;
class BattleInfo;
struct BankConfig;
template<typename T> class CApplier;
#if SCRIPTING_ENABLED
namespace scripting
{
@ -147,7 +143,7 @@ public:
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
void battleStarted(const BattleInfo * info);
@ -168,7 +164,7 @@ public:
void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {};
void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {};
void showBlockingDialog(BlockingDialog * iw) override {};
void showBlockingDialog(const IObjectInterface * caller, BlockingDialog * iw) override {};
void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {};
void showTeleportDialog(TeleportDialog * iw) override {};
void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {};
@ -188,7 +184,8 @@ public:
void removeAfterVisit(const CGObjectInstance * object) override {};
bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;};
bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;}
bool 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;};
void removeArtifact(const ArtifactLocation & al) override {};
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 visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {};
void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero
void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used
void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle
void startBattle(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, const BattleLayout & layout, const CGTownInstance * town) override {}; //use hero=nullptr for no hero
void startBattle(const CArmedInstance * army1, const CArmedInstance * army2) override {}; //if any of armies is hero, hero will be used
bool moveHero(ObjectInstanceID hid, int3 dst, EMovementMode movementMode, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;};
void giveHeroBonus(GiveBonus * bonus) override {};
void setMovePoints(SetMovePoints * smp) override {};
@ -211,7 +207,7 @@ public:
void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {}
void changeFogOfWar(std::unordered_set<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 setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {};
@ -237,8 +233,6 @@ private:
#endif
std::unique_ptr<events::EventBus> clientEventBus;
std::shared_ptr<CApplier<CBaseForCLApply>> applier;
mutable boost::mutex pathCacheMutex;
std::map<const CGHeroInstance *, std::shared_ptr<CPathsInfo>> pathCache;

View File

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

View File

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

View File

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

View File

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

View File

@ -31,10 +31,13 @@
#include "gui/WindowHandler.h"
#include "widgets/Buttons.h"
#include "widgets/TextControls.h"
#include "media/CMusicHandler.h"
#include "media/IVideoPlayer.h"
#include "../lib/CConfigHandler.h"
#include "../lib/texts/CGeneralTextHandler.h"
#include "../lib/serializer/Connection.h"
#include "../lib/campaign/CampaignState.h"
void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack)
{
@ -202,8 +205,19 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
if(!lobby->bonusSel && handler.si->campState && handler.getState() == EClientState::LOBBY_CAMPAIGN)
{
lobby->bonusSel = std::make_shared<CBonusSelection>();
GH.windows().pushWindow(lobby->bonusSel);
auto bonusSel = std::make_shared<CBonusSelection>();
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@
AdventureOptions::AdventureOptions()
: 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->addCallback([] { LOCPLINT->viewWorldMap(); });

View File

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

View File

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

View File

@ -18,26 +18,29 @@
#include "../widgets/ObjectLists.h"
#include "../widgets/RadialMenu.h"
#include "../windows/InfoWindows.h"
#include "../windows/CCastleInterface.h"
#include "../CGameInfo.h"
#include "../CPlayerInterface.h"
#include "../PlayerLocalState.h"
#include "../gui/CGuiHandler.h"
#include "../gui/Shortcut.h"
#include "../gui/WindowHandler.h"
#include "../render/Canvas.h"
#include "../render/Colors.h"
#include "../../lib/texts/CGeneralTextHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../CCallback.h"
CList::CListItem::CListItem(CList * Parent)
: CIntObject(LCLICK | SHOW_POPUP | HOVER),
parent(Parent),
selection()
{
defActions = 255-DISPOSE;
}
CList::CListItem::~CListItem() = default;
@ -71,7 +74,7 @@ void CList::CListItem::hover(bool on)
void CList::CListItem::onSelect(bool on)
{
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
selection.reset();
if(on)
selection = genSelection();
@ -96,7 +99,7 @@ void CList::showAll(Canvas & to)
void CList::createList(Point firstItemPosition, Point itemPositionDelta, size_t listAmount)
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
listBox = std::make_shared<CListBox>(std::bind(&CList::createItem, this, _1), firstItemPosition, itemPositionDelta, size, listAmount);
}
@ -207,7 +210,7 @@ void CList::selectPrev()
CHeroList::CEmptyHeroItem::CEmptyHeroItem()
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
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);
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),
hero(Hero)
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
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);
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();
addUsedEvents(GESTURE);
addUsedEvents(GESTURE | KEYBOARD);
}
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);
}
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)
{
if (LOCPLINT->localState->getWanderingHeroes().size() > index)
@ -365,12 +417,12 @@ CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
CListItem(parent),
town(Town)
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0);
pos = picture->pos;
update();
addUsedEvents(GESTURE);
addUsedEvents(GESTURE | KEYBOARD);
}
std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
@ -380,7 +432,7 @@ std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
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);
redraw();
@ -419,24 +471,77 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1;
int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1;
auto updateList = [](){
for (auto ki : GH.windows().findWindows<CCastleInterface>())
ki->townChange(); //update list
};
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--)
LOCPLINT->localState->swapOwnedTowns(i, i - 1);
updateList();
} },
{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } },
{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } },
{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [townIndex, towns]()
{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [updateList, townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); updateList(); } },
{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [updateList, townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); updateList(); } },
{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [updateList, townIndex, towns]()
{
for (int i = townIndex; i < towns.size() - 1; i++)
LOCPLINT->localState->swapOwnedTowns(i, i + 1);
updateList();
} },
};
GH.windows().createAndPushWindow<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()
{
return town->getObjectName();

View File

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

View File

@ -73,7 +73,7 @@ void CMinimapInstance::redrawMinimap()
CMinimapInstance::CMinimapInstance(CMinimap *Parent, int Level):
parent(Parent),
minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y))),
minimap(new Canvas(Point(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y), CanvasScalingPolicy::IGNORE)),
level(Level)
{
pos.w = parent->pos.w;
@ -90,7 +90,7 @@ CMinimap::CMinimap(const Rect & position)
: CIntObject(LCLICK | SHOW_POPUP | DRAG | MOVE | GESTURE, position.topLeft()),
level(0)
{
OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
double maxSideLengthSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y);
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
return;
OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
OBJECT_CONSTRUCTION;
minimap = std::make_shared<CMinimapInstance>(this, level);
redraw();
}

View File

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

View File

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

View File

@ -312,8 +312,8 @@ void BattleActionsController::castThisSpell(SpellID spellID)
heroSpellToCast = std::make_shared<BattleAction>();
heroSpellToCast->actionType = EActionType::HERO_SPELL;
heroSpellToCast->spell = spellID;
heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2;
heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
heroSpellToCast->stackNumber = -1;
heroSpellToCast->side = owner.curInt->cb->getBattle(owner.getBattleID())->battleGetMySide();
//choosing possible targets
const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance;
@ -499,9 +499,12 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
case PossiblePlayerBattleAction::WALK_AND_ATTACK:
case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
{
const auto * attacker = owner.stacksController->getActiveStack();
BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex);
int distance = attacker->position.isValid() ? owner.getBattle()->battleGetDistances(attacker, attacker->getPosition())[attackFromHex] : 0;
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.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
bool enemyMayBeKilled = estimation.kills.max == targetStack->getCount();
@ -514,7 +517,8 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
const auto * shooter = owner.stacksController->getActiveStack();
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.min = std::min<int64_t>(estimation.kills.min, targetStack->getCount());
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
{
if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS)
return owner.siegeController->getTurretCreature();
return owner.siegeController->getTurretCreature(attackingStack->initialPosition);
else
return attackingStack->unitType();
}

View File

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

View File

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

View File

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

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