1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-06-25 00:37:24 +02:00

Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
Xilmi
2024-08-24 12:25:03 +02:00
62 changed files with 1594 additions and 355 deletions

View File

@ -201,6 +201,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting) if(attackInfo.shooting)
return 0; return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker; auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex); auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
@ -208,9 +210,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true); auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker)) if(!st || !state->battleMatchOwner(st, attacker))
continue; continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st)) if(!state->battleCanShoot(st))
continue; continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true); BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex; rangeAttackInfo.defenderPos = hex;
@ -220,9 +226,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain; res += gain * cachedDmg / std::max<uint64_t>(1, averageDmg(rangeDmg.damage));
} }
return res; return res;

View File

@ -23,6 +23,7 @@
#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/StartInfo.h"
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used, // Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely // CUnitState should be private and CStack should be removed completely
@ -122,6 +123,11 @@ static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, BattleSid
return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy; return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
} }
int getSimulationTurnsCount(const StartInfo * startInfo)
{
return startInfo->difficulty < 4 ? 2 : 10;
}
void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{ {
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
@ -154,7 +160,10 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
logAi->trace("Build evaluator and targets"); logAi->trace("Build evaluator and targets");
#endif #endif
BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); BattleEvaluator evaluator(
env, cb, stack, playerID, battleID, side,
getStrengthRatio(cb->getBattle(battleID), side),
getSimulationTurnsCount(env->game()->getStartInfo()));
result = evaluator.selectStackAction(stack); result = evaluator.selectStackAction(stack);

View File

@ -49,6 +49,45 @@ SpellTypes spellType(const CSpell * spell)
return SpellTypes::OTHER; return SpellTypes::OTHER;
} }
BattleEvaluator::BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
BattleSide side,
float strengthRatio,
int simulationTurnsCount)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount),
cachedAttack(), playerID(playerID), side(side), env(env),
cb(cb), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
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);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
{ {
std::vector<BattleHex> result; std::vector<BattleHex> result;
@ -167,7 +206,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score score
); );
if (moveTarget.scorePerTurn <= score) if (moveTarget.score <= score)
{ {
if(evaluationResult.wait) if(evaluationResult.wait)
{ {
@ -197,7 +236,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
} }
//ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code. //ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
if(moveTarget.scorePerTurn > score) if(moveTarget.score > score)
{ {
score = moveTarget.score; score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack; cachedAttack = moveTarget.cachedAttack;
@ -206,14 +245,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->waited()) if(stack->waited())
{ {
logAi->debug( logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f", "Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(), stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(), moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex, moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score, moveTarget.score);
moveTarget.scorePerTurn);
return goTowardsNearest(stack, moveTarget.positions); return goTowardsNearest(stack, moveTarget.positions, *targets);
} }
else else
{ {
@ -235,7 +273,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else else
return goTowardsNearest(stack, brokenWallMoat); return goTowardsNearest(stack, brokenWallMoat, *targets);
} }
} }
@ -249,7 +287,32 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
} }
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets)
{
auto additionalScore = 0;
std::optional<AttackPossibility> attackOnTheWay;
for(auto & target : targets.possibleAttacks)
{
if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore)
{
additionalScore = target.attackValue();
attackOnTheWay = target;
}
}
if(attackOnTheWay)
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from);
}
else
{
return BattleAction::makeMove(stack, hex);
}
}
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets)
{ {
auto reachability = cb->getBattle(battleID)->getReachability(stack); auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
@ -261,49 +324,38 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
std::vector<BattleHex> targetHexes = hexes; std::vector<BattleHex> targetHexes = hexes;
for(int i = 0; i < 5; i++) vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
{
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{ {
return reachability.distances[h1] < reachability.distances[h2]; return reachability.distances[h1] < reachability.distances[h2];
}); });
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
logAi->trace("No richable hexes.");
return BattleAction::makeDefend(stack);
}
// this turn
for(auto hex : targetHexes) for(auto hex : targetHexes)
{ {
if(vstd::contains(avHexes, hex)) if(vstd::contains(avHexes, hex))
{ {
return BattleAction::makeMove(stack, hex); return moveOrAttack(stack, hex, targets);
} }
if(stack->coversPos(hex)) if(stack->coversPos(hex))
{ {
logAi->warn("Warning: already standing on neighbouring tile!"); logAi->warn("Warning: already standing on neighbouring hex!");
//We shouldn't even be here... //We shouldn't even be here...
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);
} }
} }
if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE) // not this turn
{
break;
}
std::vector<BattleHex> copy = targetHexes;
for(auto hex : copy)
vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
vstd::erase_if(targetHexes, [](const BattleHex & hex) {return !hex.isValid();});
vstd::removeDuplicates(targetHexes);
}
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
return BattleAction::makeDefend(stack);
}
scoreEvaluator.updateReachabilityMap(hb); scoreEvaluator.updateReachabilityMap(hb);
if(stack->hasBonusOfType(BonusType::FLYING)) if(stack->hasBonusOfType(BonusType::FLYING))
@ -343,7 +395,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
}); });
return BattleAction::makeMove(stack, *nearestAvailableHex); return moveOrAttack(stack, *nearestAvailableHex, targets);
} }
else else
{ {
@ -357,11 +409,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
if(vstd::contains(avHexes, currentDest) if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest); {
return moveOrAttack(stack, currentDest, targets);
}
currentDest = reachability.predecessors[currentDest]; currentDest = reachability.predecessors[currentDest];
} }
} }
logAi->error("We should either detect that hexes are unreachable or make a move!");
return BattleAction::makeDefend(stack);
} }
bool BattleEvaluator::canCastSpell() bool BattleEvaluator::canCastSpell()
@ -597,7 +654,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
#endif #endif
PotentialTargets innerTargets(activeStack, innerCache, state); PotentialTargets innerTargets(activeStack, innerCache, state);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio); BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
if(!innerTargets.possibleAttacks.empty()) if(!innerTargets.possibleAttacks.empty())
{ {

View File

@ -37,16 +37,18 @@ class BattleEvaluator
float cachedScore; float cachedScore;
DamageCache damageCache; DamageCache damageCache;
float strengthRatio; float strengthRatio;
int simulationTurnsCount;
public: public:
BattleAction selectStackAction(const CStack * stack); BattleAction selectStackAction(const CStack * stack);
bool attemptCastingSpell(const CStack * stack); bool attemptCastingSpell(const CStack * stack);
bool canCastSpell(); bool canCastSpell();
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack); std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes); BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
std::vector<BattleHex> getBrokenWallMoatHexes() const; std::vector<BattleHex> getBrokenWallMoatHexes() const;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void print(const std::string & text) const; void print(const std::string & text) const;
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
BattleEvaluator( BattleEvaluator(
std::shared_ptr<Environment> env, std::shared_ptr<Environment> env,
@ -55,15 +57,8 @@ public:
PlayerColor playerID, PlayerColor playerID,
BattleID battleID, BattleID battleID,
BattleSide side, BattleSide side,
float strengthRatio) float strengthRatio,
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID) int simulationTurnsCount);
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
BattleEvaluator( BattleEvaluator(
std::shared_ptr<Environment> env, std::shared_ptr<Environment> env,
@ -74,10 +69,6 @@ public:
PlayerColor playerID, PlayerColor playerID,
BattleID battleID, BattleID battleID,
BattleSide side, BattleSide side,
float strengthRatio) float strengthRatio,
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID) int simulationTurnsCount);
{
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
}; };

View File

@ -18,7 +18,7 @@ AttackerValue::AttackerValue()
} }
MoveTarget::MoveTarget() MoveTarget::MoveTarget()
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
{ {
turnsToRich = 1; turnsToRich = 1;
} }
@ -42,7 +42,7 @@ float BattleExchangeVariant::trackAttack(
for(auto affectedUnit : affectedUnits) for(auto affectedUnit : affectedUnits)
{ {
auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
auto damageDealt = unitToUpdate->getTotalHealth() - affectedUnit->getTotalHealth(); auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth();
if(damageDealt > 0) if(damageDealt > 0)
{ {
@ -58,7 +58,7 @@ float BattleExchangeVariant::trackAttack(
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap retaliation, %s, dps: %lld", "%s -> %s, ap retaliation, %s, dps: %lld",
ap.attack.defender->getDescription(), hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
ap.attack.attacker->getDescription(), ap.attack.attacker->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
damageDealt); damageDealt);
@ -277,6 +277,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
return result; return result;
} }
ReachabilityInfo getReachabilityWithEnemyBypass(
const battle::Unit * activeStack,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> state)
{
ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition());
if(!params.flying)
{
for(const auto * unit : state->battleAliveUnits())
{
if(unit->unitSide() == activeStack->unitSide())
continue;
auto dmg = damageCache.getOriginalDamage(activeStack, unit, state);
auto turnsToKill = unit->getAvailableHealth() / dmg;
vstd::amin(turnsToKill, 100);
for(auto & hex : unit->getHexes())
if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns
params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange();
}
params.bypassEnemyStacks = true;
}
return state->getReachability(params);
}
MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
@ -286,6 +316,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
MoveTarget result; MoveTarget result;
BattleExchangeVariant ev; BattleExchangeVariant ev;
logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size());
if(targets.unreachableEnemies.empty()) if(targets.unreachableEnemies.empty())
return result; return result;
@ -296,17 +328,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
updateReachabilityMap(hb); updateReachabilityMap(hb);
auto dists = cb->getReachability(activeStack); auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb);
auto flying = activeStack->hasBonusOfType(BonusType::FLYING);
for(const battle::Unit * enemy : targets.unreachableEnemies) for(const battle::Unit * enemy : targets.unreachableEnemies)
{ {
std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy); logAi->trace(
auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t "Checking movement towards %d of %s",
{ enemy->getCount(),
return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); enemy->creatureId().toCreature()->getNameSingularTranslated());
});
auto distance = dists.distToNearestNeighbour(activeStack, closestStack); auto distance = dists.distToNearestNeighbour(activeStack, enemy);
if(distance >= GameConstants::BFIELD_SIZE) if(distance >= GameConstants::BFIELD_SIZE)
continue; continue;
@ -315,30 +347,84 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
continue; continue;
auto turnsToRich = (distance - 1) / speed + 1; auto turnsToRich = (distance - 1) / speed + 1;
auto hexes = closestStack->getSurroundingHexes(); auto hexes = enemy->getSurroundingHexes();
auto enemySpeed = closestStack->getMovementRange(); auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed); auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = speedRatio > 1 ? 1 : speedRatio; auto multiplier = speedRatio > 1 ? 1 : speedRatio;
if(enemy->canShoot()) for(auto & hex : hexes)
multiplier *= 1.5f;
for(auto hex : hexes)
{ {
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack));
auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce);
if(result.scorePerTurn < scoreValue(scorePerTurn)) score.enemyDamageReduce *= multiplier;
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
#endif
if(result.score < scoreValue(score)
|| (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
{ {
result.scorePerTurn = scoreValue(scorePerTurn);
result.score = scoreValue(score); result.score = scoreValue(score);
result.positions = closestStack->getAttackableHexes(activeStack); result.positions.clear();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score");
#endif
for(BattleHex enemyHex : enemy->getAttackableHexes(activeStack))
{
while(!flying && dists.distances[enemyHex] > speed)
{
enemyHex = dists.predecessors.at(enemyHex);
if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
{
auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
if(defenderToBypass)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Found target to bypass at %d", enemyHex.hex);
#endif
auto attackHex = dists.predecessors[enemyHex];
auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
auto adjacentStacks = getAdjacentUnits(enemy);
adjacentStacks.push_back(defenderToBypass);
vstd::removeDuplicates(adjacentStacks);
auto bypassScore = calculateExchange(
attackBypass,
dists.distances[attackHex],
targets,
damageCache,
hb,
adjacentStacks);
if(scoreValue(bypassScore) > result.score)
{
result.score = scoreValue(bypassScore);
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score after bypass %f", scoreValue(bypassScore));
#endif
}
}
}
}
result.positions.push_back(enemyHex);
}
result.cachedAttack = attack; result.cachedAttack = attack;
result.turnsToRich = turnsToRich; result.turnsToRich = turnsToRich;
} }
@ -382,7 +468,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap, const AttackPossibility & ap,
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{ {
ReachabilityData result; ReachabilityData result;
@ -390,13 +477,26 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
if(!ap.attack.shooting) hexes.push_back(ap.from); if(!ap.attack.shooting) hexes.push_back(ap.from);
std::vector<const battle::Unit *> allReachableUnits; std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
for(auto hex : hexes) for(auto hex : hexes)
{ {
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
} }
for(auto hex : ap.attack.attacker->getHexes())
{
auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
for(auto unit : unitsReachingAttacker)
{
if(unit->unitSide() != ap.attack.attacker->unitSide())
{
allReachableUnits.push_back(unit);
result.enemyUnitsReachingAttacker.insert(unit->unitId());
}
}
}
vstd::removeDuplicates(allReachableUnits); vstd::removeDuplicates(allReachableUnits);
auto copy = allReachableUnits; auto copy = allReachableUnits;
@ -432,7 +532,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : allReachableUnits) for(auto unit : allReachableUnits)
{ {
auto accessible = !unit->canShoot(); auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
if(!accessible) if(!accessible)
{ {
@ -456,14 +556,14 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : turnOrder[turn]) for(auto unit : turnOrder[turn])
{ {
if(vstd::contains(allReachableUnits, unit)) if(vstd::contains(allReachableUnits, unit))
result.units.push_back(unit); result.units[turn].push_back(unit);
}
} }
vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool
{ {
return !hb->battleGetUnitByID(u->unitId())->alive(); return !hb->battleGetUnitByID(u->unitId())->alive();
}); });
}
return result; return result;
} }
@ -494,7 +594,8 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) const std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{ {
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
@ -513,7 +614,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender); enemyStacks.push_back(ap.attack.defender);
ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits);
if(exchangeUnits.units.empty()) if(exchangeUnits.units.empty())
{ {
@ -523,7 +624,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb); auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v; BattleExchangeVariant v;
for(auto unit : exchangeUnits.units) for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
{
for(auto unit : exchangeUnits.units.at(exchangeTurn))
{ {
if(unit->isTurret()) if(unit->isTurret())
continue; continue;
@ -541,6 +644,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
#endif #endif
} }
} }
}
auto melleeAttackers = ourStacks; auto melleeAttackers = ourStacks;
@ -552,18 +656,43 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
bool canUseAp = true; bool canUseAp = true;
for(auto activeUnit : exchangeUnits.units) std::set<uint32_t> blockedShooters;
int totalTurnsCount = simulationTurnsCount >= turn + turnOrder.size()
? simulationTurnsCount
: turn + turnOrder.size();
for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++)
{
bool isMovingTurm = exchangeTurn < turn;
int queueTurn = exchangeTurn >= exchangeUnits.units.size()
? exchangeUnits.units.size() - 1
: exchangeTurn;
for(auto activeUnit : exchangeUnits.units.at(queueTurn))
{ {
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get())
&& !vstd::contains(blockedShooters, attacker->unitId());
if(!attacker->alive()) if(!attacker->alive())
{ {
#if BATTLE_TRACE_LEVEL>=1 #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 #endif
continue; continue;
@ -573,6 +702,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{ {
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector for %s", attacker->getDescription());
#endif
auto estimateAttack = [&](const battle::Unit * u) -> float auto estimateAttack = [&](const battle::Unit * u) -> float
{ {
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
@ -585,7 +717,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
hb, hb,
true); true);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
#endif #endif
@ -599,6 +731,17 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
return vstd::contains(exchangeUnits.shooters, u); return vstd::contains(exchangeUnits.shooters, u);
}); });
if(!isOur
&& exchangeTurn == 0
&& exchangeUnits.units.at(exchangeTurn).at(0)->unitId() != ap.attack.attacker->unitId()
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool
{
return u->unitId() == ap.attack.attacker->unitId();
});
}
if(!unitsInOppositeQueueExceptInaccessible.empty()) if(!unitsInOppositeQueueExceptInaccessible.empty())
{ {
targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
@ -613,7 +756,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(!exchangeBattle->getForUpdate(u->unitId())->alive()) if(!exchangeBattle->getForUpdate(u->unitId())->alive())
return false; return false;
if (!u->getPosition().isValid()) if(!u->getPosition().isValid())
return false; // e.g. tower shooters return false; // e.g. tower shooters
return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
@ -638,7 +781,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting); const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
@ -657,6 +799,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
} }
} }
if(!shooting)
blockedShooters.insert(defender->unitId());
canUseAp = false; canUseAp = false;
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
@ -670,6 +815,9 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
}); });
} }
exchangeBattle->nextRound();
}
// avoid blocking path for stronger stack by weaker stack // avoid blocking path for stronger stack by weaker stack
// the method checks if all stacks can be placed around enemy // the method checks if all stacks can be placed around enemy
std::map<BattleHex, battle::Units> reachabilityMap; std::map<BattleHex, battle::Units> reachabilityMap;
@ -679,11 +827,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
for(auto hex : hexes) for(auto hex : hexes)
reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
auto score = v.getScore();
if(simulationTurnsCount < totalTurnsCount)
{
float scalingRatio = simulationTurnsCount / static_cast<float>(totalTurnsCount);
score.enemyDamageReduce *= scalingRatio;
score.ourDamageReduce *= scalingRatio;
}
if(turn > 0)
{
auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn);
score.enemyDamageReduce *= turnMultiplier;
}
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce);
#endif #endif
return v.getScore(); return score;
} }
bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)

View File

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

View File

@ -570,6 +570,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
LOG_TRACE(logAi); LOG_TRACE(logAi);
myCb = CB; myCb = CB;
cbc = CB; cbc = CB;
this->env = env;
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
playerID = *myCb->getPlayerID(); playerID = *myCb->getPlayerID();

View File

@ -89,13 +89,12 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN; heroes[hero->tempOwner][hero] = HeroRole::MAIN;
} }
if (obj->ID == Obj::TOWN) if(obj->ID == Obj::TOWN)
{ {
auto town = dynamic_cast<const CGTownInstance*>(obj); auto town = dynamic_cast<const CGTownInstance *>(obj);
auto hero = town->garrisonHero;
if(hero) if(town->garrisonHero)
heroes[hero->tempOwner][hero] = HeroRole::MAIN; heroes[town->garrisonHero->tempOwner][town->garrisonHero] = HeroRole::MAIN;
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -162,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now "vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now
"vcmi.systemOptions.townsGroup" : "Town Screen", "vcmi.systemOptions.townsGroup" : "Town Screen",
"vcmi.statisticWindow.statistics" : "Statistics",
"vcmi.statisticWindow.tsvCopy" : "Data to clipboard",
"vcmi.statisticWindow.selectView" : "Select view",
"vcmi.statisticWindow.value" : "Value",
"vcmi.statisticWindow.title.overview" : "Overview",
"vcmi.statisticWindow.title.resources" : "Resources",
"vcmi.statisticWindow.title.income" : "Income",
"vcmi.statisticWindow.title.numberOfHeroes" : "No. of heroes",
"vcmi.statisticWindow.title.numberOfTowns" : "No. of towns",
"vcmi.statisticWindow.title.numberOfArtifacts" : "No. of artifacts",
"vcmi.statisticWindow.title.numberOfDwellings" : "No. of dwellings",
"vcmi.statisticWindow.title.numberOfMines" : "No. of mines",
"vcmi.statisticWindow.title.armyStrength" : "Army strength",
"vcmi.statisticWindow.title.experience" : "Experience",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Army costs",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Building costs",
"vcmi.statisticWindow.title.mapExplored" : "Map explore ratio",
"vcmi.statisticWindow.param.playerName" : "Player name",
"vcmi.statisticWindow.param.daysSurvived" : "Days survived",
"vcmi.statisticWindow.param.maxHeroLevel" : "Max hero level",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Win ratio (vs. hero)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Win ratio (vs. neutral)",
"vcmi.statisticWindow.param.battlesHero" : "Battles (vs. hero)",
"vcmi.statisticWindow.param.battlesNeutral" : "Battles (vs. neutral)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Max total army strength",
"vcmi.statisticWindow.param.tradeVolume" : "Trade volume",
"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk visited",
"vcmi.statisticWindow.icon.townCaptured" : "Town captured",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Strongest hero of opponent defeated",
"vcmi.statisticWindow.icon.grailFound" : "Grail found",
"vcmi.statisticWindow.icon.defeated" : "Defeated",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)",

View File

@ -162,6 +162,38 @@
"vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now
"vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm",
"vcmi.statisticWindow.statistics" : "Statistik",
"vcmi.statisticWindow.tsvCopy" : "Daten in Zwischenabl.",
"vcmi.statisticWindow.selectView" : "Ansicht wählen",
"vcmi.statisticWindow.value" : "Wert",
"vcmi.statisticWindow.title.overview" : "Überblick",
"vcmi.statisticWindow.title.resources" : "Ressourcen",
"vcmi.statisticWindow.title.income" : "Einkommen",
"vcmi.statisticWindow.title.numberOfHeroes" : "Nr. der Helden",
"vcmi.statisticWindow.title.numberOfTowns" : "Nr. der Städte",
"vcmi.statisticWindow.title.numberOfArtifacts" : "Nr. der Artefakte",
"vcmi.statisticWindow.title.numberOfDwellings" : "Nr. der Behausungen",
"vcmi.statisticWindow.title.numberOfMines" : "Nr. der Minen",
"vcmi.statisticWindow.title.armyStrength" : "Armeestärke",
"vcmi.statisticWindow.title.experience" : "Erfahrung",
"vcmi.statisticWindow.title.resourcesSpentArmy" : "Armeekosten",
"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Gebäudekosten",
"vcmi.statisticWindow.title.mapExplored" : "Maperkundungsrate",
"vcmi.statisticWindow.param.playerName" : "Spielername",
"vcmi.statisticWindow.param.daysSurvived" : "Tage überlebt",
"vcmi.statisticWindow.param.maxHeroLevel" : "Max Heldenlevel",
"vcmi.statisticWindow.param.battleWinRatioHero" : "Sieg Verh. (Helden)",
"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Sieg Verh. (Neutral)",
"vcmi.statisticWindow.param.battlesHero" : "Kämpfe (Helden)",
"vcmi.statisticWindow.param.battlesNeutral" : "Kämpfe (Neutral)",
"vcmi.statisticWindow.param.maxArmyStrength" : "Max Gesamt-Armeestärke",
"vcmi.statisticWindow.param.tradeVolume" : "Handelsvol.",
"vcmi.statisticWindow.param.obeliskVisited" : "Obelisk besucht",
"vcmi.statisticWindow.icon.townCaptured" : "Stadt erobert",
"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Stärksten Helden eines Gegners besiegt",
"vcmi.statisticWindow.icon.grailFound" : "Gral gefunden",
"vcmi.statisticWindow.icon.defeated" : "Besiegt",
"vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)", "vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)",
"vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.", "vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.",
"vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)", "vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)",

View File

@ -65,6 +65,7 @@ set(client_SRCS
mainmenu/CPrologEpilogVideo.cpp mainmenu/CPrologEpilogVideo.cpp
mainmenu/CreditsScreen.cpp mainmenu/CreditsScreen.cpp
mainmenu/CHighScoreScreen.cpp mainmenu/CHighScoreScreen.cpp
mainmenu/CStatisticScreen.cpp
mapView/MapRenderer.cpp mapView/MapRenderer.cpp
mapView/MapRendererContext.cpp mapView/MapRendererContext.cpp
@ -260,6 +261,7 @@ set(client_HEADERS
mainmenu/CPrologEpilogVideo.h mainmenu/CPrologEpilogVideo.h
mainmenu/CreditsScreen.h mainmenu/CreditsScreen.h
mainmenu/CHighScoreScreen.h mainmenu/CHighScoreScreen.h
mainmenu/CStatisticScreen.h
mapView/IMapRendererContext.h mapView/IMapRendererContext.h
mapView/IMapRendererObserver.h mapView/IMapRendererObserver.h

View File

@ -673,13 +673,13 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
setState(EClientState::GAMEPLAY); setState(EClientState::GAMEPLAY);
} }
void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory) void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic)
{ {
HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory); HighScoreParameter param = HighScore::prepareHighScores(client->gameState(), player, victory);
if(victory && client->gameState()->getStartInfo()->campState) if(victory && client->gameState()->getStartInfo()->campState)
{ {
startCampaignScenario(param, client->gameState()->getStartInfo()->campState); startCampaignScenario(param, client->gameState()->getStartInfo()->campState, statistic);
} }
else else
{ {
@ -689,7 +689,7 @@ void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victo
endGameplay(); endGameplay();
CMM->menu->switchToTab("main"); CMM->menu->switchToTab("main");
GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores); GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores, statistic);
} }
} }
@ -722,7 +722,7 @@ void CServerHandler::restartGameplay()
logicConnection->enterLobbyConnectionMode(); logicConnection->enterLobbyConnectionMode();
} }
void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs) void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic)
{ {
std::shared_ptr<CampaignState> ourCampaign = cs; std::shared_ptr<CampaignState> ourCampaign = cs;
@ -738,7 +738,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
endGameplay(); endGameplay();
auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
auto finisher = [ourCampaign, campaignScoreCalculator]() auto finisher = [ourCampaign, campaignScoreCalculator, statistic]()
{ {
if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished()) if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
{ {
@ -754,7 +754,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
else else
{ {
CMM->openCampaignScreen(ourCampaign->campaignSet); CMM->openCampaignScreen(ourCampaign->campaignSet);
GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator); GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator, statistic);
} }
}; };

View File

@ -13,6 +13,7 @@
#include "../lib/network/NetworkInterface.h" #include "../lib/network/NetworkInterface.h"
#include "../lib/StartInfo.h" #include "../lib/StartInfo.h"
#include "../lib/gameState/GameStatistics.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -204,11 +205,11 @@ public:
void debugStartTest(std::string filename, bool save = false); void debugStartTest(std::string filename, bool save = false);
void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
void showHighScoresAndEndGameplay(PlayerColor player, bool victory); void showHighScoresAndEndGameplay(PlayerColor player, bool victory, const StatisticDataSet & statistic);
void endNetwork(); void endNetwork();
void endGameplay(); void endGameplay();
void restartGameplay(); void restartGameplay();
void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {}); void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs, const StatisticDataSet & statistic);
void showServerError(const std::string & txt) const; void showServerError(const std::string & txt) const;
// TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle

View File

@ -420,7 +420,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
adventureInt.reset(); adventureInt.reset();
} }
CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory()); CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory(), pack.statistic);
} }
// In auto testing pack.mode we always close client if red pack.player won or lose // In auto testing pack.mode we always close client if red pack.player won or lose

View File

@ -32,6 +32,7 @@
#include <SDL_events.h> #include <SDL_events.h>
#include <SDL_timer.h> #include <SDL_timer.h>
#include <SDL_clipboard.h>
InputHandler::InputHandler() InputHandler::InputHandler()
: enableMouse(settings["input"]["enableMouse"].Bool()) : enableMouse(settings["input"]["enableMouse"].Bool())
@ -142,6 +143,11 @@ InputMode InputHandler::getCurrentInputMode()
return currentInputMode; return currentInputMode;
} }
void InputHandler::copyToClipBoard(const std::string & text)
{
SDL_SetClipboardText(text.c_str());
}
std::vector<SDL_Event> InputHandler::acquireEvents() std::vector<SDL_Event> InputHandler::acquireEvents()
{ {
boost::unique_lock<boost::mutex> lock(eventsMutex); boost::unique_lock<boost::mutex> lock(eventsMutex);

View File

@ -103,4 +103,6 @@ public:
bool isKeyboardShiftDown() const; bool isKeyboardShiftDown() const;
InputMode getCurrentInputMode(); InputMode getCurrentInputMode();
void copyToClipBoard(const std::string & text);
}; };

View File

@ -156,12 +156,17 @@ void CIntObject::setRedrawParent(bool on)
} }
void CIntObject::fitToScreen(int borderWidth, bool propagate) void CIntObject::fitToScreen(int borderWidth, bool propagate)
{
fitToRect(Rect(Point(0, 0), GH.screenDimensions()), borderWidth, propagate);
}
void CIntObject::fitToRect(Rect rect, int borderWidth, bool propagate)
{ {
Point newPos = pos.topLeft(); Point newPos = pos.topLeft();
vstd::amax(newPos.x, borderWidth); vstd::amax(newPos.x, rect.x + borderWidth);
vstd::amax(newPos.y, borderWidth); vstd::amax(newPos.y, rect.y + borderWidth);
vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w); vstd::amin(newPos.x, rect.x + rect.w - borderWidth - pos.w);
vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h); vstd::amin(newPos.y, rect.y + rect.h - borderWidth - pos.h);
if (newPos != pos.topLeft()) if (newPos != pos.topLeft())
moveTo(newPos, propagate); moveTo(newPos, propagate);
} }

View File

@ -122,6 +122,7 @@ public:
const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center
const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position
void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen
void fitToRect(Rect rect, int borderWidth, bool propagate = true); //moves window to fit into rect
void moveBy(const Point &p, bool propagate = true); void moveBy(const Point &p, bool propagate = true);
void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner) void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner)

View File

@ -74,6 +74,7 @@ enum class EShortcut
HIGH_SCORES_CAMPAIGNS, HIGH_SCORES_CAMPAIGNS,
HIGH_SCORES_SCENARIOS, HIGH_SCORES_SCENARIOS,
HIGH_SCORES_RESET, HIGH_SCORES_RESET,
HIGH_SCORES_STATISTICS,
// Game lobby / scenario selection // Game lobby / scenario selection
LOBBY_BEGIN_STANDARD_GAME, // b LOBBY_BEGIN_STANDARD_GAME, // b

View File

@ -290,6 +290,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
{"highScoresCampaigns", EShortcut::HIGH_SCORES_CAMPAIGNS }, {"highScoresCampaigns", EShortcut::HIGH_SCORES_CAMPAIGNS },
{"highScoresScenarios", EShortcut::HIGH_SCORES_SCENARIOS }, {"highScoresScenarios", EShortcut::HIGH_SCORES_SCENARIOS },
{"highScoresReset", EShortcut::HIGH_SCORES_RESET }, {"highScoresReset", EShortcut::HIGH_SCORES_RESET },
{"highScoresStatistics", EShortcut::HIGH_SCORES_STATISTICS },
{"lobbyReplayVideo", EShortcut::LOBBY_REPLAY_VIDEO }, {"lobbyReplayVideo", EShortcut::LOBBY_REPLAY_VIDEO },
{"lobbyExtraOptions", EShortcut::LOBBY_EXTRA_OPTIONS }, {"lobbyExtraOptions", EShortcut::LOBBY_EXTRA_OPTIONS },
{"lobbyTurnOptions", EShortcut::LOBBY_TURN_OPTIONS }, {"lobbyTurnOptions", EShortcut::LOBBY_TURN_OPTIONS },

View File

@ -11,6 +11,7 @@
#include "StdInc.h" #include "StdInc.h"
#include "CHighScoreScreen.h" #include "CHighScoreScreen.h"
#include "CStatisticScreen.h"
#include "../gui/CGuiHandler.h" #include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h" #include "../gui/WindowHandler.h"
#include "../gui/Shortcut.h" #include "../gui/Shortcut.h"
@ -33,6 +34,7 @@
#include "../../lib/CCreatureHandler.h" #include "../../lib/CCreatureHandler.h"
#include "../../lib/constants/EntityIdentifiers.h" #include "../../lib/constants/EntityIdentifiers.h"
#include "../../lib/gameState/HighScore.h" #include "../../lib/gameState/HighScore.h"
#include "../../lib/gameState/GameStatistics.h"
CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
: CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted)
@ -170,8 +172,8 @@ void CHighScoreScreen::buttonExitClick()
close(); close();
} }
CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic)
: CWindowObject(BORDERED), won(won), calc(calc) : CWindowObject(BORDERED), won(won), calc(calc), stat(statistic)
{ {
addUsedEvents(LCLICK | KEYBOARD); addUsedEvents(LCLICK | KEYBOARD);
@ -204,6 +206,12 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();}); videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), VideoPath::builtin("LOSEGAME.SMK"), true, [this](){close();});
CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true);
} }
if (settings["general"]["enableUiEnhancements"].Bool())
{
statisticButton = std::make_shared<CButton>(Point(726, 10), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.statisticWindow.statistics")), [this](){ GH.windows().createAndPushWindow<CStatisticScreen>(stat); }, EShortcut::HIGH_SCORES_STATISTICS);
texts.push_back(std::make_shared<CLabel>(716, 25, EFonts::FONT_HIGH_SCORE, ETextAlignment::CENTERRIGHT, Colors::WHITE, CGI->generaltexth->translate("vcmi.statisticWindow.statistics") + ":"));
}
} }
int CHighScoreInputScreen::addEntry(std::string text) { int CHighScoreInputScreen::addEntry(std::string text) {
@ -253,6 +261,9 @@ void CHighScoreInputScreen::show(Canvas & to)
void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
{ {
if(statisticButton->pos.isInside(cursorPosition))
return;
OBJECT_CONSTRUCTION; OBJECT_CONSTRUCTION;
if(!won) if(!won)
@ -280,6 +291,8 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
void CHighScoreInputScreen::keyPressed(EShortcut key) void CHighScoreInputScreen::keyPressed(EShortcut key)
{ {
if(key == EShortcut::HIGH_SCORES_STATISTICS) // ignore shortcut for skipping video with key
return;
clickPressed(Point()); clickPressed(Point());
} }

View File

@ -10,6 +10,7 @@
#pragma once #pragma once
#include "../windows/CWindowObject.h" #include "../windows/CWindowObject.h"
#include "../../lib/gameState/HighScore.h" #include "../../lib/gameState/HighScore.h"
#include "../../lib/gameState/GameStatistics.h"
class CButton; class CButton;
class CLabel; class CLabel;
@ -70,16 +71,19 @@ public:
class CHighScoreInputScreen : public CWindowObject class CHighScoreInputScreen : public CWindowObject
{ {
std::vector<std::shared_ptr<CMultiLineLabel>> texts; std::vector<std::shared_ptr<CLabel>> texts;
std::shared_ptr<CHighScoreInput> input; std::shared_ptr<CHighScoreInput> input;
std::shared_ptr<TransparentFilledRectangle> background; std::shared_ptr<TransparentFilledRectangle> background;
std::shared_ptr<VideoWidgetBase> videoPlayer; std::shared_ptr<VideoWidgetBase> videoPlayer;
std::shared_ptr<CFilledTexture> backgroundAroundMenu; std::shared_ptr<CFilledTexture> backgroundAroundMenu;
std::shared_ptr<CButton> statisticButton;
bool won; bool won;
HighScoreCalculation calc; HighScoreCalculation calc;
StatisticDataSet stat;
public: public:
CHighScoreInputScreen(bool won, HighScoreCalculation calc); CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic);
int addEntry(std::string text); int addEntry(std::string text);

View File

@ -0,0 +1,521 @@
/*
* CStatisticScreen.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CStatisticScreen.h"
#include "../CGameInfo.h"
#include "../gui/CGuiHandler.h"
#include "../gui/WindowHandler.h"
#include "../eventsSDL/InputHandler.h"
#include "../gui/Shortcut.h"
#include "../render/Graphics.h"
#include "../render/IImage.h"
#include "../render/IRenderHandler.h"
#include "../widgets/ComboBox.h"
#include "../widgets/Images.h"
#include "../widgets/GraphicalPrimitiveCanvas.h"
#include "../widgets/TextControls.h"
#include "../widgets/Buttons.h"
#include "../windows/InfoWindows.h"
#include "../widgets/Slider.h"
#include "../../lib/gameState/GameStatistics.h"
#include "../../lib/gameState/CGameState.h"
#include "../../lib/texts/CGeneralTextHandler.h"
#include "../../lib/texts/TextOperations.h"
#include <vstd/DateUtils.h>
std::string CStatisticScreen::getDay(int d)
{
return std::to_string(CGameState::getDate(d, Date::MONTH)) + "/" + std::to_string(CGameState::getDate(d, Date::WEEK)) + "/" + std::to_string(CGameState::getDate(d, Date::DAY_OF_WEEK));
}
CStatisticScreen::CStatisticScreen(const StatisticDataSet & stat)
: CWindowObject(BORDERED), statistic(stat)
{
OBJECT_CONSTRUCTION;
pos = center(Rect(0, 0, 800, 600));
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
filledBackground->setPlayerColor(PlayerColor(1));
contentArea = Rect(10, 40, 780, 510);
layout.emplace_back(std::make_shared<CLabel>(400, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.statisticWindow.statistics")));
layout.emplace_back(std::make_shared<TransparentFilledRectangle>(contentArea, ColorRGBA(0, 0, 0, 128), ColorRGBA(64, 80, 128, 255), 1));
layout.emplace_back(std::make_shared<CButton>(Point(725, 558), AnimationPath::builtin("MUBCHCK"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT));
buttonSelect = std::make_shared<CToggleButton>(Point(10, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ onSelectButton(); });
buttonSelect->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.selectView"), EFonts::FONT_SMALL, Colors::YELLOW);
buttonCsvSave = std::make_shared<CToggleButton>(Point(150, 564), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this](bool on){ GH.input().copyToClipBoard(statistic.toCsv("\t")); });
buttonCsvSave->setTextOverlay(CGI->generaltexth->translate("vcmi.statisticWindow.tsvCopy"), EFonts::FONT_SMALL, Colors::YELLOW);
mainContent = getContent(OVERVIEW, EGameResID::NONE);
}
void CStatisticScreen::onSelectButton()
{
std::vector<std::string> texts;
for(auto & val : contentInfo)
texts.emplace_back(CGI->generaltexth->translate(std::get<0>(val.second)));
GH.windows().createAndPushWindow<StatisticSelector>(texts, [this](int selectedIndex)
{
OBJECT_CONSTRUCTION;
if(!std::get<1>(contentInfo[static_cast<Content>(selectedIndex)]))
mainContent = getContent(static_cast<Content>(selectedIndex), EGameResID::NONE);
else
{
auto content = static_cast<Content>(selectedIndex);
auto possibleRes = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
std::vector<std::string> resourceText;
for(const auto & res : possibleRes)
resourceText.emplace_back(CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()));
GH.windows().createAndPushWindow<StatisticSelector>(resourceText, [this, content, possibleRes](int index)
{
OBJECT_CONSTRUCTION;
mainContent = getContent(content, possibleRes[index]);
});
}
});
}
TData CStatisticScreen::extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const
{
auto tmpData = stat.data;
std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; });
PlayerColor tmpColor = PlayerColor::NEUTRAL;
std::vector<float> tmpColorSet;
TData plotData;
EPlayerStatus statusLastRound = EPlayerStatus::INGAME;
for(const auto & val : tmpData)
{
if(tmpColor != val.player)
{
if(tmpColorSet.size())
{
plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector<float>(tmpColorSet)});
tmpColorSet.clear();
}
tmpColor = val.player;
}
if(val.status == EPlayerStatus::INGAME || (statusLastRound == EPlayerStatus::INGAME && val.status == EPlayerStatus::LOSER))
tmpColorSet.emplace_back(selector(val));
statusLastRound = val.status; //to keep at least one dataset after loose
}
if(tmpColorSet.size())
plotData.push_back({graphics->playerColors[tmpColor.getNum()], std::vector<float>(tmpColorSet)});
return plotData;
}
TIcons CStatisticScreen::extractIcons() const
{
TIcons icons;
auto tmpData = statistic.data;
std::sort(tmpData.begin(), tmpData.end(), [](const StatisticDataSetEntry & v1, const StatisticDataSetEntry & v2){ return v1.player == v2.player ? v1.day < v2.day : v1.player < v2.player; });
auto imageTown = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 3, 0, EImageBlitMode::COLORKEY);
imageTown->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
auto imageBattle = GH.renderHandler().loadImage(AnimationPath::builtin("cradvntr"), 5, 0, EImageBlitMode::COLORKEY);
imageBattle->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
auto imageDefeated = GH.renderHandler().loadImage(AnimationPath::builtin("tpthchk"), 1, 0, EImageBlitMode::COLORKEY);
imageDefeated->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
auto imageGrail = GH.renderHandler().loadImage(AnimationPath::builtin("vwsymbol"), 2, 0, EImageBlitMode::COLORKEY);
imageGrail->scaleTo(Point(CHART_ICON_SIZE, CHART_ICON_SIZE));
std::map<PlayerColor, bool> foundDefeated;
std::map<PlayerColor, bool> foundGrail;
for(const auto & val : tmpData)
{
if(val.eventCapturedTown)
icons.push_back({ graphics->playerColors[val.player], val.day, imageTown, CGI->generaltexth->translate("vcmi.statisticWindow.icon.townCaptured") });
if(val.eventDefeatedStrongestHero)
icons.push_back({ graphics->playerColors[val.player], val.day, imageBattle, CGI->generaltexth->translate("vcmi.statisticWindow.icon.strongestHeroDefeated") });
if(val.status == EPlayerStatus::LOSER && !foundDefeated[val.player])
{
foundDefeated[val.player] = true;
icons.push_back({ graphics->playerColors[val.player], val.day, imageDefeated, CGI->generaltexth->translate("vcmi.statisticWindow.icon.defeated") });
}
if(val.hasGrail && !foundGrail[val.player])
{
foundGrail[val.player] = true;
icons.push_back({ graphics->playerColors[val.player], val.day, imageGrail, CGI->generaltexth->translate("vcmi.statisticWindow.icon.grailFound") });
}
}
return icons;
}
std::shared_ptr<CIntObject> CStatisticScreen::getContent(Content c, EGameResID res)
{
TData plotData;
TIcons icons = extractIcons();
switch (c)
{
case OVERVIEW:
return std::make_shared<OverviewPanel>(contentArea.resize(-15), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), statistic);
case CHART_RESOURCES:
plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.resources[res]; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
case CHART_INCOME:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.income; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_NUMBER_OF_HEROES:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberHeroes; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_NUMBER_OF_TOWNS:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberTowns; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_NUMBER_OF_ARTIFACTS:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberArtifacts; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_NUMBER_OF_DWELLINGS:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.numberDwellings; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_NUMBER_OF_MINES:
plotData = extractData(statistic, [res](StatisticDataSetEntry val) -> float { return val.numMines[res]; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
case CHART_ARMY_STRENGTH:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.armyStrength; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_EXPERIENCE:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.totalExperience; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 0);
case CHART_RESOURCES_SPENT_ARMY:
plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForArmy[res]; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
case CHART_RESOURCES_SPENT_BUILDINGS:
plotData = extractData(statistic, [res](const StatisticDataSetEntry & val) -> float { return val.spentResourcesForBuildings[res]; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])) + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", res.getNum()).get()), plotData, icons, 0);
case CHART_MAP_EXPLORED:
plotData = extractData(statistic, [](const StatisticDataSetEntry & val) -> float { return val.mapExploredRatio; });
return std::make_shared<LineChart>(contentArea.resize(-5), CGI->generaltexth->translate(std::get<0>(contentInfo[c])), plotData, icons, 1);
}
return nullptr;
}
StatisticSelector::StatisticSelector(const std::vector<std::string> & texts, const std::function<void(int selectedIndex)> & cb)
: CWindowObject(BORDERED | NEEDS_ANIMATED_BACKGROUND), texts(texts), cb(cb)
{
OBJECT_CONSTRUCTION;
pos = center(Rect(0, 0, 128 + 16, std::min(static_cast<int>(texts.size()), LINES) * 40));
filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
filledBackground->setPlayerColor(PlayerColor(1));
slider = std::make_shared<CSlider>(Point(pos.w - 16, 0), pos.h, [this](int to){ update(to); redraw(); }, LINES, texts.size(), 0, Orientation::VERTICAL, CSlider::BLUE);
slider->setPanningStep(40);
slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, pos.h));
update(0);
}
void StatisticSelector::update(int to)
{
OBJECT_CONSTRUCTION;
buttons.clear();
for(int i = to; i < LINES + to; i++)
{
if(i>=texts.size())
continue;
auto button = std::make_shared<CToggleButton>(Point(0, 10 + (i - to) * 40), AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), [this, i](bool on){ close(); cb(i); });
button->setTextOverlay(texts[i], EFonts::FONT_SMALL, Colors::WHITE);
buttons.emplace_back(button);
}
}
OverviewPanel::OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat)
: CIntObject(), data(stat)
{
OBJECT_CONSTRUCTION;
pos = position + pos.topLeft();
layout.emplace_back(std::make_shared<CLabel>(pos.w / 2, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title));
canvas = std::make_shared<GraphicalPrimitiveCanvas>(Rect(0, Y_OFFS, pos.w - 16, pos.h - Y_OFFS));
dataExtract = {
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.playerName"), [this](PlayerColor color){
return playerDataFilter(color).front().playerName;
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.daysSurvived"), [this](PlayerColor color){
return CStatisticScreen::getDay(playerDataFilter(color).size());
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.maxHeroLevel"), [this](PlayerColor color){
int maxLevel = 0;
for(const auto & val : playerDataFilter(color))
if(maxLevel < val.maxHeroLevel)
maxLevel = val.maxHeroLevel;
return std::to_string(maxLevel);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioHero"), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
if(!val.numBattlesPlayer)
return std::string("");
float tmp = (static_cast<float>(val.numWinBattlesPlayer) / static_cast<float>(val.numBattlesPlayer)) * 100;
return std::to_string(static_cast<int>(tmp)) + " %";
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.battleWinRatioNeutral"), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
if(!val.numWinBattlesNeutral)
return std::string("");
float tmp = (static_cast<float>(val.numWinBattlesNeutral) / static_cast<float>(val.numBattlesNeutral)) * 100;
return std::to_string(static_cast<int>(tmp)) + " %";
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesHero"), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.numBattlesPlayer);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.battlesNeutral"), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.numBattlesNeutral);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.obeliskVisited"), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(static_cast<int>(val.obeliskVisitedRatio * 100)) + " %";
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.maxArmyStrength"), [this](PlayerColor color){
int maxArmyStrength = 0;
for(const auto & val : playerDataFilter(color))
if(maxArmyStrength < val.armyStrength)
maxArmyStrength = val.armyStrength;
return TextOperations::formatMetric(maxArmyStrength, 6);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GOLD).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::GOLD]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::WOOD).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::WOOD]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::MERCURY).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::MERCURY]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::ORE).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::ORE]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::SULFUR).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::SULFUR]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::CRYSTAL).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::CRYSTAL]);
}
},
{
CGI->generaltexth->translate("vcmi.statisticWindow.param.tradeVolume") + " - " + CGI->generaltexth->translate(TextIdentifier("core.restypes", EGameResID::GEMS).get()), [this](PlayerColor color){
auto val = playerDataFilter(color).back();
return std::to_string(val.tradeVolume[EGameResID::GEMS]);
}
},
};
int usedLines = dataExtract.size();
slider = std::make_shared<CSlider>(Point(pos.w - 16, Y_OFFS), pos.h - Y_OFFS, [this](int to){ update(to); setRedrawParent(true); redraw(); }, LINES - 1, usedLines, 0, Orientation::VERTICAL, CSlider::BLUE);
slider->setPanningStep(canvas->pos.h / LINES);
slider->setScrollBounds(Rect(-pos.w + slider->pos.w, 0, pos.w, canvas->pos.h));
fieldSize = Point(canvas->pos.w / (graphics->playerColors.size() + 2), canvas->pos.h / LINES);
for(int x = 0; x < graphics->playerColors.size() + 1; x++)
for(int y = 0; y < LINES; y++)
{
int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x;
int yStart = y * fieldSize.y;
if(x == 0 || y == 0)
canvas->addBox(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(0, 0, 0, 100));
canvas->addRectangle(Point(xStart, yStart), Point(x == 0 ? 2 * fieldSize.x : fieldSize.x, fieldSize.y), ColorRGBA(127, 127, 127, 255));
}
update(0);
}
std::vector<StatisticDataSetEntry> OverviewPanel::playerDataFilter(PlayerColor color)
{
std::vector<StatisticDataSetEntry> tmpData;
std::copy_if(data.data.begin(), data.data.end(), std::back_inserter(tmpData), [color](const StatisticDataSetEntry & e){ return e.player == color; });
return tmpData;
}
void OverviewPanel::update(int to)
{
OBJECT_CONSTRUCTION;
content.clear();
for(int y = to; y < LINES - 1 + to; y++)
{
if(y >= dataExtract.size())
continue;
for(int x = 0; x < PlayerColor::PLAYER_LIMIT_I + 1; x++)
{
if(y == to && x < PlayerColor::PLAYER_LIMIT_I)
content.emplace_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ITGFLAGS"), x, 0, 180 + x * fieldSize.x, 35));
int xStart = (x + (x == 0 ? 0 : 1)) * fieldSize.x + (x == 0 ? fieldSize.x : (fieldSize.x / 2));
int yStart = Y_OFFS + (y + 1 - to) * fieldSize.y + (fieldSize.y / 2);
PlayerColor tmpColor(x - 1);
if(playerDataFilter(tmpColor).size() || x == 0)
content.emplace_back(std::make_shared<CLabel>(xStart, yStart, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, (x == 0 ? dataExtract[y].first : dataExtract[y].second(tmpColor)), x == 0 ? (fieldSize.x * 2) : fieldSize.x));
}
}
}
LineChart::LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY)
: CIntObject(), maxVal(0), maxDay(0)
{
OBJECT_CONSTRUCTION;
addUsedEvents(LCLICK | MOVE);
pos = position + pos.topLeft();
layout.emplace_back(std::make_shared<CLabel>(pos.w / 2, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, title));
chartArea = pos.resize(-50);
chartArea.moveTo(Point(50, 50));
canvas = std::make_shared<GraphicalPrimitiveCanvas>(Rect(0, 0, pos.w, pos.h));
statusBar = CGStatusBar::create(0, 0, ImagePath::builtin("radialMenu/statusBar"));
static_cast<std::shared_ptr<CIntObject>>(statusBar)->setEnabled(false);
// additional calculations
bool skipMaxValCalc = maxY > 0;
maxVal = maxY;
for(const auto & line : data)
{
for(auto & val : line.second)
if(maxVal < val && !skipMaxValCalc)
maxVal = val;
if(maxDay < line.second.size())
maxDay = line.second.size();
}
// draw
for(const auto & line : data)
{
Point lastPoint(-1, -1);
for(int i = 0; i < line.second.size(); i++)
{
float x = (static_cast<float>(chartArea.w) / static_cast<float>(maxDay - 1)) * static_cast<float>(i);
float y = static_cast<float>(chartArea.h) - (static_cast<float>(chartArea.h) / maxVal) * line.second[i];
Point p = Point(x, y) + chartArea.topLeft();
if(lastPoint.x != -1)
canvas->addLine(lastPoint, p, line.first);
// icons
for(auto & icon : icons)
if(std::get<0>(icon) == line.first && std::get<1>(icon) == i + 1) // color && day
{
pictures.emplace_back(std::make_shared<CPicture>(std::get<2>(icon), Point(x - (CHART_ICON_SIZE / 2), y - (CHART_ICON_SIZE / 2)) + chartArea.topLeft()));
pictures.back()->addRClickCallback([icon](){ CRClickPopup::createAndPush(std::get<3>(icon)); });
}
lastPoint = p;
}
}
// Axis
canvas->addLine(chartArea.topLeft() + Point(0, -10), chartArea.topLeft() + Point(0, chartArea.h + 10), Colors::WHITE);
canvas->addLine(chartArea.topLeft() + Point(-10, chartArea.h), chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h), Colors::WHITE);
Point p = chartArea.topLeft() + Point(-5, chartArea.h + 10);
layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, "0"));
p = chartArea.topLeft() + Point(chartArea.w + 10, chartArea.h + 10);
layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CStatisticScreen::getDay(maxDay)));
p = chartArea.topLeft() + Point(-5, -10);
layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_SMALL, ETextAlignment::CENTERRIGHT, Colors::WHITE, std::to_string(static_cast<int>(maxVal))));
p = chartArea.bottomLeft() + Point(chartArea.w / 2, + 20);
layout.emplace_back(std::make_shared<CLabel>(p.x, p.y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.64")));
}
void LineChart::updateStatusBar(const Point & cursorPosition)
{
statusBar->moveTo(cursorPosition + Point(-statusBar->pos.w / 2, 20));
statusBar->fitToRect(pos, 10);
Rect r(pos.x + chartArea.x, pos.y + chartArea.y, chartArea.w, chartArea.h);
statusBar->setEnabled(r.isInside(cursorPosition));
if(r.isInside(cursorPosition))
{
float x = (static_cast<float>(maxDay) / static_cast<float>(chartArea.w)) * (static_cast<float>(cursorPosition.x) - static_cast<float>(r.x)) + 1.0f;
float y = maxVal - (maxVal / static_cast<float>(chartArea.h)) * (static_cast<float>(cursorPosition.y) - static_cast<float>(r.y));
statusBar->write(CGI->generaltexth->translate("core.genrltxt.64") + ": " + CStatisticScreen::getDay(x) + " " + CGI->generaltexth->translate("vcmi.statisticWindow.value") + ": " + (static_cast<int>(y) > 0 ? std::to_string(static_cast<int>(y)) : std::to_string(y)));
}
setRedrawParent(true);
redraw();
}
void LineChart::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance)
{
updateStatusBar(cursorPosition);
}
void LineChart::clickPressed(const Point & cursorPosition)
{
updateStatusBar(cursorPosition);
}

View File

@ -0,0 +1,134 @@
/*
* CStatisticScreen.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../windows/CWindowObject.h"
#include "../../lib/gameState/GameStatistics.h"
class FilledTexturePlayerColored;
class CToggleButton;
class GraphicalPrimitiveCanvas;
class LineChart;
class CGStatusBar;
class ComboBox;
class CSlider;
class IImage;
class CPicture;
using TData = std::vector<std::pair<ColorRGBA, std::vector<float>>>;
using TIcons = std::vector<std::tuple<ColorRGBA, int, std::shared_ptr<IImage>, std::string>>; // Color, Day, Image, Helptext
const int CHART_ICON_SIZE = 32;
class CStatisticScreen : public CWindowObject
{
enum Content {
OVERVIEW,
CHART_RESOURCES,
CHART_INCOME,
CHART_NUMBER_OF_HEROES,
CHART_NUMBER_OF_TOWNS,
CHART_NUMBER_OF_ARTIFACTS,
CHART_NUMBER_OF_DWELLINGS,
CHART_NUMBER_OF_MINES,
CHART_ARMY_STRENGTH,
CHART_EXPERIENCE,
CHART_RESOURCES_SPENT_ARMY,
CHART_RESOURCES_SPENT_BUILDINGS,
CHART_MAP_EXPLORED,
};
std::map<Content, std::tuple<std::string, bool>> contentInfo = { // tuple: textid, resource selection needed
{ OVERVIEW, { "vcmi.statisticWindow.title.overview", false } },
{ CHART_RESOURCES, { "vcmi.statisticWindow.title.resources", true } },
{ CHART_INCOME, { "vcmi.statisticWindow.title.income", false } },
{ CHART_NUMBER_OF_HEROES, { "vcmi.statisticWindow.title.numberOfHeroes", false } },
{ CHART_NUMBER_OF_TOWNS, { "vcmi.statisticWindow.title.numberOfTowns", false } },
{ CHART_NUMBER_OF_ARTIFACTS, { "vcmi.statisticWindow.title.numberOfArtifacts", false } },
{ CHART_NUMBER_OF_DWELLINGS, { "vcmi.statisticWindow.title.numberOfDwellings", false } },
{ CHART_NUMBER_OF_MINES, { "vcmi.statisticWindow.title.numberOfMines", true } },
{ CHART_ARMY_STRENGTH, { "vcmi.statisticWindow.title.armyStrength", false } },
{ CHART_EXPERIENCE, { "vcmi.statisticWindow.title.experience", false } },
{ CHART_RESOURCES_SPENT_ARMY, { "vcmi.statisticWindow.title.resourcesSpentArmy", true } },
{ CHART_RESOURCES_SPENT_BUILDINGS, { "vcmi.statisticWindow.title.resourcesSpentBuildings", true } },
{ CHART_MAP_EXPLORED, { "vcmi.statisticWindow.title.mapExplored", false } },
};
std::shared_ptr<FilledTexturePlayerColored> filledBackground;
std::vector<std::shared_ptr<CIntObject>> layout;
std::shared_ptr<CToggleButton> buttonCsvSave;
std::shared_ptr<CToggleButton> buttonSelect;
StatisticDataSet statistic;
std::shared_ptr<CIntObject> mainContent;
Rect contentArea;
using ExtractFunctor = std::function<float(StatisticDataSetEntry val)>;
TData extractData(const StatisticDataSet & stat, const ExtractFunctor & selector) const;
TIcons extractIcons() const;
std::shared_ptr<CIntObject> getContent(Content c, EGameResID res);
void onSelectButton();
public:
CStatisticScreen(const StatisticDataSet & stat);
static std::string getDay(int day);
};
class StatisticSelector : public CWindowObject
{
std::shared_ptr<FilledTexturePlayerColored> filledBackground;
std::vector<std::shared_ptr<CToggleButton>> buttons;
std::shared_ptr<CSlider> slider;
const int LINES = 10;
std::vector<std::string> texts;
std::function<void(int selectedIndex)> cb;
void update(int to);
public:
StatisticSelector(const std::vector<std::string> & texts, const std::function<void(int selectedIndex)> & cb);
};
class OverviewPanel : public CIntObject
{
std::shared_ptr<GraphicalPrimitiveCanvas> canvas;
std::vector<std::shared_ptr<CIntObject>> layout;
std::vector<std::shared_ptr<CIntObject>> content;
std::shared_ptr<CSlider> slider;
Point fieldSize;
StatisticDataSet data;
std::vector<std::pair<std::string, std::function<std::string(PlayerColor color)>>> dataExtract;
const int LINES = 15;
const int Y_OFFS = 30;
std::vector<StatisticDataSetEntry> playerDataFilter(PlayerColor color);
void update(int to);
public:
OverviewPanel(Rect position, std::string title, const StatisticDataSet & stat);
};
class LineChart : public CIntObject
{
std::shared_ptr<GraphicalPrimitiveCanvas> canvas;
std::vector<std::shared_ptr<CIntObject>> layout;
std::shared_ptr<CGStatusBar> statusBar;
std::vector<std::shared_ptr<CPicture>> pictures;
Rect chartArea;
float maxVal;
int maxDay;
void updateStatusBar(const Point & cursorPosition);
public:
LineChart(Rect position, std::string title, TData data, TIcons icons, float maxY);
void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
void clickPressed(const Point & cursorPosition) override;
};

View File

@ -339,10 +339,11 @@ FFMpegStream::~FFMpegStream()
{ {
av_frame_free(&frame); av_frame_free(&frame);
#if (LIBAVCODEC_VERSION_MAJOR < 61 )
// deprecated, apparently no longer necessary - avcodec_free_context should suffice
avcodec_close(codecContext); avcodec_close(codecContext);
avcodec_free_context(&codecContext); #endif
avcodec_close(codecContext);
avcodec_free_context(&codecContext); avcodec_free_context(&codecContext);
avformat_close_input(&formatContext); avformat_close_input(&formatContext);

View File

@ -39,6 +39,8 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Point & position)
pos += position; pos += position;
pos.w = bg->width(); pos.w = bg->width();
pos.h = bg->height(); pos.h = bg->height();
addUsedEvents(SHOW_POPUP);
} }
CPicture::CPicture( const ImagePath &bmpname, int x, int y ) CPicture::CPicture( const ImagePath &bmpname, int x, int y )
@ -66,6 +68,8 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position )
{ {
pos.w = pos.h = 0; pos.w = pos.h = 0;
} }
addUsedEvents(SHOW_POPUP);
} }
CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y) CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
@ -74,6 +78,8 @@ CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
srcRect = SrcRect; srcRect = SrcRect;
pos.w = srcRect->w; pos.w = srcRect->w;
pos.h = srcRect->h; pos.h = srcRect->h;
addUsedEvents(SHOW_POPUP);
} }
CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, int y) CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, int y)
@ -82,6 +88,8 @@ CPicture::CPicture(std::shared_ptr<IImage> image, const Rect &SrcRect, int x, in
srcRect = SrcRect; srcRect = SrcRect;
pos.w = srcRect->w; pos.w = srcRect->w;
pos.h = srcRect->h; pos.h = srcRect->h;
addUsedEvents(SHOW_POPUP);
} }
void CPicture::show(Canvas & to) void CPicture::show(Canvas & to)
@ -119,6 +127,17 @@ void CPicture::setPlayerColor(PlayerColor player)
bg->playerColored(player); bg->playerColored(player);
} }
void CPicture::addRClickCallback(const std::function<void()> & callback)
{
rCallback = callback;
}
void CPicture::showPopupWindow(const Point & cursorPosition)
{
if(rCallback)
rCallback();
}
CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position) CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position)
: CIntObject(0, position.topLeft()) : CIntObject(0, position.topLeft())
, texture(GH.renderHandler().loadImage(imageName, EImageBlitMode::COLORKEY)) , texture(GH.renderHandler().loadImage(imageName, EImageBlitMode::COLORKEY))

View File

@ -26,6 +26,7 @@ class IImage;
class CPicture : public CIntObject class CPicture : public CIntObject
{ {
std::shared_ptr<IImage> bg; std::shared_ptr<IImage> bg;
std::function<void()> rCallback;
public: public:
/// if set, only specified section of internal image will be rendered /// if set, only specified section of internal image will be rendered
@ -57,8 +58,11 @@ public:
void scaleTo(Point size); void scaleTo(Point size);
void setPlayerColor(PlayerColor player); void setPlayerColor(PlayerColor player);
void addRClickCallback(const std::function<void()> & callback);
void show(Canvas & to) override; void show(Canvas & to) override;
void showAll(Canvas & to) override; void showAll(Canvas & to) override;
void showPopupWindow(const Point & cursorPosition) override;
}; };
/// area filled with specific texture /// area filled with specific texture

View File

@ -330,6 +330,7 @@ Rect CMultiLineLabel::getTextLocation()
case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSize); case ETextAlignment::TOPLEFT: return Rect(pos.topLeft(), textSize);
case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSize); case ETextAlignment::TOPCENTER: return Rect(pos.topLeft(), textSize);
case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSize); case ETextAlignment::CENTER: return Rect(pos.topLeft() + textOffset / 2, textSize);
case ETextAlignment::CENTERRIGHT: return Rect(pos.topLeft() + Point(textOffset.x, textOffset.y / 2), textSize);
case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize); case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize);
} }
assert(0); assert(0);
@ -543,7 +544,6 @@ void CGStatusBar::activate()
void CGStatusBar::deactivate() void CGStatusBar::deactivate()
{ {
assert(GH.statusbar().get() == this);
GH.setStatusbar(nullptr); GH.setStatusbar(nullptr);
if (enteringText) if (enteringText)

View File

@ -20,6 +20,7 @@ class VCMI(ConanFile):
"sdl_mixer/[~2.0.4]", "sdl_mixer/[~2.0.4]",
"sdl_ttf/[~2.0.18]", "sdl_ttf/[~2.0.18]",
"onetbb/[^2021.3]", "onetbb/[^2021.3]",
"xz_utils/[>=5.2.5]", # Required for innoextract
] ]
requires = _libRequires + _clientRequires requires = _libRequires + _clientRequires
@ -87,24 +88,64 @@ class VCMI(ConanFile):
self.options["boost"].without_type_erasure = True self.options["boost"].without_type_erasure = True
self.options["boost"].without_wave = True self.options["boost"].without_wave = True
self.options["ffmpeg"].avdevice = False self.options["ffmpeg"].disable_all_bitstream_filters = True
self.options["ffmpeg"].avfilter = False self.options["ffmpeg"].disable_all_decoders = True
self.options["ffmpeg"].postproc = False self.options["ffmpeg"].disable_all_demuxers = True
self.options["ffmpeg"].swresample = False self.options["ffmpeg"].disable_all_encoders = True
self.options["ffmpeg"].with_asm = self.settings.os != "Android" self.options["ffmpeg"].disable_all_filters = True
self.options["ffmpeg"].disable_all_hardware_accelerators = True
self.options["ffmpeg"].disable_all_muxers = True
self.options["ffmpeg"].disable_all_parsers = True
self.options["ffmpeg"].disable_all_protocols = True
self.options["ffmpeg"].with_asm = False
self.options["ffmpeg"].with_bzip2 = False
self.options["ffmpeg"].with_freetype = False self.options["ffmpeg"].with_freetype = False
self.options["ffmpeg"].with_libfdk_aac = False self.options["ffmpeg"].with_libaom = False
self.options["ffmpeg"].with_libdav1d = False
self.options["ffmpeg"].with_libiconv = False
self.options["ffmpeg"].with_libmp3lame = False self.options["ffmpeg"].with_libmp3lame = False
self.options["ffmpeg"].with_libsvtav1 = False
self.options["ffmpeg"].with_libvpx = False self.options["ffmpeg"].with_libvpx = False
self.options["ffmpeg"].with_libwebp = False self.options["ffmpeg"].with_libwebp = False
self.options["ffmpeg"].with_libx264 = False self.options["ffmpeg"].with_libx264 = False
self.options["ffmpeg"].with_libx265 = False self.options["ffmpeg"].with_libx265 = False
self.options["ffmpeg"].with_lzma = True
self.options["ffmpeg"].with_openh264 = False self.options["ffmpeg"].with_openh264 = False
self.options["ffmpeg"].with_openjpeg = False self.options["ffmpeg"].with_openjpeg = False
self.options["ffmpeg"].with_opus = False self.options["ffmpeg"].with_opus = False
self.options["ffmpeg"].with_programs = False self.options["ffmpeg"].with_programs = False
self.options["ffmpeg"].with_sdl = False
self.options["ffmpeg"].with_ssl = False self.options["ffmpeg"].with_ssl = False
self.options["ffmpeg"].with_vorbis = False self.options["ffmpeg"].with_vorbis = False
self.options["ffmpeg"].with_zlib = False
if self.settings.os != "Android":
self.options["ffmpeg"].with_libfdk_aac = False
self.options["ffmpeg"].avcodec = True
self.options["ffmpeg"].avdevice = False
self.options["ffmpeg"].avfilter = False
self.options["ffmpeg"].avformat = True
self.options["ffmpeg"].postproc = False
self.options["ffmpeg"].swresample = True # For resampling of audio in 'planar' formats
self.options["ffmpeg"].swscale = True # For video scaling
# We want following options supported:
# H3:SoD - .bik and .smk
# H3:HD - ogg container / theora video / vorbis sound (not supported by vcmi at the moment, but might be supported in future)
# and for mods - webm container / vp8 or vp9 video / opus sound
# TODO: add av1 support for mods (requires enabling libdav1d which currently fails to build via Conan)
self.options["ffmpeg"].enable_protocols = "file"
self.options["ffmpeg"].enable_demuxers = "bink,binka,ogg,smacker,webm_dash_manifest"
self.options["ffmpeg"].enable_parsers = "opus,vorbis,vp8,vp9,webp"
self.options["ffmpeg"].enable_decoders = "bink,binkaudio_dct,binkaudio_rdft,smackaud,smacker,theora,vorbis,vp8,vp9,opus"
#optionally, for testing - enable ffplay/ffprobe binaries in conan package:
#if self.settings.os == "Windows":
# self.options["ffmpeg"].with_programs = True
# self.options["ffmpeg"].avfilter = True
# self.options["ffmpeg"].with_sdl = True
# self.options["ffmpeg"].enable_filters = "aresample,scale"
self.options["sdl"].sdl2main = self.settings.os != "iOS" self.options["sdl"].sdl2main = self.settings.os != "iOS"
self.options["sdl"].vulkan = False self.options["sdl"].vulkan = False
@ -198,7 +239,7 @@ class VCMI(ConanFile):
# client # client
if self.options.with_ffmpeg: if self.options.with_ffmpeg:
self.requires("ffmpeg/[^4.4]") self.requires("ffmpeg/[>=4.4]")
# launcher # launcher
if self.settings.os == "Android": if self.settings.os == "Android":

View File

@ -137,6 +137,7 @@
"heroToggleTactics": "B", "heroToggleTactics": "B",
"highScoresCampaigns": "C", "highScoresCampaigns": "C",
"highScoresReset": "R", "highScoresReset": "R",
"highScoresStatistics": ".",
"highScoresScenarios": "S", "highScoresScenarios": "S",
"kingdomHeroesTab": "H", "kingdomHeroesTab": "H",
"kingdomTownsTab": "T", "kingdomTownsTab": "T",

View File

@ -27,12 +27,12 @@ The following platforms are supported and known to work, others might require ch
- **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04) - **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04)
- **Android**: libraries are built with NDK r25c (25.2.9519653) - **Android**: libraries are built with NDK r25c (25.2.9519653)
2. Download the binaries archive and unpack it to `~/.conan` directory: 2. Download the binaries archive and unpack it to `~/.conan` directory from https://github.com/vcmi/vcmi-dependencies/releases/latest
- [macOS](https://github.com/vcmi/vcmi-deps-macos/releases/latest): pick **intel.txz** if you have Intel Mac, otherwise - **intel-cross-arm.txz** - macOS: pick **dependencies-mac-intel.txz** if you have Intel Mac, otherwise - **dependencies-mac-arm.txz**
- [iOS](https://github.com/vcmi/vcmi-ios-deps/releases/latest) - iOS: pick ***dependencies-ios.txz***
- [Windows](https://github.com/vcmi/vcmi-deps-windows-conan/releases/latest): pick **vcmi-deps-windows-conan-w64.tgz** if you want x86_64, otherwise pick **vcmi-deps-windows-conan-w32.tgz** - Windows: currently only mingw is supported. Pick **dependencies-mingw.tgz** if you want x86_64, otherwise pick **dependencies-mingw-32.tgz**
- [Android](https://github.com/vcmi/vcmi-dependencies/releases): current limitation is that building works only on a macOS host due to Qt 5 for Android being compiled on macOS. Simply delete directory `~/.conan/data/qt/5.15.x/_/_/package` (`5.15.x` is a placeholder) after unpacking the archive and build Qt from source. Alternatively, if you have (or are [willing to build](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs)) Qt host tools for your platform, then simply replace those in the archive with yours and most probably it would work. - Android: current limitation is that building works only on a macOS host due to Qt 5 for Android being compiled on macOS. Simply delete directory `~/.conan/data/qt/5.15.x/_/_/package` (`5.15.x` is a placeholder) after unpacking the archive and build Qt from source. Alternatively, if you have (or are [willing to build](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs)) Qt host tools for your platform, then simply replace those in the archive with yours and most probably it would work.
3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: 3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS:

View File

@ -0,0 +1,80 @@
# File Formats
This page describes which file formats are supported by vcmi.
In most cases, VCMI supports formats that were supported by Heroes III, with addition of new formats that are more convenient to use without specialized tools. See categories below for more details on specific formats
### Images
For images VCMI supports:
- png. Recommended for usage in mods
- bmp. While this format is supported, bmp images have no compressions leading to large file sizes
- pcx (h3 version). Note that this is format that is specific to Heroes III and has nothing in common with widely known .pcx format. Files in this format generally can only be found inside of .lod archive of Heroes III and are usually extracted as .bmp files
Transparency support:
VCMI supports transparency (alpha) channel, both in png and in bmp images. There may be cases where transparency is not fully supported. If you discover such cases, please report them.
For performance reasons, please use alpha channel only in places where transparency is actually required and remove alpha channel from image othervice
Palette support:
TODO: describe how palettes work in vcmi
### Animations
For animations VCMI supports .def format from Heroes III as well as alternative json-based. See [Animation Format](Animation_Format.md) for more details
### Sounds
For sounds VCMI currently requires .wav format. Generally, VCMI will support any .wav parameters, however you might want to use high-bitrate versions, such as 44100 Hz or 48000 Hz, 32 bit, 1 or 2 channels
Support for additional formats, such as ogg/vorbis and ogg/opus is likely to be added in future
### Music
For sounds VCMI currently requires .mp3 format. Support for additional formats, such as ogg/vorbis and ogg/opus is likely to be added in future
### Video
Starting from VCMI 1.6, following video container formats are supported by VCMI:
- .bik - one of the formats used by Heroes III
- .smk - one of the formats used by Heroes III. Note that these videos generally have lower quality and are only used as fallback if no other formats are found
- .ogv - format used by Heroes III: HD Edition
- .webm - modern, free format that is recommended for modding.
Supported video codecs:
- bink and smacker - formats used by Heroes III, should be used only to avoid re-encoding
- theora - used by Heroes III: HD Edition
- vp8 - modern format with way better compression compared to formats used by Heroes III
- vp9 - recommended, this format is improvement of vp9 format and should be used as a default option
Support for av1 video codec is likely to be added in future.
Supported audio codecs:
- binkaudio and smackaud - formats used by Heroes III
- vorbis - modern format with good compression level
- opus - recommended, improvement over vorbis. Any bitrate is supported, with 128 kbit probably being the best option
### Json
For most of configuration files, VCMI uses [JSON format](http://en.wikipedia.org/wiki/Json) with some extensions from [JSON5](https://spec.json5.org/) format, such as comments.
### Maps
TODO: describe
### Campaigns
TODO: describe
### Map Templates
TODO: describe
### Archives
TODO: describe
### Txt
TODO: describe

View File

@ -19,8 +19,10 @@ Example of how directory structure of your mod may look like:
music/ - music files. Mp3 and ogg/vorbis are supported music/ - music files. Mp3 and ogg/vorbis are supported
sounds/ - sound files, in wav format. sounds/ - sound files, in wav format.
sprites/ - animation, image sets (H3 .def files or VCMI .json files) sprites/ - animation, image sets (H3 .def files or VCMI .json files)
video/ - video files, .bik or .smk video/ - video files, .bik, .smk, .ogv .webm
``` ```
See [File Formats](File_Formats.md) page for more information on which formats are supported or recommended for vcmi
## Creating mod file ## Creating mod file

View File

@ -56,7 +56,7 @@ public:
// //various // //various
virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
// const StartInfo * getStartInfo(bool beforeRandomization = false)const; virtual const StartInfo * getStartInfo(bool beforeRandomization = false) const = 0;
virtual bool isAllowed(SpellID id) const = 0; virtual bool isAllowed(SpellID id) const = 0;
virtual bool isAllowed(ArtifactID id) const = 0; virtual bool isAllowed(ArtifactID id) const = 0;
virtual bool isAllowed(SecondarySkill id) const = 0; virtual bool isAllowed(SecondarySkill id) const = 0;
@ -143,7 +143,7 @@ protected:
public: public:
//various //various
int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; const StartInfo * getStartInfo(bool beforeRandomization = false) const override;
bool isAllowed(SpellID id) const override; bool isAllowed(SpellID id) const override;
bool isAllowed(ArtifactID id) const override; bool isAllowed(ArtifactID id) const override;
bool isAllowed(SecondarySkill id) const override; bool isAllowed(SecondarySkill id) const override;

View File

@ -57,6 +57,11 @@ public:
h & b; h & b;
h & a; h & a;
} }
bool operator==(ColorRGBA const& rhs) const
{
return r == rhs.r && g == rhs.g && b == rhs.b && a == rhs.a;
}
}; };
VCMI_LIB_NAMESPACE_END VCMI_LIB_NAMESPACE_END

View File

@ -18,9 +18,19 @@ VCMI_LIB_NAMESPACE_BEGIN
bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const
{ {
//at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) //at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER)
if(at(tile) != EAccessibility::ACCESSIBLE) auto accessibility = at(tile);
if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER)
if(accessibility == EAccessibility::ALIVE_STACK)
{
auto destructible = destructibleEnemyTurns.find(tile);
return destructible != destructibleEnemyTurns.end();
}
if(accessibility != EAccessibility::ACCESSIBLE)
if(accessibility != EAccessibility::GATE || side != BattleSide::DEFENDER)
return false; return false;
return true; return true;
} }

View File

@ -35,6 +35,8 @@ using TAccessibilityArray = std::array<EAccessibility, GameConstants::BFIELD_SIZ
struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray
{ {
std::map<BattleHex, ui8> destructibleEnemyTurns;
public: public:
bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide
bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide

View File

@ -1052,16 +1052,29 @@ ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibi
continue; continue;
const int costToNeighbour = ret.distances[curHex.hex] + 1; const int costToNeighbour = ret.distances[curHex.hex] + 1;
for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex])
{ {
if(neighbour.isValid()) if(neighbour.isValid())
{ {
auto additionalCost = 0;
if(params.bypassEnemyStacks)
{
auto enemyToBypass = params.destructibleEnemyTurns.find(neighbour);
if(enemyToBypass != params.destructibleEnemyTurns.end())
{
additionalCost = enemyToBypass->second;
}
}
const int costFoundSoFar = ret.distances[neighbour.hex]; const int costFoundSoFar = ret.distances[neighbour.hex];
if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) if(accessibleCache[neighbour.hex] && costToNeighbour + additionalCost < costFoundSoFar)
{ {
hexq.push(neighbour); hexq.push(neighbour);
ret.distances[neighbour.hex] = costToNeighbour; ret.distances[neighbour.hex] = costToNeighbour + additionalCost;
ret.predecessors[neighbour.hex] = curHex; ret.predecessors[neighbour.hex] = curHex;
} }
} }
@ -1236,7 +1249,13 @@ ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Pa
if(params.flying) if(params.flying)
return getFlyingReachability(params); return getFlyingReachability(params);
else else
return makeBFS(getAccessibility(params.knownAccessible), params); {
auto accessibility = getAccessibility(params.knownAccessible);
accessibility.destructibleEnemyTurns = params.destructibleEnemyTurns;
return makeBFS(accessibility, params);
}
} }
ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters &params) const ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters &params) const

View File

@ -29,7 +29,9 @@ struct DLL_LINKAGE ReachabilityInfo
bool doubleWide = false; bool doubleWide = false;
bool flying = false; bool flying = false;
bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes
bool bypassEnemyStacks = false; // in case of true will count amount of turns needed to kill enemy and thus move forward
std::vector<BattleHex> knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) std::vector<BattleHex> knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself)
std::map<BattleHex, ui8> destructibleEnemyTurns; // hom many turns it is needed to kill enemy on specific hex
BattleHex startPosition; //assumed position of stack BattleHex startPosition; //assumed position of stack
BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side

View File

@ -115,6 +115,7 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension)
{".FLAC", EResType::SOUND}, {".FLAC", EResType::SOUND},
{".SMK", EResType::VIDEO_LOW_QUALITY}, {".SMK", EResType::VIDEO_LOW_QUALITY},
{".BIK", EResType::VIDEO}, {".BIK", EResType::VIDEO},
{".OGV", EResType::VIDEO},
{".WEBM", EResType::VIDEO}, {".WEBM", EResType::VIDEO},
{".ZIP", EResType::ARCHIVE_ZIP}, {".ZIP", EResType::ARCHIVE_ZIP},
{".LOD", EResType::ARCHIVE_LOD}, {".LOD", EResType::ARCHIVE_LOD},

View File

@ -28,7 +28,7 @@ class JsonSerializeFormat;
* Font: .fnt * Font: .fnt
* Image: .bmp, .jpg, .pcx, .png, .tga * Image: .bmp, .jpg, .pcx, .png, .tga
* Sound: .wav .82m * Sound: .wav .82m
* Video: .smk, .bik .mjpg .mpg .webm * Video: .smk, .bik .ogv .webm
* Music: .mp3, .ogg * Music: .mp3, .ogg
* Archive: .lod, .snd, .vid .pac .zip * Archive: .lod, .snd, .vid .pac .zip
* Palette: .pal * Palette: .pal

View File

@ -129,26 +129,26 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner)
throw std::runtime_error("Can not allocate hero. All heroes are already used."); throw std::runtime_error("Can not allocate hero. All heroes are already used.");
} }
int CGameState::getDate(Date mode) const int CGameState::getDate(int d, Date mode)
{ {
int temp; int temp;
switch (mode) switch (mode)
{ {
case Date::DAY: case Date::DAY:
return day; return d;
case Date::DAY_OF_WEEK: //day of week case Date::DAY_OF_WEEK: //day of week
temp = (day)%7; // 1 - Monday, 7 - Sunday temp = (d)%7; // 1 - Monday, 7 - Sunday
return temp ? temp : 7; return temp ? temp : 7;
case Date::WEEK: //current week case Date::WEEK: //current week
temp = ((day-1)/7)+1; temp = ((d-1)/7)+1;
if (!(temp%4)) if (!(temp%4))
return 4; return 4;
else else
return (temp%4); return (temp%4);
case Date::MONTH: //current month case Date::MONTH: //current month
return ((day-1)/28)+1; return ((d-1)/28)+1;
case Date::DAY_OF_MONTH: //day of month case Date::DAY_OF_MONTH: //day of month
temp = (day)%28; temp = (d)%28;
if (temp) if (temp)
return temp; return temp;
else return 28; else return 28;
@ -156,6 +156,11 @@ int CGameState::getDate(Date mode) const
return 0; return 0;
} }
int CGameState::getDate(Date mode) const
{
return getDate(day, mode);
}
CGameState::CGameState() CGameState::CGameState()
{ {
gs = this; gs = this;

View File

@ -138,6 +138,7 @@ public:
bool isVisible(int3 pos, const std::optional<PlayerColor> & player) const override; bool isVisible(int3 pos, const std::optional<PlayerColor> & player) const override;
bool isVisible(const CGObjectInstance * obj, const std::optional<PlayerColor> & player) const override; bool isVisible(const CGObjectInstance * obj, const std::optional<PlayerColor> & player) const override;
static int getDate(int day, Date mode);
int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
// ----- getters, setters ----- // ----- getters, setters -----

View File

@ -11,6 +11,7 @@
#include "GameStatistics.h" #include "GameStatistics.h"
#include "../CPlayerState.h" #include "../CPlayerState.h"
#include "../constants/StringConstants.h" #include "../constants/StringConstants.h"
#include "../VCMIDirs.h"
#include "CGameState.h" #include "CGameState.h"
#include "TerrainHandler.h" #include "TerrainHandler.h"
#include "CHeroHandler.h" #include "CHeroHandler.h"
@ -44,6 +45,7 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
data.timestamp = std::time(nullptr); data.timestamp = std::time(nullptr);
data.day = gs->getDate(Date::DAY); data.day = gs->getDate(Date::DAY);
data.player = ps->color; data.player = ps->color;
data.playerName = gs->getStartInfo()->playerInfos.at(ps->color).name;
data.team = ps->team; data.team = ps->team;
data.isHuman = ps->isHuman(); data.isHuman = ps->isHuman();
data.status = ps->status; data.status = ps->status;
@ -71,101 +73,122 @@ StatisticDataSetEntry StatisticDataSet::createEntry(const PlayerState * ps, cons
data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources(); data.spentResourcesForArmy = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForArmy : TResources();
data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources(); data.spentResourcesForBuildings = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).spentResourcesForBuildings : TResources();
data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources(); data.tradeVolume = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).tradeVolume : TResources();
data.eventCapturedTown = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastCapturedTownDay == gs->getDate(Date::DAY) : false;
data.eventDefeatedStrongestHero = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).lastDefeatedStrongestHeroDay == gs->getDate(Date::DAY) : false;
data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0; data.movementPointsUsed = gs->statistic.accumulatedValues.count(ps->color) ? gs->statistic.accumulatedValues.at(ps->color).movementPointsUsed : 0;
return data; return data;
} }
std::string StatisticDataSet::toCsv() std::string StatisticDataSet::toCsv(std::string sep)
{ {
std::stringstream ss; std::stringstream ss;
auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS}; auto resources = std::vector<EGameResID>{EGameResID::GOLD, EGameResID::WOOD, EGameResID::MERCURY, EGameResID::ORE, EGameResID::SULFUR, EGameResID::CRYSTAL, EGameResID::GEMS};
ss << "Map" << ";"; ss << "Map" << sep;
ss << "Timestamp" << ";"; ss << "Timestamp" << sep;
ss << "Day" << ";"; ss << "Day" << sep;
ss << "Player" << ";"; ss << "Player" << sep;
ss << "Team" << ";"; ss << "PlayerName" << sep;
ss << "IsHuman" << ";"; ss << "Team" << sep;
ss << "Status" << ";"; ss << "IsHuman" << sep;
ss << "NumberHeroes" << ";"; ss << "Status" << sep;
ss << "NumberTowns" << ";"; ss << "NumberHeroes" << sep;
ss << "NumberArtifacts" << ";"; ss << "NumberTowns" << sep;
ss << "NumberDwellings" << ";"; ss << "NumberArtifacts" << sep;
ss << "ArmyStrength" << ";"; ss << "NumberDwellings" << sep;
ss << "TotalExperience" << ";"; ss << "ArmyStrength" << sep;
ss << "Income" << ";"; ss << "TotalExperience" << sep;
ss << "MapExploredRatio" << ";"; ss << "Income" << sep;
ss << "ObeliskVisitedRatio" << ";"; ss << "MapExploredRatio" << sep;
ss << "TownBuiltRatio" << ";"; ss << "ObeliskVisitedRatio" << sep;
ss << "HasGrail" << ";"; ss << "TownBuiltRatio" << sep;
ss << "Score" << ";"; ss << "HasGrail" << sep;
ss << "MaxHeroLevel" << ";"; ss << "Score" << sep;
ss << "NumBattlesNeutral" << ";"; ss << "MaxHeroLevel" << sep;
ss << "NumBattlesPlayer" << ";"; ss << "NumBattlesNeutral" << sep;
ss << "NumWinBattlesNeutral" << ";"; ss << "NumBattlesPlayer" << sep;
ss << "NumWinBattlesPlayer" << ";"; ss << "NumWinBattlesNeutral" << sep;
ss << "NumHeroSurrendered" << ";"; ss << "NumWinBattlesPlayer" << sep;
ss << "NumHeroEscaped" << ";"; ss << "NumHeroSurrendered" << sep;
ss << "NumHeroEscaped" << sep;
ss << "EventCapturedTown" << sep;
ss << "EventDefeatedStrongestHero" << sep;
ss << "MovementPointsUsed"; ss << "MovementPointsUsed";
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource]; ss << sep << GameConstants::RESOURCE_NAMES[resource];
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "Mines"; ss << sep << GameConstants::RESOURCE_NAMES[resource] + "Mines";
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy"; ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForArmy";
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings"; ss << sep << GameConstants::RESOURCE_NAMES[resource] + "SpentResourcesForBuildings";
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume"; ss << sep << GameConstants::RESOURCE_NAMES[resource] + "TradeVolume";
ss << "\r\n"; ss << "\r\n";
for(auto & entry : data) for(auto & entry : data)
{ {
ss << entry.map << ";"; ss << entry.map << sep;
ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << ";"; ss << vstd::getFormattedDateTime(entry.timestamp, "%Y-%m-%dT%H:%M:%S") << sep;
ss << entry.day << ";"; ss << entry.day << sep;
ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << ";"; ss << GameConstants::PLAYER_COLOR_NAMES[entry.player] << sep;
ss << entry.team.getNum() << ";"; ss << entry.playerName << sep;
ss << entry.isHuman << ";"; ss << entry.team.getNum() << sep;
ss << static_cast<int>(entry.status) << ";"; ss << entry.isHuman << sep;
ss << entry.numberHeroes << ";"; ss << static_cast<int>(entry.status) << sep;
ss << entry.numberTowns << ";"; ss << entry.numberHeroes << sep;
ss << entry.numberArtifacts << ";"; ss << entry.numberTowns << sep;
ss << entry.numberDwellings << ";"; ss << entry.numberArtifacts << sep;
ss << entry.armyStrength << ";"; ss << entry.numberDwellings << sep;
ss << entry.totalExperience << ";"; ss << entry.armyStrength << sep;
ss << entry.income << ";"; ss << entry.totalExperience << sep;
ss << entry.mapExploredRatio << ";"; ss << entry.income << sep;
ss << entry.obeliskVisitedRatio << ";"; ss << entry.mapExploredRatio << sep;
ss << entry.townBuiltRatio << ";"; ss << entry.obeliskVisitedRatio << sep;
ss << entry.hasGrail << ";"; ss << entry.townBuiltRatio << sep;
ss << entry.score << ";"; ss << entry.hasGrail << sep;
ss << entry.maxHeroLevel << ";"; ss << entry.score << sep;
ss << entry.numBattlesNeutral << ";"; ss << entry.maxHeroLevel << sep;
ss << entry.numBattlesPlayer << ";"; ss << entry.numBattlesNeutral << sep;
ss << entry.numWinBattlesNeutral << ";"; ss << entry.numBattlesPlayer << sep;
ss << entry.numWinBattlesPlayer << ";"; ss << entry.numWinBattlesNeutral << sep;
ss << entry.numHeroSurrendered << ";"; ss << entry.numWinBattlesPlayer << sep;
ss << entry.numHeroEscaped << ";"; ss << entry.numHeroSurrendered << sep;
ss << entry.numHeroEscaped << sep;
ss << entry.eventCapturedTown << sep;
ss << entry.eventDefeatedStrongestHero << sep;
ss << entry.movementPointsUsed; ss << entry.movementPointsUsed;
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << entry.resources[resource]; ss << sep << entry.resources[resource];
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << entry.numMines[resource]; ss << sep << entry.numMines[resource];
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << entry.spentResourcesForArmy[resource]; ss << sep << entry.spentResourcesForArmy[resource];
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << entry.spentResourcesForBuildings[resource]; ss << sep << entry.spentResourcesForBuildings[resource];
for(auto & resource : resources) for(auto & resource : resources)
ss << ";" << entry.tradeVolume[resource]; ss << sep << entry.tradeVolume[resource];
ss << "\r\n"; ss << "\r\n";
} }
return ss.str(); return ss.str();
} }
std::string StatisticDataSet::writeCsv()
{
const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic";
boost::filesystem::create_directories(outPath);
const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv");
std::ofstream file(filePath.c_str());
std::string csv = toCsv(";");
file << csv;
return filePath.string();
}
std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const PlayerState * ps) std::vector<const CGMine *> Statistic::getMines(const CGameState * gs, const PlayerState * ps)
{ {
std::vector<const CGMine *> tmp; std::vector<const CGMine *> tmp;

View File

@ -25,6 +25,7 @@ struct DLL_LINKAGE StatisticDataSetEntry
time_t timestamp; time_t timestamp;
int day; int day;
PlayerColor player; PlayerColor player;
std::string playerName;
TeamID team; TeamID team;
bool isHuman; bool isHuman;
EPlayerStatus status; EPlayerStatus status;
@ -52,6 +53,8 @@ struct DLL_LINKAGE StatisticDataSetEntry
TResources spentResourcesForArmy; TResources spentResourcesForArmy;
TResources spentResourcesForBuildings; TResources spentResourcesForBuildings;
TResources tradeVolume; TResources tradeVolume;
bool eventCapturedTown;
bool eventDefeatedStrongestHero;
si64 movementPointsUsed; si64 movementPointsUsed;
template <typename Handler> void serialize(Handler &h) template <typename Handler> void serialize(Handler &h)
@ -60,6 +63,8 @@ struct DLL_LINKAGE StatisticDataSetEntry
h & timestamp; h & timestamp;
h & day; h & day;
h & player; h & player;
if(h.version >= Handler::Version::STATISTICS_SCREEN)
h & playerName;
h & team; h & team;
h & isHuman; h & isHuman;
h & status; h & status;
@ -87,18 +92,22 @@ struct DLL_LINKAGE StatisticDataSetEntry
h & spentResourcesForArmy; h & spentResourcesForArmy;
h & spentResourcesForBuildings; h & spentResourcesForBuildings;
h & tradeVolume; h & tradeVolume;
if(h.version >= Handler::Version::STATISTICS_SCREEN)
{
h & eventCapturedTown;
h & eventDefeatedStrongestHero;
}
h & movementPointsUsed; h & movementPointsUsed;
} }
}; };
class DLL_LINKAGE StatisticDataSet class DLL_LINKAGE StatisticDataSet
{ {
std::vector<StatisticDataSetEntry> data;
public: public:
void add(StatisticDataSetEntry entry); void add(StatisticDataSetEntry entry);
static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs); static StatisticDataSetEntry createEntry(const PlayerState * ps, const CGameState * gs);
std::string toCsv(); std::string toCsv(std::string sep);
std::string writeCsv();
struct PlayerAccumulatedValueStorage // holds some actual values needed for stats struct PlayerAccumulatedValueStorage // holds some actual values needed for stats
{ {
@ -112,6 +121,8 @@ public:
TResources spentResourcesForBuildings; TResources spentResourcesForBuildings;
TResources tradeVolume; TResources tradeVolume;
si64 movementPointsUsed; si64 movementPointsUsed;
int lastCapturedTownDay;
int lastDefeatedStrongestHeroDay;
template <typename Handler> void serialize(Handler &h) template <typename Handler> void serialize(Handler &h)
{ {
@ -125,8 +136,14 @@ public:
h & spentResourcesForBuildings; h & spentResourcesForBuildings;
h & tradeVolume; h & tradeVolume;
h & movementPointsUsed; h & movementPointsUsed;
if(h.version >= Handler::Version::STATISTICS_SCREEN)
{
h & lastCapturedTownDay;
h & lastDefeatedStrongestHeroDay;
}
} }
}; };
std::vector<StatisticDataSetEntry> data;
std::map<PlayerColor, PlayerAccumulatedValueStorage> accumulatedValues; std::map<PlayerColor, PlayerAccumulatedValueStorage> accumulatedValues;
template <typename Handler> void serialize(Handler &h) template <typename Handler> void serialize(Handler &h)

View File

@ -24,6 +24,7 @@
#include "../gameState/RumorState.h" #include "../gameState/RumorState.h"
#include "../gameState/QuestInfo.h" #include "../gameState/QuestInfo.h"
#include "../gameState/TavernSlot.h" #include "../gameState/TavernSlot.h"
#include "../gameState/GameStatistics.h"
#include "../int3.h" #include "../int3.h"
#include "../mapping/CMapDefines.h" #include "../mapping/CMapDefines.h"
#include "../spells/ViewSpellInt.h" #include "../spells/ViewSpellInt.h"
@ -435,6 +436,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient
PlayerColor player; PlayerColor player;
EVictoryLossCheckResult victoryLossCheckResult; EVictoryLossCheckResult victoryLossCheckResult;
StatisticDataSet statistic;
void visitTyped(ICPackVisitor & visitor) override; void visitTyped(ICPackVisitor & visitor) override;
@ -442,6 +444,8 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient
{ {
h & player; h & player;
h & victoryLossCheckResult; h & victoryLossCheckResult;
if (h.version >= Handler::Version::STATISTICS_SCREEN)
h & statistic;
} }
}; };

View File

@ -64,6 +64,7 @@ enum class ESerializationVersion : int32_t
CAMPAIGN_REGIONS, // 853 - configurable campaign regions CAMPAIGN_REGIONS, // 853 - configurable campaign regions
EVENTS_PLAYER_SET, // 854 - map & town events use std::set instead of bitmask to store player list EVENTS_PLAYER_SET, // 854 - map & town events use std::set instead of bitmask to store player list
NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed NEW_TOWN_BUILDINGS, // 855 - old bonusing buildings have been removed
STATISTICS_SCREEN, // 856 - extent statistic functions
CURRENT = NEW_TOWN_BUILDINGS CURRENT = STATISTICS_SCREEN
}; };

View File

@ -681,7 +681,7 @@ void CGameHandler::onPlayerTurnEnded(PlayerColor which)
heroPool->onNewWeek(which); heroPool->onNewWeek(which);
} }
void CGameHandler::addStatistics() void CGameHandler::addStatistics(StatisticDataSet &stat) const
{ {
for (const auto & elem : gs->players) for (const auto & elem : gs->players)
{ {
@ -690,7 +690,7 @@ void CGameHandler::addStatistics()
auto data = StatisticDataSet::createEntry(&elem.second, gs); auto data = StatisticDataSet::createEntry(&elem.second, gs);
gameState()->statistic.add(data); stat.add(data);
} }
} }
@ -718,6 +718,10 @@ void CGameHandler::onNewTurn()
} }
} }
} }
else
{
addStatistics(gameState()->statistic); // write at end of turn
}
for (const auto & player : gs->players) for (const auto & player : gs->players)
{ {
@ -1036,8 +1040,6 @@ void CGameHandler::onNewTurn()
} }
synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that
addStatistics();
} }
void CGameHandler::start(bool resume) void CGameHandler::start(bool resume)
@ -1414,6 +1416,8 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne
const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(obj); const CGTownInstance * town = dynamic_cast<const CGTownInstance *>(obj);
if (town) //town captured if (town) //town captured
{ {
gs->statistic.accumulatedValues[owner].lastCapturedTownDay = gs->getDate(Date::DAY);
if (owner.isValidPlayer()) //new owner is real player if (owner.isValidPlayer()) //new owner is real player
{ {
if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING))
@ -3733,6 +3737,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player)
PlayerEndsGame peg; PlayerEndsGame peg;
peg.player = player; peg.player = player;
peg.victoryLossCheckResult = victoryLossCheckResult; peg.victoryLossCheckResult = victoryLossCheckResult;
peg.statistic = StatisticDataSet(gameState()->statistic);
addStatistics(peg.statistic); // add last turn befor win / loss
sendAndApply(&peg); sendAndApply(&peg);
turnOrder->onPlayerEndsGame(player); turnOrder->onPlayerEndsGame(player);

View File

@ -14,6 +14,7 @@
#include "../lib/IGameCallback.h" #include "../lib/IGameCallback.h"
#include "../lib/LoadProgress.h" #include "../lib/LoadProgress.h"
#include "../lib/ScriptHandler.h" #include "../lib/ScriptHandler.h"
#include "../lib/gameState/GameStatistics.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN
@ -227,7 +228,7 @@ public:
void onPlayerTurnStarted(PlayerColor which); void onPlayerTurnStarted(PlayerColor which);
void onPlayerTurnEnded(PlayerColor which); void onPlayerTurnEnded(PlayerColor which);
void onNewTurn(); void onNewTurn();
void addStatistics(); void addStatistics(StatisticDataSet &stat) const;
void handleTimeEvents(PlayerColor player); void handleTimeEvents(PlayerColor player);
void handleTownEvents(CGTownInstance *town); void handleTownEvents(CGTownInstance *town);

View File

@ -480,6 +480,17 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle)
// Remove beaten hero // Remove beaten hero
if(finishingBattle->loserHero) if(finishingBattle->loserHero)
{ {
//add statistics
if(!finishingBattle->isDraw())
{
ConstTransitivePtr<CGHeroInstance> strongestHero = nullptr;
for(auto & hero : gameHandler->gameState()->getPlayerState(finishingBattle->loser)->heroes)
if(!strongestHero || hero->exp > strongestHero->exp)
strongestHero = hero;
if(strongestHero->id == finishingBattle->loserHero->id && strongestHero->level > 5)
gameHandler->gameState()->statistic.accumulatedValues[finishingBattle->victor].lastDefeatedStrongestHeroDay = gameHandler->gameState()->getDate(Date::DAY);
}
RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor); RemoveObject ro(finishingBattle->loserHero->id, finishingBattle->victor);
gameHandler->sendAndApply(&ro); gameHandler->sendAndApply(&ro);
} }

View File

@ -140,15 +140,9 @@ void PlayerMessageProcessor::commandStatistic(PlayerColor player, const std::vec
if(!isHost) if(!isHost)
return; return;
const boost::filesystem::path outPath = VCMIDirs::get().userCachePath() / "statistic"; std::string path = gameHandler->gameState()->statistic.writeCsv();
boost::filesystem::create_directories(outPath);
const boost::filesystem::path filePath = outPath / (vstd::getDateTimeISO8601Basic(std::time(nullptr)) + ".csv"); broadcastSystemMessage("Statistic files can be found in " + path + " directory\n");
std::ofstream file(filePath.c_str());
std::string csv = gameHandler->gameState()->statistic.toCsv();
file << csv;
broadcastSystemMessage("Statistic files can be found in " + outPath.string() + " directory\n");
} }
void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector<std::string> & words) void PlayerMessageProcessor::commandHelp(PlayerColor player, const std::vector<std::string> & words)

View File

@ -17,6 +17,7 @@ class IGameInfoCallbackMock : public IGameInfoCallback
public: public:
//various //various
MOCK_CONST_METHOD1(getDate, int(Date)); MOCK_CONST_METHOD1(getDate, int(Date));
MOCK_CONST_METHOD1(getStartInfo, const StartInfo *(bool));
MOCK_CONST_METHOD1(isAllowed, bool(SpellID)); MOCK_CONST_METHOD1(isAllowed, bool(SpellID));
MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID)); MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID));